原创

图文全解java锁知识体系

一、synchronized关键字的底层原理

1.锁住对象

synchronized (this) {
System.out.println("synchronized 代码块");
}

使用javap -verbose TestClass查看使用的字节码指令为:monitorenterMonitorexit

2.锁住方法

public synchronized void method1() {
System.out.println("synchronized 方法");
}

使用ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

3.JDK1.6之后对synchronized的优化


图为java锁知识体系及synchronized的状态切换流程

Java 早期版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。

4.锁状态切换流程

JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
①偏向锁
引入偏向锁的目的和引入轻量级锁的目的很像,他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。但是不同是:轻量级锁在无竞争的情况下使用 CAS 操作去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉。
但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
② 轻量级锁
倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)。轻量级锁不是为了代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了CAS操作。
轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁!
③ 自旋锁和自适应自旋
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。
一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋。
自旋锁在 JDK1.6 之前其实就已经引入了,不过是默认关闭的,需要通过--XX:+UseSpinning参数来开启。JDK1.6及1.6之后,就改为默认开启的了。需要注意的是:自旋等待不能完全替代阻塞,因为它还是要占用处理器时间。如果锁被占用的时间短,那么效果当然就很好了!反之,相反!自旋等待的时间必须要有限度。如果自旋超过了限定次数任然没有获得锁,就应该挂起线程。自旋次数的默认值是10次,用户可以修改--XX:PreBlockSpin来更改。
另外,在 JDK1.6 中引入了自适应的自旋锁。自适应的自旋锁带来的改进就是:自旋的时间不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定,虚拟机变得越来越“聪明”了。
④ 锁消除
锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。
⑤ 锁粗化
原则上,我们再编写代码的时候,总是推荐将同步块的作用范围限制得尽量小——只在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。

二、JUC原理


 

图为:concurrent包的实现示意图

1.基石

java并发包结构如上图,基石为volatile关键字和CAS技术

1.1 CAS原理:

参考博客:https://www.cnblogs.com/nullllun/p/9039049.html
Java并没有直接实现CAS,CAS 相关的实现是通过C++ 内联汇编的形式实现的。Java 代码需通过 JNI 才能调用。
CAS 的实现离不开处理器的支持,核心代码就是一条带lock 前缀的 cmpxchg 指令,即lock cmpxchg dword ptr [edx], ecx。
1.2 CAS的优缺点
优点:非阻塞的轻量级的乐观锁,通过CPU指令实现,在资源竞争不激烈的情况下性能高,相比synchronized重量锁,synchronized会进行比较复杂的加锁、解锁和唤醒操作。
缺点:
1.ABA问题: 线程C、D;线程D将A修改为B后又修改为A,此时C线程以为A没有改变过,java的原子类AtomicStampedReference,通过控制变量值的版本号来保证CAS的正确性。具体解决思路就是在变量前追加上版本号,每次变量更新的时候把版本号加一,那么A - B - A就会变成1A - 2B - 3A。

2.自旋时间过长,消耗CPU资源,如果资源竞争激烈,多线程自旋长时间消耗资源

1.2 volatile原理

1.java内存模型概念

处理器,高速缓存,主内存间的交互关系


 

线程,主内存,工作内存三者的交互关系


2.java内存模型中定义了以下8种操作来完成主内存与工作内存之间交互的实现细节:

  • luck(锁定):作用于主内存的变量,它把一个变量标示为一条线程独占的状态。
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到工作内存中,以便随后的load动作使用。
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,它把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,它把工作内存中的一个变量的值传递到主内存中,以便随后的write操作使用。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量值放入主内存的变量中。

3.Java内存模型还规定了执行上述8种基本操作时必须满足如下规则:

  • 不允许readloadstorewrite操作之一单独出现,以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,readload之间、storewrite之间是可插入其他指令的。
  • 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
  • 一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(loadassign)的变量,换句话说就是对一个变量实施usestore操作之前,必须先执行过了assignload操作。
  • 一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行loadassign操作初始化变量的值。
  • 如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行storewrite操作)。

