🌍JUC 简介
JAVA 在 1.5版本中引入了 java.util.concurrent工具包,JUC 就是它的简称,用于处理线程的工具包。
🌞重温进程与线程
进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程——资源分配的最小单位。
线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程——程序执行的最小单位。
线程的状态
- NEW:新建
- RUNNABLE:准备就绪
- BLOCKED:阻塞
- WAITING:等待(不见不散)
- TIMED_WAITING:过时不候
- TERMINATED:终止
wait/sleep 的区别
-
相同点
(1)一旦执行方法,都可以是当前线程进入阻塞状态
-
不同点
(1)sleep是 Thread的静态方法,wait是 Object的方法,任何对象实例都能调用。
(2)sleep 不会释放锁,它也不需要占用锁。wait会释放锁,但调用它的前提是当前线程占有锁(既代码要在 synchronized中)。
(3)他们都可以被 interrupted方法中断
并发与并行
并发(Concurrent):指在同一个 CPU 上同时(不是真正的同时,而是看来是同时,因为 CPU 要在多个程序间切换)运行多个程序——抢票、秒杀。
并行(parallel):指多个 CPU 运行一个程序,多个 CPU 运行时有一个公共的资源被共享——多窗口卖票。
异步与同步
同步:指发起一次任务直到收到响应结果才算结束,再开始下一个任务。
例:去饭店吃饭,点了一桌菜。然后等厨师做好饭菜,等菜上桌。然后你吃完饭,付完钱。然后再去购物。这就相当于同步调用
异步:发起一次任务无需等到任务结束响应结果,直接开启下一个任务。之前的任务结束再响应给你结果。
例:我们正处于互联网时代,当我们想吃饭了,直接打开外卖平台,点了一些菜,支付了餐费,这个时候你完成了点菜就可以继续打你的游戏、去购物,无需默默等待。而商家接到你的订单就开始准备饭菜,等做好后再送到你手上。这就相当于异步调用
🖤CORE
volatile 关键字
通过防止指令重排和缓存一致性协议,保证多线程并发下的可见性问题。指令重排是指,在不影响代码执行的最终结果前提下,为了最大化cpu利用率以及性能,将代码乱序执行。
引入场景:由于每个不同的线程在运行时会产生属于自己独立的一份缓存,并在某个线程中对公共资源进行了修改操作。而这时其他线程进来读取公共的资源,可能会出现数据不一致的情况,有点类似于脏读的概念。
- 当多个线程操作共享数据时,可以保证内存中的数据可见,相较于 synchronized是一种较为轻量级的同步策略
- volatile不具备“互斥性”
- volatile不能保证变量的“原子性”
线程8锁核心
非静态方法的锁为 this,静态方法的锁为对应类的Class实例。某一个时刻内只能有一个线程持有锁,无论有多少个方法
Lock 锁
非公平锁
Lock nonFairSync = new ReentrantLock();
/**
* 默认空参构造器为非公平锁,多个线程执行任务,非公平锁会出现每次都是第一个线程抢到资源,其他线程(饿死)没有资源可执行。
* 只要发现有资源是"空闲状态"直接抢占所以执行效率高
*/
公平锁
Lock fairSync = new ReentrantLock(true);
/**
* 公平锁:多个线程都能分到资源进行执行(阳光普照),获取资源前进行判断这个资源是否有其他线程在使用,有则排队没有才进行执行。
* 所以效率较低。
*/
可重入锁
可重复锁又叫递归锁 synchronized(隐式)和 Lock (显示) 都是可重入锁。
如果当前任务有多层锁,只要线程拿到最外层锁,那么不管里面还有多少层锁,这个线程都可以自由进入。
死锁必少不了
两个或多个线程在执行过程中,因为争夺资源(我等你,你等我)而造成一种等待的现象,如果没有外力的干涉,他们无法再执行下去。
可能产生原因:
- 系统资源不足
- 线程运行推进顺序不合适
- 资源分配不当
查看死锁:
先拿到进程号,然后使用自带的栈跟踪工具查看信息
jps -l
:类似于linux系统的 ps -ef 查看进程jstack <进程号>
:查看栈信息
辅助类
CountDownLatch 减少计数
CountDownLatch downLatch = new CountDownLatch(int count);
//countDown() count -1
//指定一个数据初始化,调用await()使其以后的代码进入等待状态,当count为0时,自动唤醒
public static void main(String[] args) throws ExecutionException, InterruptedException { //请使用try-catch
FutureTask<List<Integer>> futureTask = new FutureTask<>(() -> {
CountDownLatch countDownLatch = new CountDownLatch(3000);
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
for (String s : Arrays.asList("A","B","C")){
new Thread(()->{
for (int i = 0; i < 1000; i++) {
list.add(i);
countDownLatch.countDown();
}
},s).start();
}
countDownLatch.await();
return list;
});
new Thread(futureTask).start();
System.out.println(futureTask.get().size());
}
CyclicBarrier 循环栅栏
CyclicBarrier cyclicBarrier = new CyclicBarrier(int parties,Runnable runnable);
//parties:线程进入数
//runnable:满足条件后执行的线程
//初始化一个栅栏,指定 parties数量,每个线程完成后调用 cyclicBarrier.await();,当线程进入数达到指定的parties数,自动执行 runnable线程中的方法
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> System.out.println("点火~~"));
for (String s : Arrays.asList("A", "B", "C")) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "通过!");
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}, s).start();
}
}
Semaphore 信号灯
Semaphore semaphore = new Semaphore(int count);
//初始化一个线程许可数量的信号灯,每次只允许count个线程抢占,直到释放许可,其他线程才能够开始抢占
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(2);
for (String s : Arrays.asList("A", "B", "C", "D","E")) {
new Thread(() -> {
try {
//抢占许可
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "吃饭");
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println(Thread.currentThread().getName() + "吃饱了,离开");
} catch (Exception e) {
} finally {
//释放许可
semaphore.release();
}
}, s).start();
}
}
读写锁
一个资源可以被多个读线程访问,或者可以被一个写线程访问,但是不能同时存在读写线程,读写是互斥的,读读是共享的。
悲观锁
基于一种悲观的态度来防止一切数据冲突,它是一种预防的姿态在修改数据之前把数据锁住,然后再对数据进行读写,在它释放锁之前,任何人都不能再对其数据进行操作,直到前面一个人操作完成后释放锁。一般数据库本身锁的机制都是基于悲观锁的机制实现的
特点:可以完全保证数据的独占性和正确性,因为每次请求都会先对数据进行加锁, 然后进行数据操作,最后再解锁,而加锁释放锁的过程会造成消耗,所以性能不高
乐观锁
对于数据冲突保持一种乐观态度,其本身不对数据进行加锁,而是通过业务(版本)实现锁的功能,它允许多个请求同时访问数据,同时也省掉了对数据加锁和解锁的过程,所以可以一定程度的提高操作性能。但是高并发的场景下,会导致大量的请求冲突,进而导致大部分操作都是无功而返,浪费掉了资源,这个时候反而乐观锁的性能不如悲观锁。
演变过程
一阶段 | 二阶段 | 三阶段 |
---|---|---|
无锁:多个线程可以同时抢占资源,线程不安全,资源分配乱 | 加锁:(synchronized、ReentrantLock)无论是读读,读写,写写都不能共享锁,只能有一个线程持有 | 读写锁:(ReentrantReadWriteLock)读读可以共享,提升性能,同时多人进行读操作,只有写写是独占的,但是存在锁饥饿的情况,当有线程一直在读的时候,永远无法进行写操作 |
读写锁示例
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Stream;
/**
* @author Duanyu
* @date 2021-11-18 10:01
*/
public class ReadWriteTest {
public static void main(String[] args) {
Cache cache = new Cache();
//5个线程写数据
Stream.of("A", "B", "C", "D", "E").forEach(v -> new Thread(() -> cache.set(v, v), v).start());
//5个线程读数据
Stream.of("A", "B", "C", "D", "E").forEach(v -> new Thread(() -> cache.get(v), v).start());
}
}
class Cache {
private volatile Map<String, Object> map = new HashMap<>();
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//写数据
public void set(String key, Object value) {
readWriteLock.writeLock().lock();
try {
//暂停一会
System.out.println(Thread.currentThread().getName() + "写:" + key);
TimeUnit.SECONDS.sleep(2);
//写
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写结束:" + key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
//读数据
public Object get(String key) {
Object result = null;
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "正在读取:" + key);
//暂停一会
TimeUnit.SECONDS.sleep(2);
result = map.get(key);
System.out.println(Thread.currentThread().getName() + "读完了:" + key+"--"+result);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
return result;
}
}
写锁降级
写锁可以降为读锁,但是读锁无法升级成写锁。操作:首先获取到写锁再获取读锁,然后释放写锁,最后释放读锁。
阻塞队列
队列:先进先出(FIFO)——栈:先进后出(LIFO)
-
当队列是空的,从队列中获取元素的操作将被阻塞:试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空队列插入新的元素
-
当队列是满的,从队列中添加元素的操作将被阻塞:试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中消费掉一个或多个
BlockingQueue常用实现类
- ArrayBlockingQueue:基于数组的阻塞队列实现,在ArrayBlockingQueue内部维护了一个初始化给定长度数组。ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象
- LinkedBlockingQueue:基于链表的阻塞队列,它能够高效的处理并发数据,因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能,同时我们还需要注意初始化时一定要指定长度, 否则它默认为Integer.MAX_VALUE,这样一旦生产速度高于消费速度,系统内存直接就消耗殆尽了。
- DelayQueue:它里面的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。常见的例子比如使用一个DelayQueue来管理一个超时未响应的连接队列。
- PriorityBlockingQueue:基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的元素时阻塞消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。
- SynchronousQueue:一种无缓冲的等待队列,没有中间商,生产者直接对接消费者,如果一方没有找到合适的目标,那么大家都要等待.
线程池
/**
* 1.线程池接口体系:
* java.util.concurrent.Executor:负责线程的使用与调度的根接口
* -> ExecutorService:线程池的主要接口
* --> ThreadPoolExecutor:线程池的实现类
* -> ScheduleExecutorService :负责线程的调度
* ->ScheduledThreadPoolExecutor:继承于 ThreadPoolExecutor又实现了 ScheduleExecutorService
* 2.Executors工具类:
* newFixedThreadPool(int count):创建指定大小的线程池
* newCachedThreadPool():缓存线程池,线程池数量根据需求自动更改数量
* newSingleThreadExecutor():创建单个线程池,线程池中只有一个线程
* newScheduledThreadPool():创建指定大小的线程,可以延迟或定时的执行任务
* 3.线程池常用方法:
* execute(Runnable x) 没有返回值。可以执行任务,但无法判断任务是否成功完成。——实现Runnable接口
* submit(Runnable x) 返回一个future。可以用这个future来判断任务是否成功完成。——实现Callable接口
*/
在开发中我们一般禁止直接使用Executors工具类来创建线程
- FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
- CachedThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
推荐使用 ThreadPoolExecutor的方式
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(8,16,1L, TimeUnit.SECONDS,new LinkedBlockingQueue<>(2000));
//几个参数的分别含义:
//int corePoolSize:常驻(核心)线程数量
//int maximumPoolSize:最大线程数量
//long keepAliveTime:突然增加的线程存活时间
//TimeUnit unit:上面存活时间的单位
//BlockingQueue<Runnable> workQueue:阻塞队列,当所有线程用完时,再来的任务会丢到阻塞队列中
//ThreadFactory factory:线程工厂,用于创建线程
//RejectedExecutionHandler handler:当队列中元素已满触发的拒绝策略
线程池两个细节
- 当我们调用它的执行方法时,才会使用工厂创建线程
- 当核心线程已满再来的线程任务,将会丢到阻塞队列中,当阻塞队列已满又来的线程任务就会新创建线程进行执行,直到阻塞队列和最大线程数都满了才会执行拒绝策略
JDK内置的4种拒绝策略
- AbortPolicy(默认策略):直接抛出RejectedExecutionException异常
- CallerRunsPolicy(调用者策略):它是一种调节机制,既不丢弃任务也不抛出异常,哪里来的退回到哪去
- DiscardOldestPolicy(抛弃策略):抛弃队列中等待最久的任务,然后把当前任务(新进来的任务)加入到队列中尝试再次提交执行当前任务
- DiscardPolicy(不作为策略):该策略不作任何处理,默默的丢弃无法处理的任务
异步回调
Future设计的初衷是对将来某个时刻会发生的结果进行建模 CompletableFuture 作为Future的一个实现类它里面提供了回调的方法
Future的优点:比更底层的Thread更易用。要使用Future,通常只需要将耗时的操作封装在一个Callable对象中,再将它提交给ExecutorService
//没有返回值
CompletableFuture<Void> completableFuture1 = CompletableFuture.runAsync(()->{
//休息2s
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "runAsync=>Void");
});
//获取阻塞执行结果
completableFuture1.get();
System.out.println("123");
//有返回值
// 2.1 有返回值的supplyAsync 异步回调
CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName() + "supplyAsync=>Integer");
int i = 10/0;
return 666;
});
CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName() + "supplyAsync=>Integer");
int i = 10/0;
return 666;
});
// t为正常的返回结果,如果异常则在u中返回
int integer = completableFuture2.whenComplete((t, u) -> {
// 正常的返回结果
System.out.println("t==>" + t);
// java.util.concurrent.CompletionException
System.out.println("u==>" + u);
}).exceptionally(e -> {
// 这里面可以对异常再次进行处理并且返回
System.out.println(e.getMessage());
return 111;
}).get();
System.out.println(integer);