JUC多线程编程
1.多线程编程步骤
第一步 创建资源类,在资源类创建属性和操作方法
第二步 在资源类操作方法
- (1).判断
- (2).干活
- (3).通知
第三步 创建多个线程,调用资源类的操作方法
第四步 防止虚假唤醒问题
2.线程间通信
例子:有两个线程,实现对一个初始值是0的变量(线程间通信),进行有序的+1、-1操作,效果:a 1 b 0 ; a 1 b 0 ; a 1 b 0
代码实现1(synchronized):
Ⅰ.创建资源类,在资源类创建属性和操作方法
//第一步 创建资源类 定义属性和操作方法
class Share {
//初始值
private int number = 0;
//+1的方法
public synchronized void incr() throws InterruptedException {
//第二步 判断 干活 通知
while (number != 0) {
this.wait(); //在哪里睡就在哪里醒,使用循环判断防止虚假唤醒问题
}
number++;
System.out.println(Thread.currentThread().getName() + "::" + number);
//通知其他线程
this.notifyAll();
}
//-1的方法
public synchronized void decr() throws InterruptedException {
//第二步 判断 干活 通知
while (number != 1) {
this.wait(); //在哪里睡就在哪里醒,使用循环判断防止虚假唤醒问题
}
number--;
System.out.println(Thread.currentThread().getName() + "::" + number);
//通知其他线程
this.notifyAll();
}
}
Ⅱ.创建多个线程,调用资源类的操作方法
public class ThreadDemo1 {
public static void main(String[] args) {
//第三步 创建多个线程 调用资源类的操作方法
Share share = new Share();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share.incr(); //+1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "AA").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share.decr(); //-1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "BB").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share.incr(); //+1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "CC").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share.decr(); //-1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "DD").start();
}
}
代码实现2(Lock实现):
//第一步 创建资源类 定义属性和操作方法
class Share {
//初始值
private int number = 0;
//创建Lock
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
//+1
public void incr() throws InterruptedException {
try {
//上锁
lock.lock();
//第二步 判断 干活 通知
while (number != 0) {
condition.await(); //在哪睡在哪醒,使用循环判断防止虚假唤醒
}
number++;
System.out.println(Thread.currentThread().getName() + "::" + number);
condition.signalAll();//通知其他线程
} finally {
//释放锁
lock.unlock();
}
}
//-1
public void decr() throws InterruptedException {
try {
lock.lock();
//第二步 判断 干活 通知
while (number != 1) {
condition.await(); //在哪睡在哪醒,使用循环判断防止虚假唤醒
}
number--;
System.out.println(Thread.currentThread().getName() + "::" + number);
condition.signalAll();//通知其他线程
} finally {
//释放锁
lock.unlock();
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
//第三步 创建多个线程 调用资源类的操作方法
Share share = new Share();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share.incr(); //+1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "AA").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share.decr(); //-1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "BB").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share.incr(); //+1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "CC").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share.decr(); //-1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "DD").start();
}
}
3.线程间定制化通信
例子:启动三个线程,按照如下要求输出10轮:AA打印1次,BB打印2次,CC打印3次
代码实现(Lock):
//第一步 创建资源类 定义属性和操作方法
class ShareResource {
//定义标志位
private int flag = 1; // 1 AA ; 2 BB ; 3 CC
//创建Lock锁
private Lock lock = new ReentrantLock();
//创建三个condition
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
//第二步 创建操作方法
/**
* 打印1次,参数第几轮
*
* @param loop
*/
public void print1(int loop) throws InterruptedException {
//上锁
lock.lock();
try {
// 判断 干活 通知
while (flag != 1) {
c1.await(); //等待
}
for (int i = 0; i < 1; i++) {
System.out.println(Thread.currentThread().getName() + "::" + i + ":轮数:" + loop);
}
flag = 2; //修改标志位
c2.signal(); //通知BB线程
} finally {
//释放锁
lock.unlock();
}
}
/**
* 打印2次,参数第几轮
*
* @param loop
*/
public void print2(int loop) throws InterruptedException {
//上锁
lock.lock();
try {
//判断 干活 通知
while (flag != 2) {
c2.await(); //等待
}
for (int i = 0; i < 2; i++) {
System.out.println(Thread.currentThread().getName() + "::" + i + ":轮数:" + loop);
}
flag = 3; //修改标志位
c3.signal(); //通知CC线程
} finally {
//释放锁
lock.unlock();
}
}
/**
* 打印3次,参数第几轮
*
* @param loop
*/
public void print3(int loop) throws InterruptedException {
//上锁
lock.lock();
try {
//判断 干活 通知
while (flag != 3) {
c3.await(); //等待
}
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + "::" + i + ":轮数:" + loop);
}
flag = 1; //修改标志位
c1.signal(); //通知AA线程
} finally {
//释放锁
lock.unlock();
}
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
//第三步 创建多个线程 调用操作方法
ShareResource shareResource = new ShareResource();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
shareResource.print1(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "AA").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
shareResource.print2(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "BB").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
shareResource.print3(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "CC").start();
}
}
4.谈谈Volatile
Volatile是Java虚拟机提供的轻量级
的同步机制(三大特性)
- 保证可见性
- 不保证原子性
- 禁止指令重排
4.1.JMM内存模型的可见性
4.1.1.JMM是什么
JMM是Java内存模型,也就是Java Memory Model,简称JMM,本身是一种抽象的概念,实际上并不存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式
JMM关于同步的规定:
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值,到自己的工作内存
- 加锁和解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程:
数据传输速率:硬盘 < 内存 < < cache < CPU
上面提到了两个概念:主内存 和 工作内存
- 主内存:就是计算机的内存,也就是经常提到的8G内存,16G内存
- 工作内存:我们实例化 new student,那么 age = 25 也是存储在主内存中;当有三个线程同时访问 student中的age变量时,那么每个线程都会拷贝一份,到各自的工作内存,从而实现了变量的拷贝
即:JMM内存模型的可见性,指的是当主内存区域中的值被某个线程写入更改后,其它线程会马上知晓更改后的值,并重新得到更改后的值。
MESI(缓存一致性协议):
当CPU写数据时,如果发现操作的变量是共享变量,即在其它CPU中也存在该变量的副本,会发出信号通知其它CPU将该内存变量的缓存行设置为无效,因此当其它CPU读取这个变量时,发现自己缓存该变量的缓存行是无效的,那么它就会从内存中重新读取。
那么是如何发现数据是否失效呢?
这里是用到了总线嗅探技术,就是每个处理器通过嗅探在总线上传播的数据来检查自己缓存值是否过期了,当处理器发现自己的缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行修改操作的时候,会重新从内存中把数据读取到处理器缓存中。
总线嗅探技术有哪些缺点?
总线风暴:
由于volatile的MESI缓存一致性协议,需要不断的从主内存嗅探和CAS循环,无效的交互会导致总线带宽达到峰值。因此不要大量使用volatile关键字,至于什么时候使用volatile、什么时候用锁以及syschonized都是需要根据实际场景的。
4.1.2.可见性代码验证
1).对于成员变量没有添加任何修饰时,是无法感知其它线程修改后的值
/**
* 假设是主物理内存
*/
class MyData {
int number = 0;
public void addTo60() {
this.number = 60;
}
}
/**
* 验证volatile的可见性
* 1.假设int number = 0, number变量之前没有添加volatile关键字修饰
*/
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();//资源类
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in");
try {
TimeUnit.SECONDS.sleep(3); //线程睡眠3秒,假设在进行运算
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.addTo60();//修改number的值
System.out.println(Thread.currentThread().getName() + "\t update number value:" + myData.number);
}, "AAA").start();
while (myData.number == 0) {
//main线程就一直在这里等待循环,直到number的值不等于零
}
// 按道理这个值是不可能打印出来的,因为主线程运行的时候,number的值为0,所以一直在循环
// 如果能输出这句话,说明AAA线程在睡眠3秒后,更新的number的值,重新写入到主内存,并被main线程感知到了
System.out.println(Thread.currentThread().getName() + "\t mission is over");
/**
* 最后输出结果:
* AAA come in
* AAA update number value:60
* 最后线程没有停止,并行没有输出 mission is over 这句话,说明没有用volatile修饰的变量,是没有可见性
*/
}
}
输出结果w为:
2).当我们修改MyData类中的成员变量时,并且添加volatile关键字修饰
/**
* 假设是主物理内存
*/
class MyData {
/**
* volatile 修饰的关键字,是为了增加 主线程和线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
*/
volatile int number = 0;
public void addTo60() {
this.number = 60;
}
}
最后输出的结果为:
主线程也执行完毕了,说明volatile修饰的变量,是具备JVM轻量级同步机制的,能够感知其它线程的修改后的值。
4.2.Volatile不保证原子性
原子性:不可分割,完整性,也就是说某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要具体完成,要么同时成功,要么同时失败。
4.2.1.验证volatile不保证原子性
为了测试volatile是否保证原子性,我们创建了20个线程,然后每个线程分别循环1000次,来调用number++的方法
/**
* 假设是主物理内存
*/
class MyData {
/**
* volatile 修饰的关键字,是为了增加 主线程和线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
*/
volatile int number = 0;
public void addTo60() {
this.number = 60;
}
public void addPlusPlus() {
number++;
}
}
/**
* 验证volatile的可见性
* 1.假设int number = 0, number变量之前没有添加volatile关键字修饰
* 2.添加volatile,可以解决可见性问题
* <p>
* 验证volatile不保证原子性
* 1.原子性指的是什么意思?
*/
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();
//创建20个线程,线程里面进行1000次循环
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
myData.addPlusPlus();
}
}, String.valueOf(i)).start();
}
// 需要等待上面20个线程都计算完成后,在用main线程取得最终的结果值
// 这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程
while (Thread.activeCount() > 2) {
Thread.yield();//yield表示不执行
}
// 查看最终的值,假设volatile保证原子性,那么输出的值应该为: 20 * 1000 = 20000
System.out.println(Thread.currentThread().getName() + "\t finally number value:" + myData.number);
}
}
第一次运行结果:
第二次运行结果:
第三次运行结果:
最终结果我们会发现,number输出的值并没有20000,而且是每次运行的结果都不一致的,这说明了volatile修饰的变量不保证原子性
4.2.2.为什么出现数值丢失
各自线程在写入主内存的时候,出现了数据的丢失,而引起的数值缺失的问题
将一个简单的number++操作,转换为字节码文件一探究竟
public class T1 {
volatile int n = 0;
public void add() {
n++;
}
}
转换后的字节码文件:
public class jmm.T1 {
volatile int n;
public jmm.T1();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_0
6: putfield #2 // Field n:I
9: return
public void add();
Code:
0: aload_0
1: dup
2: getfield #2 // Field n:I
5: iconst_1
6: iadd
7: putfield #2 // Field n:I
10: return
}
我们能够发现 n++这条命令,被拆分成了3个指令
- 执行
getfield
从主内存拿到原始n - 执行
iadd
进行加1操作 - 执行
putfileld
把累加后的值写回主内存
假设我们没有加synchronized
那么第一步就可能存在着,三个线程同时通过getfield
命令,拿到主存中的n值,然后三个线程,各自在自己的工作内存中进行加1操作,但他们并发进行iadd
命令的时候,因为只能一个进行写,所以其它操作会被挂起,假设1线程,先进行了写操作,在写完后,volatile的可见性,应该需要告诉其它两个线程,主内存的值已经被修改了,但是因为太快了,其它两个线程,陆续执行iadd
命令,进行写入操作,这就造成了其他线程没有接受到主内存n的改变,从而覆盖了原来的值,出现写丢失,这样也就让最终的结果少于20000
4.2.3.如何解决
在多线程环境下number++
在多线程环境下是非线程安全的,解决的方法有哪些呢
1.在方法上加入synchronized
public synchronized void addPlusPlus() {
number++;
}
运行结果:
发现引入synchronized
关键字后,保证了该方法每次只能够一个线程进行访问和操作,最终输出的结果也就为20000。
上面的方法引入synchronized
,虽然能够保证原子性,但是为了解决number++
,而引入重量级的同步机制,有种杀鸡焉用牛刀
,除了引用synchronized关键字外,还可以使用JUC下面的原子包装类,即刚刚的int类型的number,可以使用AtomicInteger来代替
2.使用JUC原子包装类AtomicInteger
/**
* 创建一个原子Integer包装类,默认为0
*/
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomic() {
//相当于atomicInteger++
atomicInteger.getAndIncrement();
}
然后同理,继续刚刚的操作
// 创建20个线程,线程里面进行1000次循环
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
myData.addPlusPlus();
myData.addAtomic();
}
}, String.valueOf(i)).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();//yield表示不执行
}
System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);
System.out.println(Thread.currentThread().getName() + "\t finally atomicNumber value: " + myData.atomicInteger);
运行结果,一个是引入synchronized,一个是使用原子包装类AtomicInteger
4.3.Volatile禁止指令重排
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令重排,一般分为以下三种:
源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 -> 最终执行指令
单线程环境里面确保最终执行结果和代码顺序的结果一致
处理器在进行重排序时,必须要考虑指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
4.3.1.指令重排 - example 1
public void mySort() {
int x = 11;
int y = 12;
x = x + 5;
y = x * x;
}
按照正常单线程环境,执行顺序是 1 2 3 4,但是在多线程环境下,可能出现以下的顺序:
- 2 1 3 4
- 1 3 2 4
上述的过程就可以当做是指令的重排,即内部执行顺序,和我们的代码顺序不一样
但是指令重排也是有限制的,即不会出现下面的顺序
- 4 3 2 1
因为处理器在进行重排时候,必须考虑到指令之间的数据依赖性
因为步骤 4:需要依赖于 y的申明,以及x的申明,故因为存在数据依赖,无法首先执行
例子:
int a,b,x,y=0
线程1 | 线程2 |
---|---|
x = a; | y = b; |
b = 1; | a = 2; |
x = 0; y = 0 |
因为上面的代码,不存在数据的依赖性,因此编译器可能对数据进行重排
线程1 | 线程2 |
---|---|
b = 1; | a = 2; |
x = a; | y = b; |
x = 2; y = 1; |
这样造成的结果,和最开始的就不一致了,这就是导致重排后,结果和最开始的不一样,因此为了防止这种结果出现,volatile就规定禁止指令重排,为了保证数据的一致性
4.3.2.指令重排 - example 2
比如下面这段代码:
public class ResortSeqDemo {
int a = 0;
boolean flag = false;
public void method01() {
a = 1;
flag = true;
}
public void method02() {
if (flag) {
a = a + 5;
System.out.println("retValue:" + a);
}
}
}
我们按照正常的顺序,分别调用method01() 和 method02() 那么,最终输出就是 a = 6
但是如果在多线程环境下,因为方法1 和 方法2,他们之间不能存在数据依赖的问题,因此原先的顺序可能是
a = 1;
flag = true;
a = a + 5;
System.out.println("reValue:" + a);
但是在经过编译器,指令,或者内存的重排后,可能会出现这样的情况
flag = true;
a = a + 5;
System.out.println("reValue:" + a);
a = 1;
也就是先执行 flag = true后,另外一个线程马上调用方法2,满足 flag的判断,最终让a + 5,结果为5,这样同样出现了数据不一致的问题
为什么会出现这个结果:多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
这样就需要通过volatile来修饰,来保证线程安全性
4.3.3.Volatile针对指令重排做了啥
Volatile实现禁止指令重排优化,从而避免了多线程环境下程序出现乱序执行的现象
首先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:
- 保证特定操作的顺序
- 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)
由于编译器和处理器都能执行指令重排的优化,如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化
。 内存屏障另外一个作用是刷新出各种CPU的缓存数,因此任何CPU上的线程都能读取到这些数据的最新版本。
也就是过在Volatile的写 和 读的时候,加入屏障,防止出现指令重排的
4.3.4.线程安全获得保证
工作内存与主内存同步延迟现象导致的可见性问题
- 可通过synchronized或volatile关键字解决,他们都可以使一个线程修改后的变量立即对其它线程可见
对于指令重排导致的可见性问题和有序性问题
- 可以使用volatile关键字解决,因为volatile关键字的另一个作用就是禁止重排序优化
4.4.Volatile的应用
4.4.1.单例模式DCL代码
首先回顾一下,单线程下的单例模式代码
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo() {
System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
}
public static SingletonDemo getInstance() {
if (instance == null) {
instance = new SingletonDemo();
}
return instance;
}
public static void main(String[] args) {
// 这里的 == 是比较内存地址
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
}
}
最后输出的结果
但是在多线程的环境下,我们的单例模式是否还是同一个对象了
for (int i = 0; i < 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, String.valueOf(i)).start();
}
从下面的结果我们可以看出,我们通过SingletonDemo.getInstance() 获取到的对象,并不是同一个,而是被下面几个线程都进行了创建,那么在多线程环境下,单例模式如何保证呢?
4.4.2.多线程保证单例模式-方案1
引入synchronized关键字
public synchronized static SingletonDemo getInstance() {
if (instance == null) {
instance = new SingletonDemo();
}
return instance;
}
输出结果
我们能够发现,通过引入Synchronized关键字,能够解决高并发环境下的单例模式问题
但是synchronized属于重量级的同步机制,它只允许一个线程同时访问获取实例的方法,但是为了保证数据一致性,而减低了并发性,因此采用的比较少
4.4.3.多线程保证单例模式-方案2
通过引入DCL Double Check Lock
双端检锁机制
就是在进来和出去的时候,进行检测
public static SingletonDemo getInstance() {
if (instance == null) {
//同步代码段的时候,进行检测
synchronized (SingletonDemo.class) {
if (instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
最后输出的结果为
从输出结果来看,确实能够保证单例模式的正确性,但是上面的方法还是存在问题的
DCL(双端检锁)机制不一定是线程安全的,原因是有指令重排的存在,加入volatile可以禁止指令重排
原因是在某一个线程执行到第一次检测的时候,读取到 instance 不为null,instance的引用对象可能没有完成实例化。因为 instance = new SingletonDemo();可以分为以下三步进行完成:
- memory = allocate(); // 1、分配对象内存空间
- instance(memory); // 2、初始化对象
- instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null
但是我们通过上面的三个步骤,能够发现,步骤2 和 步骤3之间不存在 数据依赖关系,而且无论重排前 还是重排后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。
- memory = allocate(); // 1、分配对象内存空间
- instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null,但是对象还没有初始化完成
- instance(memory); // 2、初始化对象
这样就会造成什么问题呢?
也就是当我们执行到重排后的步骤2,试图获取instance的时候,会得到null,因为对象的初始化还没有完成,而是在重排后的步骤3才完成,因此执行单例模式的代码时候,就会重新在创建一个instance实例
指令重排只会保证串行语义的执行一致性(单线程),但并不会关系多线程间的语义一致性
所以当一个线程访问instance不为null时,由于instance实例未必已初始化完成,这就造成了线程安全的问题
所以需要引入volatile,来保证出现指令重排的问题,从而保证单例模式的线程安全性
private static volatile SingletonDemo instance = null;
4.4.4.多线程保证单例模式-volatile
最终代码:
public class SingletonDemo {
private volatile static SingletonDemo instance = null;
private SingletonDemo() {
System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
}
public static SingletonDemo getInstance() {
if (instance == null) {
//a 双重检查加锁多线程情况下会出现某个线程虽然这里已经为空,但是另外一个线程已经执行到d处
synchronized (SingletonDemo.class) { //b
//c 不加volitale关键字的话有可能会出现尚未完全初始化就获取到的情况。原因是内存模型允许无序写入
if (instance == null) {
//d 此时才开始初始化
instance = new SingletonDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
/*// 这里的 == 是比较内存地址
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());*/
for (int i = 0; i < 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, String.valueOf(i)).start();
}
}
}
5.谈谈CAS
5.1.概述
CAS的全称是Compare-And-Swap,它是CPU并发原语
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的
CAS并发原语体现在Java语言中就是sun.misc.Unsafe类的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作,再次强调,由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致的问题,也就是说CAS是线程安全的。
代码使用
首先调用AtomicInteger创建了一个实例, 并初始化为5
然后调用CAS方法,企图更新成2019,这里有两个参数,一个是5,表示期望值,第二个就是我们要更新的值
然后再次使用了一个方法,同样将值改成1024
// 创建一个原子类
AtomicInteger atomicInteger = new AtomicInteger(5);
atomicInteger.compareAndSet(5, 2019);
atomicInteger.compareAndSet(5, 1024);
完整代码如下:
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5, 2014) + "\t current data: " + atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5, 2019) + "\t current data: " + atomicInteger.get());
atomicInteger.getAndIncrement();
}
}
上面代码的执行结果为
这是因为我们执行第一个的时候,期望值和原本值是满足的,因此修改成功,但是第二次后,主内存的值已经修改成了2019,不满足期望值,因此返回了false,本次写入失败
这个就类似于SVN或者Git的版本号,如果没有人更改过,就能够正常提交,否者需要先将代码pull下来,合并代码后,然后提交
5.2.CAS底层原理
首先我们先看看 atomicInteger.getAndIncrement()方法的源码
从这里能够看到,底层又调用了一个unsafe类的getAndAddInt方法
1、unsafe类
Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(Native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定的内存数据。Unsafe类存在sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中的CAS操作的执行依赖于Unsafe类的方法。
注意Unsafe类的所有方法都是native修饰的,也就是说unsafe类中的方法都直接调用操作系统底层资源执行相应的任务
为什么Atomic修饰的包装类,能够保证原子性,依靠的就是底层的unsafe类
2、变量valueOffset
表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
从这里我们能够看到,通过valueOffset,直接通过内存地址,获取到值,然后进行加1的操作
3、变量value用volatile修饰
保证了多线程之间的内存可见性
var5:就是我们从主内存中拷贝到工作内存中的值(每次都要从主内存拿到最新的值到自己的本地内存,然后执行compareAndSwapInt()再和主内存的值进行比较。因为线程不可以直接越过高速缓存,直接操作主内存,所以执行上述方法需要比较一次,在执行加1操作)
那么操作的时候,需要比较工作内存中的值,和主内存中的值进行比较
假设执行 compareAndSwapInt返回false,那么就一直执行 while方法,直到期望的值和真实值一样
- val1:AtomicInteger对象本身
- var2:该对象值的引用地址
- var4:需要变动的数量
- var5:用var1和var2找到的内存中的真实值
- 用该对象当前的值与var5比较
- 如果相同,更新var5 + var4 并返回true
- 如果不同,继续取值然后再比较,直到更新完成
这里没有用synchronized,而用CAS,这样提高了并发性,也能够实现一致性,是因为每个线程进来后,进入的do while循环,然后不断的获取内存中的值,判断是否为最新,然后在进行更新操作。
假设线程A和线程B同时执行getAndAddInt操作(分别跑在不同的CPU上)
- AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的 value 为3,根据JMM模型,线程A和线程B各自持有一份值为3的副本,分别存储在各自的工作内存
- 线程A通过getIntVolatile(var1 , var2) 拿到value值3,这时线程A被挂起(该线程失去CPU执行权)
- 线程B也通过getIntVolatile(var1, var2)方法获取到value值也是3,此时刚好线程B没有被挂起,并执行了compareAndSwapInt方法,比较内存的值也是3,成功修改内存值为4,线程B打完收工,一切OK
- 这是线程A恢复,执行CAS方法,比较发现自己手里的数字3和主内存中的数字4不一致,说明该值已经被其它线程抢先一步修改过了,那么A线程本次修改失败,只能够重新读取后再来一遍了,也就是再执行do while
- 线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。
Unsafe类 + CAS思想: 也就是自旋,自我旋转
5.3.CAS缺点
CAS不加锁保证一致性,但是需要多次比较
- 循环时间长,开销大(因为执行的是do while,如果比较不成功一直在循环,最差的情况,就是某个线程一直取到的值和预期值都不一样,这样就会无限循环)
- 只能保证一个共享变量的原子操作
- 当对一个共享变量执行操作时,我们可以通过循环CAS的方式来保证原子操作
- 但是对于多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候只能用锁来保证原子性
- ABA问题,狸猫换太子
总结
CAS是compareAndSwap,比较当前工作内存中的值和主物理内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存的值一致为止
CAS应用:CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否者什么都不做
6.谈谈原子类的ABA问题
从AtomicInteger引出下面的问题
CAS -> Unsafe -> CAS底层思想 -> ABA -> 原子引用更新 -> 如何规避ABA问题
6.1.ABA问题是什么
狸猫换太子
假设现在有两个线程,分别是T1 和 T2,然后T1执行某个操作的时间为10秒,T2执行某个时间的操作是2秒,最开始AB两个线程,分别从主内存中获取A值,但是因为B的执行速度更快,他先把A的值改成B,然后再修改成A,然后执行完毕,T1线程在10秒后,执行完毕,判断内存中的值为A,并且和自己预期的值一样,它就认为没有人更改了主内存中的值,就快乐的修改成B,但是实际上 可能中间经历了 ABCDEFA 这个变换,也就是中间的值经历了狸猫换太子。
所以ABA问题就是,在进行获取主内存值的时候,该内存值在我们写入主内存的时候,已经被修改了N次,但是最终又改成原来的值了
6.1.1.CAS导致ABA问题
CAS算法实现了一个重要的前提,需要取出内存中某时刻的数据,并在当下时刻比较并替换,那么这个时间差会导致数据的变化。
比如说一个线程one从内存位置V中取出A,这时候另外一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功
尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的
CAS只管开头和结尾,也就是头和尾是一样,那就修改成功,中间的这个过程,可能会被人修改过
6.1.2.原子引用
原子引用其实和原子包装类是差不多的概念,就是将一个java类,用原子引用类进行包装起来,那么这个类就具备了原子性
class User {
String username;
Integer age;
public User(String username, Integer age) {
this.username = username;
this.age = age;
}
public String getUsername() {
return username;
}
public Integer getAge() {
return age;
}
public void setUsername(String username) {
this.username = username;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", age=" + age +
'}';
}
}
public class AtomicReferenceDemo {
public static void main(String[] args) {
User z3 = new User("z3", 22);
User l4 = new User("l4", 25);
// 创建原子引用包装类
AtomicReference<User> atomicReference = new AtomicReference<>();
// 现在主物理内存的共享变量,为z3
atomicReference.set(z3);
// 比较并交换,如果现在主物理内存的值为z3,那么交换成l4
System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
// 比较并交换,现在主物理内存的值是l4了,但是预期为z3,因此交换失败
System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
}
}
6.1.3.基于原子引用的ABA问题
我们首先创建了两个线程,然后T1线程,执行一次ABA的操作,T2线程在一秒后修改主内存的值
public class ABADemo {
//普通的原子引用包装类
public static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
public static void main(String[] args) {
new Thread(() -> {
// 把100 改成 101 然后再改成100,也就是ABA
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
}, "t1").start();
new Thread(() -> {
try {
// 睡眠一秒,保证t1线程,完成了ABA操作
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 把100 改成 101 然后再改成100,也就是ABA
System.out.println(atomicReference.compareAndSet(100, 2021) + "\t" + atomicReference.get());
}, "t2").start();
}
}
我们发现,它能够成功的修改,这就是ABA问题
6.2.解决ABA问题
新增一种机制,也就是修改版本号,类似于时间戳的概念
T1: 100 1 2021 2
T2: 100 1 101 2 100 3
如果T1修改的时候,版本号为2,落后于现在的版本号3,所以要重新获取最新值,这里就提出了一个使用时间戳版本号,来解决ABA问题的思路
6.2.1.AtomicStampedReference
时间戳原子引用,来这里应用于版本号的更新,也就是每次更新的时候,需要比较期望值和当前值,以及期望版本号和当前版本号
public class ABADemo {
//普通的原子引用包装类
public static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
// 传递两个值,一个是初始值,一个是初始版本号
public static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {
System.out.println("============以下是ABA问题的产生==========");
new Thread(() -> {
// 把100 改成 101 然后再改成100,也就是ABA
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
}, "t1").start();
new Thread(() -> {
try {
// 睡眠一秒,保证t1线程,完成了ABA操作
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 把100 改成 101 然后再改成100,也就是ABA
System.out.println(atomicReference.compareAndSet(100, 2021) + "\t" + atomicReference.get());
}, "t2").start();
System.out.println("============以下是ABA问题的解决==========");
new Thread(() -> {
// 获取版本号
System.out.println(Thread.currentThread().getName() + "\t第一次版本号:" + atomicStampedReference.getStamp());
try {
// 暂停t3一秒钟
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 传入4个值,期望值,更新值,期望版本号,更新版本号
atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t第二次版本号:" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t第三次版本号:" + atomicStampedReference.getStamp());
}, "t3").start();
new Thread(() -> {
// 获取版本号
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t第一次版本号:" + stamp);
try {
// 暂停t4 3秒钟,保证t3线程也进行一次ABA问题
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t修改成功否:" +
atomicStampedReference.compareAndSet(100, 2021, stamp, stamp + 1) +
"\t当前实际版本号:" + atomicStampedReference.getStamp() +
"\t当前实际最新值:" + atomicStampedReference.getReference());
}, "t4").start();
}
}
运行结果为:
我们能够发现,线程t3,在进行ABA操作后,版本号变更成了3,而线程t4在进行操作的时候,就出现操作失败了,因为版本号和当初拿到的不一样
7.集合的线程安全
7.1.ArrayList集合线程不安全问题
例子:创建多个线程,向List集合添加内容,并获取内容
演示代码:
public class ThreadDemo4 {
public static void main(String[] args) {
//创建ArrayList集合
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
//向集合添加内容
list.add(UUID.randomUUID().toString().substring(0, 8));
//从集合获取内容
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
结果:抛出并发修改异常java.util.ConcurrentModificationException,原因是从集合中获取内容时,有线程向集合中添加内容或修改内容
7.1.1.解决方案-Vector--JDK1.0比较古老,开发中比较少用
代码实现:
public class ThreadDemo4 {
public static void main(String[] args) {
//创建ArrayList集合
// List<String> list = new ArrayList<>();
//Vector解决
List<String> list = new Vector<>();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
//向集合添加内容
list.add(UUID.randomUUID().toString().substring(0, 8));
//从集合获取内容
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
结果:
7.1.2.解决方案-Collections--开发中比较少用
java.util.Collections类静态方法synchronizedList(List
代码实现:
public class ThreadDemo4 {
public static void main(String[] args) {
//创建ArrayList集合
// List<String> list = new ArrayList<>();
//Vector解决
// List<String> list = new Vector<>();
//Collections解决
List<String> list = Collections.synchronizedList(new ArrayList<>());
for (int i = 0; i < 10; i++) {
new Thread(() -> {
//向集合添加内容
list.add(UUID.randomUUID().toString().substring(0, 8));
//从集合获取内容
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
7.1.3.解决方案-JUC类CopyOnWriteArrayList--推荐
代码实现:
public class ThreadDemo4 {
public static void main(String[] args) {
//创建ArrayList集合
// List<String> list = new ArrayList<>();
//Vector解决
// List<String> list = new Vector<>();
//Collections解决
// List<String> list = Collections.synchronizedList(new ArrayList<>());
//CopyOnWriteArrayList解决
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
//向集合添加内容
list.add(UUID.randomUUID().toString().substring(0, 8));
//从集合获取内容
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
CopyOnWriteArrayList使用写时复制技术
,实现集合并发读-独立写功能
原理:向集合中写数据时支持独立写:复制一份集合写入新的内容,写完后合并或覆盖原集合
源码分析:
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
final ReentrantLock lock = this.lock;
//上锁
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);//复制一份
newElements[len] = e;
setArray(newElements);//覆盖
return true;
} finally {
//释放锁
lock.unlock();
}
}
7.2.HashSet集合线程不安全
代码演示:
public class ThreadDemo4 {
public static void main(String[] args) {
//创建HashSet集合
Set<String> set = new HashSet<>();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
//向集合添加内容
set.add(UUID.randomUUID().toString().substring(0, 8));
//从集合获取内容
System.out.println(set);
}, String.valueOf(i)).start();
}
}
}
结果:抛出并发修改异常java.util.ConcurrentModificationException
7.2.1.解决方案-JUC类CopyOnWriteArraySet
代码演示:
public class ThreadDemo4 {
public static void main(String[] args) {
//创建HashSet集合
// Set<String> set = new HashSet<>();
//CopyOnWriteArraySet解决
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
//向集合添加内容
set.add(UUID.randomUUID().toString().substring(0, 8));
//从集合获取内容
System.out.println(set);
}, String.valueOf(i)).start();
}
}
}
7.3.HashMap集合线程不安全
代码演示:
public class ThreadDemo4 {
public static void main(String[] args) {
//创建HashMap集合
Map<String, String> map = new HashMap<>();
for (int i = 0; i < 10; i++) {
String key = String.valueOf(i);
new Thread(() -> {
//向集合添加内容
map.put(key, UUID.randomUUID().toString().substring(0, 8));
//从集合获取内容
System.out.println(map);
}, String.valueOf(i)).start();
}
}
}
结果:抛出并发修改异常java.util.ConcurrentModificationException
7.3.1.解决方案-JUC类ConcurrentHashMap
代码演示:
public class ThreadDemo4 {
public static void main(String[] args) {
//创建HashMap集合
// Map<String, String> map = new HashMap<>();
//ConcurrentHashMap解决
Map<String, String> map = new ConcurrentHashMap<>();
for (int i = 0; i < 10; i++) {
String key = String.valueOf(i);
new Thread(() -> {
//向集合添加内容
map.put(key, UUID.randomUUID().toString().substring(0, 8));
//从集合获取内容
System.out.println(map);
}, String.valueOf(i)).start();
}
}
}
8.多线程锁
synchronized实现同步的基础:Java中的每一个对象都可以作为锁,具体表现为以下3中形式。
对于普通同步方法,锁匙当前实例对象。
对于静态同步方法,锁是当前类的Class对象。
对于同步方法块,锁是synchronized括号里配置的对象。
8.1.公平锁和非公平锁
非公平锁特点:线程饿死,效率高,不需要排队,谁先抢到就是谁的
公平锁特点:阳光普照,进行操作之前,先检查是否有人排队,如果有人就排队,没有才直接操作,效率相对低
非公平锁源码:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
公平锁源码:
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {//有人排队吗
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或者非公平锁,默认是非公平锁,因为非公平锁的优点在于吞吐量比公平锁大,对于synchronized
而言,也是一种非公平锁
/**
* 创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁
*/
Lock lock = new ReentrantLock(true);
8.2.可重入锁
synchronized和Lock都是可重入锁
区别:
1.sychronized是隐式的可重入锁,而Lock是显式的可重入锁。
2.sychronized加锁和释放锁是自动完成的,而Lock加锁和释放锁需要手动完成。
可重入锁又叫递归锁,进入第一道大门,里面的门可以实现无障碍进入
8.3.自旋锁
自旋锁:spinlock,是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
原来提到的比较并交换,底层使用的就是自旋,自旋就是多次尝试,多次访问,不会阻塞的状态就是自旋。
优缺点
优点:循环比较获取直到成功为止,没有类似于wait的阻塞
缺点:当不断自旋的线程越来越多的时候,会因为执行while循环不断的消耗CPU资源
手写自旋锁
通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒,B随后进来发现当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后抢到
/**
* 手写一个自旋锁
* 循环比较获取直到成功为止,没有类似于wait的阻塞
* <p>
* 通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒,B随后进来发现当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后抢到
*/
public class SpinLockDemo {
// 现在的泛型装的是Thread,原子引用线程
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void myLock() {
// 获取当前进来的线程
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "\t come in");
// 开始自旋,期望值是null,更新值是当前线程,如果是null,则更新为当前线程,否者自旋
while (!atomicReference.compareAndSet(null, thread)) {
}
}
public void myUnLock() {
// 获取当前进来的线程
Thread thread = Thread.currentThread();
// 自己用完了后,把atomicReference变成null
atomicReference.compareAndSet(thread, null);
System.out.println(Thread.currentThread().getName() + "\t invoked myUnLock");
}
public static void main(String[] args) throws InterruptedException {
SpinLockDemo spinLockDemo = new SpinLockDemo();
// 启动t1线程,开始操作
new Thread(() -> {
// 开始占有锁
spinLockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 开始释放锁
spinLockDemo.myUnLock();
}, "t1").start();
// 让main线程暂停1秒,使得t1线程,先执行
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
// 开始占有锁
spinLockDemo.myLock();
// 开始释放锁
spinLockDemo.myUnLock();
}, "t2").start();
}
}
最后输出结果
首先输出的是 t1 come in
然后1秒后,t2线程启动,发现锁被t1占有,所有不断的执行 compareAndSet方法,来进行比较,直到t1释放锁后,也就是5秒后,t2成功获取到锁,然后释放
8.4.读写锁
独占锁(写锁) / 共享锁(读锁) / 互斥锁
独占锁:指该锁一次只能被一个线程所持有。对ReentrantLock和Synchronized而言都是独占锁
共享锁:指该锁可以被多个线程锁持有
对ReentrantReadWriteLock其读锁是共享,其写锁是独占
写的时候只能一个人写,但是读的时候,可以多个人同时读
为什么会有写锁和读锁?
原来我们使用ReentrantLock创建锁的时候,是独占锁,也就是说一次只能一个线程访问,但是有一个读写分离场景,读的时候想同时进行,因此原来独占锁的并发性就没这么好了,因为读锁并不会造成数据不一致的问题,因此可以多个人共享读
读-读:能共存
读-写:不能共存
写-写:不能共存
8.5.死锁
1.什么是死锁
两个或者两个以上进程在执行过程中,因为争夺资源而造成一种互相等待的现象
,如果没有外力干涉,它们无法再执行下去。
2.产生死锁的原因
- 第一 系统资源不足
- 第二 进程进行推进顺序不合适
- 第三 资源分配不当
3.代码演示
//演示死锁
public class DeadLock {
//创建两个对象
static Object a = new Object();
static Object b = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (a) {
System.out.println(Thread.currentThread().getName() + " 持有锁a,试图获取锁b");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b) {
System.out.println(Thread.currentThread().getName() + " 获取锁b");
}
}
}, "A").start();
new Thread(() -> {
synchronized (b) {
System.out.println(Thread.currentThread().getName() + " 持有锁b,试图获取锁a");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a) {
System.out.println(Thread.currentThread().getName() + " 获取锁a");
}
}
}, "B").start();
}
}
死锁结果:
4.验证是否是死锁
(1).jps
类似Linux中ps -ef,能查看到当前正在运行的进程
使用要求:需要配置jdk/bin/jps.exe到环境变量,或者直接在该路径打开运行
(2).jstack
JVM自带的堆栈跟踪工具,使用jstack PID查看堆栈信息
死锁的验证:
9.Callable接口
使用Runable接口创建线程,无法使线程返回结果,而Callable接口可以实现线程返回结果。
创建线程的多种方式:
- 第一种 继承Thread类
- 第二种 实现Runable接口
- 第三种 实现Callable接口
- 第四种 使用线程池
Runable接口与Callable接口的区别:
- (1)是否有返回值,Callable接口有返回值,而Runable接口没有
- (2)是否抛出异常,Callable接口可以抛出异常,而Ruanble接口没有
- (3)实现方法名称不同,一个是run方法,一个是call方法
使用Callable接口:
分析:在Thread构造方法中,有Runable参数,而没有Callable参数来创建线程
解决方案:找到一个类,既和Runable有关系,又和Callable有关系
- Runable接口有实现类FutureTask
- FutureTask(Callable
callable):创建一个 FutureTask,一旦运行就执行给定的 Callable。
- FutureTask(Callable
- FutureTask构造可以传递Callable
创建线程:
//实现Runable接口
class MyThread implements Runnable {
@Override
public void run() {
}
}
public class Demo1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//Runable接口创建线程
new Thread(new MyThread(), "AA").start();
/**
* FutureTask原理 未来任务
* 1、老师上课,口渴了,去买水不合适,讲课线程继续。
* 单开线程找班长帮我买水,把水买回来,需要时候直接get
* 2、4个同学:1同学 1+2+...+5, 2同学 10+11+12+...+50, 3同学 60+61+62, 4同学 100+200
* 第二个同学计算量比较大,FutureTask单开线程给2同学计算,先汇总1、3、4,最后等2同学计算完成,统一汇总
* 3、考试,先做会做的题目,最后看不会做的题目
*/
//FutureTask使用Callable创建线程
FutureTask<Integer> futureTask = new FutureTask<>(() -> {
System.out.println(Thread.currentThread().getName() + " come in callable");
return 1024;
});
//创建一个线程
new Thread(futureTask, "Lucy").start();
while (!futureTask.isDone()) {
System.out.println("wait...");
}
//调用FutureTask的get方法
System.out.println(futureTask.get());
System.out.println(futureTask.get());
System.out.println(Thread.currentThread().getName() + "come over");
}
}
注意:
多个线程执行 一个FutureTask的时候,只会计算一次
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread2());
// 开启两个线程计算futureTask
new Thread(futureTask, "AAA").start();
new Thread(futureTask, "BBB").start();
如果我们要两个线程同时计算任务的话,那么需要这样写,需要定义两个futureTask
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread2());
FutureTask<Integer> futureTask2 = new FutureTask<>(new MyThread2());
// 开启两个线程计算futureTask
new Thread(futureTask, "AAA").start();
new Thread(futureTask2, "BBB").start();
10.JUC强大的辅助类
10.1.减少计数CountDownLatch
CountDownLatch类可以设置一个计数器,然后通过countDown方法来进行减1的操作,使用await方法等待计数器不大于0,然后继续执行await方法之后的语句。
- CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞
- 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞)
- 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行
场景:6个同学陆续离开教室后值班同学才可以锁门
//演示CountDownLatch
public class CountDownLatchDemo {
//6个同学陆续离开教室后,班长锁门
public static void main(String[] args) throws InterruptedException {
//创建CountDownLatch对象,设置初始值
CountDownLatch countDownLatch = new CountDownLatch(6);
//6个同学陆续离开教室
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 号同学离开了教室");
//计数-1
countDownLatch.countDown();
}, String.valueOf(i)).start();
}
//等待
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + " 班长锁门走人了");
}
}
10.2.循环栅栏CyclicBarrier
和CountDownLatch相反,需要集齐七颗龙珠,召唤神龙。也就是做加法,开始是0,加到某个值的时候就执行
CyclicBarrier大概就是循环阻塞的意思,在使用中CyclicBarrier的构造方法第一个参数是目标障碍数,每次执行CyclicBarrier一次障碍数会加一,如果达到了目标障碍数,才会执行cyclicBarrier.await()之后的语句。可以将CyclicBarrier理解为加1操作
场景:集齐7颗龙珠就可以召唤神龙
//集齐7颗龙珠可以召唤神龙
public class CyclicBarrierDemo {
//创建固定值
public static final int NUMBER = 7;
public static void main(String[] args) {
//创建CyclicBarrier
CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER, () -> {
System.out.println("已集齐7颗龙珠,召唤神龙");
});
//集齐7颗龙珠过程
for (int i = 1; i <= NUMBER; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 号龙珠被收集了");
//等待
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}
10.3.信号灯Samaphore
信号灯主要用于两个目的
- 一个是用于共享资源的互斥使用
- 另一个用于并发线程数的控制
一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。
场景:抢占车位,6辆汽车,停3个车位
//6辆汽车,停3个车位
public class SemaphoreDemo {
public static void main(String[] args) {
//创建Semaphore,设置许可数量
Semaphore semaphore = new Semaphore(3);
//模拟6辆汽车
for (int i = 1; i <= 6; i++) {
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 (InterruptedException e) {
e.printStackTrace();
} finally {
//释放
semaphore.release();
}
}, String.valueOf(i)).start();
}
}
}
11.ReentrantReadWriteLock读写锁
读锁:共享锁,会发生死锁
- 场景:1线程修改时,需要等待2线程读完之后;2线程修改时,需要等待1线程读完之后,互相等待对方读锁释放并占有锁,造成死锁
写锁:独占锁,会发生死锁
- 场景:1线程对第1行记录持有写操作时,并试图获取第2行记录的写操作;而2线程对第2行记录持有写操作时,并试图获取第1行记录的写操作,互相等待对方写锁释放并占有锁,造成死锁
11.1.读写锁案例
读写操作问题演示:
//资源类
class MyCache {
//创建map集合
private volatile Map<String, Object> map = new HashMap<>();
//放数据
public void put(String key, Object value) {
System.out.println(Thread.currentThread().getName() + " 正在写操作" + key);
//暂停一会
try {
TimeUnit.MICROSECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
//放数据
map.put(key, value);
System.out.println(Thread.currentThread().getName() + " 写完了" + key);
}
//取数据
public Object get(String key) {
Object result = null;
System.out.println(Thread.currentThread().getName() + " 正在读操作" + key);
//暂停一会
try {
TimeUnit.MICROSECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
//取数据
result = map.get(key);
System.out.println(Thread.currentThread().getName() + " 读完了" + key);
return result;
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
//创建线程放数据
for (int i = 1; i <= 5; i++) {
int num = i;
new Thread(() -> {
myCache.put(num + "", num + "");
}, String.valueOf(i)).start();
}
//创建线程取数据
for (int i = 1; i <= 5; i++) {
int num = i;
new Thread(() -> {
myCache.get(num + "");
}, String.valueOf(i)).start();
}
}
}
结果:数据读写未同步,读取数据在写入之前先发生了
解决方案:ReentrantReadWriteLock加读写锁
- 在资源类中创建读写锁对象:private ReadWriteLock rwLock = new ReentrantReadWriteLock();
- 操作方法中添加读/写锁:rwLock.writeLock().lock();或rwLock.readLock().lock();
- 操作方法中释放读/写锁:rwLock.writeLock().unlock();或rwLock.readLock().unlock();
效果:
11.2.读写锁深入
读写锁:一个资源可以被多个读线程访问,或者可以被一个写线程访问,但是不能同时存在读写线程,读写互斥,读读共享的。
读写锁的演变:
读写锁的降级:将写锁降级为读锁,读锁不能升级为写锁
jdk8说明:获取写锁=>获取读锁=>释放写锁=>释放读锁
写锁降级为读锁演示:
public class Demo1 {
public static void main(String[] args) {
//可重入读写锁对象
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();//读锁
ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();//写锁
//锁降级
//1.获取写锁
writeLock.lock();
System.out.println("1-获取到写锁");
//2.获取读锁
readLock.lock();
System.out.println("2-获取到读锁");
//3.释放写锁
writeLock.unlock();
//4.释放读锁
readLock.unlock();
}
}
结果:
反之,读锁不能升级为写锁:
12.BlockingQueue阻塞队列
阻塞队列,顾名思义,首先它是一个队列,通过一个共享的队列,可以使得数据由队列的一端输入,从另一端输出;
当队列是空的,从队列中获取元素的操作将会被阻塞;
当队列是满的,从队列中添加元素的操作将会被阻塞;
试图从空的队列中获取元素的线程将会被阻塞,直到其它线程往空的队列插入新的元素;
试图向已满的队列中添加新元素的线程将会被阻塞,直到其它线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起。
为什么需要BlockingQueue?
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了。
在java.util.concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。
12.1.常见的BlockingQueue
BlockingQueue阻塞队列是属于一个接口,底下有七个实现类
-
ArrayBlockingQueue(常用)
基于数组的阻塞队列实现,维护了一个定长的数组,以便缓存队列中的数据对象。
总结:由数组结构组成的有界阻塞队列。 -
LinkedBlockingQueu(常用)
基于链表的阻塞队列。
总结:由链表结构组成的有界(但大小默认值为Integer.MAX_VALUE)的阻塞队列。 -
DelayQueue
使用优先级队列实现的延迟无界阻塞队列。 -
PriorityBlockingQueue
支持优先级排序的无界阻塞队列 -
SynchronousQueue(常用)
不存储元素的阻塞队列,也即单个元素的队列。 -
LinkedTransferQueue
由链表组成的无界阻塞队列。 -
LinkedBlockingDeque
由链表组成的双向阻塞队列。
这里需要掌握的是:ArrayBlockQueue、LinkedBlockingQueue、SynchronousQueue
12.2.BlockingQueue核心方法
方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除 | remove() | poll() | take() | poll(time,unit) |
检查 | element() | peek() | 不可用 | 不可用 |
核心方法演示:
public class BlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
//创建阻塞队列
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
//第一组,执行add方法,向已经满的ArrayBlockingQueue中添加元素时候,会抛出异常;同时如果我们多取出元素的时候,也会抛出异常
/*System.out.println(blockingQueue.add("a")); //true
System.out.println(blockingQueue.add("b")); //true
System.out.println(blockingQueue.add("c")); //true
System.out.println(blockingQueue.element()); //a
//System.out.println(blockingQueue.add("w")); //Exception in thread "main" java.lang.IllegalStateException: Queue full
System.out.println(blockingQueue.remove()); //a
System.out.println(blockingQueue.remove()); //b
System.out.println(blockingQueue.remove()); //c
System.out.println(blockingQueue.remove()); //Exception in thread "main" java.util.NoSuchElementException*/
//第二组,使用 offer的方法,添加元素时候,如果阻塞队列满了后,会返回false,否者返回true;同时在取的时候,如果队列已空,那么会返回null
/*System.out.println(blockingQueue.offer("a")); //true
System.out.println(blockingQueue.offer("b")); //true
System.out.println(blockingQueue.offer("c")); //true
System.out.println(blockingQueue.offer("w")); //false
System.out.println(blockingQueue.poll()); //a
System.out.println(blockingQueue.poll()); //b
System.out.println(blockingQueue.poll()); //c
System.out.println(blockingQueue.poll()); //null*/
//第三组,使用 put的方法,添加元素时候,如果阻塞队列满了后,添加消息的线程会一直阻塞,直到队列元素减少或被清空,才会唤醒;同时使用take取消息的时候,如果内容不存在的时候,也会被阻塞
//一般在消息中间件,比如RabbitMQ中会使用到,因为需要保证消息百分百不丢失,因此只有让它阻塞
/*blockingQueue.put("a");
blockingQueue.put("b");
blockingQueue.put("c");
//blockingQueue.put("w"); //队列满了,线程阻塞
System.out.println(blockingQueue.take()); //a
System.out.println(blockingQueue.take()); //b
System.out.println(blockingQueue.take()); //c
//System.out.println(blockingQueue.take()); //队列空了,线程阻塞*/
//第四组,使用offer插入的时候,需要指定时间,如果2秒还没有插入,那么就放弃插入;同时取的时候也进行判断,如果2秒内取不出来,那么就返回null
System.out.println(blockingQueue.offer("a")); //true
System.out.println(blockingQueue.offer("b")); //true
System.out.println(blockingQueue.offer("c")); //true
System.out.println(blockingQueue.offer("w", 2L, TimeUnit.SECONDS)); //false,超时后阻塞结束
System.out.println(blockingQueue.poll()); //a
System.out.println(blockingQueue.poll()); //b
System.out.println(blockingQueue.poll()); //c
System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS)); //null,超时后阻塞结束
}
}
SynchronousQueue
SynchronousQueue没有容量,与其他BlockingQueue不同,SynchronousQueue是一个不存储的BlockingQueue,每一个put操作必须等待一个take操作,否则不能继续添加元素
例子:创建了两个线程,一个线程用于生产,一个线程用于消费
public class SynchronousQueueDemo {
public static void main(String[] args) {
BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
new Thread(() -> {
//生产的线程分别put了 A、B、C这三个字段
try {
System.out.println(Thread.currentThread().getName() + "\t put A");
blockingQueue.put("A");
System.out.println(Thread.currentThread().getName() + "\t put B");
blockingQueue.put("B");
System.out.println(Thread.currentThread().getName() + "\t put C");
blockingQueue.put("C");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1").start();
new Thread(() -> {
//消费线程使用take,取出消费阻塞队列中的内容,并且每次消费前,都等待5秒
try {
TimeUnit.SECONDS.sleep(5);
blockingQueue.take();
System.out.println(Thread.currentThread().getName() + "\t take A");
TimeUnit.SECONDS.sleep(5);
blockingQueue.take();
System.out.println(Thread.currentThread().getName() + "\t take B");
TimeUnit.SECONDS.sleep(5);
blockingQueue.take();
System.out.println(Thread.currentThread().getName() + "\t take C");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2").start();
}
}
最后结果输出为:
从最后的运行结果可以看出,每次t1线程向阻塞队列添加元素后,t1输入线程就会等待 t2消费线程,t2消费后,t2处于挂起状态,等待t1再存入,从而周而复始,形成 一存一取的状态
12.3.阻塞队列的用处
12.3.1.生产者消费者模式-传统版
//第一步 创建资源类 定义属性和操作方法
class Share {
//初始值
private int number = 0;
//创建Lock
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
//+1
public void incr() throws InterruptedException {
try {
//上锁
lock.lock();
//第二步 判断 干活 通知
while (number != 0) {
condition.await(); //在哪睡在哪醒,使用循环判断防止虚假唤醒
}
number++;
System.out.println(Thread.currentThread().getName() + "::" + number);
condition.signalAll();//通知其他线程
} finally {
//释放锁
lock.unlock();
}
}
//-1
public void decr() throws InterruptedException {
try {
lock.lock();
//第二步 判断 干活 通知
while (number != 1) {
condition.await(); //在哪睡在哪醒,使用循环判断防止虚假唤醒
}
number--;
System.out.println(Thread.currentThread().getName() + "::" + number);
condition.signalAll();//通知其他线程
} finally {
//释放锁
lock.unlock();
}
}
}
public class ProdConsumerTraditionDemo {
public static void main(String[] args) {
//第三步 创建多个线程 调用资源类的操作方法
Share share = new Share();
// t1线程,生产
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share.incr(); //+1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1").start();
// t2线程,消费
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share.decr(); //-1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t2").start();
}
}
最后运行成功后,一个进行生产,一个进行消费
12.3.2.生产者消费者模式-阻塞队列版
在concurrent包发布以前,在多线程环境下,我们每个程序员都必须自己去控制这些细节,尤其还要兼顾效率和线程安全,则这会给我们的程序带来不小的时间复杂度
现在我们使用新版的阻塞队列版生产者和消费者,使用:volatile、CAS、atomicInteger、BlockQueue、线程交互、原子引用
class MyResource {
// 默认开启,进行生产消费
// 这里用到了volatile是为了保持数据的可见性,也就是当flag修改时,要马上通知其它线程进行修改
private volatile boolean flag = true;
// 使用原子包装类,而不用number++
private AtomicInteger atomicInteger = new AtomicInteger();
// 这里不能为了满足条件,而实例化一个具体的SynchronousBlockingQueue
BlockingQueue<String> blockingQueue;
// 而应该采用依赖注入里面的,构造注入方法传入
public MyResource(BlockingQueue<String> blockingQueue) {
this.blockingQueue = blockingQueue;
System.out.println(blockingQueue.getClass().getName());
}
public void myProd() throws InterruptedException {
String data;
boolean retValue;
// 多线程环境的判断,一定要使用while进行,防止出现虚假唤醒
// 当flag为true的时候,开始生产
while (flag) {
data = atomicInteger.incrementAndGet() + "";
retValue = blockingQueue.offer(data, 2L, TimeUnit.SECONDS);
if (retValue) {
System.out.println(Thread.currentThread().getName() + "\t 插入队列:" + data + "成功");
} else {
System.out.println(Thread.currentThread().getName() + "\t 插入队列:" + data + "失败");
}
TimeUnit.SECONDS.sleep(1);
}
System.out.println(Thread.currentThread().getName() + "\t 停止生产,表示flag=false,生产结束");
}
public void myConsumer() throws InterruptedException {
String retValue;
// 多线程环境的判断,一定要使用while进行,防止出现虚假唤醒
// 当flag为true的时候,开始消费
while (flag) {
retValue = blockingQueue.poll(2L, TimeUnit.SECONDS);
if (retValue != null && retValue != "") {
System.out.println(Thread.currentThread().getName() + "\t 消费队列:" + retValue + "成功");
} else {
flag = false;
System.out.println(Thread.currentThread().getName() + "\t 消费失败,队列中已为空,退出");
return;
}
TimeUnit.SECONDS.sleep(1);
}
}
public void stop() {
this.flag = false;
}
}
public class ProdConsumerBlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
// 传入具体的实现类, ArrayBlockingQueue
MyResource myResource = new MyResource(new ArrayBlockingQueue<>(10));
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t 生产线程启动");
System.out.println();
System.out.println();
try {
myResource.myProd();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println();
System.out.println();
}, "prod").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t 消费线程启动");
try {
myResource.myConsumer();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "consumer").start();
// 5秒后,停止生产和消费
TimeUnit.SECONDS.sleep(5);
System.out.println();
System.out.println();
System.out.println("5秒钟后,生产和消费线程停止,线程结束");
myResource.stop();
}
}
最后运行结果
13.Synchronized和Lock的区别
早期的时候我们对线程的主要操作为:
- synchronized wait notify
然后后面出现了替代方案
- lock await signal
1.synchronized 和 lock 有什么区别?用新的lock有什么好处?举例说明
1)synchronized属于JVM层面,属于java的关键字
- monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象 只能在同步块或者方法中才能调用 wait/ notify等方法)
- Lock是具体类(java.util.concurrent.locks.Lock)是api层面的锁
2)使用方法:
- synchronized:不需要用户去手动释放锁,当synchronized代码执行后,系统会自动让线程释放对锁的占用
- ReentrantLock:则需要用户去手动释放锁,若没有主动释放锁,就有可能出现死锁的现象,需要lock() 和 unlock() 配置try catch语句来完成
3)等待是否中断
- synchronized:不可中断,除非抛出异常或者正常运行完成
- ReentrantLock:可中断,可以设置超时方法
- 设置超时方法,trylock(long timeout, TimeUnit unit)
- lockInterruptibly() 放代码块中,调用interrupt() 方法可以中断
4)加锁是否公平
- synchronized:非公平锁
- ReentrantLock:默认非公平锁,构造函数可以传递boolean值,true为公平锁,false为非公平锁
5)锁绑定多个条件Condition
- synchronized:没有,要么随机,要么全部唤醒
- ReentrantLock:用来实现分组唤醒需要唤醒的线程,可以精确唤醒,而不是像synchronized那样,要么随机,要么全部唤醒
针对刚刚提到的区别的第5条,场景:3.线程间定制化通信
章节
14.ThreadPool线程池
连接池:预先创建好连接,每次操作从连接池中取到连接,用完之后放回连接池中,供其它操作使用。
作用:减少每次操作都创建和关闭,可以提高连接的复用性。
而线程池与连接池类似的。
1.线程池是什么?
线程池:一种线程使用模式,线程过多会带来调度开销,进而影响缓存局部和整体性能,而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务,避免了在处理短时间任务时创建与销毁线程的代价,线程池不仅能够保证内核的充分利用,还能防止过度调度。
例子:10年前单核CPU电脑,假的多线程,像马戏团小丑玩多个球,CPU需要来回切换,现在是多核电脑,多个线程各自跑在独立的CPU上,不用切换效率高。
2.为什么要用线程池?
线程池的优势:线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等待,等其他线程执行完毕,再从队列中取出任务来执行。
它的主要特点为:线程复用、控制最大并发数、管理线程;线程池中的任务是放入到阻塞队列中的
线程池的好处:
- 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度:当任务到达时,任务可以不需要等待线程创建就能立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
3.架构说明
Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors(代表工具类),ExecutorService,ThreadPoolExecutor这几个类
14.1.Executors三种线程池创建方式
- 一池N线程:Executors.newFixedThreadPool(int),执行长期的任务,性能好很多
- 一池一线程:Executors.newSingleThreadExecutor(),一个任务一个任务执行的场景
- 一池可扩容线程:Executors.newCachedThreadPool(),执行很多短期异步的小程序或者负载较轻的服务器
三种线程创建方式,底层都是使用ThreadPoolExecutor类创建线程。
银行办理业务代码演示:
public class ThreadPoolDemo1 {
public static void main(String[] args) {
// Array Arrays(辅助工具类)
// Collection Collections(辅助工具类)
// Executor Executors(辅助工具类)
//一池5线程
//ExecutorService threadPool1 = Executors.newFixedThreadPool(5);//5个窗口
//一池一线程
//ExecutorService threadPool2 = Executors.newSingleThreadExecutor();//一个窗口
//一池可扩容线程
ExecutorService threadPool3 = Executors.newCachedThreadPool();
try {
//10个顾客
for (int i = 1; i <= 10; i++) {
threadPool3.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//用池化技术,一定要记得关闭
threadPool3.shutdown();
}
}
}
底层实现:
我们通过查看源码,点击了Executors.newSingleThreadExecutor 和 Executors.newFixedThreadPool能够发现底层都是使用了ThreadPoolExecutor
我们可以看到线程池的内部,还使用到了LinkedBlockingQueue 链表阻塞队列
同时在查看Executors.newCacheThreadPool 看到底层用的是 SynchronousBlockingQueue阻塞队列
最后查看一下,完整的三个创建线程的方法
14.2.ThreadPoolExecutor七个参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
- int corePoolSize:核心线程数,线程池中的常驻线程数
- 在创建线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程
- 当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列中
- int maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
- 相当有扩容后的线程数,这个线程池能容纳的最多线程数
- long keepAliveTime:多余的空闲线程存活时间
- 当线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余的空闲线程会被销毁,直到只剩下corePoolSize个线程为止
- 默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用
- TimeUnit unit:keepAliveTime的单位
- BlockingQueue
workQueue:阻塞队列,核心线程数都用完后,会放到阻塞队列中等待,被提交的但未被执行的任务 - LinkedBlockingQueue:链表阻塞队列
- SynchronousBlockingQueue:同步阻塞队列
- ThreadFactory threadFactory:生成线程池中工作线程的线程工厂,用于创建线程的,一般用默认即可
- RejectedExecutionHandler handler:拒绝策略,表示当队列满了并且工作线程大于线程池的最大线程数(maximumPoolSize)时,新来的线程会被拒绝策略处理
- 当营业窗口和阻塞队列中都满了时候,就需要设置拒绝策略
14.3.线程池底层工作流程
说明:
- 在创建了线程池后,等待提交过来的任务请求
- 当调用execute()方法添加一个请求任务时,线程池会做出如下判断
1). 如果正在运行的线程池数量小于corePoolSize,那么马上创建线程运行这个任务
2). 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
3). 如果这时候队列满了,并且正在运行的线程数量还小于maximumPoolSize,那么还是创建非核心线程来运行这个任务
4). 如果队列满了并且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行 - 当一个线程完成任务时,它会从队列中取下一个任务来执行
- 当一个线程无事可做操作一定的时间(keepAliveTime)时,线程池会判断:
1). 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉
2). 所以线程池的所有任务完成后,它会最终收缩到corePoolSize的大小
以顾客去银行办理业务为例,谈谈线程池的底层工作原理
- 最开始假设来了两个顾客,因为corePoolSize为2,因此这两个顾客直接能够去窗口办理
- 后面又来了三个顾客,因为corePool已经被顾客占用了,因此只有去候客区,也就是阻塞队列中等待
- 后面的人又陆陆续续来了,候客区可能不够用了,因此需要申请增加处理请求的窗口,这里的窗口指的是线程池中的线程数,以此来解决线程不够用的问题
- 假设受理窗口已经达到最大数,并且请求数还是不断递增,此时候客区和线程池都已经满了,为了防止大量请求冲垮线程池,已经需要开启拒绝策略
- 临时增加的线程会因为超过了最大存活时间,就会销毁,最后从最大数削减到核心数
JVM内置的四种拒绝策略,都实现了RejectedExecutionHandler接口
- AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行。
- CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。(谁让你来的找谁去,有点不讲理)
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
- DicardPolicy:该策略默默地抛弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种策略。
14.4.自定义线程池(实际开发)
线程池创建的方法有:固定数的,单一的,可变的,那么在实际开发中,应该使用哪个?
我们一个都不用,在生产环境中是使用自己自定义的
实际开发中,Executors三种线程创建方式都不使用,为什么不允许使用Executors的方法手动创建线程池?
阿里巴巴Java开发手册(并发控制这章):
- 【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程
- 使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题,如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题
- 【强制】线程池不允许使用Executors去创建,而是通过
ThreadPoolExecutor
的方式,这样的处理方式让写的同学更加明确线程池的运行规则,避免资源耗尽的风险。- 说明:Executors返回的线程池对象的弊端如下:
- 1).FixedThreadPool和SingleThreadPool:
允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。 - 2).CachedThreadPool和SheduledThreaPool:
允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
- 1).FixedThreadPool和SingleThreadPool:
- 说明:Executors返回的线程池对象的弊端如下:
自定义线程池代码演示:
下面我们创建了一个 核心线程数为2,最大线程数为5,并且阻塞队列数为3的线程池,模拟10个用户办理业务
1.采用AbortPolicy默认拒绝策略
//自定义线程池创建
public class ThreadPoolDemo2 {
public static void main(String[] args) {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2,
5,
2L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
try {
//10个顾客
for (int i = 1; i <= 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//关闭
threadPool.shutdown();
}
}
}
结果:拒绝策略超出线程池处理能力,抛出异常java.util.concurrent.RejectedExecutionException
触发条件是,请求的线程大于 阻塞队列大小 + 最大线程数 = 8 的时候,也就是说第9个线程来获取线程池中的线程时,就会抛出异常从而报错退出。
2.采用CallerRunsPolicy拒绝策略
采用CallerRunsPolicy拒绝策略,也称为回退策略,就是把任务丢回原来的请求开启线程着,我们看运行结果
输出的结果里面出现了main线程,因为线程池出发了拒绝策略,把任务回退到main线程,然后main线程对任务进行处理
3.采用DiscardPolicy拒绝策略
采用DiscardPolicy拒绝策略会,线程池会自动把后面的任务都直接丢弃,也不报异常,当任务无关紧要的时候,可以采用这个方式
4.采用DiscardOldestPolicy拒绝策略
这个策略和刚刚差不多,会把最久的队列中的任务替换掉
14.5.如何合理配置线程池
生产环境中如何配置 corePoolSize 和 maximumPoolSize,这个是根据具体业务来配置的,分为CPU密集型和IO密集型
-
CPU密集型
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行
CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些
CPU密集型任务配置尽可能少的线程数量:
一般公式:CPU核数 + 1个线程数 -
IO密集型
由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如 CPU核数 * 2
IO密集型,即该任务需要大量的IO操作,即大量的阻塞
在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力花费在等待上,所以IO密集型任务中使用多线程可以大大的加速程序的运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
IO密集时,大部分线程都被阻塞,故需要多配置线程数:
参考公式:CPU核数 / (1 - 阻塞系数) 阻塞系数在0.8 ~ 0.9左右
例如:8核CPU:8/ (1 - 0.9) = 80个线程数
15.Fork/Join分支合并框架
Fork:把一个复杂任务进行拆分,大事化小
Join:把分拆任务的结果进行合并
例子:1+2+3+...+100
拆分1:1+2+3+...+20
拆分2:21+22+...+40
拆分3:41+42+...+60
...
拆分n:81+82+...+100
合并:拆分1+拆分2+拆分3+...+拆分n
Fork/Join方法:
计算斐波那契数列的任务:
class Fibonacci extends RecursiveTask<Integer> {
final int n;
Fibonacci(int n) { this.n = n; }
Integer compute() {
if (n <= 1)
return n;
Fibonacci f1 = new Fibonacci(n - 1);
f1.fork();
Fibonacci f2 = new Fibonacci(n - 2);
return f2.compute() + f1.join();
}
}
15.1.案例实现
例子:从1+2+...+100,要求相加两个数,差值不能超过10
class MyTask extends RecursiveTask<Integer> {
//拆分差值不能超过10,计算10以内运算
public static final Integer VALUE = 10;
private int begin; //拆分开始值
private int end; //拆分结束值
private int result; //返回结果
//创建有参构造
public MyTask(int begin, int end) {
this.begin = begin;
this.end = end;
}
//拆分和合并过程
@Override
protected Integer compute() {
//判断相加两个数值是否大于10
if (end - begin <= VALUE) {
//相加操作
for (int i = begin; i <= end; i++) {
result += i;
}
} else {//进一步拆分
//获取中间值
int middle = (begin + end) / 2;
//拆分左边
MyTask myTask01 = new MyTask(begin, middle);
//拆分右边
MyTask myTask02 = new MyTask(middle + 1, end);
//调用方法拆分
myTask01.fork();
myTask02.fork();
//合并结果
result = myTask01.join() + myTask02.join();
}
return result;
}
}
public class ForkJoinDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建MyTask对象
MyTask myTask = new MyTask(1, 100);
//创建分支合并池对象
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(myTask);
//获取最终合并之后结果
Integer result = forkJoinTask.get();
System.out.println(result);
//关闭池对象
forkJoinPool.shutdown();
}
}
16.CompletableFuture异步回调
代码演示:
public class CompletableFutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//异步调用 没有返回值
CompletableFuture<Void> completableFuture1 = CompletableFuture.runAsync(() -> {
System.out.println(Thread.currentThread().getName() + "completableFuture1");
});
completableFuture1.get();
//异步调用 有返回值
CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "completableFuture2");
//模拟异常
//int age = 10 / 0;
return 1024;
});
completableFuture2.whenComplete((t, u) -> {
System.out.println("---t=" + t);
System.out.println("---u=" + u);
}).get();
}
}
16.1.CompletableFuture异步编排
业务场景:
查询商品详情页的逻辑比较复杂,有些数据还需要远程调用,必然需要花费更多的时间。
假如商品详情页的每个查询,需要如上标注的时间才能完成,那么,用户需要 5.5s 后才能看到商品详情页的内容。 很显然是不能接受的。
如果有多个线程同时完成这 6 步操作, 也许只需要 1.5s 即可完成响应。
16.1.1.创建异步对象
- 1、runXxxx 都是没有返回结果的, supplyXxx 都是可以获取返回结果的
- 2、可以传入自定义的线程池, 否则就用默认的线程池;
System.out.println("main......start.....");
//没有返回结果
CompletableFuture.runAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
}, executor);
//有返回结果
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
return i;
}, executor);
System.out.println("main......end....." + future.get());
16.1.2.计算完成时回调方法
- whenComplete 可以处理正常和异常的计算结果, exceptionally 处理异常情况。
- whenComplete 和 whenCompleteAsync 的区别:
- whenComplete: 是执行当前任务的线程执行继续执行 whenComplete 的任务。
- whenCompleteAsync: 是执行把 whenCompleteAsync 这个任务继续提交给线程池来进行执行。
//方法完成后的处理
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 0;
System.out.println("运行结果:" + i);
return i;
}, executor).whenComplete((res, exception) -> {
//虽然能得到异常信息,但是没法修改返回数据
System.out.println("异步任务成功完成了...结果是:" + res + "异常是:" + exception);
}).exceptionally(throwable -> {
//可以感知异常,同时返回默认值
return 10;
});
System.out.println("main......end....." + future.get());
16.1.3.handle 方法
- 和 complete 一样,可对结果做最后的处理(可处理异常),并可改变返回值
//方法完成后的处理
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
return i;
}, executor).handle((res, thr) -> {
if (res != null) {
return res * 2;
}
if (thr != null) {
System.out.println("异步任务成功完成了...结果是:" + res + "异常是:" + thr);
return 0;
}
return 0;
});
System.out.println("main......end....." + future.get());
16.1.4.线程串行化方法
- 1、thenRunAsync:不能获取上一步的执行结果
- 2、thenAcceptAsync:能接受上一步结果,但是无返回值
- 3、thenApplyAsync:能接受上一步结果,有返回值
/**
* 线程串行化
* 1、thenRunAsync:不能获取上一步的执行结果
* 2、thenAcceptAsync:能接受上一步结果,但是无返回值
* 3、thenApplyAsync:能接受上一步结果,有返回值
*
*/
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
return i;
}, executor).thenApplyAsync(res -> {
System.out.println("任务2启动了..." + res);
return "Hello" + res;
}, executor);
System.out.println("main......end....." + future.get());
16.1.5.两任务组合 - 都有完成
- 两个任务必须都完成, 触发该任务
- 1、runAfterBothAsync:组合两个future,不能获取future的结果
- 2、thenAcceptBothAsync:组合两个future,获取两个 future 任务的返回结果,但无返回值
- 3、thenCombineAsync:组合两个future,获取两个 future 任务的返回结果,有返回值
/**
* 两个都要完成
*/
CompletableFuture<Object> future01 = CompletableFuture.supplyAsync(() -> {
System.out.println("任务1线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("任务1结束");
return i;
}, executor);
CompletableFuture<Object> future02 = CompletableFuture.supplyAsync(() -> {
System.out.println("任务2线程:" + Thread.currentThread().getId());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("任务2结束");
return "Hello";
}, executor);
// future01.runAfterBothAsync(future02, () -> {
// System.out.println("任务3开始...");
// }, executor);
// future01.thenAcceptBothAsync(future02, (f1, f2) -> {
// System.out.println("任务3开始...之前的结果:" + f1 + "-->" + f2);
// }, executor);
CompletableFuture<String> future = future01.thenCombineAsync(future02, (f1, f2) -> {
return f1 + ":" + f2 + " -> Haha";
}, executor);
System.out.println("main......end....." + future.get());
16.1.6.两任务组合 - 一个完成
- 当两个任务中,任意一个future任务完成,触发该任务
- 1、runAfterEitherAsync:两个future任意一个完成,不能获取future的结果
- 2、acceptEitherAsync:两个future任意一个完成,获取它的返回结果,但无返回值
- 3、applyToEitherAsync:两个future任意一个完成,获取它的返回结果,有返回值
/**
* 两个任务,只要有一个完成,我们就执行任务3
* runAfterEitherAsync:不感知结果,自己无返回值
* acceptEitherAsync:感知结果,自己没有返回值
* applyToEitherAsync:感知结果,自己有返回值
*/
// future01.runAfterEitherAsync(future02, () -> {
// System.out.println("任务3开始...");
// }, executor);
// future01.acceptEitherAsync(future02, res -> {
// System.out.println("任务3开始...之前的结果:" + res);
// }, executor);
CompletableFuture<String> future = future01.applyToEitherAsync(future02, res -> {
System.out.println("任务3开始...之前的结果:" + res);
return res.toString() + "->哈哈";
}, executor);
System.out.println("main......end....." + future.get());
16.1.7.多任务组合
- allOf: 等待所有任务完成
- anyOf: 只要有一个任务完成
CompletableFuture<String> futureImg = CompletableFuture.supplyAsync(() -> {
System.out.println("查询商品的图片信息");
return "hello.jpg";
}, executor);
CompletableFuture<String> futureAttr = CompletableFuture.supplyAsync(() -> {
System.out.println("查询商品的属性");
return "黑色+256G";
}, executor);
CompletableFuture<String> futureDesc = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(3);
System.out.println("查询商品介绍");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "华为";
}, executor);
CompletableFuture<Void> allOf = CompletableFuture.allOf(futureImg, futureAttr, futureDesc);
allOf.get();//等待所有结果完成
System.out.println("main......end....." + futureImg.get() + "=>" + futureAttr.get() + "=>" + futureDesc.get());
CompletableFuture<Object> anyOf = CompletableFuture.anyOf(futureImg, futureAttr, futureDesc);
anyOf.get();//等待任一结果完成
System.out.println("main......end....." + anyOf.get());
17.补充
17.1.谈谈LockSupport
17.1.1.LockSupport是什么
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
LockSupport中的park()和 unpark()的作用分别是阻塞线程和解除阻塞线程。
总之,比wait/notify,await/signal更强。
3种让线程等待和唤醒的方法:
- 方式1:使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程
- 方式2:使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
- 方式3:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
1). waitNotify限制
Object类中的wait和notify方法实现线程等待和唤醒
public class WaitNotifyDemo {
static Object lock = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " come in.");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 唤醒.");
}
}, "Thread A").start();
new Thread(() -> {
synchronized (lock) {
lock.notify();
System.out.println(Thread.currentThread().getName() + " 通知.");
}
}, "Thread B").start();
}
}
wait和notify方法必须要在同步块或者方法里面且成对出现使用,否则会抛出java.lang.IllegalMonitorStateException。
调用顺序要先wait后notify才OK。
2). awaitSignal限制
Condition接口中的await后signal方法实现线程的等待和唤醒,与Object类中的wait和notify方法实现线程等待和唤醒类似。
public class ConditionAwaitSignalDemo {
static Lock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " come in.");
try {
lock.lock();
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
System.out.println(Thread.currentThread().getName() + " 唤醒.");
}, "Thread A").start();
new Thread(() -> {
try {
lock.lock();
condition.signal();
System.out.println(Thread.currentThread().getName() + " 通知.");
} finally {
lock.unlock();
}
}, "Thread B").start();
}
}
输出结果:
await和signal方法必须要在同步块或者方法里面且成对出现使用,否则会抛出java.lang.IllegalMonitorStateException。
调用顺序要先await后signal才OK。
3). LockSupport方法介绍
传统的synchronized和Lock实现等待唤醒通知的约束
- 线程先要获得并持有锁,必须在锁块(synchronized或lock)中
- 必须要先等待后唤醒,线程才能够被唤醒
LockSupport类中的park等待和unpark唤醒
Basic thread blocking primitives for creating locks and other synchronization classes.
This class associates, with each thread that uses it, a permit (in the sense of theSemaphore
class). A call topark
will return immediately if the permit is available, consuming it in the process; otherwise it may block. A call tounpark
makes the permit available, if it was not already available. (Unlike with Semaphores though, permits do not accumulate. There is at most one.)
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和0,默认是零。
可以把许可看成是一种(0.1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1。
通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作
park()/park(Object blocker) - 阻塞当前线程阻塞传入的具体线程
public class LockSupport {
...
public static void park() {
UNSAFE.park(false, 0L);
}
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
...
}
permit默认是0,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为0并返回。
unpark(Thread thread) - 唤醒处于阻塞状态的指定线程
public class LockSupport {
...
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
...
}
调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,pemit值还是1)会自动唤醒thead线程,即之前阻塞中的LockSupport.park()方法会立即返回。
17.1.2.LockSupport案例解析
public class LockSupportDemo {
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
// try {
// TimeUnit.SECONDS.sleep(2);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }
System.out.println(Thread.currentThread().getName() + " come in. " + System.currentTimeMillis());
LockSupport.park();
System.out.println(Thread.currentThread().getName() + " 唤醒. " + System.currentTimeMillis());
}, "Thread A");
threadA.start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
LockSupport.unpark(threadA);
System.out.println(Thread.currentThread().getName() + " 通知.");
}, "Thread B").start();
}
}
输出结果:
正常 + 无锁块要求。
先前错误的先唤醒后等待顺序,LockSupport可无视这顺序。
1). 重点说明
LockSupport是用来创建锁和共他同步类的基本线程阻塞原语。
LockSuport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻寨之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码。
LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程
LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0,调用一次unpark就加1变成1,调用一次park会消费permit,也就是将1变成0,同时park立即返回。
如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累凭证。
2). 形象的理解
线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。
当调用park方法时
- 如果有凭证,则会直接消耗掉这个凭证然后正常退出。
- 如果无凭证,就必须阻塞等待凭证可用。
而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效。
面试题
1.为什么可以先唤醒线程后阻塞线程?
因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。
2.为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
因为凭证的数量最多为1(不能累加),连续调用两次 unpark和调用一次 unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行。
17.2.谈谈AQS
AQS 前置知识
- 公平锁和非公平锁
- 可重入锁
- LockSupport
- 自旋锁
- 数据结构之链表
- 设计模式之模板设计模式
17.2.1.AQS是什么
1、字面意思
AQS(AbstractQueuedSynchronizer):抽象的队列同步器
一般我们说的 AQS 指的是 java.util.concurrent.locks
包下的 AbstractQueuedSynchronizer
,但其实还有另外三种抽象队列同步器:AbstractOwnableSynchronizer、AbstractQueuedLongSynchronizer 和 AbstractQueuedSynchronizer
2、技术翻译
AQS 是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石, 通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量(state)表示持有锁的状态
CLH:Craig、Landin and Hagersten 队列,是一个双向链表,AQS中的队列是CLH变体的虚拟双向队列FIFO
17.2.2.AQS 是 JUC 的基石
和AQS有关的并发编程类
举几个常见的例子
-
ReentrantLock
-
CountDownLatch
-
ReentrantReadWriteLock
-
Semaphore
-
...
进一步理解锁和同步器的关系
锁,面向锁的使用者。定义了程序员和锁交互的使用层API,隐藏了实现细节,你调用即可,可以理解为用户层面的 API。
同步器,面向锁的实现者。比如Java并发大神Douglee,提出统一规范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等,Java 中有那么多的锁,就能简化锁的实现啦。
17.2.3.AQS 能干嘛
AQS:加锁会导致阻塞
有阻塞就需要排队,实现排队必然需要有某种形式的队列来进行管理
抢到资源的线程直接使用办理业务,抢占不到资源的线程的必然涉及一种排队等候机制,抢占资源失败的线程继续去等待(类似办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。
既然说到了排队等候机制,那么就一定 会有某种队列形成,这样的队列是什么数据结构呢?如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node) ,通过CAS、自旋以及LockSuport.park()的方式,维护state变量的状态,使并发达到同步的效果。
17.2.4.AQS 初步认识
1、AQS初识
官网解释
有阻塞就需要排队,实现排队必然需要队列
- AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的 FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成 一个Node节点来实现锁的分配,通过CAS完成对State值的修改。
- Node 节点是啥?答:你有见过 HashMap 的 Node 节点吗?JDK 用
static class Node<K,V> implements Map.Entry<K,V> {
来封装我们传入的 KV 键值对。这里也是一样的道理,JDK 使用 Node 来封装(管理)Thread - 可以将 Node 和 Thread 类比于候客区的椅子和等待用餐的顾客
2、AQS内部体系架构
1、AQS的int变量
AQS的同步状态State成员变量,类似于银行办理业务的受理窗口状态:零就是没人,自由状态可以办理;大于等于1,有人占用窗口,等着去
/**
* The synchronization state.
*/
private volatile int state;
2、AQS的CLH队列
CLH队列(三个大牛的名字组成),为一个双向队列,类似于银行侯客区的等待顾客
3、内部类Node(Node类在AQS类内部)
Node的等待状态waitState成员变量,类似于等候区其它顾客(其它线程)的等待状态,队列中每个排队的个体就是一个Node
/**
* Status field, taking on only the values:
* SIGNAL: The successor of this node is (or will soon be)
* blocked (via park), so the current node must
* unpark its successor when it releases or
* cancels. To avoid races, acquire methods must
* first indicate they need a signal,
* then retry the atomic acquire, and then,
* on failure, block.
* CANCELLED: This node is cancelled due to timeout or interrupt.
* Nodes never leave this state. In particular,
* a thread with cancelled node never again blocks.
* CONDITION: This node is currently on a condition queue.
* It will not be used as a sync queue node
* until transferred, at which time the status
* will be set to 0. (Use of this value here has
* nothing to do with the other uses of the
* field, but simplifies mechanics.)
* PROPAGATE: A releaseShared should be propagated to other
* nodes. This is set (for head node only) in
* doReleaseShared to ensure propagation
* continues, even if other operations have
* since intervened.
* 0: None of the above
*
* The values are arranged numerically to simplify use.
* Non-negative values mean that a node doesn't need to
* signal. So, most code doesn't need to check for particular
* values, just for sign.
*
* The field is initialized to 0 for normal sync nodes, and
* CONDITION for condition nodes. It is modified using CAS
* (or when possible, unconditional volatile writes).
*/
volatile int waitStatus;
Node类的内部结构
static final class Node{
//共享
static final Node SHARED = new Node();
//独占
static final Node EXCLUSIVE = null;
//线程被取消了
static final int CANCELLED = 1;
//后继线程需要唤醒
static final int SIGNAL = -1;
//等待condition唤醒
static final int CONDITION = -2;
//共享式同步状态获取将会无条件地传播下去
static final int PROPAGATE = -3;
// 初始为e,状态是上面的几种
volatile int waitStatus;
// 前置节点
volatile Node prev;
// 后继节点
volatile Node next;
// ...
4、总结
有阻塞就需要排队,实现排队必然需要队列,通过state 变量 + CLH双端 Node 队列实现
3、AQS同步队列的基本结构
4、AQS底层是怎么排队的?
通过调用 LockSupport.park()
来进行排队
17.2.5.从 ReentrantLock 进入 AQS
ReentrantLock
类是 Lock
接口的实现类,基本都是通过【聚合】了一个【队列同步器】的子类完成线程访问控制的
ReentrantLock 的原理
ReentrantLock
实现了 Lock
接口,在 ReentrantLock
内部聚合了一个 AbstractQueuedSynchronizer
的实现类
1.公平锁 & 非公平锁
通过
ReentrantLock
的源码来讲解公平锁和非公平锁
在 ReentrantLock
内定义了静态内部类,分别为 NoFairSync
(非公平锁)和 FairSync
(公平锁)
ReentrantLock
的构造函数:不传参数表示创建非公平锁;参数为 true 表示创建公平锁;参数为 false 表示创建非公平锁
瞄一眼 lock()
方法的执行流程:以 NonfairSync
为例
在 ReentrantLock
中,NoFairSync
和 FairSync
中 tryAcquire()
方法的区别,可以明显看出公平锁与非公平锁的lock()
方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:
hasQueuedPredecessors()
hasQueuedPredecessors()
方法是公平锁加锁时判断等待队列中是否存在有效节点的方法
公平锁与非公平锁的总结
对比公平锁和非公平锁的tryAcquire()
方法的实现代码, 其实差别就在于非公平锁获取锁时比公平锁中少了一个判断!hasQueuedPredecessors()
,hasQueuedPredecessors()
中判断了是否需要排队,导致公平锁和非公平锁的差异如下:
-
公平锁:公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中;
-
非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。也就是说队列的第一个排队线程在
unpark()
,之后还是需要竞争锁(存在线程竞争的情况下)
而 acquire()
方法最终都会调用 tryAcquire()
方法
在 NonfairSync
和 FairSync
中均重写了其父类 AbstractQueuedSynchronizer
中的 tryAcquire()
方法
2.从非公平锁的 lock() 入手
先从示例代码入手
源码解读比较困难,我们这里举个例子,假设 A、B、C 三个人都要去银行窗口办理业务,但是银行窗口只有一个,我们使用 lock.lock()
模拟这种情况
public class AQSDemo {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
//带入一个银行办理业务的案例来模拟我们的AQS如何进行线程的管理和通知唤醒机制
//3个线程模拟3个来银行网点,受理窗口办理业务的顾客
//A顾客就是第一个顾客,此时受理窗口没有任何人,A可以直接去办理
new Thread(() -> {
lock.lock();
try {
System.out.println("------A thread come in.");
try {
TimeUnit.MINUTES.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} finally {
lock.unlock();
}
}, "A").start();
// 第二个顾客,第二个线程---》由于受理业务的窗口只有一个(只能一个线程持有锁),此时B只能等待,
// 进入候客区
new Thread(() -> {
lock.lock();
try {
System.out.println("------B thread come in.");
} finally {
lock.unlock();
}
}, "B").start();
// 第三个顾客,第三个线程---》由于受理业务的窗口只有一个(只能一个线程持有锁),此时C只能等待,
// 进入候客区
new Thread(() -> {
lock.lock();
try {
System.out.println("------C thread come in.");
} finally {
lock.unlock();
}
}, "C").start();
}
}
先来看看线程 A(客户 A)的执行流程
之前已经讲到过,new ReentrantLock()
不传参默认是非公平锁,调用 lock.lock()
方法最终都会执行 NonfairSync
重写后的 lock()
方法
第一次执行 lock() 方法
由于第一次执行 lock()
方法,state
变量的值等于 0,表示 lock 锁没有被占用,此时执行 compareAndSetState(0, 1)
CAS 判断,可得 state == expected == 0
,因此 CAS 成功,将 state
的值修改为 1
再来复习下 CAS:通过 Unsafe
提供的 compareAndSwapXxx()
方法保证修改操作的原子性(通过 CPU 原语保证),如果变量的值等于期望值,则修改变量的值为 update
,并返回 true
;若不等,则返回 false
。this
代表当前对象,stateOffset
表示 state
变量在该对象中的偏移量
再来看看 setExclusiveOwnerThread()
方法做了啥:将拥有 lock 锁的线程修改为线程 A
再来看看线程 B(客户 B)的执行流程
第二次执行 lock() 方法
由于第二次执行 lock()
方法,state
变量的值等于 1,表示 lock 锁被占用,此时执行 compareAndSetState(0, 1)
CAS 判断,可得 state != expected
,因此 CAS 失败,进入 acquire()
方法
acquire()
方法主要包含如下几个方法,下面我们一个一个来讲解
tryAcquire(arg)
方法的执行流程
先来看看 tryAcquire()
方法,诶,怎么抛了个异常?别着急,仔细一看是 AbstractQueuedSynchronizer
抽象队列同步器中定义的方法,既然抛出了异常,就证明父类强制要求子类去实现
Ctrl + Alt + B 找到子类中的实现
这里以非公平锁 NonfairSync
为例,在 tryAcquire()
方法中调用了 nonfairTryAcquire()
方法,注意,这里传入的参数都是 1
nonfairTryAcquire(acquires)
正常的执行流程:
在 nonfairTryAcquire()
方法中,大多数情况都是如下的执行流程:线程 B 执行 int c = getState()
时,获取到 state
变量的值为 1,表示 lock 锁正在被占用;于是执行 if (c == 0) {
发现条件不成立,接着执行下一个判断条件 else if (current == getExclusiveOwnerThread()) {
,current 线程为线程 B,而 getExclusiveOwnerThread()
方法返回正在占用 lock 锁的线程,为线程 A,因此 tryAcquire()
方法最后会 return false
,表示并没有抢占到 lock 锁
补充:getExclusiveOwnerThread()
方法返回正在占用 lock 锁的线程(排他锁,exclusive)
nonfairTryAcquire(acquires)
比较特殊的执行流程:
第一种情况是,走到 int c = getState()
语句时,此时线程 A 恰好执行完成,让出了 lock 锁,那么 state
变量的值为 0,当然发生这种情况的概率很小,那么线程 B 执行 CAS 操作成功后,将占用 lock 锁的线程修改为自己,然后返回 true
,表示抢占锁成功。其实这里还有一种情况,需要留到 unlock()
方法才能说清楚
第二种情况为可重入锁的表现,假设 A 线程又再次抢占 lock 锁(当然示例代码里面并没有体现出来),这时 current == getExclusiveOwnerThread()
条件成立,将 state
变量的值加上 acquires
,这种情况下也应该 return true
,表示线程 A 正在占用 lock 锁。因此,state
变量的值是可以大于 1 的
继续往下走,执行 addWaiter(Node.EXCLUSIVE) 方法
在 tryAcquire()
方法返回 false
之后,进行 !
操作后为 true
,那么会继续执行 addWaiter()
方法
来看看 addWaiter()
方法做了些啥?
之前讲过,Node
节点用于封装用户线程,这里将当前正在执行的线程通过 Node
封装起来(当前线程正是抢占 lock 锁没有抢占到的线程)
判断 tail 尾指针是否为空,双端队列此时还没有元素呢~肯定为空呀,那么执行 enq(node)
方法,将封装了线程 B 的 Node
节点入队
enq(node) 方法:构建双端同步队列
也许看到这里的代码有点蒙,需要有些前置知识,在双端同步队列中,第一个节点为虚节点(也叫哨兵节点,或傀儡节点),其实并不存储任何信息,只是占位。 真正的第一个有数据的节点,是从第二个节点开始的。
第一次执行 for 循环:现在解释起来就不费劲了,当线程 B 进来时,双端同步队列为空,此时肯定要先构建一个哨兵节点。此时 tail == null
,因此进入 if(t == null) {
的分支,头指针指向哨兵节点,此时队列中只有一个节点,尾节点即是头结点,因此尾指针也指向该哨兵节点
第二次执行 for 循环:现在该将装着线程 B 的节点放入双端同步队列中,此时 tail
指向了哨兵节点,并不等于 null
,因此 if (t == null)
不成立,进入 else
分支。以尾插法的方式,先将 node
(装着线程 B 的节点)的 prev
指向之前的 tail
,再将 node 设置为尾节点(执行 compareAndSetTail(t, node)
),最后将 t.next
指向 node
,最后执行 return t
结束 for 循环
补充:compareAndSetTail(t, node)
方法的实现
注意:哨兵节点和 nodeB
节点的 waitStatus
均为 0,表示在等待队列中
acquireQueued()
方法的执行
执行完 addWaiter()
方法之后,就该执行 acquireQueued()
方法了,这个方法有点东西,我们放到后面再去讲它
最后来看看线程 C(客户 C)的执行流程
线程 C 和线程 B 的执行流程很类似,都是执行 acquire()
中的方法
但是在 addWaiter()
方法中,执行流程有些区别。此时 tail != null
,因此在 addWaiter()
方法中就已经将 nodeC
添加至队尾了
执行完 addWaiter()
方法后,就已经将 nodeC 挂在了双端同步队列的队尾,不需要再执行 enq(node)
方法
补前面的坑:acquireQueued() 方法的执行逻辑
先来看 acquireQueued()
方法的源代码,其实这样直接看代码有点懵逼,我们接下来举例来理解。注意看:两个 if
判断中的代码都放在 for( ; ; )
中执行,这样可以实现自旋的操作
线程 B 的执行流程
线程 B 执行 addWaiter()
方法之后,就进入了 acquireQueued()
方法中,此时传入的参数为封装了线程 B 的 nodeB
节点,nodeB
的前驱结点为哨兵节点,因此 final Node p = node.predecessor()
执行完后,p
将指向哨兵节点。哨兵节点满足 p == head
,但是线程 B 执行 tryAcquire(arg)
方法尝试抢占 lock 锁时还是会失败,因此会执行下面 if
判断中的 shouldParkAfterFailedAcquire(p, node)
方法,该方法的代码如下:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
哨兵节点的 waitStatus == 0
,因此执行 CAS 操作将哨兵节点的 waitStatus
改为 Node.SIGNAL(-1)
注意:compareAndSetWaitStatus(pred, ws, Node.SIGNAL)
调用 unsafe.compareAndSwapInt(node, waitStatusOffset, expect, update);
实现,虽然 compareAndSwapInt()
方法内无自旋,但是在 acquireQueued()
方法中的 for( ; ; )
能保证此自旋操作成功(另一种情况就是线程 B 抢占到 lock 锁)
执行完上述操作,将哨兵节点的 waitStatus 设置为了 -1
执行完毕将退出 if
判断,又会重新进入 for( ; ; )
循环,此时执行 shouldParkAfterFailedAcquire(p, node)
方法时会返回 true
,因此此时会接着执行 parkAndCheckInterrupt()
方法
线程 B 调用 park()
方法后被挂起,程序不会然续向下执行,程序就在这儿排队等待
线程 C 的执行流程
线程 C 最终也会执行到 LockSupport.park(this);
处,然后被挂起,进入等待区
总结:
如果前驱节点的 waitStatus
是 SIGNAL
状态(-1),即 shouldParkAfterFailedAcquire()
方法会返回 true
,程序会继续向下执行 parkAndCheckInterrupt()
方法,用于将当前线程挂起
根据 park()
方法 API 描述,程序在下面三种情况会继续向下执行:
- 被 unpark
- 被中断(interrupt)
- 其他不合逻辑的返回才会然续向下执行
因上述三种情况程序执行至此,返回当前线程的中断状态,并清空中断状态。如果程序由于被中断,该方法会返回 true
3.可总算要unlock()了
线程 A 执行
unlock()
方法
A 线程终于要 unlock()
了吗?真不容易啊!
unlock()
方法调用了 sync.release(1)
方法
release()
方法的执行流程
其实主要就是看看 tryRelease(arg)
方法和 unparkSuccessor(h)
方法的执行流程,这里先大概说以下,能有个印象:线程 A 即将让出 lock 锁,因此 tryRelease()
执行后将返回 true
,表示礼让成功,head
指针指向哨兵节点,并且 if
条件满足,可执行 unparkSuccessor(h)
方法
tryRelease(arg)
方法的执行逻辑
又是 AbstractQueuedSynchronizer
类中定义的方法,又是抛了个异常
老样子 Ctrl + Alt + B,查看其具体实现
线程 A 只加锁过一次,因此 state
的值为 1,参数 release
的值也为 1,因此 c == 0
。将 free
设置为 true
,表示当前 lock 锁已被释放,将排他锁占有的线程设置为 null
,表示没有任何线程占用 lock 锁
unparkSuccessor(h)
方法的执行逻辑
在 release()
方法中获取到的头结点 h
为哨兵节点,h.waitStatus == -1
,因此执行 CAS操作将哨兵节点的 waitStatus
设置为 0,并将哨兵节点的下一个节点(s = node.next = nodeB
)获取出来,并唤醒 nodeB
中封装的线程(if (s == null || s.waitStatus > 0)
不成立,只有 if (s != null)
成立)
执行完上述操作后,当前占用 lock 锁的线程为 null
,哨兵节点的 waitStatus
设置为 0,state
的值为 0(表示当前没有任何线程占用 lock 锁)
杀个回马枪:继续来看 B 线程被唤醒之后的执行逻辑
再次回到 lock()
方法的执行流程中来,线程 B 被 unpark()
之后将不再阻塞,继续执行下面的程序,线程 B 正常被唤醒,因此 Thread.interrupted()
的值为 false
,表示线程 B 未被中断
回到上一层方法中,此时 lock 锁未被占用,线程 B 执行 tryAcquire(arg)
方法能够抢到 lock 锁,并且将 state
变量的值设置为 1,表示该 lock 锁已经被占用
接着来研究下 setHead(node)
方法:传入的节点为 nodeB
,头指针指向 nodeB
节点;将 nodeB
中封装的线程置为 null
(因为已经获得锁了);nodeB
不再指向其前驱节点(哨兵节点)。这一切都是为了将 nodeB
作为新的哨兵节点
执行完 setHead(node)
方法的状态如下图所示
将 p.next
设置为 null
,这是原来的哨兵节点就是完全孤立的一个节点,此时 nodeB
作为新的哨兵节点
哇哦,通透,线程 C 也是类似的执行流程
17.2.6.AQS 总结
AQS 的考点
第一个考点:我相信你应该看过源码了,那么AQS里面有个变量叫State,它的值有几种?
答:3个状态:没占用是0,占用了是1,大于1是可重入锁
第二个考点:如果锁正在被占用,AB两个线程进来了以后,请问这个总共有多少个Node节点?
答:答案是3个,分别是哨兵节点、nodeA、nodeB
AQS 源码解读案例图示
18.参考文献
哔哩哔哩-尚硅谷JUC并发编程视频
哔哩哔哩-尚硅谷Java大厂面试题第二季视频
https://gitee.com/moxi159753/LearningNotes/tree/master/校招面试/JUC
https://blog.csdn.net/u011863024/article/details/115270840
https://blog.csdn.net/oneby1314/article/details/113789332
本文来自博客园,作者:冰枫丶,转载请注明原文链接:https://www.cnblogs.com/lqsblog/p/15660519.html