文章目录
一.简介
JUC(java.util .concurrent):用于处理线程的Java并发工具包,里边提供了各种各样的控制同步和线程通信的工具类,JDK 1.5以上支持。
包结构如下:
二.常用工具类
1.ReentrantLock
1.1 介绍
ReentrantLock是一个互斥锁,也是一个可重入锁。ReentrantLock锁在同一个时间点只能被一个线程锁持有,但是它可以被同一个线程多次获取,每获取一次AQS的state就加1,每释放一次state就减1。主要解决的问题是避免线程死锁。如果一个锁不可重入,一个已经获得同步锁X的线程,在释放锁X之前再去竞争锁X的时候,相当于会出现自己要等待自己释放锁。
1.2 ReentrantLock 和 Synchronized的对比
ReentrantLock通过AQS实现,Synchronized通过监视器模式;
ReentrantLock通过使用lock、unlock加锁解锁,更灵活,而且支持tryLock、lockInterruptibly等方法;
ReentrantLock有公平模式和非公平模式,Synchronized是非公平锁;
ReentrantLock可以关联多个条件队列,Synchronized只能关联一个条件队列。
1.3 简单使用
package com.example.springb_web.juc;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockTest {
//默认是非公平锁,这里可以指定为公平锁
//公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,
// 永远都是队列的第一位才能得到锁
Lock lock_fair = new ReentrantLock(true);
public static void main(String[] args) {
Thread t1 = new Thread(()->{
Lock lock = new ReentrantLock();
//加锁,相当于synchronized(this),这里必须要紧跟在try代码
lock.lock();
try {
System.out.println("t1执行开始");
//每隔0.5秒执行一个任务
for(int i=0;i<5;i++){
Thread.sleep(500);
System.out.println("t1执行任务"+i);
}
System.out.println("t1执行结束");
} catch (Exception e) {
e.printStackTrace();
} finally{
//lock必须手动释放锁,且必须放在finally第一行
lock.unlock();
}
});
t1.start();
Thread t2 = new Thread(()->{
Lock lock = new ReentrantLock();
boolean locked = false;
try {
Instant start = Instant.now();
//tryLock:在指定时间内获取锁,如果获取不到,线程可以继续执行
locked = lock.tryLock(5, TimeUnit.SECONDS);
Instant end = Instant.now();
long timeElapsed = Duration.between(start, end).toMillis();
if(locked){
System.out.println("t2获取锁,耗时"+timeElapsed);
}else{
System.out.println("t2没获取锁,耗时"+timeElapsed);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
//lock必须手动释放锁
if(locked){
lock.unlock();
}
}
});
t2.start();
Thread t3 = new Thread(()->{
Lock lock = new ReentrantLock();
try {
//在一个线程等待锁的过程中,可以被打断
lock.lockInterruptibly();
System.out.println("t3执行开始");
//每隔0.5秒执行一个任务
for(int i=0;i<5;i++){
Thread.sleep(500);
System.out.println("t3执行任务"+i);
}
System.out.println("t3执行结束");
} catch (Exception e) {
System.out.println("t3被打断了");
} finally{
//lock必须手动释放锁
lock.unlock();
}
});
t3.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//这里让线程3秒后被打断,只有t3能成功打断,t1打断会报错
//t1.interrupt();
t3.interrupt();
}
}
代码正确运行后结果如下:
2.CountDownLatch
2.1 介绍
CountDownLatch:一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。CountDownLatch最重要的方法是countDown()和await(),前者作用是是计数器数据减一,后者在当前计数到达零之前,await 方法会一直阻塞。
应用场景: 有一个任务想要往下执行, 但必须要等到其他的任务执行完毕后才可以继续往下执行。
2.2 实现原理
①CountDownLatch是一个同步计数器,他允许一个或者多个线程在另外一组线程执行完成之前一直等待,基于AQS共享模式实现的
②是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作来。
2.3 CountDownLatch与join
CountDownLatch可以手动控制在n个线程里调用n次countDown()方法使计数器进行减一操作,也可以在一个线程里调用n次执行减一操作。
而 join() 的实现原理是不停检查join线程是否存活,如果 join 线程存活则让当前线程永远等待。所以两者之间相对来说还是CountDownLatch使用起来较为灵活。
2.4 CountDownLatch和CyclicBarrier
(1)CountdownLatch 不能重复使用,CyclicBarrier 可以;
(2)CountdownLatch 是主线程等待多个工作线程结束,CyclicBarrier是多个线程之间互相等待,直到所有线程达到一个障碍点(Barrier point);
(3)CountDownLatch 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束。CyclicBarrier 参与的线程职责是一样的CyclicBarrier其中一个线程由于中断,错误,或超时导致永久离开屏障点,其他线程也将抛出异常(前者await方法写在线程外面,后者写在线程里面)
2.5 简单使用
package com.example.springb_web.juc;
import java.util.concurrent.CountDownLatch;
public class CountDownLatchTest {
public static volatile int job_count;
public static void main(String[] args) {
new CountDownLatchTest().test();
}
//假设有100个任务,有五个线程同时运行,每个线程最多可执行30次任务,并且在任务执行完成后需要第一时间
//得到通知,以便执行后续的任务,此时可以使用countDownLatch
public void test(){
Thread[] threads = new Thread[5];
job_count = 100;
CountDownLatch latch = new CountDownLatch(job_count);
for(int i = 0;i<threads.length;i++){
int order = i;
threads[i] = new Thread(()->{
for(int j=0;j<30;j++){
synchronized (this) {
if (job_count > 0) {
System.out.println("线程" + order + "执行了第" + j + "个任务");
}
job_count --;
latch.countDown();
}
}
});
}
for(int i = 0;i<threads.length;i++){
threads[i].start();
}
try {
//计数器归0前会一直阻塞,后面的程序不会走
latch.await();
System.out.println("任务执行完毕");
} catch (Exception e) {
e.printStackTrace();
}
}
}
3.CyclicBarrier
3.1 介绍
CyclicBarrier:循环屏障,可以给离散的任务添加逻辑层次。
各个线程会互相等待,直到所有线程都完成了,再一起突破屏障。如上课时,只有所有学生都到了,老师才会上课。
3.2 简单使用
package com.example.springb_web.juc;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierTest {
public static void main(String[] args) {
new CyclicBarrierTest().test();
}
//假设有2个班级,每个班级学生数量为5个,每个班级为一个单位,一个班级交卷了才会执行给出提示。
// 如果使用countDownLatch的话,需要写两遍代码,CyclicBarrier就可以执行两次
public void test(){
Thread[] threads = new Thread[5];
//第一个参数代表执行的任务数,第二个参数
CyclicBarrier cyclicBarrier = new CyclicBarrier(threads.length,()->System.out.println("这个班级已经全部交卷"));
//将屏障重置为初始状态。
cyclicBarrier.reset();
for(int i = 0;i<threads.length*2;i++){
int order = i;
new Thread(()->{
//这里必须加锁
synchronized (this) {
System.out.println("学生" + order + "已经交卷");
//查询这个障碍是否处于破碎状态。
boolean broken = cyclicBarrier.isBroken();
if (!broken) {
//返回目前正在等待障碍的各方的数量。
int done = cyclicBarrier.getNumberWaiting();
//返回突破障碍所需的数量
int wait = cyclicBarrier.getParties();
System.out.println( "共有"+ wait + "个学生,已交卷" + done + "份");
}
}
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
4.Semaphore
(1)介绍:Semaphore 通常我们叫它信号量, 可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。
(2)使用场景:主要用于那些资源有明确访问数量限制的场景,常用于限流。
如:数据库连接池,同时连接的线程有数量限制,当连接达到了限制数量后,后来的线程只能排队等前面的线程释放了数据库连接才能获得连接。
(3)实现原理:
①semaphore类的核心是Sync内部类,它继承了AQS类,适当重写了一些方法,其他的方法都调用的这个Sync中的方法,包括Sync类的两个子类:FairSync(公平锁)和NonFairSync(非公平锁)。默认使用的是非公平锁。
②semaphore的信号量机制使用的是AQS类的state属性,利用CAS自旋来保证对state属性的操作是原子性的,默认每次获取或释放信号量都是1,除非你指定要使用的信号量或释放的信号量数。
(4)使用示例
①常用API:
public void acquire()
:表示一个线程获取1个许可,那么线程许可数量相应减少一个
public void release()
:表示释放1个许可,那么线程许可数量相应会增加
void acquire(int permits)
:表示一个线程获取n个许可,这个数量由参数permits决定
void release(int permits)
:表示一个线程释放n个许可,这个数量由参数permits决定
int availablePermits()
:返回当前信号量线程许可数量
int getQueueLength()
: 返回等待获取许可的线程数的预估值
②示例:
package com.example.user.utils;
import java.util.concurrent.Semaphore;
public class SemaphoreTest {
//模拟线程池连接数量控制
public void dataBaseConnectionControl() {
int maxConnectionNum = 8;
int threadNum = 30;
Semaphore semaphore = new Semaphore(maxConnectionNum);
for(int i=0;i<threadNum;i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println("成功获取到数据库连接,当前可用的连接数共"+semaphore.availablePermits()
+"个,当前等待的线程共"+semaphore.getQueueLength()+"个。");
Thread.sleep(1000);
semaphore.release();
System.out.println("已有一个连接使用完毕!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
public static void main(String[] args) {
SemaphoreTest demo = new SemaphoreTest();
demo.dataBaseConnectionControl();
}
}
5.Exchanger
(1)介绍:Exchanger类的功能是使两个线程之间进行数据交换。Exchanger类中的exchange()方法具有阻塞的功能,也就是此方法在被调用后等待其他线程来获取数据,如果没有其他线程取得数据,在一直阻塞等待。
它比生产者、消费者模式使用的wait/notify要更加方便。
(2)使用场景:基因算法、数据校对、挂牌交易等场景。
如:基因算法(高中生物学过),很多性状都有基因序列,根据不同的基因序列可以计算出孩子的各种性状可能性(如AA/Aa与aa结合是Aa或者aa))。
(3)实现原理:
两个线程通过exchange方法交换数据, 如果第一个线程先执行exchange方法,它会一直等待第二个线程也执行exchange,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。
(4)使用示例
①常用API:
String exchange(V x)
:用于交换,启动交换并等待另一个线程调用exchange;
String exchange(V x,long timeout,TimeUnit unit)
:用于交换,启动交换并等待另一个线程调用exchange,并且设置最大等待时间,当等待时间超过timeout便停止等待
②示例:
package com.example.user.utils;
import java.util.concurrent.Exchanger;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExchangerTest {
private static void lplTrade(String data1, Exchanger exchanger) {
try {
System.out.println(Thread.currentThread().getName() + "在交易截止之前把 " + data1 + " 交易出去");
Thread.sleep((long) (Math.random() * 1000));
String data2 = (String) exchanger.exchange(data1);
System.out.println(Thread.currentThread().getName() + "交易得到" + data2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
final Exchanger exchanger = new Exchanger();
//注意这里的交易成员的数量是双数,如果是单数,最后会有一个线程得不到交换,一直在阻塞。
executor.execute(new Runnable() {
String data1 = "the shy";
@Override
public void run() {
lplTrade(data1, exchanger);
}
});
executor.execute(new Runnable() {
String data2 = "369";
@Override
public void run() {
lplTrade(data2, exchanger);
}
});
executor.execute(new Runnable() {
String data3 = "bin";
@Override
public void run() {
lplTrade(data3, exchanger);
}
});
executor.execute(new Runnable() {
String data4 = "ale";
@Override
public void run() {
lplTrade(data4, exchanger);
}
});
executor.execute(new Runnable() {
String data5 = "yskm";
@Override
public void run() {
lplTrade(data5, exchanger);
}
});
executor.execute(new Runnable() {
String data5 = "zeus";
@Override
public void run() {
lplTrade(data5, exchanger);
}
});
executor.shutdown();
}
}
6.phaser
(1)介绍:Phaser是JDK 7新增的一个同步辅助类“阶段器”,用来解决控制多个线程分阶段共同完成任务的情景问题。其作用相比CountDownLatch和CyclicBarrier更加灵活。
(2)使用场景:同CoutDownLatch和CyclicBarrier,而且它支持对任务的动态调整,并支持分层结构来达到更高的吞吐量。
如:5个学生一起参加考试,一共有三道题,要求所有学生到齐才能开始考试,全部同学都做完第一题,学生才能继续做第二题,全部学生做完了第二题,才能做第三题,所有学生都做完的第三题,考试才结束。
分析这个题目:这是一个多线程(5个学生)分阶段问题(考试考试、第一题做完、第二题做完、第三题做完),所以很适合用Phaser解决这个问题。。
(3)实现原理:
参考链接
(4)使用示例
①API:
int register()
:增加一个数,返回当前阶段号
int arriveAndAwaitAdvance()
:到达后等待其他任务到达,返回到达阶段号
protected boolean onAdvance(int Phase , int registeredParties)
:类似于CyclicBarrier的触发命令,通过重写该方法来增加阶段到达动作
boolean isTerMinated()
:判断是否结束
②示例:
package com.example.user.utils;
import java.util.concurrent.Phaser;
import java.util.concurrent.TimeUnit;
public class PhaserTest extends Phaser {
/**
* 题目:20个学生参加考试,一共有两道题,要求所有学生到齐才能开始考试,全部做完第一题,才能继续做第二题。
*
* Phaser有phase和party两个重要状态,
* phase表示阶段,party表示每个阶段的线程个数,
* 只有每个线程都执行了phaser.arriveAndAwaitAdvance();
* 才会进入下一个阶段,否则阻塞等待。
* 例如题目中20个学生(线程)都调用phaser.arriveAndAwaitAdvance();就进入下一步
*/
public static void main(String[] args) {
MyPhaser phaser = new MyPhaser();
StudentTask[] studentTask = new StudentTask[20];
//注册
for (int i = 0; i < studentTask.length; i++) {
studentTask[i] = new StudentTask(phaser);
phaser.register(); //注册一次表示phaser维护的线程个数
}
Thread[] threads = new Thread[studentTask.length];
for (int i = 0; i < studentTask.length; i++) {
threads[i] = new Thread(studentTask[i], "Student "+i);
threads[i].start();
}
//等待所有线程执行结束
for (int i = 0; i < studentTask.length; i++) {
try {
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Phaser has finished:"+phaser.isTerminated());
}
}
class MyPhaser extends Phaser {
@Override
protected boolean onAdvance(int phase, int registeredParties) { //在每个阶段执行完成后回调的方法
switch (phase) {
case 0:
return studentArrived();
case 1:
return finishFirstExercise();
case 2:
return finishExam();
default:
return true;
}
}
private boolean studentArrived(){
System.out.println("学生准备好了,学生人数:"+getRegisteredParties());
return false;
}
private boolean finishFirstExercise(){
System.out.println("第一题所有学生做完");
return false;
}
private boolean finishExam(){
System.out.println("第二题所有学生做完,结束考试");
return true;
}
}
class StudentTask implements Runnable {
private Phaser phaser;
public StudentTask(Phaser phaser) {
this.phaser = phaser;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"到达考试");
phaser.arriveAndAwaitAdvance();
System.out.println(Thread.currentThread().getName()+"做第1题时间...");
doExercise1();
System.out.println(Thread.currentThread().getName()+"做第1题完成...");
phaser.arriveAndAwaitAdvance();
System.out.println(Thread.currentThread().getName()+"做第2题时间...");
doExercise2();
System.out.println(Thread.currentThread().getName()+"做第2题完成...");
phaser.arriveAndAwaitAdvance();
}
private void doExercise1() {
long duration = (long)(Math.random()*10);
try {
TimeUnit.SECONDS.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void doExercise2() {
long duration = (long)(Math.random()*10);
try {
TimeUnit.SECONDS.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
7.ReentrantReadWriteLock
(1)介绍:读写锁,包含两种锁–读锁(持有时可进行读操作)和写锁(持有时可进行写操作)。其中读锁是共享锁,写锁是排他锁。允许并发读,只能独占写。
(2)使用场景:读读并发、读写互斥、写写互斥。如果一个对象并发读的场景大于并发写的场景,那就可以使用 ReentrantReadWriteLock来达到保证线程安全的前提下提高并发效率。如:缓存的更新操作。
PS:高并发的情况下,读写锁性能比排它锁要好一些。如果同时都在读的时候,是不需要锁资源的,只有读和写在同时工作的时候才需要锁资源,如果直接用互斥锁,会导致资源的浪费。
(3)实现原理:
自定义队列同步器实现的,读写状态就是其同步器的同步状态,有公平锁和非公平锁两种实现方式;
同步状态(state,一个整型变量)分成了两部分,高16位表示读,低16位表示写,通过位运算确定读和写各自的状态;
写锁是一个支持重进入的排他锁,读锁是一个支持重进入的共享锁,CAS操作修改state的值;
使用ThreadLocal封装HoldCounter对象,保证每个线程记录自己的重入锁数量;
使用锁降级提高效率。
锁降级(不支持锁升级):是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。
(4)使用示例
①API:
构造方法:
public ReentrantReadWriteLock():默认构造方法,非公平锁
public ReentrantReadWriteLock(boolean fair):true 为公平锁
常用API:
public ReentrantReadWriteLock.ReadLock readLock():返回读锁
public ReentrantReadWriteLock.WriteLock writeLock():返回写锁
public void lock():加锁
public void unlock():解锁
public boolean tryLock():尝试获取锁
②使用示例:
redis使用教程可以参考:链接
JedisServiceImpl.java中我定义了方法getDataByReadWriteLock()
public String getDataByReadWriteLock(String namespace,String key) {
//获取读锁
lock.readLock().lock();
try{
//如果缓存有效, 直接使用data
String data = (String) cacheManager.get(namespace,key);
if(!StringUtils.isEmpty(data)){
return data;
}
}finally {
//释放读锁
lock.readLock().unlock();
}
//获取写锁
lock.writeLock().lock();
try{
//如果缓存无效,更新cache;有效时间120秒
cacheManager.save(namespace,key,"从数据库查的数据",120);
return "从数据库查的数据";
}finally {
//释放写锁
lock.writeLock().unlock();
}
}
8.LockSupport
(1)介绍:用来挂起和唤醒线程的工具类。park()和unpark()方法,分别可以阻塞和唤醒线程(作用类似于wait/notify方法)
(2)与wait/notify,await/signal对比
①wait/notify必须配合Synchronized关键字进行使用,否则会报异常
②ReentrantLock的await()和signal()方法也是一样,必须配置lock()方法进行使用,否则会报监视器异常,同时必须先阻塞才能被唤醒
③LockSupport可以直接进行使用,不需要配合其他关键词或方法;即使先执行"唤醒"方法,再执行阻塞方法,线程依旧可以顺利执行
(3)实现原理
LockSupport引入了许可证的思想,对象最多只能拥有1个许可证,当执行unpark()方法的时候会赋给指定对象一个许可证,执行park()方法的时候判断当前对象是否拥有许可证,没有则进行阻塞,
直到其拥有一个许可证才放行,如果拥有则直接放行。放行后将该对象拥有的许可证置空。
PS:使用LockSupport进行多线程操作时,通常需要配合阻塞队列同步使用,以便存储我们需要唤醒的对象。
9.常用队列的使用
参考:https://blog.csdn.net/tttalk/article/details/121947982?spm=1001.2014.3001.5501