4.先行发生原则

  • 程序次序规则(PRogram Order Rule):在一个线程中,按照程序代码顺序,书写在前面的代码操作先行发生于后面的操作
  • 管程锁定规则(Monitor Lock Rule):一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。“后面”指的是时间的先后顺序。
  • volatile 变量规则(Volatile Variable Rule):对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。“后面”指的是时间的先后顺序。
  • 线程启动规则(Thread Start Rule):Thread 对象的 start() 方法先行发生于此线程的每一个动作。
  • 线程终止规则(Thread Termination Rule):线程中所有操作先行发生于对此线程的终止检测。
  • 线程中断规则(Thread Interruption Rule):对线程 interrupt() 方法的调用先行发生于代码检测到中断事件的发生。
  • 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数结束)先行发生于它的 finalize() 方法的开始。
  • 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C

5.volatile变量的特点

1、保证可见性,也就是说对volatile变量进行写入操作的时候,其它线程能立即看到变化,而普通变量是做不到的。实现的原理就是volatile变量每次使用前都会去刷新,而每次写入都会实时同步到主内存中。
2.禁止指令重排序优化
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

6.代码验证:

代码如下:

public class VolitaleTest {
private static volatile int i = 0;
public static void main(String[] args) {
i++;
}
}

汇编代码执行脚本

java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*ClassName.methodName ClassFullPath

实际脚本:

java -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*VolitaleTest.main VolitaleTest

反编译期间报错,解决参考:

https://www.jianshu.com/p/125f0ecf385f

 执行结果比较

左边为volatile 修饰的汇编指令,右边为普通变量,左边多了一个lock指令


 图为汇编代码,左边为变量带有volatile关键字的汇编指令,右边为普通变量的汇编指令

7.volatile的内存语义实现

我们都知道,为了性能优化,JMM在不改变正确语义的前提下,会允许编译器处理器对指令序列进行重排序,那如果想阻止重排序要怎么办了?答案是可以添加内存屏障。

JMM内存屏障分为四类见下图

 

图为内存屏障分类表

java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。为了实现volatile的内存语义,JMM会限制特定类型的编译器和处理器重排序,JMM会针对编译器制定volatile重排序规则表:

 

图为volatile重排序规则表

"NO"表示禁止重排序。为了实现volatile内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎是不可能的,为此,JMM采取了保守策略:
在每个volatile写操作的前面插入一个StoreStore屏障;
在每个volatile写操作的后面插入一个StoreLoad屏障;
在每个volatile读操作的后面插入一个LoadLoad屏障;
在每个volatile读操作的后面插入一个LoadStore屏障。
需要注意的是:volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障
StoreStore屏障:禁止上面的普通写和下面的volatile写重排序;
StoreLoad屏障:防止上面的volatile写与下面可能有的volatile读/写重排序
LoadLoad屏障:禁止下面所有的普通读操作和上面的volatile读重排序
LoadStore屏障:禁止下面所有的普通写操作和上面的volatile读重排序
下面以两个示意图进行理解,图片摘自相当好的一本书《java并发编程的艺术》。

下图为保守策略下,volatile写插入内存屏障后生产的指令序列示意图


 

下图为保守策略下,volatile读插入内存屏障后生产的指令序列示意图

 

 

2.JUC关键核心类

JUC包里面,最为核心的类就是AQS和原子包里面的类,他们为java并发lock提供了模板支持,因为AQS是一个抽象类,要使用必须写子类,ReentrantLock就是一个很好的子类

2.1 ReentrantLock概念:

ReentrantLock内部包含了一个AQS对象,也就是AbstractQueuedSynchronizer类型的对象。这个AQS对象就是ReentrantLock可以实现加锁和释放锁的关键性的核心组件。ReentrantLock之所以用Reentrant打头,意思就是他是一个可重入锁。可重入锁的意思,就是你可以对一个ReentrantLock对象多次执行lock()加锁和unlock()释放锁,也就是可以对一个锁加多次,叫做可重入加锁。

2.2 Demo:

private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
};
});
thread.start();
}
}

2.3 ReentrantLock加锁和释放锁的底层原理流程:

