ReentrantLock / CountDownLatch / CyclicBarrier / Semaphore / LockSupport
- java.util.concurrent.locks.ReentrantLock 可重入锁,类似Synchronized(优势是可精确唤醒)
- java.util.concurrent.locks.Condition 线程间协作条件
- java.util.concurrent.CountDownLatch 门栓(主线程控制,等待其他线程)
- java.util.concurrent.CyclicBarrier 循环屏障(非主线程控制,等待其他线程)
- java.util.concurrent.Semaphore 信号量(指定空位,线程竞争)
- java.util.concurrent.locks.LockSupport 不可重入锁,许可锁机制
一、ReentrantLock与Synchronized比较:
相似点:都是加锁阻塞式的同步,ReentranLock(显示锁)和synchronized(隐式锁)都是可重入锁,可重入锁的一个优点是可在一定程度避免死锁 。 隐式锁:(即synchronized关键字使用的锁)默认是可重入锁(同步块、同步方法)
区分:
- 底层实现:Synchronized是原生语法层面的互斥,JVM实现,涉及到锁的升级(无锁、偏向锁、自旋锁等)。而ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层面可重入( 支持一个线程对资源的重复加锁 )的互斥锁,需要lock()/unlock()方法结合try/finally语块来完成,通过利用CAS自旋机制保证线程操作的原子性和volatile保证数据可见性实现锁的功能。
- 公平锁:Synchronized锁非公平锁,多个线程等待同一个锁时要按申请锁的时间顺序获取锁。而ReetrantLock默认的构造函数是创建非公平锁,也可以通过参数创建公平锁,但公平锁表现的性能不是很好。
- 等待中断:Synchronized长期等待(不能中断)可能会出现死锁的情况。而ReentrantLock持有锁长期不释放时,正在等待的线程可以选择放弃等待,通过lock.lockInterruptibly()来实现。
- 绑定条件:Synchronized是维护一个线程队列,当多线程同时被唤醒,只能随机唤醒或全部唤醒,无法进行精确线程的唤醒。而ReentrantLock是可以维护多个线程队列(绑定多个条件),可以对线程精确唤醒。
二、CountDownLatch | CyclicBarrier | Semaphore
JUC三个工具类:CountDownLatch | CyclicBarrier | Semaphore 底层都是AQS来实现的
1、CountDownLatch门栓(主线程等待其他线程countdown进行减操作,直到0时才被唤醒)
CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞;其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞);计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行
/*
1
2
3
4
5
6
main over
* */
public static void main(String[] args) throws Exception{
CountDownLatch countDownLatch=new CountDownLatch(6);
for (int i = 1; i <=6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName());
countDownLatch.countDown();
},Thread.currentThread().getName()).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+"\t"+"over");
}
2、CyclicBarrier(类似CountDownLatch,动作实施者为其他线程)
CyclicBarrier的字面意思是可循环(Cyclic) 使用的屏障(barrier)。 它要做的事情是,让一组线程到达一个屏障(也可以叫做同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await()方法。和CountDownLatch比较相似,都表示完成工作之后进行下一步动作, 对于CountDownLatch,当计数为0的时候,下一步的动作实施者是“主线程“;对于CyclicBarrier,下一步动作实施者是“其他线程”。
/*
1
2
3
4
5
6
main over
* */
public static void main(String[] args) {
// public CyclicBarrier(int parties, Runnable barrierAction) {}
CyclicBarrier cyclicBarrier=new CyclicBarrier(7,()->{
System.out.println(Thread.currentThread().getName()+"/t"+"over");
});
for (int i = 1; i <=7; i++) {
final int temp=i;
new Thread(()->{
System.out.println(Thread.currentThread().getName());
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
3、Semaphore(信号量)
cquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
public static void main(String[] args) {
Semaphore semaphore=new Semaphore(3);
for (int i = 1; i <=6; i++) {
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"\t抢之前空车位数:"+semaphore.availablePermits());
System.out.println(Thread.currentThread().getName()+"\t抢占了车位");
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"\t离开了车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
System.out.println(Thread.currentThread().getName()+"\t离开后空车位数:"+semaphore.availablePermits());
}
},String.valueOf(i)).start();
}
}
三、ReentrantLock使用
下面结合ReentrantLock写个案例:使三个线程按顺序输出ABC循环字母(线程1输出A,线程2输出B,线程3输出C)
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* 循环ABC
*/
public class ReentrantLockTest {
private static ReentrantLock lock = new ReentrantLock();
private static Condition cA = lock.newCondition();
private static Condition cB = lock.newCondition();
private static Condition cC = lock.newCondition();
//门栓
private static CountDownLatch latchB = new CountDownLatch(1);
private static CountDownLatch latchC = new CountDownLatch(1);
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
try{
lock.lock();
for(int i=0;i<10;i++){
System.out.print("A");
cB.signal();
if(i==0) latchB.countDown();
cA.await();
}
cB.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}, "Thread A");
Thread threadB = new Thread(() -> {
try {
latchB.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
try{
lock.lock();
for(int i=0;i<10;i++){
System.out.print("B");
cC.signal();
if(i==0) latchC.countDown();
cB.await();
}
cC.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}, "Thread B");
Thread threadC = new Thread(() -> {
try {
latchC.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
try{
lock.lock();
for(int i=0;i<10;i++){
System.out.print("C");
cA.signal();
cC.await();
}
cA.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}, "Thread C");
threadA.start();
threadB.start();
threadC.start();
}
}
1、必须先唤醒其他线程signal()再将自己转为等待状态await()
cB.signal();
cA.await();
2、基于ReentrantLock,需要在方法前后加入lock(),unlock()
3、每个线程结束后要再唤醒一下其他线程,保证所有线程都执行完才能结束。否则始终处于执行等待状态。
4、三个线程并不一定会按顺序依次执行 threadA.start();/threadB.start();/threadC.start(); 为了保证从一开始就按ABC循环输出,就需要加入ReentrantLock的CountDownLatch(门栓,设置门栓await()后只会执行门栓上面的代码,取消门栓countdown()后才会执行门栓下面的代码)上述代码满足需求只需要第一次设置门栓即可。
四、LockSupport
Synchronized的wait与notify组合的方式看起来是个不错的解决方式,但其面向的主体是对象object,阻塞的是当前线程,而唤醒的是随机的某个线程或所有线程,偏重于线程之间的通信交互。 假如换个角度,面向主体是线程的话,想要轻而易举地对指定的线程进行阻塞唤醒(主体是线程),这个时候就需要LockSupport,它提供的park与unpark方法分别用于阻塞和唤醒。
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语,是JDK中用来实现线程阻塞和唤醒的工具。使用它可以在任何场合使线程阻塞,可以指定任何线程进行唤醒,并且不用担心阻塞和唤醒操作的顺序,但要注意连续多次唤醒的效果和一次唤醒是一样的。JDK并发包下的锁和其他同步工具的底层实现中大量使用了LockSupport进行线程的阻塞和唤醒,掌握它的用法和原理可以让我们更好的理解锁和其它同步工具的底层实现。
public class LockSupportTest {
public static void main(String[] args) {
Thread parkThread = new Thread(new ParkThread());
parkThread.start();
System.out.println("开始线程唤醒");
LockSupport.unpark(parkThread);
System.out.println("结束线程唤醒");
}
static class ParkThread implements Runnable{
@Override
public void run() {
System.out.println("开始线程阻塞");
LockSupport.park();
System.out.println("结束线程阻塞");
}
}
}
LockSupport类为线程阻塞唤醒提供了基础,同时,在竞争条件问题上具有wait和notify无可比拟的优势。使用wait和notify组合时,某一线程在被另一线程notify之前必须要保证此线程已经执行到wait等待点,错过notify则可能永远都在等待,另外notify也不能保证唤醒指定的某线程。反观LockSupport,由于park与unpark引入了许可机制,许可逻辑为:
-
park将许可在等于0的时候阻塞,等于1的时候返回并将许可减为0。
-
unpark尝试唤醒线程,许可加1
class Parker : public os::PlatformParker {
private:
volatile int _counter ;
...
public:
void park(bool isAbsolute, jlong time);
void unpark();
...
}
class PlatformParker : public CHeapObj<mtInternal> {
protected:
pthread_mutex_t _mutex [1] ;
pthread_cond_t _cond [1] ;
...
}
下面同样模仿上面案例,循环输出ABC
import java.util.concurrent.locks.LockSupport;
public class LockSupportTest1 {
private static Thread threadA;
private static Thread threadB;
private static Thread threadC;
public static void main(String[] args) throws InterruptedException {
threadA = new Thread(() -> {
for(int i=0;i<10;i++){
System.out.print("A");
LockSupport.unpark(threadB);
LockSupport.park(threadA);
}
});
threadB = new Thread(() -> {
LockSupport.park(threadB);
for(int i=0;i<10;i++){
System.out.print("B");
LockSupport.unpark(threadC);
LockSupport.park(threadB);
}
});
threadC = new Thread(() -> {
LockSupport.park(threadC);
for(int i=0;i<10;i++){
System.out.print("C");
LockSupport.unpark(threadA);
LockSupport.park(threadC);
}
});
threadA.start();
threadB.start();
threadC.start();
}
}
LockSupport的park与unpark组合真正解耦了线程之间的同步,不再需要另外的对象变量存储状态,并且也不需要考虑同步锁,wait与notify要保证必须有锁才能执行,而且执行notify操作释放锁后还要将当前线程扔进该对象锁的等待队列,LockSupport则完全不用考虑对象、锁、等待队列等问题。