- 前言
- 一,线程基础
- 1.1 线程的状态
- 1.2 创建线程
- 1.3 线程让步
- 1.4 线程休眠
- 1.5 等待线程结束
- 1.6 线程中断
- 1.7 不推荐的方法
- 1.8 守护线程
- 二、锁
- 2.1 线程安全问题
- 2.2 synchronized使用
- 2.3 线程安全分析
- 2.4 探究synchronized实现原理
- 2.4.1 Java对象头
- 2.4.2 重量级锁-Monitor
- 2.4.3 轻量级锁
- 2.4.4 偏向锁
- 2.4.5 锁消除
- 2.5 wait/notify
- 2.6 死锁
- 2.6.1 死锁现象
- 2.6.2 死锁定位
- 三、无锁
- 3.1 CAS
- 3.1.1 CAS的使用
- 3.1.2 CAS效率分析
- 3.2 原子更新基本类型
- 3.2.1 AtomicInteger
- 3.3 原子更新引用类型
- 3.3.1 AtomicReference
- 3.3.2 ABA问题
- 3.4 原子更新数组
- 3.4.1 AtomicIntegerArray
- 3.5 原子更新字段
- 3.5.1 AtomicIntegerFieldUpdater
- 3.6 Unsafe
本文内容主要参考黑马JUC并发编程,《Java并发编程的艺术》,所有代码可在github我的主页下载。
一,线程基础 1.1 线程的状态我们知道,操作系统把线程分为了初始、可运行、运行、终止、阻塞五种状态。
在Java中,把线程分为新建、可运行、阻塞、等待、限时等待、终止六种状态,其中可运行又可分为就绪和运行两种状态,如下图所示,后续将围绕线程状态的转化介绍各种线程操作方法。
有三种常见创建线程的方式,一种是重写run方法(继承),另外两种均采用接口(组合)。
我们先来看看采用重写的方式:
// 方式一:直接重写Thread的run方法
Thread t1 = new Thread("t1") {
@Override
public void run() {
System.out.println("I am t1");
}
};
t1.start();
线程start以后,会自动调用线程的run方法执行。注意,直接t1.run()并不会开启一个新的线程。
学过设计模式都知道,编程推荐使用组合而不是继承,因此jdk的设计者们还提供了组和的方式。
// 方式二:实现Runnable接口,调用Thread的构造器
Runnable runnable = () -> System.out.println("I am t2");
Thread t2 = new Thread(runnable, "t2");
t2.start();
看看Thread的构造器,这里可以传入一个Runnable接口。
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
而init方法内部将该线程的target赋值为这个传入的target,这就是组合。
// 内部变量 private Runnable target; // init方法 this.target = target;
而在调用run方法时,可以看到,如果target不为空,则会调用传入的实现Runnable接口的run方法。
public void run() {
if (target != null) {
target.run();
}
}
而Runnable接口是什么样的,想必你已经大概猜出来了,只有一个run方法的函数式接口
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
在工程实际的应用中,我们会采用一种叫做“异步”的方式去编写业务逻辑,比如老板让员工去做一件事情A,吩咐完成后,员工下去做A,老板则可以做事情B,员工完成任务后,就来向老板汇报A的工作情况。如果采用Runnable接口,可以看到执行方法run时,没有返回值,也就是说,该接口只会去做上级安排的任务,而不会给上级汇报工作情况,不符合我们上述的这种业务场景,于是,jdk开发者们在此之上,又设计了一个新的接口Callable,使线程执行拥有返回值。
@FunctionalInterface public interface Callable{ V call() throws Exception; }
那么,线程有了返回值,我们怎么去拿呢?我们怎么知道线程有没有执行完?什么时候该去拿呢?
这里就要用到future模式,在这种模式下,我们先让被调用者返回一个凭证,让它在后台慢慢处理这个请求,然后可以通过凭证去取得处理后的数据。
// 方式三:使用Future模式 FutureTaskft = new FutureTask<>(() -> { System.out.println("I am t3"); return 1; }); Thread t3 = new Thread(ft, "t3"); t3.start();
具体关系图如下所示,可以看到,Callable接口被注册到了FutureTask中,由于FutureTask实现了Runnable,所以它能通过构造器注册到Thread中。
不难猜出,FutureTask实现了Runnable,它的run方法必然是调用了Callable的call方法。
而FutureTask的对象ft就是我们拿到返回值的凭证,通过调用get方法,如果线程还未执行完,则当前线程等待,直到拿到返回值。
线程创建过后,就进入ready状态,与其他线程一起竞争cpu,通过操作系统的线程调度机制,进入cpu运行,在running状态时,可以使用yield方法使线程放弃cpu进入ready状态。
Thread.yield();
注意,ready状态的线程仍然可以竞争cpu资源,也就是说让步方法虽然放弃了cpu,但起决定因素的还是操作系统的调度,想通过让步让其他线程运行或许并不好使。
1.4 线程休眠在线程running状态时,可以使用sleep(long m)方法使线程放弃cpu进入timed-wating状态,时间到后,就会被唤醒。
Thread.sleep(1000);
可以使用TimeUnit.SECONDS.sleep(1)替换sleep,具备更好的可读性。
线程休眠的一种典型应用是对于守护线程,需要一直运行在后台,通常使用while(true)的循环,但这样会使cpu占用过高,典型的处理是在循环内加入休眠,例如每一次休眠50ms能有非常不错的性能提升。
注意:
- 线程休眠、以及后文的线程阻塞,它们被唤醒的时候都是变为ready状态,竞争cpu。
- 线程休眠并不会释放掉它所获得的锁。
线程之间存在协作关系,假如主线程让线程A去帮忙做一件事,主线程需要等待A的返回结果才能继续后面的工作,比如(a+b)*c,主线程让A去做a+b,然后它等待a+b的结果才能计算(a+b)*c,这时,就可以用等待线程结束。
t1.join(); t1.join(1000); // 限时等待
上述两种方法分别将主线程的状态从running变为waiting和timed-waiting。
其实,join方法的底层仍然是调用的wait方法(见3.2节)。
线程中断本身不会更改线程的状态,只会更改线程的isInterrupted标志位,这是为了让程序能够在接收到中断命令后,能自行决定后续的工作。
t1.interrupt();
- 对于正在运行的线程来讲,interrupt打断它并且把isInterrupted标志位置为true,线程是否终止由线程决定,比如程序员需要在打断后保留现场信息再退出。
Thread t1 = new Thread() {
@Override
public void run() {
while (true) {
System.out.println("do work1...");
// 检查到此时线程被中断
if (Thread.currentThread().isInterrupted()) {
System.out.println("save...");
System.out.println(Thread.currentThread().isInterrupted());
break;
}
}
}
};
中断t1后输出
... do work1... do work1... save... true
- 对于waiting,timed-waiting这种没有在运行的线程,线程没有办法去判断是否中断,所以采用异常捕获的方式,这种方式会清除打断标记,且捕获异常后,并不会终止线程,而是把线程变为ready状态,执行异常代码块,循环仍然继续。
Thread t2 = new Thread("t2") {
@Override
public void run() {
while (true) {
// 在sleep的时候会抛出中断异常,此时会被捕获,但中断标志位会被清除
try {
System.out.println("t2 sleep...");
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println("Interrupted when sleep");
System.out.println(Thread.currentThread().isInterrupted());
}
}
}
};
中断t2后输出
t2 sleep... Interrupted when sleep false t2 sleep... t2 sleep...
可以看到,标志位仍然为false且线程依然在运行。
两阶段终止模式
运行中的线程,我们怎么从外界将它终止,jdk最初提供了stop()方法,但这种方法类似于电脑之间拔电源,暴力中止线程会导致数据不一致,所以,这个方法现在已经废弃了,而使用其他方法代替。线程中断就是其中一种。
由于线程处于运行和阻塞(这里把等待、阻塞统称为阻塞)状态对于中断的响应不同,所以需要分两种情况考虑,下图基于一种常见的情景,线程A需要不断在后台运行,对于while(true),需要增加线程休眠来防止cpu占用过高,
Thread t3 = new Thread("t3") {
@Override
public void run() {
while (true) {
Thread t = Thread.currentThread();
if (t.isInterrupted()) {
System.out.println("save...");
break;
}
System.out.println("do work...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 重新设置打断标记
e.printStackTrace();
t.interrupt();
}
}
}
};
补充:也可以设置停止标记stop,当标记为真时,结束循环
private volatile boolean stop = false;
LockSupport支持中断影响,它不会抛出异常,也不会清除中断标记,并且中断一次后,再使用park方法将失效。
Thread t4 = new Thread("t4") {
@Override
public void run() {
System.out.println("t4 park...");
LockSupport.park();
System.out.println("t4 unpark...");
System.out.println("t4 interrupted status:" + Thread.currentThread().isInterrupted());
System.out.println("t4 park again...");
LockSupport.park(); //不会再生效
System.out.println("t4 unpark again...");
}
};
输出:
t4 park... t4 unpark... t4 interrupted status:true t4 park again... t4 unpark again...1.7 不推荐的方法
- stop():线程终止。该方法类似于电脑拔插头,会导致数据不一致。一般使用中断来替换。
- suspend()与resume():线程挂起与继续执行。该方法暂停的同时不会释放锁资源,一旦出现resume比suspend先执行的情况,线程将永远得不到继续执行。最奇怪的是,被挂起的线程仍会处于runnable状态。可使用LockSupport来替换。
守护线程是一种特殊的线程,它一直在后台默默完成一些任务,当非守护线程全部结束时,守护线程会结束。
比如一个while(true)的线程,若没有把它设置为守护线程,当主线程结束时,它还会继续运行,若将它设置为守护线程,主线程结束它也会跟着结束。
以下t1为守护线程,t2为非守护线程,主线程先行退出,然后t2再退出,可以看到,t1一直等待所有线程(main, t2)执行结束,才会退出。
public class DaemonDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1") {
@Override
public void run() {
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
System.out.println("t1 work...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread t2 = new Thread("t2") {
@Override
public void run() {
try {
System.out.println("t2 work...");
TimeUnit.SECONDS.sleep(5);
System.out.println("t2 out...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t1.setDaemon(true);
t1.start();
t2.start();
TimeUnit.SECONDS.sleep(3);
System.out.println("main out...");
}
}
输出:
t2 work... t1 work... t1 work... main out... t1 work... t1 work... t2 out...二、锁 2.1 线程安全问题
由操作系统知识可以知道,一个进程可以拥有多个线程,线程是调度的基本单位,CPU根据调度算法自行在线程中切换执行,实现并发。比如下面这一段代码,线程t1,t2分别使cnt自增5000,理论上结果应该为10000。
public class ThreadSafe {
public static int cnt = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1") {
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
cnt++;
}
}
};
Thread t2 = new Thread("t2") {
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
cnt++;
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(cnt);
}
}
然而实际执行中,却总是比10000小。
8928
这是因为java字写码把++命令拆分为了以下三步:
- 取cnt的值
- 在线程内执行cnt+1
- 将计算的结果赋值给cnt
在每一步之间,如果操作系统执行线程切换,都会导致计算不能符合预期。
比如线程t1先拿到cnt的值(0),然后执行+1操作,这时线程切换,t2拿到cnt的值(0),执行+1操作并写入cnt,此时cnt为1,线程t1又拿到了cpu资源,并将它计算的结果(1)写入。这样,两次cnt++操作却得到1。
一块代码块如果存在对共享资源的多线程读写操作,这段代码块就称为临界区(比如上述cnt++)。
synchronized又称为对象锁,它采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其他线程想再获得这个对象锁都会被阻塞。
还是上面的那个例子,我们在进入临界区前,使用synchronized加锁。
public class SynchronizedDemo {
public static int cnt = 0;
public static Object o = new Object(); // 创建一个对象,供synchronized使用
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1") {
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
synchronized (o) {
cnt++;
}
}
}
};
Thread t2 = new Thread("t2") {
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
synchronized (o) {
cnt++;
}
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(cnt);
}
}
输出:
10000
可以把synchronized看做是一道门,门内是临界区代码,而对象是这把门的钥匙,谁先拿到钥匙,谁进门,后续想进入的线程都将进入阻塞状态(blocked),注意,即使进入门的线程执行了线程切换,也不会释放掉它手中的钥匙,当他执行结束时,通知所有阻塞在此门的线程(blocked->runnable),由它们再一次竞争门的钥匙。
理解了对象锁的概念之后,我们尝试改进一下SynchronizedDemo,考虑把加锁动作抽离出来,应用面向对象思想,将具体行为交由另一个对象来做。
public class SynchronizedObj {
static class Room {
private int cnt = 0;
public void increment() {
synchronized (this) {
cnt++;
}
}
public void decrement() {
synchronized (this) {
cnt--;
}
}
public int getCnt() {
synchronized (this) {
return cnt;
}
}
}
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread("t1") {
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
room.increment();
}
}
};
Thread t2 = new Thread("t2") {
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
room.increment();
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(room.getCnt());
}
}
可以看到,我们使用Room类包装了对cnt的加减等操作,程序员只需要调用它的方法而不需要关注线程安全问题。
仔细看SynchronizedObj这个例子,我们不难发现,increment()的整个方法都被synchronized包含了,这种情况下,我们可以这样进行简化:
public synchronized void increment() {
cnt++;
}
当synchronized写在实例方法上时,相当于对this(当前对象)加锁。
同理,它也能写在静态方法上,相当于对当前类加锁:
public static synchronized void test() {
}
等价于
public static void test() {
synchronized (Room.class) {
}
}
2.3 线程安全分析
哪些变量的使用需要考虑线程安全?哪些变量又不用呢?
只要是多线程共享的变量就就一定要万分小心,我们知道,变量可以分为静态变量、成员变量和局部变量。
由JVM的基础可以知道:
静态变量:
- 当声明的是基本类型的变量时,其变量名及值是放在方法区中。
- 当声明的是引用类型的变量时,所声明的变量名放在java方法区中,该变量所指向的对象是放在堆类存中的。
成员变量:
- 不管声明的是基本数据类型还是引用类型,变量名和值都存放在堆中。
局部变量:
- 当声明的是基本类型的变量的时,其变量名及值(变量名及值是两个概念)是放在JAVA虚拟机栈中
- 当声明的是引用类型变量时,所声明的变量名(该变量实际上是在方法中存储的是内存地址值)是放在JAVA虚拟机的栈中,该变量所指向的对象是放在堆类存中的。
我们知道,方法区和堆多线程共享,JAVA虚拟机栈线程独有,变量是否线程安全,就看它是否存放在线程共享的区域就可以了。
比如对于静态变量和成员变量来讲,任何类型的变量都线程不安全;对于局部变量来讲,如果是基本类型,那么它是安全的,如果是引用类型,它就是不安全的。
特别的,对于String和Integer这两个类,都使用了final作为修饰符,为不可变类,所有线程只能读不能改,所以在任何情况下都是线程安全的。
我们先来看看Java对象头的概念,Java中的每个对象都存在一个对象头,如果是数组类型,虚拟机用3个字宽存储,如果是非数组类型,则用2个字宽存储,以32位系统为例,1子宽等于4字节,32bit,这里我们关注非数组类型的对象头,它由8个字节(64bit)表示。
对于Mark Word,在无锁时的存储结构如下所示:
随着运行,Mark Word可能存储以下4种数据:
synchronized(obj) {
// 临界区
}
我们先来探究Mark Word状态变化的重量级锁,这也是synchronized最开始使用的方法。例如,Thread2执行以上方法,尝试将obj对象和操作系统关联,给对象加锁后,对象的锁标志位变为10(2bits),并且具有指向Monitor对象的指针(30bits),对应的Monitor对象的Owner则指向Thread2,表示被线程占用。
此时,Thread1也执行以上方法,它发现对象已经关联上了Monitor,随即检查Monitor的Owner,发现为Thread2,于是它只能进入EntryList中等待(Blocked),同理后续线程全部等待。
当Thread2执行完成后,Owner空,就会唤醒EntryList里面的所有线程,由它们再次竞争获得锁(非公平)。
下面我们来看看字节码是怎样解析以下代码的
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized(lock) {
counter++;
}
}
可以看到,对于方式异常的情况,也能释放锁。
总的来说,代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式(虚拟机规范并没有详细说明),monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。
Java6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,我们先来看一看轻量级锁。
线程在遇到synchronized(obj)时,JVM先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录。
假如线程1先执行,它会使用CAS将对象头中的Mark Word替换为指向锁记录的指针,并附上00,对象进入轻量级锁状态。
假如此时线程1重入synchronized(obj),会发生什么呢?
同样,它会将Mark Word复制到锁记录中,并尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针,此时,它发现Mark Word为00,已经具有了轻量级锁,且判断出Mark Word指向当前线程的栈帧,为锁重入,则将栈帧中的Mark Word置null。
线程1拿到两次锁后,每释放一次就将对应的锁记录出栈,当出栈到最后一次时(锁记录的Mark Word不为null),运用CAS将对象原本的Mark Word替换回去,这样一次加锁与释放锁就完成了。
上面假设是只有一个线程的情况,假设线程2现在也来竞争锁了,我们看看线程2竞争的时机以及它对应的策略。
假如线程1已经获得锁且还未释放锁,线程2此时尝试CAS竞争,它发现对象已经进入了00状态,此时它进入自旋来获得锁。
若是自旋获得锁成功,则它获得轻量级锁。
若是失败,则进入锁膨胀,线程2为obj申请Monitor锁,更改Mark Word为重量级锁地址,标志位改为10,同时,线程2进入阻塞。
后来,线程1执行完毕准备执行CAS解锁,它发现obj的Mark Word的标志位已经为10,此时它采用重量级解锁的过程,置Owner为null,唤醒EntryList里面的线程。
总结:轻量级锁的升级过程如下所示
轻量级锁在没有竞争时,每次重入仍然需要执行CAS操作,而偏向锁则做了进一步优化,只有第一次使用CAS将线程ID设置到对象的Mark Word,之后发现这个线程ID是自己的就表示没有竞争,不用再CAS。如果发现线程ID不指向自己,则升级为轻量级锁,完事之后偏向也会撤销,进入无锁状态。
一个对象创建时:
- 如果开启了偏向锁(默认开启,使用-XX:-UseBiasedLocking禁用),那么对象创建后,Mark Word的最后三位为101,这时它的thread, epoch, age都为0
- 偏向锁默认延迟,不会在程序启动时生效,如想避免延迟,可以加VM参数-XX:BiasedLockingStartupDelay=0来禁用延迟
- 如果没有开启偏向锁,那么对象创建后,Mark Word的最后三位为101,这时它的hashcode, age都为0,第一次用到hashcode才会赋值
偏向锁的撤销:
- 对象调用hashCode方法,调用hashCode方法时,Mark Word要用25bit存hashCode码,所以没有地方存线程id了
- 其他线程使用对象,当线程发现对象的线程id不是指向自己时,就升级为轻量级锁,完事之后还原到无锁状态(撤销偏向)。当撤销偏向锁的阈值超过20之后,JVM就会以为自己偏向错了,这时便不会撤销,而是重新偏向至加锁的线程;当撤销偏向锁的阈值超过40之后,JVM就会觉得起初一开始就不应该偏向,它会将整个类的所有对象变为不可偏向的,新建的对象也是不可偏向的。
- 调用wait/notify,这两个依赖于重量锁,所以会撤销偏向
在编译的时候,JIT即时编译器会对不可能出现竞争的对象进行锁消除,如下所示,对象o处在方法内部,每一个进入方法的线程都会重新new一个对象,这种情况下,对o而言,不可能出现竞争,编译器就会消除掉synchronized。
public class LockEliminate{
static int x = 0;
public void b() {
Object o = new Object();
synchronized (o) {
x++;
}
}
}
可以使用-XX:-EliminateLocks关闭这个功能。
2.5 wait/notify当线程拿到锁进入临界区执行代码后,它可能需要等待一些准备操作完成才能执行,这时不能一直占据着锁,可以执行wait方法,将owner中的线程一刀WaitSet中(如下图的Thread-0, Thread-1),并进入waiting状态,这时,线程2才能得以进入临界区执行,当等待的操作完成时,由Owner上的线程执行notify操作,唤醒WaitSet里面的线程,线程唤醒后,又得重新竞争锁。
主要方法:
- o.wait():让进入监视器的线程到WaitSet等待
- o.wait(long timeout):限时等待timeout毫秒
- o.notify():在o监视器上正在WaitSet等待的线程挑一个唤醒
- o.notifyAll():在o监视器上正在WaitSet等待的线程全部唤醒
既然wait等待的线程在Monitor的WaitSet里面,这就意味着线程必须获得Monitor才能够调用wait方法,notify方法也一样。
public class WaitDemo {
public final static Object o = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1") {
@Override
public void run() {
System.out.println("t1 执行...");
synchronized (o) {
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t1 唤醒...");
}
};
Thread t2 = new Thread("t2") {
@Override
public void run() {
System.out.println("t2 执行...");
synchronized (o) {
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2 唤醒...");
}
};
t1.start();
t2.start();
TimeUnit.SECONDS.sleep(5);
synchronized (o) {
o.notify();
}
}
}
输出:
t1 执行... t2 执行... t1 唤醒...
由于notify()操作会随机挑选一个线程唤醒,当有多个线程等待时,只能使用notifyAll()方法,此时,就需要线程内部进行检查,避免虚假唤醒。
synchronized (o) {
while(如果条件未满足){
o.wait();
}
}
sleep(long n)和wait(long n)的区别
- sleep是Thread的方法,而wait是Object的方法
- sleep不强制和synchronized匹配使用,wait需要
- sleep不会释放锁,wait会
由操作系统知识可以知道,当线程拥有一把锁又去申请另一把锁时,就可能会出现死锁这种现象,若没有外力作用,谁也无法执行下去。
例如,线程1先后获取A,B两个锁,线程B先后获取B,A两个锁,某一时刻,线程1拥有了A锁且申请B锁,线程2拥有了B锁且申请A锁,此时,它们两个都无法继续执行下去。
public class DeadLockDemo {
public static void main(String[] args) {
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread("t1") {
@Override
public void run() {
synchronized (A) {
System.out.println("t1 lock A");
synchronized (B) {
System.out.println("t1 lock B");
System.out.println("t1 work...");
}
}
}
};
Thread t2 = new Thread("t2") {
@Override
public void run() {
synchronized (B) {
System.out.println("t2 lock B");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (A) {
System.out.println("t2 lock A");
System.out.println("t2 work...");
}
}
}
};
t1.start();
t2.start();
}
}
输出:
t1 lock A t2 lock B2.6.2 死锁定位
可以使用jdk自带的jconsole工具,打开idea控制台,输入jconsole命令,连接到正在运行的进程。
点击左上角的线程,然后左下角有检测死锁的字样。
就可以看到,t1和t2两个线程出现了死锁。
前面我们已经讲解了使用锁synchronized来保证原子性,为了保证锁的效率,也引入了偏向锁和轻量级锁,但是呢,锁始终是锁,对于性能多少会有一些影响,本章我们介绍一种无锁的方式(Compare And Swap),也能保证原子性。
3.1 CAS 3.1.1 CAS的使用我们通过一个例子认识cas并探究它与加锁方式的性能。
假如有1000个线程,分别对10000减10。
interface CountGame {
int getCnt();
void decrement(int num);
static void game(CountGame game) {
List list = new ArrayList<>(1000);
for (int i = 0; i < 1000; i++) {
list.add(new Thread(()->game.decrement(10), "t" + i));
}
long start = System.nanoTime();
// 启动线程
list.forEach(Thread::start);
// 等待线程执行结束
list.forEach(t->{
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(game.getCnt() + " cost:" + (end - start)/1000_000 + "ms");
}
}
为了达到我们期望的结果0,我们使用锁的方式保证原子性。
class CountGameForLock implements CountGame{
private int cnt;
public CountGameForLock(int cnt) {
this.cnt = cnt;
}
public int getCnt() {
return cnt;
}
@Override
public void decrement(int num) {
synchronized (this) {
cnt -= num;
}
}
}
本节,我们介绍一种基于比较和交换的技术(Compare And Swap),即对于每一个变量,都有一个预期值与交换值,预期值是我们最开始读的时候读到的值,再线程内部经过计算过后,准备将该值放入临界区原变量,此时再读一次,若变量的值不再是我们之前读到的值(预期值),则说明在此间隙中被其他线程更改,那么本次操作放弃,重新再来一次,若变量的值与之前读到的值相等,则可以写入。
class CountGameForUnlock implements CountGame{
private AtomicInteger cnt;
public CountGameForUnlock(Integer cnt) {
this.cnt = new AtomicInteger(cnt);
}
public int getCnt() {
return cnt.get();
}
@Override
public void decrement(int num) {
// 不断进行比较和交换
while (true) {
// 预期值
int prev = cnt.get();
// 修改值
int next = prev - num;
// cas 如果cas成功 返回true
if (cnt.compareAndSet(prev, next)) {
break;
}
}
}
}
以上的compareAndSet为原子操作,输出结果如下:
0 cost:58ms 0 cost:53ms
可以看到,使用无锁比使用锁性能更优。
补充:基于CAS的方法,我们可以大致猜出,它的值应该是用到了我们之前学的volatile关键字,因为比较交换需要保证每次读取的都是最新的值,查看AtomicInteger源代吗也是如此。
为什么CAS效率比synchronized更高,这是因为CAS有更少的上下文切换,我们知道,无锁情况下,即使重试失败,线程仍然在运行,而在有锁情况下,如果获取锁失败,将进入阻塞状态,这样就会发生线程上下文切换,影响效率。当然,CAS不是没有上下文切换,当线程时间片结束时,也会发生切换,这就要求CAS的使用场景要线程数较少,多核cpu,不然性能也不会很好。
3.2 原子更新基本类型Atomic提供了以下3个类
- AtomicBoolean
- AtomicInteger
- AtomicLong
3个类提供的方法几乎一样,我们以AtomicInteger为例,它实现对整数的原子操作。
public class AtomicIntegerDemo {
public static void main(String[] args) {
AtomicInteger a = new AtomicInteger(0);
// 加1
a.incrementAndGet(); // 相当于++a
a.getAndIncrement(); // 相当于a++
// 加任意数
a.getAndAdd(5);
a.addAndGet(5);
// 读取到 设置值
a.updateAndGet(x->x*10);
a.getAndUpdate(x->x*10);
}
}
我们着重来看一下updateAndGet的源代码
public final int updateAndGet(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get();
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));
return next;
}
底层仍然是我们6.1.1节讲到的CAS,这里将数的算法抽象了出来成为函数式接口IntUnaryOperator,
// IntUnaryOperator int applyAsInt(int operand);
这个接口只有一个方法applyAsInt,输出一个数,返回一个数,由程序员来决定对这个数的具体操作。
3.3 原子更新引用类型Atomic提供了以下3个类
- AtomicReference
- AtomicReferenceFieldUpdater
- AtomicMarkableReference
4.3节的AtomicInteger是对基本类型做原子操作,实际上,我们也能对引用类型做原子操作,CAS对象的地址。
设有一个对象User,
class User {
private String name;
private int old;
public User(String name, int old) {
this.name = name;
this.old = old;
}
public String getName() {
return name;
}
public int getOld() {
return old;
}
}
我们原子性的对User做修改
public class AtomicReferenceDemo {
public static AtomicReference atomicUserRef = new AtomicReference<>();
public static void main(String[] args) {
User user = new User("jojo", 17);
atomicUserRef.set(user);
User updateUser = new User("kaka", 20);
atomicUserRef.compareAndSet(user, updateUser);
System.out.println(atomicUserRef.get().getName() + ": " + atomicUserRef.get().getOld());
}
}
输出
kaka: 203.3.2 ABA问题
什么是ABA问题,假设我们在多线程环境下使用CAS来确保原子性,线程1准备将变量从A修改到C,如果在线程1写入之前,存在线程2将A修改为B,又将B修改为A,那么到线程1写入时,就不会察觉到线程2所做的改动而执行成功。
试想一下,假如存在栈[1,2,3,3](自底向上),线程1需要修改栈顶元素3为4,在修改过程中,线程2执行出栈,栈中元素变为[1,2,3],这时线程1cas,值并没有变,修改成功,但是却不是我们想要修改的数了。
解决办法:增加版本号
public class AtomicStampedReferenceDemo {
static AtomicStampedReference ref = new AtomicStampedReference<>(new User("jojo", 17), 0);
public static void main(String[] args) {
// 预期引用
User user = ref.getReference();
// 预期版本号
int stamp = ref.getStamp();
User updateUser = new User("kaka", 20);
ref.compareAndSet(user, updateUser, stamp, stamp + 1);
}
}
3.4 原子更新数组
Atomic提供了以下3个类
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
public class AtomicIntegerArrayDemo {
static int[] value = new int[]{1, 2};
static AtomicIntegerArray ai = new AtomicIntegerArray(value);
public static void main(String[] args) {
// 对0号位做更改
ai.getAndSet(0, 3);
System.out.println(ai.get(0));
System.out.println(value[0]);
}
}
输出:
3 1
数组value通过构造方法传递进去,然后AtomicIntegerArray会在当前数组复制一份,所以当AtomicIntegerArray对内部元素修改时,并不会影响传入的数组。
3.5 原子更新字段Atomic提供了以下3个类
- AtomicIntegerFieldUpdater
- AtomicLongFieldUpdater
- AtomicStampedReference
public class AtomicIntegerFieldUpdaterDemo {
private static AtomicIntegerFieldUpdater a = AtomicIntegerFieldUpdater.newUpdater(Client.class, "old");
public static void main(String[] args) {
Client client = new Client("lihua", 8);
a.compareAndSet(client, a.get(client), 10);
System.out.println(a.get(client));
}
}
class Client {
private String name;
public volatile int old;
public Client(String name, int old) {
this.name = name;
this.old = old;
}
public String getName() {
return name;
}
public int getOld() {
return old;
}
}
可以看到
- AtomicIntegerFieldUpdater是一个抽象类,使用静态方法newUpdater创建更新器,并且需要设置更新的属性。
- 更新的属性必须使用public volatile修饰。
点进AtomicInteger类的源码,我们可以看到它使用了Unsafe这个类,该类提供了以下原子方法
- compareAndSwapInt 对int型做原子赋值操作
- compareAndSwapLong 对long型做原子赋值操作
- compareAndSwapObject 对对象地址做原子赋值操作
我们尝试直接调用Unsafe这个类的方法,自己实现对Teacher类的原子操作
@Data
class Teacher {
volatile int id;
volatile String name;
}
观察Unsafe类,存在theUnsafe这个属性,我们需要获得这个属性来执行相关操作。
// Unsafe.java private static final Unsafe theUnsafe;
这就需要使用反射,具体实现如下所示。
public class UnsafeDemo {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
// 获得类中的指定字段theUnsafe
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
// 由于theUnsafe私有,需要覆盖访问控制
theUnsafe.setAccessible(true);
// 获得这个字段的值 由于是静态字段 所以参数为null
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
System.out.println(unsafe);
// 1.获取域的偏移地址
long idOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("id"));
long nameOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("name"));
Teacher t = new Teacher();
// 2.执行cas操作
unsafe.compareAndSwapInt(t, idOffset, 0, 1);
unsafe.compareAndSwapObject(t, nameOffset, null, "lihua");
// 3.验证
System.out.println(t);
}
}
输出:
sun.misc.Unsafe@7440e464 Teacher(id=1, name=lihua)
理解了Unsafe这个类后,我们就能看懂AtomicInteger的代码了。无非就是获取Unsafe,获取偏移量,使用unsafe进行cas原子操作。