流程:

  1. AQS对象内部有一个核心的变量叫做state,是int类型的,代表了加锁的状态。初始状态下,这个state的值是0AQS内部还有一个关键变量,用来记录当前加锁的是哪个线程,初始化状态下,这个变量是null
  2. 线程1跑过来调用ReentrantLocklock()方法尝试进行加锁,这个加锁的过程,直接就是用CAS操作将state值从0变为1,如果之前没人加过锁,那么state的值肯定是0,此时线程1就可以加锁成功。
  3. 其实每次线程1可重入加锁一次,会判断一下当前加锁线程就是自己,那么他自己就可以可重入多次加锁,每次加锁就是把state的值给累加1,别的没啥变化。.接着,如果线程1加锁了之后,线程2跑过来一下看到,哎呀!state的值不是0啊?所以CAS操作将state0变为1的过程会失败,因为state的值当前为1,说明已经有人加锁了!
  4. 接着线程2会看一下,是不是自己之前加的锁啊?当然不是了,“加锁线程”这个变量明确记录了是线程1占用了这个锁,所以线程2此时就是加锁失败。
  5. 接着,线程2会将自己放入AQS中的一个等待队列,因为自己尝试加锁失败了,此时就要将自己放入队列中来等待,等待线程1释放锁之后,自己就可以重新尝试加锁了
  6. 接着,线程1在执行完自己的业务逻辑代码之后,就会释放锁!他释放锁的过程非常的简单,就是将AQS内的state变量的值递减1,如果state值为0,则彻底释放锁,会将“加锁线程”变量也设置为null
  7. 接下来,会从等待队列的队头唤醒线程2重新尝试加锁。线程2现在就重新尝试加锁,这时还是用CAS操作将state0变为1,此时就会成功,成功之后代表加锁成功,就会将state设置为1。此外,还要把“加锁线程”设置为线程2自己,同时线程2自己就从等待队列中出队了。

3.JUC组件

java并发包有很多已经提供的lock,下面列举几个的使用场景和demo演示(Condition、CountDownLatch、CyclicBarrier、Semaphore、ReentrantReadWriteLock

3.1.Condition

Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,阻塞队列实际上是使用了Condition来模拟线程间协作。

Condition是个接口,基本的方法就是await()和signal()方法;
Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()
调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用
Demo:

public class ConditionTest2 {
private static BoundedBuffer bb = new BoundedBuffer();
public static void main(String[] args) {
// 启动10个“写线程”,向BoundedBuffer中不断的写数据(写入0-9);
// 启动10个“读线程”,从BoundedBuffer中不断的读数据。
for (int i = 0; i < 10; i++) {
new PutThread("p" + i, i).start();
new TakeThread("t" + i).start();
}
}

static class PutThread extends Thread {
private int num;

public PutThread(String name, int num) {
super(name);
this.num = num;
}

public void run() {
try {
Thread.sleep(1); // 线程休眠1ms
bb.put(num); // 向BoundedBuffer中写入数据
} catch (InterruptedException e) {
}
}
}

static class TakeThread extends Thread {
public TakeThread(String name) {
super(name);
}

public void run() {
try {
Thread.sleep(10); // 线程休眠1ms
Integer num = (Integer) bb.take(); // 从BoundedBuffer中取出数据
} catch (InterruptedException e) {
}
}
}
}

class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();

final Object[] items = new Object[5];
int putptr, takeptr, count;

public void put(Object x) throws InterruptedException {
lock.lock(); // 获取锁
try {
// 如果“缓冲已满”,则等待;直到“缓冲”不是满的,才将x添加到缓冲中。
while (count == items.length)
notFull.await();
// 将x添加到缓冲中
items[putptr] = x;
// 将“put统计数putptr+1”;如果“缓冲已满”,则设putptr为0。
if (++putptr == items.length)
putptr = 0;
// 将“缓冲”数量+1
++count;
// 唤醒take线程,因为take线程通过notEmpty.await()等待
notEmpty.signal();
// 打印写入的数据
System.out.println(Thread.currentThread().getName() + " put " + (Integer) x);
} finally {
lock.unlock(); // 释放锁
}
}

public Object take() throws InterruptedException {
lock.lock(); // 获取锁
try {
// 如果“缓冲为空”,则等待;直到“缓冲”不为空,才将x从缓冲中取出。
while (count == 0)
notEmpty.await();
// 将x从缓冲中取出
Object x = items[takeptr];
// 将“take统计数takeptr+1”;如果“缓冲为空”,则设takeptr为0。
if (++takeptr == items.length)
takeptr = 0;
// 将“缓冲”数量-1
--count;
// 唤醒put线程,因为put线程通过notFull.await()等待
notFull.signal();
// 打印取出的数据
System.out.println(Thread.currentThread().getName() + " take " + (Integer) x);
return x;
} finally {
lock.unlock(); // 释放锁
}
}
}

