JUC概述
什么是JUC?
juc指的是java api中的java.util .concurrent包并且包括java.util.concurrent.atomic,java.util.concurrent.locks这两个包
进程,线程,并发,并行
进程
进程是计算机中正在运行的程序的实例。每个进程都有自己的内存空间、代码、数据和系统资源,它们可以独立地运行和执行任务。进程是线程的容器。
线程
线程是计算机操作系统进行系统调度的最小单位
并发
并发是在同一时间内处理多个任务(也可以理解为在同一时刻有多个任务进来),并发的任务虽然在同一时刻进来了,但是不会同时执行,会在同一时间间隔去处理,在并发中,多个任务可以交替执行,每个任务都有一定的执行时间
并行
并行是指同时处理多个任务的能力,在并行中,多个任务可以同时执行,每个任务都可以获得完整的处理时间。
并行和并发可以通过多线程或者多进程来实现,多核cpu可以提高并发性能但是不能解决并发现象
线程状态
1,NEW 尚未启动的线程处于此状态
2,RUNABLE 正在运行
3,BLOCK 阻塞等待被监视器锁定的线程处于此状态
4,WAITING 等待另一线程执行的线程处于此状态
5,TIMED_WAITING 线程只等待给定的时间
6,TERMINATED 结束的线程处于此状态
wait,sleep
wait
wait是Object类中的方法,任何对象实例都可以调用 它用于将当前线程处于等待状态,直至另一个线程调用notify()或者notifyAll()方法来唤醒它,wait()方法必须在synchronized块中调用,以确保线程安全。wait()方法的主要作用是实现线程之间的协作和同步。调用wait()方法的线程在进入等待状态后会释放锁。
sleep
sleep()方法是Thread类中的方法,它用于将当前线程暂停指定的时间,以便其他线程有机会执行。sleep()方法不需要在synchronized块中调用,但它不会释放锁,因此其他线程仍然无法访问被锁定的资源。sleep()方法的主要作用是实现线程的时间控制。
用户线程,守护线程
用户线程:自定义线程 用户线程不会随着主线程的结束而结束,用户线程执行完会自己结束
守护线程:会随着主线程的结束而结束
可以通过线程对象调用setDeamon(true)方法来将用户线程设置为守护线程,必须在start()方法前调用
synchronized
synchronized 是 Java 中的关键字,是一种同步锁。
synchronized可以修饰以下几个地方
修饰方法
作用范围是整个方法,修饰方法时synchronized锁的是调用方法的对象
修饰普通代码块
作用范围是整个代码块,修饰代码块的时候锁的是对象
修饰静态方法
synchronized修饰静态方法时锁的时类级别的,即所有该类的对象共享同一个锁。
修饰代码块为静态对象
这时锁的为类模板 即这个类的所有对象共用一个模板
稍后会有一篇文章来讨论这个话题
Lock接口
lock接口的实现类有 ReentrantLock , ReentrantReadWriteLock.ReadLock , ReentrantReadWriteLock.WriteLock lock锁不会自己释放,需要我们手动释放
ReentrantLock是一个可重入锁ReentrantLock锁使用步骤
public class Temp {
ReentrantLock reentrantLock = new ReentrantLock();
public void eat(){
reentrantLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"吃吃吃");
}finally {//由于lock锁要手动释放锁,如果在线程执行过程中出现意外
// finally中的释放锁操作仍可以继续执行避免造成死锁
reentrantLock.unlock();
}
}
}
lock锁和synchronized的区别
1,synchronized是java关键字,lock是一个接口
2,synchronized在发生异常的时候会释放线程锁占有的锁,持有lock锁的线程在发生异常时不会释放,需要我们在finally语句中手动释放
3,lock可以让等待获得锁的线程中断等待,synchronized不会,等待synchronized锁的线程会一直等待下去直到后的锁
4,资源竞争激烈的情况下lock锁的性能远大于synchronized
线程间通信
Object类中提供了三个方法分别是 wait(),notify(),notifyAll()
wait方法使线程处于等待状态
notify方法随机唤醒等待获取所得线程
notifyAll方法唤醒全部等待锁的线程然后再去竞争锁
如果线程是否进入等待状态需要条件判断那么必须使用while()循环来进行判断,使用if会出现虚假唤醒的情况
线程间通信实体类构成
1,判断线程是否需要等待( while判断 )
2,执行任务
3,任务执行完毕唤醒其他线程
synchronized与lock的实现代码如下
public class Temp {
int num=0;
public synchronized void increase() throws InterruptedException {
while (num==1){
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName()+":"+num);
this.notifyAll();
}
public synchronized void decrease() throws InterruptedException {
while(num==0){
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName()+":"+num);
this.notifyAll();
}
}
添加synchronized锁是通过wait()与notifyAll()来实现线程间通信的,这里并不推荐使用notify()来实现,因为notify是随机唤醒一个线程,如果随机唤醒的线程不满足条件会一直处于阻塞状态,这样就会造成死锁
public class Temp {
int num=0;
Lock lock=new ReentrantLock();
Condition condition=lock.newCondition();
public void increase() throws InterruptedException {
lock.lock();
try {
while (num==1){
condition.await();
}
num++;
System.out.println(Thread.currentThread().getName()+":"+num);
condition.signalAll();
} finally {
lock.unlock();
}
}
public void decrease() throws InterruptedException {
lock.lock();
try {
while (num==0){
condition.await();
}
num--;
System.out.println(Thread.currentThread().getName()+":"+num);
condition.signalAll();
} finally {
lock.unlock();
}
}
}
lock锁是通过Condition接口的的await()与signalAll()方法实现的,这里也并不推荐通过signal()方法实现
线程间定制化通信
在上述的线程间通信时我们唤醒的线程都是随机的,如果我们多添加几个Condition接口的对象用来阻塞和唤醒线程那么我们就能让多线程按照我们的规划执行
public class Temp {
int flag=1;
Lock lock=new ReentrantLock();
Condition conditionA=lock.newCondition();
Condition conditionB=lock.newCondition();
Condition conditionC=lock.newCondition();
public void eat() throws InterruptedException {
try {
lock.lock();
while (flag!=1){
System.out.println(Thread.currentThread().getName()+"线程抢到了锁被阻塞了");
conditionA.await();
}
System.out.println(Thread.currentThread().getName()+"正在吃吃吃");
flag=2;
conditionB.signal();
} finally {
lock.unlock();
}
}
public void sleep() throws InterruptedException {
try {
lock.lock();
while (flag!=2){
System.out.println(Thread.currentThread().getName()+"线程抢到了锁被阻塞了");
conditionB.await();
}
System.out.println(Thread.currentThread().getName()+"正在睡睡睡");
flag=3;
conditionC.signal();
} finally {
lock.unlock();
}
}
public void drink() throws InterruptedException {
try {
lock.lock();
while (flag!=3){
System.out.println(Thread.currentThread().getName()+"线程抢到了锁被阻塞了");
conditionC.await();
}
System.out.println(Thread.currentThread().getName()+"正在喝喝喝");
flag=1;
conditionA.signal();
} finally {
lock.unlock();
}
}
}
当我们使用signal或者signallAll来唤醒因为await阻塞的线程时,被唤醒的线程将会重新去竞争锁,如果竞争锁成功那么会在继续执行await之后的代码
集合线程安全
ArrayList集合
多线程操作ArrayList集合进行读写操作时会引发线程安全问题,具体原因是当有两个线程同时访问集合时如果一个进行读操作一个进行写操作那么会引发ConcurrentModificationException异常
解决ArrayList集合的线程不安全问题主要有三种方法
1,使用Vector集合,这个集合中所有方法都添加了synchronized关键字,这也就导致读的性能也大大下降
2,使用Collections中的synchronizedList(List list)方法将结合转为线程安全的集合
3
,使用CopyOnWriteArrayList()集合这个集合通过读写分离来实现线程安全的 在写的时候会将之前数据复制一份然后添加
synchronized锁,写完之后用新的数据覆盖老的数据
HashSet和HashMap集合
HashSet使用CopyOnWriteArraySet()来解决
HashMap使用ConcurrentHashMap<K,V>来解决
公平锁和非公平锁
公平锁和非公平锁是ReentrantLock类中的两种锁定机制。公平锁和非公平锁的区别在于它们在获取锁时的顺序不同。
公平锁会按照线程请求锁的顺序来获取锁,即先到先得。如果一个线程请求锁,但锁已经被其他线程持有,那么该线程将被放入等待队列中,直到锁被释放并且该线程在等待队列中处于第一位时才能获取锁。这种锁定机制可以避免线程饥饿现象的发生,但是会降低系统的吞吐量。
非公平锁则不考虑线程请求锁的顺序,它允许一个线程在释放锁后立即重新获取锁。如果一个线程请求锁,但锁已经被其他线程持有,那么该线程将尝试获取锁,而不是被放入等待队列中。这种锁定机制可以提高系统的吞吐量,但是可能会导致某些线程长时间地等待锁。
ReentrantLock默认是一个非公平锁,可以在创建ReentrantLock时使用含有boolean类型的构造方法来指定公平锁和非公平锁
Lock lock = new ReentrantLock(true); // 创建一个公平锁
Lock lock = new ReentrantLock(false); // 创建一个非公平锁
可重入锁
synchronized和lock锁都是可重入锁,它允许一个线程多次获取同一把锁而不会发生死锁,假如在A代码块中我们添加了一个synchronized锁,A代码块中的B代码块也添加同样的一把锁,那么当线程获得A代码块的锁时也就获取了B代码块的锁
Callable接口
创建线程的方式除了实现Runnable接口,继承Thread类之外还可以通过实现Callable接口重写call方法,与run方法不同的是call方法有返回值
Thred类的构造方法并不支持传入Callable接口的实现类,在Runnable接口中有一个FutureTask实现类,在FutureTask中有一个构造方法可以传入Callable接口的实现类,因此我们使用Callable接口来创建线程时可以通过向Thread构造方法传入FutureTask类的对象来实现,并且这个对象是通过带有Callable接口的构造方法来创建的 具体代码如下
FutureTask<Integer> futureTask=new FutureTask(()->{ //传入的泛型就表示call方法返回值的类型
System.out.println(Thread.currentThread().getName());
return 1;
});
new Thread(futureTask,"a").start();
Integer integer = futureTask.get();
System.out.println(integer);
FutuerTask提供了一系列的方法让我们去操作线程
get()用来获取异步计算任务的返回值如果调用此方法时还没有计算完那么调用此方法的线程将会阻塞
isDone()用来判断计算任务是否完成,完成返回true
JUC辅助类
CountDownLatch
CountDownLatch只提供了一个带有参数的构造方法用来构造一个给定大小的计数器
countDown()用来减少计数器的大小
await()那个线程调用那个线程就会阻塞被阻塞的线程无法主动唤醒,只有当计算器大小减为0时这个线程才会唤醒
CyclicBarrier
CyclicBarrier(int parties, Runnable barrierAction) 用来设置给定数量的等待线程 当等待的线程达到给定的数量时 将会由最后一个进入等待状态的线程来执行Runnable接口中的run方法
并且别的线程也会结束等待,继续执行任务
Semaphore
Semaphore(int permits
) 创建包含一定信号量的Semaphore
Semaphore(int permits, boolean fair)
创建一个 Semaphore
与给定数量的许可证并且给定公平的设置,和公平锁,非公平锁类似。
信号量通常用于限制线程数,假如我们一次性创建了6个线程那么,如果我们只期望有三个线程可以执行那么我们可以创建一个初始值为3的Semaphore,每次线程在执行前要获取一个信号量(许可证),在线程执行完毕后把信号量(许可证)释放,没有获取信号量(许可证)的线程将会处于阻塞状态,直到别的线程释放信号量,再去竞争
acquire()
获得信号量
release()
释放信号量
读写锁
在java.util.concurrent.locks包中ReadWriteLock接口有一个实现类ReentrantReadWriteLock用于产生读写锁。
读锁也叫共享锁,允许多个线程同时获得读锁,读锁也可以进行写操作但不能保证一致性
写锁也叫排他锁,只允许一个线程获取
如果有线程获取了写锁,那么读锁将会无法获取,想要获取读锁的线程将会阻塞
在多线程环境下读锁和写锁不能共存,也就是说,如果一个线程获取了读锁那么别的线程就不能获取写锁,反之同理。
但是在一个线程中读锁和写锁是可以共存这是锁降级的支撑条件
为什么要有读锁?
加上读锁可以保证在读操作期间,其他线程或进程无法对该资源进行写操作,从而保证数据的一致性。因此,读锁是一种保护共享资源的机制,可以有效避免数据竞争和数据不一致的问题 。
锁降级
当线程获取到写锁的时候别的锁是无法进行读操作的,写锁降级为读锁的时候别的线程就可以进行读操作,但是读锁不能升级为写锁,获取读锁的线程想要进行写操作就必须先释放读锁在获取写锁
writeLock.lock();
System.out.println(Thread.currentThread().getName() + "获取写锁,开始执行");
System.out.println(Thread.currentThread().getName() +"尝试降级写锁为读锁");
readLock.lock();
System.out.println(Thread.currentThread().getName()+ "写锁降级为读锁成功")
writeLock.unlock()
readLock.unlock();
阻塞队列
阻塞队列的本质是队列这种数据结构,与普通队列不同的是阻塞队列可以在队列为空时取数据(因为队列中没有数据发生阻塞,直到有数据放到队列中结束阻塞),在队列满的时候放入数据(因为队列中已经满了无法放入发生阻塞,直到有数据被从队列中取出结束阻塞)
JUC(Java Util Concurrent)包提供了多种阻塞队列的实现,如ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue
线程池
除了继承Thread类,实现Runnable接口,Callable接口这三种创建线程的方法,线程池也是创建线程的一种方法
线程池是一种管理和复用线程的机制,它可以在应用程序中预先创建一组线程,并将任务分配给这些线程来执行,从而避免了频繁创建和销毁线程带来的开销。
Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors, ExecutorService,ThreadPoolExecutor 这几个类
Executors工具类使用Executors类创建线程池可以避免手动创建线程池的繁琐过程,同时也可以提高程序的并发性能。但是需要注意的是,Executors类创建的线程池都是基于ThreadPoolExecutor类实现的
线程池的七个参数
前面我们已经知道Executors创建线程都是基于ThreadPoolExecutor来创建的ThreadPoolExecutor的构造方法有7个参数,分别是
int corePoolSize 线程核心数量也叫做线程池的常驻线程
int maximumPoolSize 线程池所能容纳的最大线程数
long keepAliveTime 临时线程的空闲时间 超过这个空闲时间就会回收临时线程
TimeUnit unit
设置线程池中非常驻线程的空闲时间的单位
BlockingQueue<Runnable> workQueue 常驻线程不能及时处理的线程将进入阻塞队列
ThreadFactory threadFactory
如果当前线程池中务队列已满,就会调用threadFactory来创建新的线程(非常驻线程),或者当前线程池中的线程数小于核心线程数
RejectedExecutionHandler handler 当线程的处理能力已经饱和时 此参数用来拒绝新的任务
线程池的执行过程和拒绝策略
线程池中的线程并不是在我们声明线程池的时候创建的,线程池中的线程是在任务提交时创建的。当任务提交后,线程池会根据当前的线程数和任务队列的状态来决定是否创建新的线程来执行任务。当线程池中的线程数达到核心线程数的时候(并且此时核心线程都在处理任务),那么新来的任务将会进入到阻塞队列中等待,当阻塞对列已满仍然有新来的任务时,此时会创建新的临时线程来处理新来的任务,当阻塞队列已满,并且线程池中的线程已经达到了最大线程数时,如果还有新的任务进来,那么新来的任务不会被处理将会执行拒绝策略。
线程池的4种拒绝策略
1,ThreadPoolExecutor.AbortPolicy
,程序直接抛出RejectedExecutionException
异常。
2,ThreadPoolExecutor.CallerRunsPolicy
,将新来的任务返回给任务的发起者
3,ThreadPoolExecutor.DiscardPolicy
,删除新来的任务。
4,ThreadPoolExecutor.DiscardOldestPolicy
,删除阻塞队列中等待时间最长的任务(队列头部的任务),将新来的任务放到队列中
阻塞队列中的任务即可能由核心线程来执行也有可能由非核心线程来执行
CompletableFuture
CompletableFuture主要用异步回调,在前面我们已经了解了用与异步编程的FutureTask,但是它对于我们获取任务结果很不友好,如果我们调用get()获取结果的时,此时异步线程没有执行完,那么调用get()的线程就会阻塞
CompletableFuture实现了CompletionStage和Future这两个接口CompletionStag可以在任务完成计算后对计算结果进行处理
CompletableFuture是由线程池来创建线程的,在我们不声明线程池的情况下,CompletableFuture会使用默认的线程池来创建线程,但是此时的线程是守护线程,在我们提供线程池的情况下创建的线程为用户线程
在API文档中并不推荐我们使用构造器去创建对象,因此CompletableFuture提供了四个静态方法来创建CompletableFuture对象
runAsync(Runnable runnable, Executor executor)
supplyAsync(Supplier<U> supplier)
supplyAsync(Supplier<U> supplier, Executor executor)
在创建CompletableFuture对象完成后,我们可以使用链式编程来调用whenComplete()方法来获取任务的返回值和在执行任务过程中抛出的异常,异常抛出后会自动触发exceptionally()方法
ExecutorService executorService = Executors.newCachedThreadPool();
try {
CompletableFuture.supplyAsync(()->{
try {TimeUnit.SECONDS.sleep(1L);} catch (InterruptedException e) {e.printStackTrace();}
return "鱼买好了";
},executorService).whenComplete((res,exc)->{
if(exc==null){
System.out.println(res);
}
}).exceptionally((e)->{
System.out.println(e.getMessage());
return null;
});
System.out.println("main线程执行别的任务");
} finally {
executorService.shutdown();
}
CompletableFuture的常用方法
获取结果
get() 调用时如果结果没有计算完毕会造成阻塞
get(long timeout, TimeUnit unit) 调用时如果结果没有计算完毕之后阻塞给定的时间
join() 调用时如果结果没有计算完毕会造成阻塞与get不同的是join不会在编译时报检查异常
getNow(T valueIfAbsent) 调用时如果结果没有计算完毕会将valueIfAbsent作为结果返回
触发计算
boolean complete(T value) 调用时如果结果没有计算完毕将会把value作为计算结果 使用get等方法来获取
对计算结果进行处理
CompletableFuture<Void> thenApply(Consumer<? super T> action) Consumer函数式接口(有返回值)可以将计算结果作为参数传到lambda表达式做进一步处理
<U> CompletableFuture<U> handle(BiFunction<? super T,Throwable,? extends U> fn)与thenAccept不同的是 thenAccept在执行过程抛出异常如果不处理将会终止运行 handle在执行过程中 如果抛出异常即使没有处理也会继续运行
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2,5,1, TimeUnit.SECONDS,
new ArrayBlockingQueue(3), Executors.defaultThreadFactory(),new ThreadPoolExecutor.DiscardOldestPolicy()
);
CompletableFuture completableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println("开始计算");
return 1;
}, threadPoolExecutor).handle((value,exception)->{
int i=1/0;
return value+1;
}).handle((value,exception)->{
return value+1;
}).exceptionally((e) -> {
System.out.println(e.getMessage());
return null;//发生异常时我们用join获取的值为这个返回值
});
threadPoolExecutor.shutdown();
Object join = completableFuture.join();
System.out.println(join);//没发生异常时我们获取的值是计算结果处理完毕后的值
CompletableFuture<Void>
thenRun(Runnable action) A任务执行完开始执行B两个任务之间没有联系 相当于新开了一个线程
CompletableFuture<Void> thenAccept(Consumer<? super T> action) A任务执行完开始执行B B任务需要A的结果但是B任务无返回值
<U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)
A任务执行完开始执行B B任务需要A的结果并且B任务有返回值
计算速度选用
CompletableFuture applyToEither(CompletionStage<? extends T> other, Function<? super T,U> fn) 当有两个异步线程时如果我们要选出计算结果最快的那个可以使用此方法
ExecutorService executorService = Executors.newCachedThreadPool();
long l = System.currentTimeMillis();
CompletableFuture<String> stringCompletableFuture1 = CompletableFuture.supplyAsync(() -> {
try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}
return "任务一";
}, executorService);
CompletableFuture<String> stringCompletableFuture2 = CompletableFuture.supplyAsync(() -> {
try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
return "任务二";
}, executorService);
CompletableFuture<String> stringCompletableFuture = stringCompletableFuture1.applyToEither(stringCompletableFuture2, (w) -> {
return w;
});
System.out.println(stringCompletableFuture.join());
long l1 = System.currentTimeMillis();
System.out.println(l1-l);//运行时间1058
executorService.shutdown();
结果合并
thenCombine(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn) 将两个异步线程的结果合并
线程池选择
在API文档中我们可以看到有些具有相同功能的方法却有不同的方法名列如:thenApply()
thenApplyAsync() 这两个方法的区别主要是 调用thenApply()使用前一个任务的线程池 调用thenApplyAsync() 则会使用thenApplyAsync()这个方法的线程池 如果没声明了则会使用默认的线程池
ExecutorService executorService = Executors.newCachedThreadPool();
long l = System.currentTimeMillis();
CompletableFuture<String> stringCompletableFuture1 = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}
return "任务一";
}, executorService);
CompletableFuture<String> stringCompletableFuture2 = CompletableFuture.supplyAsync(() -> {
try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
return "任务二";
}, executorService);
CompletableFuture<String> stringCompletableFuture3 = stringCompletableFuture1.thenCombine(
stringCompletableFuture2, (o1, o2) -> {
return o1 + o2;
});
System.out.println(stringCompletableFuture3.join());
long l1 = System.currentTimeMillis();
System.out.println(l1-l);//运行时间2051
executorService.shutdown();