例子展示了一个有界缓存的实现,当缓存中无数据时,阻塞take线程,直到put线程通知,类似和可替代Object类的notify()wait();

3.2CountDownLatch

CountDownLatch(倒计时器)是java.util.concurrent包中一个类,CountDownLatch只要提供的机制是多个(具体数量等于初始化CountDownLatchcount的值)线程都达到了预期状态或者完成了预期工作时触发事件,其他线程可以等待这个事件来触发自己后续的工作。等待的线程可以是多个,即CountDownLatch可以唤醒多个等待的线程。到达自己预期状态的线程会调用CountDownLatchcountDown方法,而等待的线程会调用CountDownLatchawait方法。

CoundDownLatch这个类能够使一个线程等待其他线程完成各自的工作后再执行。例如,应用程序的主线程希望在负责启动框架服务的线程已经启动所有的框架服务之后再执行。

CoundDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。

Demo

public class CountDownLatchTest {
private static int LATCH_SIZE = 5;
private static CountDownLatch doneSignal;
public static void main(String[] args) {
try {
doneSignal = new CountDownLatch(LATCH_SIZE);
// 新建5个任务
for (int i = 0; i < LATCH_SIZE; i++){
new InnerThread().start();
}
System.out.println("main await begin.");
// "主线程"等待线程池中5个任务的完成
doneSignal.await();
System.out.println("main await finished.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

static class InnerThread extends Thread {
public void run() {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " sleep 1000ms.");
// 将CountDownLatch的数值减1
doneSignal.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

代码中main线程在doneSignal.await()的时候阻塞了,直到设定的次数doneSignal.countDown()才会通知main线程继续执行

3.3 CyclicBarrier

CyclicBarrier(循环栅栏)CyclicBarrier CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。

CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。

Demo

public class CyclicBarrierTest {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(3, new Runnable() {
public void run() {
System.out.println("CyclicBarrier arrived." );
}
});
for (int i = 0; i < barrier.getParties(); i++) {
new Thread(new MyRunnable(barrier), "队友" + i).start();
}
System.out.println("main function is finished.");
}

private static class MyRunnable implements Runnable {
private CyclicBarrier barrier;

public MyRunnable(CyclicBarrier barrier) {
this.barrier = barrier;
}

@Override
public void run() {
for (int i = 0; i < 3; i++) {
try {
Random rand = new Random();
int randomNum = rand.nextInt((3000 - 1000) + 1) + 1000;// 产生1000到3000之间的随机整数
Thread.sleep(randomNum);
System.out.println(Thread.currentThread().getName() + ", 通过了第" + i + "个障碍物, 使用了 " + ((double) randomNum / 1000) + "s");
this.barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
}

上面代码模拟的是
假设有一家公司要全体员工进行团建活动,活动内容为翻越三个障碍物,每一个人翻越障碍物所用的时间是不一样的。但是公司要求所有人在翻越当前障碍物之后再开始翻越下一个障碍物,也就是所有人翻越第一个障碍物之后,才开始翻越第二个,以此类推。类比地,每一个员工都是一个“其他线程”。当所有人都翻越的所有的障碍物之后,程序才结束。而主线程可能早就结束了,这里我们不用管主线程。
总结:
CountDownLatch和CyclicBarrier都有让多个线程等待同步然后再开始下一步动作的意思,但是CountDownLatch的下一步的动作实施者是主线程,具有不可重复性;而CyclicBarrier的下一步动作实施者还是“其他线程”本身,具有往复多次实施动作的特点。
3.4 Semaphore
Semaphore(信号量)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。

Demo

public class SemaphoreTest1 {
private static final int SEM_MAX = 10;

public static void main(String[] args) {
Semaphore sem = new Semaphore(SEM_MAX);
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(3);
// 在线程池中执行任务
threadPool.execute(new MyThread(sem, 5));
threadPool.execute(new MyThread(sem, 4));
threadPool.execute(new MyThread(sem, 7));
// 关闭池
threadPool.shutdown();
}
}

class MyThread extends Thread {
private volatile Semaphore sem; // 信号量
private int count; // 申请信号量的大小

MyThread(Semaphore sem, int count) {
this.sem = sem;
this.count = count;
}

public void run() {
try {
// 从信号量中获取count个许可
sem.acquire(count);
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " acquire count=" + count);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放给定数目的许可,将其返回到信号量。
sem.release(count);
System.out.println(Thread.currentThread().getName() + " release " + count + "");
}
}
}

上面代码中,信号量sem的许可总数是10个;共3个线程,分别需要获取的信号量许可数是5,4,7。前面两个线程获取到信号量的许可后,sem中剩余的可用的许可数是1;因此,最后一个线程必须等前两个线程释放了它们所持有的信号量许可之后,才能获取到7个信号量许可。该思想有点类似限流算法里面的令牌桶算法

3.5 ReentrantReadWriteLock

ReentrantReadWriteLockLock的另一种实现方式,我们已经知道了ReentrantLock是一个排他锁,同一时间只允许一个线程访问,而ReentrantReadWriteLock允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。相对于排他锁,提高了并发性。在实际应用中,大部分情况下对共享数据(如缓存)的访问都是读操作远多于写操作,这时ReentrantReadWriteLock能够提供比排他锁更好的并发性和吞吐量。

读写锁内部维护了两个锁,一个用于读操作,一个用于写操作。所有 ReadWriteLock实现都必须保证 writeLock操作的内存同步效果也要保持与相关 readLock的联系。也就是说,成功获取读锁的线程会看到写入锁之前版本所做的所有更新。

ReentrantReadWriteLock支持以下功能:

  1. 支持公平和非公平的获取锁的方式;
  2. 支持可重入。读线程在获取了读锁后还可以获取读锁;写线程在获取了写锁之后既可以再次获取写锁又可以获取读锁;
  3. 还允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。但是,从读取锁升级到写入锁是不允许的;
  4. 读取锁和写入锁都支持锁获取期间的中断;
  5. Condition支持。仅写入锁提供了一个 Conditon 实现;读取锁不支持 Conditon readLock().newCondition() 会抛出 UnsupportedOperationException

Demo

public class ReadWriteLockTest {

public static void main(String[] args) {
ReadWriteLock lock = new ReentrantReadWriteLock();
final Lock readLock = lock.readLock();
final Lock writeLock = lock.writeLock();
final Resource resource = new Resource();
final Random random = new Random();
for (int i = 0; i < 20; ++i) {// 写线程
new Thread() {
public void run() {
writeLock.lock();
try {
resource.setValue(resource.getValue() + 1);
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date()) + " - " + Thread.currentThread() + "获取了写锁,修正数据为:" + resource.getValue());
Thread.sleep(random.nextInt(1000));// 随机休眠
} catch (Exception e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
};
}.start();
}
for (int i = 0; i < 20; ++i) {// 读线程
new Thread() {
public void run() {
readLock.lock();
try {
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date()) + " - " + Thread.currentThread() + "获取了读锁,读取的数据为:" + resource.getValue());
Thread.sleep(random.nextInt(800));// 随机休眠
} catch (Exception e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
};
}.start();
}
}

}

// 资源
class Resource {
private int value;
public void setValue(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}




正文到此结束
本文目录