Java并发- 4 共享模型管程👦🏻
本章内容
4.1. 共享带来的问题
4.1.1. 小故事
- 老王(操作系统)有一个功能强大的算盘(CPU),现在想把它租出去,赚一点外快

- 小南、小女(线程)来使用这个算盘来进行一些计算,并按照时间给老王支付费用
- 但小南不能一天24小时使用算盘,他经常要小憩一会(sleep),又或是去吃饭上厕所(阻塞 io 操作),有时还需要一根烟,没烟时思路全无(wait)这些情况统称为(阻塞)

- 在这些时候,算盘没利用起来(不能收钱了),老王觉得有点不划算
- 另外,小女也想用用算盘,如果总是小南占着算盘,让小女觉得不公平
- 于是,老王灵机一动,想了个办法 [ 让他们每人用一会,轮流使用算盘 ]
- 这样,当小南阻塞的时候,算盘可以分给小女使用,不会浪费,反之亦然
- 最近执行的计算比较复杂,需要存储一些中间结果,而学生们的脑容量(工作内存)不够,所以老王申请了一个笔记本(主存),把一些中间结果先记在本上
- 计算流程是这样的

- 但是由于分时系统,有一天还是发生了事故
- 小南刚读取了初始值 0 做了个 +1 运算,还没来得及写回结果
- 老王说 [ 小南,你的时间到了,该别人了,记住结果走吧 ],于是小南念叨着 [ 结果是1,结果是1...] 不甘心地到一边待着去了(上下文切换)
- 老王说 [ 小女,该你了 ],小女看到了笔记本上还写着 0 做了一个 -1 运算,将结果 -1 写入笔记本
- 这时小女的时间也用完了,老王又叫醒了小南:[小南,把你上次的题目算完吧],小南将他脑海中的结果 1 写入了笔记本

4.1.2. Java 的体现
两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?
package com.beatshadow.concurrent.chapter4;
import lombok.extern.slf4j.Slf4j;
/**
*
* @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
* @since : 2020/4/28 19:53
*/
@Slf4j
public class Example1 {
private static int count = 0 ;
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5000 ; i++) {
count++;
}
}, "t1");
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5000 ; i++) {
count--;
}
}, "t2");
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(String.valueOf(count));
}
}
4.1.3. 问题分析
以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析
例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
而对应 i-- 也是类似:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i
而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:

如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题: 但多线程下这 8 行代码可能交错运行:
出现负数的情况: 出现正数的情况:  ### 4.1.4. 临界区 Critical Section
- 一个程序运行多个线程本身是没有问题的
- 问题出在多个线程访问共享资源
- 多个线程读共享资源其实也没有问题
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
- 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
例如,下面代码中的临界区
static int counter = 0;
static void increment()
// 临界区
{
//++既有读,又有写,而且counter为共享资源
counter++;
}
static void decrement()
// 临界区
{
counter--;
}
4.1.5. 竞态条件 Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了**竞态条件。
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
4.2. synchronized 解决方案
本次课使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
注意> 虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
4.2.1. 语法
synchronized(对象) // 线程1, 线程2(blocked)
{
临界区
}
4.2.2. 解决
package com.bloom.concurrent.chapter4;
import lombok.extern.slf4j.Slf4j;
/**
* @description:
* @author: teago
* @time: 2020/5/16 23:11
*/
@Slf4j
public class Example2 {
private static int count = 0;
private static Object lock = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock) {
for (int i = 0; i < 5000; i++) {
count++;
}
}
}, "t1");
Thread thread2 = new Thread(() -> {
synchronized (lock) {
for (int i = 0; i < 5000; i++) {
count--;
}
}
}, "t2");
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(String.valueOf(count));
}
}
4.2.3. 理解

你可以做这样的类比:
synchronized(对象)中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人进行计算,线程 t1,t2 想象成两个人- 当线程 t1 执行到
synchronized(room)时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行count++代码 - 这时候如果 t2 也运行到了
synchronized(room)时,它发现门被锁住了,只能在门外等待,发生了上下文切换,阻塞住了 - 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦),这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才能开门进入
- 当 t1 执行完
synchronized{}块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的count--代码
4.2.4. 思考
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
为了加深理解,请思考下面的问题:
- 如果把
synchronized(obj)放在 for 循环的外面,如何理解?-- 原子性- 只对count++或者count--加锁,只是对其中的4行代码加锁,如果加在for循环外,那其实是对5000*4=20000行代码加了说,虽然结果没有问题,但是效率不高。
- 如果 t1
synchronized(obj1)而 t2synchronized(obj2)会怎样运作?-- 锁对象【错误】- 因为使用的锁对象不同,故而在运行t1的时候,t2并不会阻塞。
- 如果 t1
synchronized(obj)而 t2 没有加会怎么样?如何理解?-- 锁对象【错误】
4.2.5. 面向对象改造
package com.bloom.concurrent.chapter4;
import java.util.concurrent.TimeUnit;
/**
* @description:
* @author: teago
* @time: 2020/5/16 23:16
*/
public class Example3 {
public static void main(String[] args) throws InterruptedException {
Room room = new Room() ;
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.add();
}
}, "td1");
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.subtract();
}
}, "td2");
thread1.start();
// thread1.join();
thread2.start();
// thread2.join();
TimeUnit.SECONDS.sleep(1);
System.out.println(room.getCount());
}
}
class Room{
private int count = 0;
//加法
public void add(){
synchronized (this){
count ++ ;
}
}
//减法
public void subtract(){
synchronized (this){
count -- ;
}
}
//获取
public int getCount(){
synchronized (this){
return count ;
}
}
}
4.3. synchronized加在方法上
4.3.1. 加在成员方法上(锁的是this对象)
class Test{
public synchronized void test() {
}
}
等价于
class Test{
public void test() {
synchronized(this) {
}
}
}
4.3.2. 加在静态方法上(锁的是类对象)
class Test{
public synchronized static void test() {
}
}
等价于
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}
4.3.3. 不加 synchronized 的方法
不加 synchronzied 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的),不保证原子性。
4.3.4. 所谓的“线程八锁”
其实就是考察 synchronized 锁住的是哪个对象
- 情况1
package com.bloom.concurrent.chapter4;
import com.sun.org.apache.bcel.internal.generic.NEW;
import lombok.extern.slf4j.Slf4j;
/**
* @description:
* @author: teago
* @time: 2020/5/17 08:51
*/
@Slf4j
public class TestThread8Monitor_1 {
public static void main(String[] args) {
Number1 number1 = new Number1();
Thread t1 = new Thread(() -> {
number1.a();
}, "t1");
Thread t2 = new Thread(() -> {
number1.b();
}, "t2");
t1.start();
t2.start();
}
// 1 2 或者 2 1
}
@Slf4j
class Number1 {
public synchronized void a() {
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
- 情况2
package com.bloom.concurrent.chapter4;
import lombok.extern.slf4j.Slf4j;
import static java.lang.Thread.sleep;
/**
* @description:
* @author: teago
* @time: 2020/5/17 08:55
*/
@Slf4j
public class TestThread8Monitor_2 {
public static void main(String[] args) {
Number2 n1 = new Number2();
new Thread(()->{
try {
n1.a();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{ n1.b(); }).start();
}
// 1s 1 2 或 2 1s 1
}
@Slf4j(topic = "c.Number")
class Number2{
public synchronized void a() throws InterruptedException {
sleep(1000);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
- 情况3
package com.bloom.concurrent.chapter4;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
/**
* @description:
* @author: teago
* @time: 2020/5/17 08:57
*/
public class TestThread8Monitor_3 {
public static void main(String[] args) {
Number3 n1 = new Number3();
new Thread(() -> {
try {
n1.a();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
n1.b();
}).start();
new Thread(() -> {
n1.c();
}).start();
}
//3 1s 1 2
//2 3 1S 1
//3 2 1s 1
}
@Slf4j(topic = "c.Number")
class Number3 {
public synchronized void a() throws InterruptedException {
TimeUnit.SECONDS.sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
public void c() {
log.debug("3");
}
}
- 情况4
package com.bloom.concurrent.chapter4;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
/**
* @description:
* @author: teago
* @time: 2020/5/17 08:59
*/
public class TestThread8Monitor_4 {
public static void main(String[] args) {
Number4 n1 = new Number4();
Number4 n2 = new Number4();
new Thread(()->{
try {
n1.a();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{ n2.b(); }).start();
}
// 2 1S 1
}
@Slf4j(topic = "c.Number")
class Number4{
public synchronized void a() throws InterruptedException {
TimeUnit.SECONDS.sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
- 情况5
package com.bloom.concurrent.chapter4;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
/**
* @description:
* @author: teago
* @time: 2020/5/17 09:01
*/
@Slf4j
public class TestThread8Monitor_5 {
public static void main(String[] args) {
Number5 n1 = new Number5();
new Thread(()->{
try {
n1.a();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{ n1.b(); }).start();
}
// 2 1S 1
}
@Slf4j(topic = "c.Number")
class Number5{
public static synchronized void a() throws InterruptedException {
TimeUnit.SECONDS.sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
- 情况6
package com.bloom.concurrent.chapter4;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
/**
* @description:
* @author: teago
* @time: 2020/5/17 09:02
*/
@Slf4j
public class TestThread8Monitor_6 {
public static void main(String[] args) {
Number6 number6 = new Number6() ;
new Thread(() -> {
try {
number6.a();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
number6.b();
}).start();
// 2 1S 1
// 1S 1 2
}
}
@Slf4j(topic = "c.Number")
class Number6{
public static synchronized void a() throws InterruptedException {
TimeUnit.SECONDS.sleep(1);
log.debug("1");
}
public static synchronized void b() {
log.debug("2");
}
}
package com.bloom.concurrent.chapter4;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
/**
* @description:
* @author: teago
* @time: 2020/5/17 09:02
*/
@Slf4j
public class TestThread8Monitor_6 {
public static void main(String[] args) {
Number6 number6 = new Number6() ;
new Thread(() -> {
try {
number6.a();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
number6.b();
}).start();
// 2 1S 1
// 1S 1 2
}
}
@Slf4j(topic = "c.Number")
class Number6{
public static synchronized void a() throws InterruptedException {
TimeUnit.SECONDS.sleep(1);
log.debug("1");
}
public static synchronized void b() {
log.debug("2");
}
}
- 情况7
package com.beatshadow.concurrent.chapter4;
import lombok.extern.slf4j.Slf4j;
import static java.lang.Thread.sleep;
/**
* @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
* @since : 2020/4/28 21:22
*/
@Slf4j
public class TestThread8Monitor_7 {
public static void main(String[] args) {
Number7 n1 = new Number7();
Number7 n2 = new Number7();
new Thread(()->{
try {
n1.a();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{ n2.b(); }).start();
}
// 2 1S 1
}
@Slf4j(topic = "c.Number")
class Number7{
public static synchronized void a() throws InterruptedException {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
- 情况8
package com.bloom.concurrent.chapter4;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
/**
* @description:
* @author: teago
* @time: 2020/5/17 09:06
*/
@Slf4j
public class TestThread8Monitor_8 {
public static void main(String[] args) {
Number8 n1 = new Number8();
Number8 n2 = new Number8();
new Thread(()->{
try {
n1.a();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{ n2.b(); }).start();
}
// 2 1S 1
//1 1S 2
}
@Slf4j(topic = "c.Number")
class Number8{
public static synchronized void a() throws InterruptedException {
TimeUnit.SECONDS.sleep(1);
log.debug("1");
}
public static synchronized void b() {
log.debug("2");
}
}
4.4. 变量的线程安全分析
4.4.1. 成员变量和静态变量是否线程安全?
4.4.2. 局部变量是否线程安全?
4.4.3. 局部变量的线程安全
public static void test1() {
int i = 10;
i++;
}
每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享
public static void test1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: bipush 10
2: istore_0
3: iinc 0, 1
6: return
LineNumberTable:
line 10: 0
line 11: 3
line 12: 6
LocalVariableTable:
Start Length Slot Name Signature
3 4 0 i I
4.4.3.1. 成员变量的例子
package com.bloom.concurrent.chapter4;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
/**
* @description:
* @author: teago
* @time: 2020/5/17 09:12
*/
@Slf4j
public class TestThreadSafe {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadUnsafe test = new ThreadUnsafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + i).start();
}
}
}
class ThreadUnsafe {
List<String> list = new ArrayList<>();
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
// { 临界区, 会产生竞态条件
method2();
method3();
// } 临界区
}
}
private void method2() {
list.add("1");
}
private void method3() {
list.remove(0);
}
}
其中一种情况是,如果线程2 还未 add,线程1 remove 就会报错:
分析:
- 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
- method3 与 method2 分析相同
4.4.3.2. 改为局部变量
package com.bloom.concurrent.chapter4;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
/**
* @description:
* @author: teago
* @time: 2020/5/17 09:12
*/
@Slf4j
public class TestThreadSafe {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
/* ThreadUnsafe test = new ThreadUnsafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + i).start();
}*/
ThreadSafe test2 = new ThreadSafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test2.method1(LOOP_NUMBER);
}, "Thread" + i).start();
}
}
}
class ThreadUnsafe {
List<String> list = new ArrayList<>();
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
// { 临界区, 会产生竞态条件
method2();
method3();
// } 临界区
}
}
private void method2() {
list.add("1");
}
private void method3() {
list.remove(0);
}
}
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list) {
list.add("1");
}
private void method3(ArrayList<String> list) {
list.remove(0);
}
}
那么就不会有上述问题了
分析:
- list 是局部变量,每个线程调用时会创建其不同实例,没有共享
- 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象
- method3 的参数分析与 method2 相同
4.4.3.3. 局部变量——暴露引用
方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?
- 情况1:有其它线程调用 method2 和 method3
- 不会有线程安全问题
- 情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,即
package com.bloom.concurrent.chapter4;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
/**
* @description:
* @author: teago
* @time: 2020/5/17 09:12
*/
@Slf4j
public class TestThreadSafe {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
/* ThreadUnsafe test = new ThreadUnsafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + i).start();
}
ThreadSafe test2 = new ThreadSafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test2.method1(LOOP_NUMBER);
}, "Thread" + i).start();
}*/
ThreadSafeSubClass test2 = new ThreadSafeSubClass();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test2.method1(LOOP_NUMBER);
}, "Thread" + i).start();
}
}
}
class ThreadUnsafe {
List<String> list = new ArrayList<>();
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
// { 临界区, 会产生竞态条件
method2();
method3();
// } 临界区
}
}
private void method2() {
list.add("1");
}
private void method3() {
list.remove(0);
}
}
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
public void method2(ArrayList<String> list) {
list.add("1");
}
public void method3(ArrayList<String> list) {
list.remove(0);
}
}
class ThreadSafeSubClass extends ThreadSafe {
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}
运行结果:(出错)
因为产生了共享资源,所谓的共享资源就是两个不同的线程访问了同一资源。
这时候我们把代码改成final、private,让其不能被继承,也是可以保证线程安全的。
从这个例子可以看出 private 或 final 提供【安全】的意义所在,请体会开闭原则中的【闭】
4.5. 线程安全类
4.5.1. 常见线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent 包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为:
Hashtable table = new Hashtable();
new Thread(()->{
table.put("key", "value1");
}).start();
new Thread(()->{
table.put("key", "value2");
}).start();
4.5.2. 线程安全类方法的组合
分析下面代码是否线程安全?
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
table.put("key", value);
}
4.5.3. 不可变类线程安全性
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的。
有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?
查看substring源码:
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
//创建了一个新的String对象
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
//对原有数据进行了copy,然后放入到新的String对象中
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
4.5.4. 实例分析
4.5.4.1. 例1
public class MyServlet extends HttpServlet {
// 是否安全?
Map<String,Object> map = new HashMap<>();
// 是否安全?
String S1 = "...";
// 是否安全?
final String S2 = "...";
// 是否安全?
Date D1 = new Date();
// 是否安全?
final Date D2 = new Date();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
// 使用上述变量
}
}
4.5.4.2. 例2
public class MyServlet extends HttpServlet {
// 是否安全?
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 记录调用次数
private int count = 0;
public void update() {
// ...
count++;
}
}
因为Servlet是单例对象。故而是线程共享的,那么MyServlet也是线程共享的,故而userService也是线程不安全的,上述程序应该避免在UserServiceImpl中定义成员变量,使用局部变量的方式去控制线程安全,或者去加互斥锁。
4.5.4.3. 例3
@Aspect
@Component
public class MyAspect {
// 是否安全?
private long start = 0L;
@Before("execution(* *(..))")
public void before() {
start = System.nanoTime();
}
@After("execution(* *(..))")
public void after() {
long end = System.nanoTime();
System.out.println("cost time:" + (end-start));
}
}
- 这是Spring中的一个切面类,Spring中没有强调的对象都是单例的,故而,start是线程共享的,会造成线程不安全
- 改法尝试:
4.5.4.4. 例4
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 是否安全
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
public void update() {
String sql = "update user set password = ? where username = ?";
// 是否安全
try (Connection conn = DriverManager.getConnection("","","")){
// ...
} catch (Exception e) {
// ...
}
}
}
UserDaoImpl虽然是线程共享的,但是其中没有全局变量,而是一个单独的一个方法,故而是线程安全的,所以整体是线程安全的。
4.5.4.5. 例5
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 是否安全
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
// 是否安全
private Connection conn = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}
线程不安全的,因为Connection conn被作为了共享变量。
4.5.4.6. 例6
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
public void update() {
UserDao userDao = new UserDaoImpl();
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
// 是否安全
private Connection = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}
虽然Connection 不是线程安全的,但是每次在进行userDao.update()调用的时候,使用的都是new出来的新的对象。
4.5.4.7. 例7
public abstract class Test {
public void bar() {
// 是否安全
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
foo(sdf);
}
public abstract foo(SimpleDateFormat sdf);
public static void main(String[] args) {
new Test().bar();
}
}
其中 foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法
public void foo(SimpleDateFormat sdf) {
String dateStr = "1999-10-11 00:00:00";
for (int i = 0; i < 20; i++) {
new Thread(() -> {
try {
sdf.parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
请比较 JDK 中 String 类的实现【String是开闭原则的最好体现者】
4.5.5. 是否存在线程安全问题
4.5.5.1. 卖票
package com.bloom.concurrent.chapter4;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Vector;
import java.util.concurrent.TimeUnit;
/**
* @description:
* @author: teago
* @time: 2020/5/17 09:33
*/
@Slf4j
public class ExerciseSell {
public static void main(String[] args) {
TicketWindow ticketWindow = new TicketWindow(2000);
List<Thread> list = new ArrayList<>();
// 用来存储买出去多少张票
List<Integer> sellCount = new Vector<>();
for (int i = 0; i < 2000; i++) {
Thread t = new Thread(() -> {
// 分析这里的竞态条件
int count = ticketWindow.sell(randomAmount());
try {
TimeUnit.SECONDS.sleep(randomAmount());
} catch (InterruptedException e) {
e.printStackTrace();
}
sellCount.add(count);
});
list.add(t);
t.start();
}
list.forEach((t) -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 买出去的票求和
log.debug("selled count:{}",sellCount.stream().mapToInt(c -> c).sum());
// 剩余票数
log.debug("remainder count:{}", ticketWindow.getCount());
}
// Random 为线程安全
static Random random = new Random();
// 随机 1~5
public static int randomAmount() {
return random.nextInt(5) + 1;
}
}
class TicketWindow {
private int count;
public TicketWindow(int count) {
this.count = count;
}
public int getCount() {
return count;
}
public int sell(int amount) {
if (this.count >= amount) {
this.count -= amount;
return amount;
} else {
return 0;
}
}
}
测试问题:
有时候线程之间没有产生指令的混乱,不好演示,所以可以采用下面两中方式:
- 方式一:休眠一个随机的时间,增加上下文切换的几率:

发现以下问题:售出的票数+剩余的票数=2002>2000。
09:38:31 [main] com.bloom.concurrent.chapter4.ExerciseSell - selled count:2002
09:38:31 [main] com.bloom.concurrent.chapter4.ExerciseSell - remainder count:0
- 方式二:如果通过上述修改还不能演示出来问题所在,进行此操作。
- 测试脚本:(目的是为了多运行几次)【window下】
for /L %n in (1,1,10) do java -cp "
.;C:\Users\manyh\.m2\repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;
C:\Users\manyh\.m2\repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;
C:\Users\manyh\.m2\repository\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar"
com.bloom.concurrent.chapter4.ExerciseSell
说明线程是不安全的,那么哪个地方是不安全的呢?
- 考虑临界区,根据临界区的定义,可以定位到如下位置:

- sellCount是一个线程安全的集合,那么问题只有能出现在sell方法
- 参看sell方法,发现count是共享变量,并且sell方法没有做互斥处理。

解决方案:将sell方法设定为synchronized方法。
4.5.5.2. 转账
package com.bloom.concurrent.chapter4;
import lombok.extern.slf4j.Slf4j;
import java.util.Random;
/**
* @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
* @since : 2020/4/29 02:50
*/
@Slf4j
public class ExerciseTransfer {
public static void main(String[] args) throws InterruptedException {
//账户A 1000
Account a = new Account(1000);
//账户B 1000
Account b = new Account(1000);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
//A转账给B
a.transfer(b, randomAmount());
}
}, "t1");
Thread t2 = new Thread(() -> {
//B转账给A
for (int i = 0; i < 1000; i++) {
b.transfer(a, randomAmount());
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
// 查看转账2000次后的总金额
log.debug("total:{}", (a.getMoney() + b.getMoney()));
}
// Random 为线程安全
static Random random = new Random();
// 随机 1~100
public static int randomAmount() {
return random.nextInt(100) + 1;
}
}
class Account {
private int money;
public Account(int money) {
this.money = money;
}
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
public void transfer(Account target, int amount) {
if (this.money > amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}
运行结果:
说明线程不安全。
分析:考虑谁是共享变量,发现A和B的account是都是共享变量,如何锁住共同的共享变量呢?可以使用下述方式:
4.6. Monitor原理
Java对象在内存中是由两部分组成:对象头和对象的成员变量。
HopSpot虚拟机的对象头(Object Header)分为两部分,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄(Generational GC Age)等。这部分数据的长度在32位和64位的Java虚拟机中分别占用32个或64个比特,官方称它为“Mark Word”。这部分是实现轻量级锁和偏向锁的关键。另一部分用于存储指向方法区对象类型数据的指针,如果是数组对象,还有一个额外的部分用于存储数组长度。
- 普通对象
|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|
- 数组对象
|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|
- 32 位虚拟机 Mark Word 结构为
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| hashcode:25 | age:4 | biased_lock:0 | 01 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | 00 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|-------------------------------------------------------|--------------------|
- 64 位虚拟机 Mark Word
|--------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|--------------------------------------------------------------------|--------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
|--------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
|--------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | 00 | Lightweight Locked |
|--------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked |
|--------------------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|--------------------------------------------------------------------|--------------------|
参考资料
https://stackoverflow.com/questions/26357186/what-is-in-java-object-header【stackoverflow】
https://blog.csdn.net/scdn_cp/article/details/86491792【csnd】
- Monitor
Monitor被翻译为监视器或管程
每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针。
Monitor结构如下所示:![image.png]()
- 刚开始Monitor中Owner为null;
- 当Thread-2执行了synchronized(obj)就会将Monitor的所有者Owner置为Thread-2,Monitor中只有一个Owner;
- 在Thread-2上锁的过程中,如果Thread-3,Thread-4,Thread-5也来执行synchronized(obj),就会进入EntryList BLOCKED ;
- Thread-2执行完同步代码块的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争是非公平的;
- 图中WaitSet中的Thread-0,Thread-1是之前获得过锁,但条件不满足进入WAITING状态的线程,后面讲wait-notify时会分析;
注意:
- synchronized必须是进入同一对象的monitor才有上述结果
- 不加synchronized的对象不会关联监视器,不遵守以上规则
4.7. synchronized原理
- 演示代码
package com.bloom.concurrent.chapter4;
import lombok.extern.slf4j.Slf4j;
/**
* @description:
* @author: teago
* @time: 2020/5/24 11:13
*/
@Slf4j
public class Example5 {
private final static Object lock = new Object();
static int num = 0;
public static void main(String[] args) {
synchronized (lock){
num++;
}
}
}
- 对应的字节码为:
由以下可知:synchronized中的代码出错了也可以释放锁****。
// class version 52.0 (52)
// access flags 0x21
public class com/bloom/concurrent/chapter4/Example5 {
// compiled from: Example5.java
// access flags 0x1A
private final static Lorg/slf4j/Logger; log
// access flags 0x1A
private final static Ljava/lang/Object; lock
// access flags 0x8
static I num
// access flags 0x1
public <init>()V
L0
LINENUMBER 11 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/bloom/concurrent/chapter4/Example5; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x9
public static main([Ljava/lang/String;)V
TRYCATCHBLOCK L0 L1 L2 null
TRYCATCHBLOCK L2 L3 L2 null
L4
LINENUMBER 18 L4
GETSTATIC com/bloom/concurrent/chapter4/Example5.lock : Ljava/lang/Object;
DUP
ASTORE 1
MONITORENTER
L0
LINENUMBER 19 L0
GETSTATIC com/bloom/concurrent/chapter4/Example5.num : I
ICONST_1
IADD
PUTSTATIC com/bloom/concurrent/chapter4/Example5.num : I
L5
LINENUMBER 20 L5
ALOAD 1
MONITOREXIT
L1
GOTO L6
L2
FRAME FULL [[Ljava/lang/String; java/lang/Object] [java/lang/Throwable]
ASTORE 2
ALOAD 1
MONITOREXIT
L3
ALOAD 2
ATHROW
L6
LINENUMBER 22 L6
FRAME CHOP 1
RETURN
L7
LOCALVARIABLE args [Ljava/lang/String; L4 L7 0
MAXSTACK = 2
MAXLOCALS = 3
// access flags 0x8
static <clinit>()V
L0
LINENUMBER 10 L0
LDC Lcom/bloom/concurrent/chapter4/Example5;.class
INVOKESTATIC org/slf4j/LoggerFactory.getLogger (Ljava/lang/Class;)Lorg/slf4j/Logger;
PUTSTATIC com/bloom/concurrent/chapter4/Example5.log : Lorg/slf4j/Logger;
L1
LINENUMBER 13 L1
NEW java/lang/Object
DUP
INVOKESPECIAL java/lang/Object.<init> ()V
PUTSTATIC com/bloom/concurrent/chapter4/Example5.lock : Ljava/lang/Object;
L2
LINENUMBER 15 L2
ICONST_0
PUTSTATIC com/bloom/concurrent/chapter4/Example5.num : I
RETURN
MAXSTACK = 2
MAXLOCALS = 0
}
synchronized的工作方式是让每个对象都关联一个monitor,而monitor是真正的锁,但是monitor锁是由操作系统提供的,要使用它成本是比较高的,如果是每次进入synchronized都需要获取monitor锁,对于程序运行的性能是有影响的,基于这个理由,Java6开始,对synchronized获取锁的方式进行了一些改进,进行了一些优化,从直接使用monitor锁改成了还可以使用轻量级锁、偏向锁等来进行优化。
4.8. synchronized原理进阶
4.8.1. 小故事
故事角色
- 老王 - JVM
- 小南 - 线程
- 小女 - 线程
- 房间 - 对象
- 房间门上 - 防盗锁 - Monitor
- 房间门上 - 小南书包 - 轻量级锁
- 房间门上 - 刻上小南大名 - 偏向锁
- 批量重刻名 - 一个类的偏向锁撤销到达 20 阈值
- 不能刻名字 - 批量撤销该类对象的偏向锁,设置该类不可偏向
小南要使用房间保证计算不被其它人干扰(原子性),最初,他用的是防盗锁,当上下文切换时,锁住门。这样,即使他离开了,别人也进不了门,他的工作就是安全的。
但是,很多情况下没人跟他来竞争房间的使用权。小女是要用房间,但使用的时间上是错开的,小南白天用,小女晚上用。每次上锁太麻烦了,有没有更简单的办法呢?
第一次优化:【轻量级锁】
小南和小女商量了一下,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是自己的,那么就在门外等,并通知对方下次用锁门的方式。
后来,小女回老家了,很长一段时间都不会用这个房间。小南每次还是挂书包,翻书包,虽然比锁门省事了,但仍然觉得麻烦。
第二次优化:【偏向锁】
于是,小南干脆在门上刻上了自己的名字:【小南专属房间,其它人勿用】,下次来用房间时,只要名字还在,那么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦掉,升级为挂书包的方式。
同学们都放假回老家了,小南就膨胀了,在 20 个房间刻上了自己的名字,想进哪个进哪个。后来他自己放假回老家了,这时小女回来了(她也要用这些房间),结果就是得一个个地擦掉小南刻的名字,升级为挂书包的方式。老王觉得这成本有点高,提出了一种批量重刻名的方法,他让小女不用挂书包了,可以直接在门上刻上自己的名字
后来,刻名的现象越来越频繁,老王受不了了:算了,这些房间都不能刻名了,只能挂书包。
4.8.2. 轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是synchronized。
假设有两个方法同步块,利用同一个对象加锁。
public static void method1(){
synchronized (object){
//同步块A
method2();
}
}
private static void method2() {
synchronized (object){
//同步块B
}
}
- 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的MarkWord。
- lock record:
- Mark Word
- 对象的指针
- lock record:

- 让锁记录中的Object reference指向锁对象,并尝试用CAS替换Object的Mark Word,将Mark Word的值存入锁记录。
- Lock Record 中的状态是00 ,对象中的状态是01;【参见:Mark Word】(00表示:Normal ,01表示:轻量级锁。)

- 如果CAS替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁,这时图示如下:

- 如果CAS替换失败,有两种情况:
- 如果是其他线程已经持有了该Object的轻量级锁,这是表明有竞争,进入锁膨胀过程;
- 如果是自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数。

- 当退出synchronized代码块(解锁时)如果有取值为null的锁记录,表示有重入,这是重置锁记录,表示重入计数减一。

- 当退出synchronized代码块(解锁时)锁记录的值不为null,这时使用CAS将Mark Word的值恢复给对象(还原工作)
4.8.3. 锁膨胀
如果在尝试加轻量级锁的过程中,CAS操作无法成功,这是一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这是需要进行锁膨胀,将轻量级锁变为重量级锁。
private final static Object object = new Object();
public static void method1(){
synchronized (object){
//同步块A
method2();
}
}
- 当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁,如下图所示:
![]()
- 这时Thread-1加轻量级锁失败,进入锁膨胀流程:
- 即为Object对象申请Monitor锁,让Object指向重量级锁地址
- 然后自己进入Monitor的EntityList BLOCKED(因为Thread-1没有竞争到锁)

- 当Thread-0退出同步块解锁时,使用CAS将Mark Word的值恢复给对象头,失败,这时会进入重量级锁解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null(表示此是没有主人),唤醒EntryList中的BLOCKED线程。
4.8.4. 自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这是当前线程就可以避免阻塞。【因为阻塞要发生上下文切换】
自旋成功的情况:
自旋失败的情况:
- 在Java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋的可能性会很高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
- 自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。
- Java7之后不能控制是否开启自旋锁功能。
4.8.5. 偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次发现锁重入时仍然需要执行CAS操作。
Java6中引入了偏向锁来做进一步优化;只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的表示没有竞争,不用重新CAS,以后只要不发生竞争,这个对象就归该线程所有。
例如:
private final static Object object = new Object();
public static void method1(){
synchronized (object){
//同步块A
method2();
}
}
private static void method2() {
synchronized (object){
//同步块B
method3();
}
}
private static void method3(){
synchronized (object){
//同步块C
}
}
4.8.5.1. 偏向状态
回忆一下对象头格式:
一个对象创建时:
- 如果开启了偏向锁(默认开启),那么对象创建后,mark word值为0x05即后三位为101,这是它的thread、epoch、age都为0;
- 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数:-XX:BiasedLockingStartupDelay=0来禁止延迟;
- 如果没有开启偏向锁,那么对象创建后,Mark word只为0x01即后三位为001,这时它的hashcode、age都为0,第一次用到hashcode时才会赋值。
4.8.5.1.1. 验证延迟性
- 代码:
package com.bloom.concurrent.chapter4;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
import java.util.concurrent.TimeUnit;
/**
* @description: *-XX:+UseBiasedLocking 【默认开启】-XX:BiasedLockingStartupDelay=0 【取消延时】
* @author: teago
* @time: 2020/5/24 13:07
*/
@Slf4j
public class Example6 {
public static void main(String[] args) throws Exception {
Dog dog = Dog.builder().age(1).name("Tom").sex(true).build();
String toPrintable = ClassLayout.parseInstance(dog).toPrintable();
log.info(toPrintable);
TimeUnit.SECONDS.sleep(5);
log.info("延迟5秒打印结果如下所示:");
String printable2 = ClassLayout.parseInstance(new Dog()).toPrintable();
System.out.println(printable2);
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
class Dog {
private Integer age; //4
private String name; //4
private boolean sex; //1
// 9
}
- 运行结果
Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
13:14:26 [main] com.bloom.concurrent.chapter4.Example6 - com.bloom.concurrent.chapter4.Dog object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 2a 67 01 f8 (00101010 01100111 00000001 11111000) (-134125782)
12 1 boolean Dog.sex true
13 3 (alignment/padding gap)
16 4 java.lang.Integer Dog.age 1
20 4 java.lang.String Dog.name (object)
Instance size: 24 bytes
Space losses: 3 bytes internal + 0 bytes external = 3 bytes total
13:14:31 [main] com.bloom.concurrent.chapter4.Example6 - 延迟5秒打印结果如下所示:
com.bloom.concurrent.chapter4.Dog object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 2a 67 01 f8 (00101010 01100111 00000001 11111000) (-134125782)
12 1 boolean Dog.sex false
13 3 (alignment/padding gap)
16 4 java.lang.Integer Dog.age null
20 4 java.lang.String Dog.name null
Instance size: 24 bytes
Space losses: 3 bytes internal + 0 bytes external = 3 bytes total
第一次打印为:1,不是默认开启的吗?不应该是5吗?其实是因为偏向锁默认是延迟的。
故而:中间让程序睡眠了5秒。
第二次打印为:5,说明了上述结论。
当然可以使用下述参数取消偏向锁延迟:
-XX:+UseBiasedLocking 【默认开启】
-XX:BiasedLockingStartupDelay=0 【取消延时】
4.8.5.1.2. 验证偏向锁原理
- 代码
package com.bloom.concurrent.chapter4;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
/**
* @description:
* @author: teago
* @time: 2020/5/24 13:17
*/
@Slf4j
public class TestBiased2 {
public static void main(String[] args) {
Student student = new Student();
log.debug("执行synchronized前");
System.out.println(ClassLayout.parseInstance(student).toPrintable());
synchronized (student) {
log.debug("执行synchronized中");
System.out.println(ClassLayout.parseInstance(student).toPrintable());
}
log.debug("执行synchronized后");
System.out.println(ClassLayout.parseInstance(student).toPrintable());
}
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class Student {
private Integer id;
private String name;
}
- 运行结果
4.8.5.1.3. 禁用偏向锁
添加VM参数:-XX:-UseBiasedLocking禁用偏向锁。
禁用偏向锁之后,默认就变成了轻量级锁。
4.8.5.1.4. 调用hashCode()
- 测试代码:
package com.bloom.concurrent.chapter4;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
/**
* @description: -XX:BiasedLockingStartupDelay=0
* @author: teago
* @time: 2020/5/24 13:32
*/
@Slf4j
public class TestBiased3 {
public static void main(String[] args) {
Emp emp = new Emp();
String printable1 = ClassLayout.parseInstance(emp).toPrintable();
log.info(printable1);
//调用hashcode
emp.hashCode();
String printable2 = ClassLayout.parseInstance(emp).toPrintable();
log.info(printable2);
}
}
class Emp {
}
- 运行结果:

发现偏向锁被撤销掉了,因为:
- 有偏向锁的时候,54位存储的是线程ID,
- 调用了hashCode()之后,hashCode需要31位,此时Mark Word中没有位置了,故而需要删除thread占用的位置来存放hashCode。
- 那为什么轻量级锁或重量级锁调用hashCode的时候不会有这样的问题呢?
- 轻量级锁是将这些信息存放在锁记录中的。
- 重量级锁将这些信息存储在monitor对象中。
- 对于轻量级锁没有地方存储这些信息。
4.8.5.2. 撤销偏量级锁
- 调用hashCode(),见【4.8.5.1.4. 调用hashCode()】
- 其他线程使用
- 当其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁。
程序设置前提:偏向锁和轻量级锁的前提就是线程没有交错执行。故而代码中要使用wait()和notify()。
package com.bloom.concurrent.chapter4;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
/**
* @description:-XX:BiasedLockingStartupDelay=0
* @author: teago
* @time: 2020/5/24 13:38
*/
@Slf4j
public class TestBiased4 {
public static void main(String[] args) {
Dept dept = new Dept() ;
new Thread(()->{
printInfo(dept);
//这是为了让线程td1和td2交互执行,原因是:偏向锁和轻量级锁的前提就是线程没有交错执行。
synchronized (TestBiased4.class){
TestBiased4.class.notify();
} },"td1").start();
new Thread(()->{
synchronized (TestBiased4.class){
try {
TestBiased4.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
printInfo(dept);
synchronized (TestBiased4.class){
TestBiased4.class.notify();
} },"td2").start();
}
private static void printInfo(Dept dept) {
log.debug("执行synchronized前");
System.out.println(ClassLayout.parseInstance(dept).toPrintable());
synchronized (dept) {
log.debug("执行synchronized中");
System.out.println(ClassLayout.parseInstance(dept).toPrintable());
}
log.debug("执行synchronized后");
System.out.println(ClassLayout.parseInstance(dept).toPrintable());
}
}
class Dept{
}
代码运行结果:
| td1 | td2 | |
|---|---|---|
| 执行synchronized前 | 101【偏向锁】 | 101【偏向锁】 |
| 执行synchronized中 | 101【偏向锁】 | 000【轻量级锁】 |
| 执行synchronized后 | 101【偏向锁】 | 001【无锁】 |
[td1] com.bloom.concurrent.chapter4.TestBiased4 - 执行synchronized前
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
com.bloom.concurrent.chapter4.Dept object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 2a 67 01 f8 (00101010 01100111 00000001 11111000) (-134125782)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
13:39:34 [td1] com.bloom.concurrent.chapter4.TestBiased4 - 执行synchronized中
com.bloom.concurrent.chapter4.Dept object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 50 16 92 (00000101 01010000 00010110 10010010) (-1844031483)
4 4 (object header) fe 7f 00 00 (11111110 01111111 00000000 00000000) (32766)
8 4 (object header) 2a 67 01 f8 (00101010 01100111 00000001 11111000) (-134125782)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
13:39:34 [td1] com.bloom.concurrent.chapter4.TestBiased4 - 执行synchronized后
com.bloom.concurrent.chapter4.Dept object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 50 16 92 (00000101 01010000 00010110 10010010) (-1844031483)
4 4 (object header) fe 7f 00 00 (11111110 01111111 00000000 00000000) (32766)
8 4 (object header) 2a 67 01 f8 (00101010 01100111 00000001 11111000) (-134125782)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
13:39:34 [td2] com.bloom.concurrent.chapter4.TestBiased4 - 执行synchronized前
com.bloom.concurrent.chapter4.Dept object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 50 16 92 (00000101 01010000 00010110 10010010) (-1844031483)
4 4 (object header) fe 7f 00 00 (11111110 01111111 00000000 00000000) (32766)
8 4 (object header) 2a 67 01 f8 (00101010 01100111 00000001 11111000) (-134125782)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
13:39:34 [td2] com.bloom.concurrent.chapter4.TestBiased4 - 执行synchronized中
com.bloom.concurrent.chapter4.Dept object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 80 f8 9a 0f (10000000 11111000 10011010 00001111) (261814400)
4 4 (object header) 00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
8 4 (object header) 2a 67 01 f8 (00101010 01100111 00000001 11111000) (-134125782)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
13:39:34 [td2] com.bloom.concurrent.chapter4.TestBiased4 - 执行synchronized后
com.bloom.concurrent.chapter4.Dept object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 2a 67 01 f8 (00101010 01100111 00000001 11111000) (-134125782)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
- 调用wait/notify
4.8.5.3. 批量重偏向
如果对象虽然被多个线程访问,当没有竞争,这时偏向了线程td1的对象人有机会重新偏向td2,重偏向会重置对象的Thread ID。
当撤销偏向锁阀值超过20次后,JVM会这样觉得:我是不是偏向错了呢,于是会给这些对象加锁时重新偏向至加锁线程。
观察一下代码运行结果发现:
- 刚开始偏向锁中的thread 为 td1 ;
- td2运行的时候,前20条打印结果发现,偏向锁变为轻量级锁,然后synchronized执行之后,锁状态变为normal;
- 20次之后,JVM发现撤销偏向状态影响性能,可以重新设置偏向锁的线程id。
package com.bloom.concurrent.chapter4;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
import java.util.Vector;
/**
* @description:
* @author: teago
* @time: 2020/5/24 13:43
*/
@Slf4j
public class TestBiased5 {
public static void main(String[] args) {
Vector<Teacher> teacherVector = new Vector<>();
new Thread(()->{
for (int i = 0; i < 30 ; i++) {
Teacher teacher = new Teacher();
teacherVector.add(teacher);
log.debug("执行synchronized前");
System.out.println(ClassLayout.parseInstance(teacher).toPrintable());
synchronized (teacher) {
log.debug("执行synchronized中");
System.out.println(ClassLayout.parseInstance(teacher).toPrintable());
}
log.debug("执行synchronized后");
System.out.println(ClassLayout.parseInstance(teacher).toPrintable());
synchronized (teacherVector){
teacherVector.notify();
}
}
},"td1").start();
new Thread(()->{
synchronized (teacherVector){
try {
teacherVector.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int i = 0; i < 30 ; i++) {
Teacher teacher = teacherVector.get(i);
log.debug("第"+i+"......执行synchronized前");
System.out.println(ClassLayout.parseInstance(teacher).toPrintable());
synchronized (teacher) {
log.debug("第"+i+"......执行synchronized中");
System.out.println(ClassLayout.parseInstance(teacher).toPrintable());
}
log.debug("第"+i+"......执行synchronized后");
System.out.println(ClassLayout.parseInstance(teacher).toPrintable());
}
},"td2").start();
}
}
class Teacher{
}
4.8.5.4. 批量撤销
当撤销偏向锁阀值超过40次,JVM会这样觉得:自己确实偏向错了,根本不应该偏向,于是整个类的所有对象都不会变为不可偏向的,新建的对象也是不可偏向的。
- 要求:调整 loopNumber =39 -----> 38, 观察main线程新创建的对象。
package com.bloom.concurrent.chapter4;
import org.openjdk.jol.info.ClassLayout;
import java.util.Vector;
import java.util.concurrent.locks.LockSupport;
/**
* @description:
* @author: teago
* @time: 2020/5/24 13:48
*/
public class TestBiased6 {
static Thread thread1 ,thread2 , thread3 ;
public static void main(String[] args) {
int loopNumber = 39 ;
Vector<Tiger> tigerVector = new Vector<>() ;
thread1 = new Thread(()->{
for (int i = 0; i < loopNumber ; i++) {
Tiger tiger = new Tiger() ;
tigerVector.add(tiger);
synchronized (tiger){
System.out.println(Thread.currentThread().getName()+"......"+ClassLayout.parseInstance(tiger).toPrintable());
}
}
LockSupport.unpark(thread2);
});
thread1.start();
//thread2
thread2 = new Thread(()->{
LockSupport.park();
for (int i = 0; i < loopNumber ; i++) {
Tiger tiger = tigerVector.get(i);
synchronized (tiger){
System.out.println(Thread.currentThread().getName()+"......"+ ClassLayout.parseInstance(tiger).toPrintable());
}
}
LockSupport.unpark(thread3);
});
thread2.start();
//thread3
thread3 = new Thread(()->{
LockSupport.park();
for (int i = 0; i < loopNumber ; i++) {
Tiger tiger = tigerVector.get(i);
synchronized (tiger){
System.out.println(Thread.currentThread().getName()+"......"+ClassLayout.parseInstance(tiger).toPrintable());
}
}
});
thread3.start();
try {
thread3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
//观察新建对象偏向情况
System.out.println(Thread.currentThread().getName()+"......"+ClassLayout.parseInstance(new Tiger()).toPrintable());
}
}
class Tiger {
}
4.9. wait/notify
4.9.1. 小故事 - 为什么需要wait
- 由于条件不满足,小南不能继续进行计算
- 但小南如果一直占用着锁,其它人就得一直阻塞,效率太低

- 于是老王单开了一间休息室(调用 wait 方法),让小南到休息室(WaitSet)等着去了,但这时锁释放开,其它人可以由老王随机安排进屋
- 直到小M将烟送来,大叫一声 [ 你的烟到了 ] (调用 notify 方法)

- 小南于是可以离开休息室,重新进入竞争锁的队列
4.9.2. wait/notify原理

- Owner线程发现条件不足,调用wait方法,即可进入WaitSet变为WAITING状态;
- BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片;
- BLOCKED线程会在Owner线程释放锁时唤醒;
- WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后不意味着立刻获得锁,仍需进入EntryList重现竞争。
4.9.3. wait¬ify相关API
obj.wait()让进入 object 监视器的线程到 waitSet 等待obj.notify()在 object 上正在 waitSet 等待的线程中挑一个唤醒obj.notifyAll()让 object 上正在 waitSet 等待的线程全部唤醒
它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法。
验证:
- 在没有获得对象锁的前提下调用wait方法
package com.bloom.concurrent.chapter4;
/**
* @description:
* * wait/notify 演示
* * 在没有获得对象锁的前提下调用wait方法
* @author: teago
* @time: 2020/5/24 13:55
*/
public class Example7 {
public static void main(String[] args) {
Object object = new Object();
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:
Exception in thread "main" java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at at com.bloom.concurrent.chapter4.Example7.main(Example7.java:14)
修改:
package com.bloom.concurrent.chapter4;
/**
* @description:
* * wait/notify 演示
*
* @author: teago
* @time: 2020/5/24 13:55
*/
public class Example8 {
public static void main(String[] args) {
Object o = new Object();
synchronized (o){
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
notify也是这样的,在没有获取对象锁的时候,也会出现上述一次。
- notify()和notifyAll()的区别
- notify() 挑选当前锁中的其中的一个唤醒
- notifyAll() 唤醒当前锁所有线程
package com.bloom.concurrent.chapter4;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
/**
* wait/notify 演示
* 验证notify和notifyAll的区别
* @author : teago
* @time: 2020/5/24 14:15
*/
@Slf4j
public class Example9 {
public static void main(String[] args) {
Object object = new Object() ;
new Thread(()->{
log.debug("执行");
synchronized (object){
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("执行其他代码");
}).start();
new Thread(()->{
log.debug("执行");
synchronized (object){
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("执行其他代码");
}).start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("执行");
synchronized (object){
//object.notify();
object.notifyAll();
}
}
}
- wait()、wait(long)、wait(long ,int)的区别
- wait()
public final void wait() throws InterruptedException {
wait(0);
}
- wait(long)
public final native void wait(long timeout) throws InterruptedException;
- wait(long,int)
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
4.9.4. wait/notify的正确使用姿势
4.9.4.1. wait和sleep
- 区别
- sleep是Thread的静态方法,wait是Object方法;
- sleep不需要强制和synchronized使用,但wait需要和synchronized一起用;
- sleep在睡眠的同时,不会释放对象锁,但是wait在等待的时候会释放对象锁。
- 共同点
4.9.4.2. step1
思考下面的实现行吗,为什么?
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
}, "其它人").start();
}
sleep(1);
new Thread(() -> {
synchronized (room) {
hasCigarette = true;
log.debug("烟到了噢!");
room.notify();
}
}, "送烟的").start();
- 其它干活的线程,都要一直阻塞,效率太低
- 小南线程必须睡足 2s 后才能醒来,就算烟提前送到,也无法立刻醒来
- 加了 synchronized (room) 后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main 没加 synchronized 就好像 main 线程是翻窗户进来的
- 解决方法,使用 wait - notify 机制
4.9.2.3. step2
思考下面的实现行吗,为什么?
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
}, "其它人").start();
}
sleep(1);
new Thread(() -> {
synchronized (room) {
hasCigarette = true;
log.debug("烟到了噢!");
room.notify();
}
}, "送烟的").start();
4.9.2.4. step3
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
Thread thread = Thread.currentThread();
log.debug("外卖送到没?[{}]", hasTakeout);
if (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();
sleep(1);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notify();
}
}, "送外卖的").start();
4.9.2.5. step4
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notifyAll();
}
}, "送外卖的").start();
- 用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了
- 解决方法,用 while + wait,当条件不成立,再次 wait
4.9.2.6. step5
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
4.9.4.7. 总结
synchronized(lock) {
while(条件不成立) {
lock.wait();
}
// 干活
}
//另一个线程
synchronized(lock) {
lock.notifyAll();
}
4.10. 多线程设计模式——同步模式之保护性暂停
4.11. join原理
- 源码解析
- 参见多线程设计模式【1.3. 增加超时】
4.12. 多线程设计模式——生产者和消费者模式
4.13. park和unpark
4.13.1. 基本使用
它们是 LockSupport 类中的方法
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
先 park 再 unpark
Thread t1 = new Thread(() -> {
log.debug("start...");
sleep(1);
log.debug("park...");
LockSupport.park();
log.debug("resume...");
},"t1");
t1.start();
sleep(2);
log.debug("unpark...");
LockSupport.unpark(t1);
输出
18:42:52.585 c.TestParkUnpark [t1] - start...
18:42:53.589 c.TestParkUnpark [t1] - park...
18:42:54.583 c.TestParkUnpark [main] - unpark...
18:42:54.583 c.TestParkUnpark [t1] - resume...
先 unpark 再 park
Thread t1 = new Thread(() -> {
log.debug("start...");
sleep(2);
log.debug("park...");
LockSupport.park();
log.debug("resume...");
}, "t1");
t1.start();
sleep(1);
log.debug("unpark...");
LockSupport.unpark(t1);
输出
18:43:50.765 c.TestParkUnpark [t1] - start...
18:43:51.764 c.TestParkUnpark [main] - unpark...
18:43:52.769 c.TestParkUnpark [t1] - park...
18:43:52.769 c.TestParkUnpark [t1] - resume...
4.13.2. 特点
与 Object 的 wait & notify 相比
- wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
- park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】
- park & unpark 可以先 unpark,而 wait & notify 不能先 notify
4.13.3. park & unpark原理
每个线程都有自己的一个Parker对象,由三部分组成_counter,_cond和_mutex打个比喻。
- 线程就像一个旅人,Parker就像他随身携带的背包,条件变量【_cond】就好比背包中的帐篷,_counter就好比书包中的备用干粮(0为耗尽,1为充足);
- 调用park就时要看需不需要停下来歇息
- 如果备用干粮耗尽,那么钻进帐篷歇息
- 如果备用干粮充足,那么不需要停留,继续前进
- 调用unpark,就好比令干粮充足
- 如果这时线程还在帐篷,就唤醒让他继续前进
- 如果这时线程还在运行,那么下次他调用park时,仅是消耗掉备用干粮,不需停留继续前进
- 因为背包空间有限,多次调用unpark仅会补充一份备用干粮

- 当前线程调用Unsafe.park()方法
- 检测_conter,本情况为0,这时,获得_mutex互斥锁
- 线程进入_cond条件变量阻塞
- 设置_counter=0

4.14. 重新理解线程状态转换
4.14.1. 情况 1 NEW --> RUNNABLE
4.14.2. 情况 2 RUNNABLE <--> WAITING
t 线程用 synchronized(obj) 获取了对象锁后
- 调用
obj.wait()方法时,t 线程从RUNNABLE --> WAITING - 调用
obj.notify(),obj.notifyAll(),t.interrupt()时- 竞争锁成功,t 线程从
WAITING --> RUNNABLE - 竞争锁失败,t 线程从
WAITING --> BLOCKED
- 竞争锁成功,t 线程从
public class TestWaitNotify {
final static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码...."); // 断点
}
},"t1").start();
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码...."); // 断点
}
},"t2").start();
sleep(0.5);
log.debug("唤醒 obj 上其它线程");
synchronized (obj) {
obj.notifyAll(); // 唤醒obj上所有等待线程 断点
}
}
}
4.14.3. 情况 3 RUNNABLE <--> WAITING
- 当前线程调用
t.join()方法时,当前线程从RUNNABLE --> WAITING- 注意是当前线程在t 线程对象的监视器上等待
- t 线程运行结束,或调用了当前线程的
interrupt()时,当前线程从WAITING --> RUNNABLE
4.14.4. 情况 4 RUNNABLE <--> WAITING
- 当前线程调用
LockSupport.park()方法会让当前线程从RUNNABLE --> WAITING - 调用
LockSupport.unpark(目标线程)或调用了线程 的interrupt(),会让目标线程从WAITING --> RUNNABLE
4.14.5. 情况 5 RUNNABLE <--> TIMED_WAITING
t 线程用 synchronized(obj) 获取了对象锁后
- 调用
obj.wait(long n)方法时,t 线程从RUNNABLE --> TIMED_WAITING - t 线程等待时间超过了 n 毫秒,或调用
obj.notify(),obj.notifyAll(),t.interrupt()时
4.14.6. 情况 6 RUNNABLE <--> TIMED_WAITING
- 当前线程调用
t.join(long n)方法时,当前线程从RUNNABLE --> TIMED_WAITING- 注意是当前线程在t 线程对象的监视器上等待
- 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的
interrupt()时,当前线程从TIMED_WAITING --> RUNNABLE
4.14.7. 情况 7 RUNNABLE <--> TIMED_WAITING
- 当前线程调用
Thread.sleep(long n),当前线程从RUNNABLE --> TIMED_WAITING - 当前线程等待时间超过了 n 毫秒,当前线程从
TIMED_WAITING --> RUNNABLE
4.14.8. 情况 8 RUNNABLE <--> TIMED_WAITING
- 当前线程调用
LockSupport.parkNanos(long nanos)或LockSupport.parkUntil(long millis)时,当前线程从RUNNABLE --> TIMED_WAITING - 调用
LockSupport.unpark(目标线程)或调用了线程 的interrupt(),或是等待超时,会让目标线程从TIMED_WAITING--> RUNNABLE
4.14.9. 情况 9 RUNNABLE <--> BLOCKED
- t 线程用
synchronized(obj)获取了对象锁时如果竞争失败,从RUNNABLE --> BLOCKED - 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有
BLOCKED的线程重新竞争,如果其中 t 线程竞争成功,从BLOCKED --> RUNNABLE,其它失败的线程仍然BLOCKED
4.14.10. 情况 10 RUNNABLE <--> TERMINATED
4.15. 多把锁
4.15.1. 多把不相干的锁
一间大屋子有两个功能:睡觉、学习,互不相干。
现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低
解决方法是准备多个房间(多个对象锁)
例如
class BigRoom {
public void sleep() {
synchronized (this) {
log.debug("sleeping 2 小时");
Sleeper.sleep(2);
}
}
public void study() {
synchronized (this) {
log.debug("study 1 小时");
Sleeper.sleep(1);
}
}
}
执行
BigRoom bigRoom = new BigRoom();
new Thread(() -> {
bigRoom.compute();
},"小南").start();
new Thread(() -> {
bigRoom.sleep();
},"小女").start();
某次结果
12:13:54.471 [小南] c.BigRoom - study 1 小时
12:13:55.476 [小女] c.BigRoom - sleeping 2 小时
改进
class BigRoom {
private final Object studyRoom = new Object();
private final Object bedRoom = new Object();
public void sleep() {
synchronized (bedRoom) {
log.debug("sleeping 2 小时");
Sleeper.sleep(2);
}
}
public void study() {
synchronized (studyRoom) {
log.debug("study 1 小时");
Sleeper.sleep(1);
}
}
}
某次执行结果
12:15:35.069 [小南] c.BigRoom - study 1 小时
12:15:35.069 [小女] c.BigRoom - sleeping 2 小时
将锁的粒度细分
4.16. 线程活跃性
4.16.1. 死锁
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁t1 线程 获得 A对象 锁,接下来想获取 B对象的锁
t2 线程 获得 B对象 锁,接下来想获取 A对象的锁
4.161.1. 死锁现象
例:
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A) {
log.debug("lock A");
sleep(1);
synchronized (B) {
log.debug("lock B");
log.debug("操作...");
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (B) {
log.debug("lock B");
sleep(0.5);
synchronized (A) {
log.debug("lock A");
log.debug("操作...");
}
}
}, "t2");
t1.start();
t2.start();
执行结果:(陷入死锁)
12:22:06.962 [t2] c.TestDeadLock - lock B
12:22:06.962 [t1] c.TestDeadLock - lock A
4.16.1.2. 定位死锁
- 检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁:
4947 RemoteMavenServer36
5769 Jps
3868
5631 Launcher
haoyu@haodeMacBook-Pro ~ jstack 3868
2020-05-24 14:20:55
Full thread dump OpenJDK 64-Bit Server VM (11.0.6+8-b765.25 mixed mode):
Threads class SMR info:
_java_thread_list=0x0000600006c953c0, length=66, elements={
0x00007fee84861000, 0x00007fee84853000, 0x00007fee8500c800, 0x00007fee85013800,
0x00007fee85816000, 0x00007fee85817000, 0x00007fee8501b800, 0x00007fee85835800,
0x00007fee85014800, 0x00007fee8400d800, 0x00007fee84902800, 0x00007fee850ab800,
0x00007fee84812000, 0x00007fee44012800, 0x00007fee8591b800, 0x00007fee849de800,
0x00007fee85443800, 0x00007fee86174000, 0x00007fee8629e000, 0x00007fee34022800,
0x00007fee04020000, 0x00007fee866cc800, 0x00007fee84bec000, 0x00007fee84c11800,
0x00007fee843bf800, 0x00007fee845aa800, 0x00007fee8552a000, 0x00007fee44844800,
0x00007fee06c30000, 0x00007fee86485000, 0x00007fee06a20000, 0x00007fee06a56800,
0x00007fee22254000, 0x00007fee20f1a800, 0x00007fee221dc000, 0x00007fee22a8f000,
0x00007fee6400c800, 0x00007fee20e68800, 0x00007fee20ad7800, 0x00007fee3482f800,
0x00007fee2302f000, 0x00007fee8542a800, 0x00007fee22088000, 0x00007fee851d3000,
0x00007fee854c1000, 0x00007fee4487f000, 0x00007fee21afb000, 0x00007fee4486a000,
0x00007fee219b2800, 0x00007fee44809800, 0x00007fee04016800, 0x00007fee863a3000,
0x00007fee22087000, 0x00007fee34887000, 0x00007fee440bd000, 0x00007fee24054000,
0x00007fee0400b000, 0x00007fee229d2000, 0x00007fee20bd7800, 0x00007fee22cfe800,
0x00007fee23325000, 0x00007fee2077f800, 0x00007fee2077d000, 0x00007fee64436000,
0x00007fee2030d000, 0x00007fee64571800
}
"Reference Handler" #2 daemon prio=10 os_prio=31 cpu=95.10ms elapsed=12102.11s tid=0x00007fee84861000 nid=0x3603 waiting on condition [0x0000700007bde000]
java.lang.Thread.State: RUNNABLE
at java.lang.ref.Reference.waitForReferencePendingList(java.base@11.0.6/Native Method)
at java.lang.ref.Reference.processPendingReferences(java.base@11.0.6/Reference.java:241)
at java.lang.ref.Reference$ReferenceHandler.run(java.base@11.0.6/Reference.java:213)
"Finalizer" #3 daemon prio=8 os_prio=31 cpu=46.12ms elapsed=12102.11s tid=0x00007fee84853000 nid=0x4403 in Object.wait() [0x0000700007ce1000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(java.base@11.0.6/Native Method)
- waiting on <no object reference available>
at java.lang.ref.ReferenceQueue.remove(java.base@11.0.6/ReferenceQueue.java:155)
- waiting to re-lock in wait() <0x00000007ab8b9f88> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(java.base@11.0.6/ReferenceQueue.java:176)
at java.lang.ref.Finalizer$FinalizerThread.run(java.base@11.0.6/Finalizer.java:170)
4.16.1.3. 哲学家就餐问题(演示)

有五位哲学家,围坐在圆桌旁。
- 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
- 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
- 如果筷子被身边的人拿着,自己就得等待
筷子类
class Chopstick {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}
哲学家类
class Philosopher extends Thread {
Chopstick left;
Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
private void eat() {
log.debug("eating...");
Sleeper.sleep(1);
}
@Override
public void run() {
while (true) {
// 获得左手筷子
synchronized (left) {
// 获得右手筷子
synchronized (right) {
// 吃饭
eat();
}
// 放下右手筷子
}
// 放下左手筷子
}
}
}
就餐
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("苏格拉底", c1, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c5, c1).start();

**这种线程没有按预期结束,执行不下去的情况,归类为【活跃性】问题**,除了死锁以外,还有活锁和饥饿者两种情况。
**解决:使用RenntrantLock。** ### 4.16.2. 活锁 活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束, ```java public class TestLiveLock { static volatile int count = 10; static final Object lock = new Object();
public static void main(String[] args) {
new Thread(() -> {
// 期望减到 0 退出循环
while (count > 0) {
sleep(0.2);
count--;
log.debug("count: {}", count);
}
}, "t1").start();
new Thread(() -> {
// 期望超过 20 退出循环
while (count < 20) {
sleep(0.2);
count++;
log.debug("count: {}", count);
}
}, "t2").start();
}
}
**解决:错开交错执行的时间,让线程随机睡眠。**
<a name="nSgPq"></a>
### 4.16.3. 饥锁
很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题<br />下面我讲一下我遇到的一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题。
顺序加锁的解决方案
演示饥饿现象:
- 修改标注位置
<br />**如何解决饥饿现象,使用ReentrantLock.**
<a name="GXs3b"></a>
## 4.17. ReentrantLock
<a name="EF30i"></a>
### 4.17.1. 简介
相对于 synchronized 它具备如下特点
- 可中断
- 可以设置超时时间
- 可以设置为公平锁【防止线程饥饿】
- 支持多个条件变量【synchronized中只有一个waitset,ReentrantLock有多个waitset】
与 synchronized 一样,都支持可重入<br />基本语法:<br />synchronized是关键字级别保护临界区,ReentrantLock是对象级别保护临界区。
```java
ReentrantLock reentrantLock = new ReentrantLock();
// 获取锁
reentrantLock.lock();
try{
// 临界区
}catch (Exception e){
}finally {
//释放锁
reentrantLock.unlock();
}
4.17.1. 可重入
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
method1();
}
public static void method1() {
lock.lock();
try {
log.debug("execute method1");
method2();
} finally {
lock.unlock();
}
}
public static void method2() {
lock.lock();
try {
log.debug("execute method2");
method3();
} finally {
lock.unlock();
}
}
public static void method3() {
lock.lock();
try {
log.debug("execute method3");
} finally {
lock.unlock();
}
}
输出结果
17:59:11.862 [main] c.TestReentrant - execute method1
17:59:11.865 [main] c.TestReentrant - execute method2
17:59:11.865 [main] c.TestReentrant - execute method3
4.17.2. 可打断
可打断性可以防止死锁的发生。
被动打断:reentrantLock.lockInterruptibly();避免死锁的发生,这是一个被动的,需要调用thread.interrupted();
主动打断:使用锁超时【ReentrantLock#tryLock(long, TimeUnit)】到了时间还么有获取时间,也会被打断。【参见:锁超时】
⚠️:ReentrantLock#tryLock()不可被打断。【参加:锁超时】
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("启动...");
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("等锁的过程中被打断");
return;
}
try {
log.debug("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {
sleep(1);
t1.interrupt();
log.debug("执行打断");
} finally {
lock.unlock();
}
输出结果
16:43:07 [main] com.bloom.concurrent.chapter4.ReentrantLockDemo - 获得了锁
16:43:07 [t1] com.bloom.concurrent.chapter4.ReentrantLockDemo - 启动...
16:43:07 [main] com.bloom.concurrent.chapter4.ReentrantLockDemo - 执行打断
16:43:07 [t1] com.bloom.concurrent.chapter4.ReentrantLockDemo - 等锁的过程中被打断
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at com.bloom.concurrent.chapter4.ReentrantLockDemo.lambda$main$0(ReentrantLockDemo.java:22)
at java.lang.Thread.run(Thread.java:748)
4.17.3. 锁超时
4.17.3.1. ReentrantLock#tryLock()
- 是否可打断测试
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("启动...");
lock.lock();
try {
log.debug("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {
sleep(1);
t1.interrupt();
log.debug("执行打断");
sleep(1);
} finally {
log.debug("释放了锁");
lock.unlock();
}
运行结果:
18:06:56.261 [main] c.TestInterrupt - 获得了锁
18:06:56.265 [t1] c.TestInterrupt - 启动...
18:06:57.266 [main] c.TestInterrupt - 执行打断 // 这时 t1 并没有被真正打断, 而是仍继续等待锁
18:06:58.267 [main] c.TestInterrupt - 释放了锁
18:06:58.267 [t1] c.TestInterrupt - 获得了锁
结论:
不可以被打断。
4.17.3.2. ReentrantLock#tryLock(long, TimeUnit)
- 演示释放可以被打断
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("启动...");
try {
if (!lock.tryLock(1, TimeUnit.SECONDS)) {
log.debug("获取等待 1s 后失败,返回");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
log.debug("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {
sleep(2);
} finally {
lock.unlock();
}
运行结果:
19:55:08 [t1] com.beatshadow.concurrent.chapter4.Example24 - 尝试获取锁
19:55:08 [t1] com.beatshadow.concurrent.chapter4.Example24 - 被打断
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.tryAcquireNanos(AbstractQueuedSynchronizer.java:1245)
at java.util.concurrent.locks.ReentrantLock.tryLock(ReentrantLock.java:442)
at com.beatshadow.concurrent.chapter4.Example24.lambda$main$0(Example24.java:22)
at java.lang.Thread.run(Thread.java:748)
- 演示超时
package com.beatshadow.concurrent.chapter4;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* 验证{@link ReentrantLock#tryLock(long, TimeUnit)} 的可打断性
* @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
* @since : 2020/5/4 19:08
*/
@Slf4j
public class Example25 {
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
Thread thread = new Thread(() -> {
log.debug("尝试获取锁");
try {
//如果超时,则获取不到锁
if (!reentrantLock.tryLock(6,TimeUnit.SECONDS)) {
log.debug("获取不到锁");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("被打断");
return;
}
try{
log.debug("获取到锁");
}finally {
reentrantLock.unlock();
}
}, "t1");
//让主线程获取锁
log.debug("获取锁");
reentrantLock.lock();
thread.start();
//两秒钟后,解锁
try {
TimeUnit.SECONDS.sleep(2);
log.debug("解锁");
reentrantLock.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:
19:56:10 [main] com.beatshadow.concurrent.chapter4.Example25 - 获取锁
19:56:10 [t1] com.beatshadow.concurrent.chapter4.Example25 - 尝试获取锁
19:56:12 [main] com.beatshadow.concurrent.chapter4.Example25 - 解锁
19:56:12 [t1] com.beatshadow.concurrent.chapter4.Example25 - 获取到锁
改变条件:让主线程睡上7秒,运行结果如下所示:
19:57:11 [main] com.beatshadow.concurrent.chapter4.Example25 - 获取锁
19:57:11 [t1] com.beatshadow.concurrent.chapter4.Example25 - 尝试获取锁
19:57:17 [t1] com.beatshadow.concurrent.chapter4.Example25 - 获取不到锁
19:57:18 [main] com.beatshadow.concurrent.chapter4.Example25 - 解锁
4.17.3.3. 锁超时解决哲学家就餐问题
【4.16.1.3. 哲学家就餐问题】是一个死锁的问题,但是给出的一个解决方案造成了极锁,现在尝试使用锁超时解决死锁问题。
- 让筷子作为锁对象,又想使用ReentrantLock,那么需要extends ReentrantLock
- 修改Philosopher2对象中的run方法。
class Philosopher extends Thread {
Chopstick left;
Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
@Override
public void run() {
while (true) {
// 尝试获得左手筷子
if (left.tryLock()) {
try {
// 尝试获得右手筷子
if (right.tryLock()) {
try {
eat();
} finally {
right.unlock();
}
}
} finally {
left.unlock();
}
}
}
}
private void eat() {
log.debug("eating...");
Sleeper.sleep(1);
}
}
4.17.4. 锁公平
synchronized是不公平锁,ReentrantLock默认也是不公平锁,但是我们可以设置ReentrantLock为公平锁。
所谓公平性就是:在释放锁之后,阻塞队列中的等待线程不是一拥而上的,而是按照先进入队列的顺序先获得到锁。
改为公平锁后
ReentrantLock lock = new ReentrantLock(true);
公平锁的本意是解决饥锁问题的,但是实际上公平锁一般没有必要,会降低并发度****,后面分析源码时会讲解。
使用tryLock更好。
4.17.5. 条件变量
synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比
- synchronized 是那些不满足条件的线程都在一间休息室等消息
- 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
使用要点:
- await 前需要获得锁
- await 执行后,会释放锁,进入 conditionObject 等待
- await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
- 竞争 lock 锁成功后,从 await 后继续执行`
主要方法:
- static ReentrantLock reentrantLock = new ReentrantLock();
- static Condition cigaretteCondition = reentrantLock.newCondition();
- reentrantLock.lock();
- reentrantLock.unlock();
- takeoutCondition.await();
- takeoutCondition.signal();
static ReentrantLock lock = new ReentrantLock();
static Condition waitCigaretteQueue = lock.newCondition();
static Condition waitbreakfastQueue = lock.newCondition();
static volatile boolean hasCigrette = false;
static volatile boolean hasBreakfast = false;
public static void main(String[] args) {
new Thread(() -> {
try {
lock.lock();
while (!hasCigrette) {
try {
waitCigaretteQueue.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("等到了它的烟");
} finally {
lock.unlock();
}
}).start();
new Thread(() -> {
try {
lock.lock();
while (!hasBreakfast) {
try {
waitbreakfastQueue.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("等到了它的早餐");
} finally {
lock.unlock();
}
}).start();
sleep(1);
sendBreakfast();
sleep(1);
sendCigarette();
}
private static void sendCigarette() {
lock.lock();
try {
log.debug("送烟来了");
hasCigrette = true;
waitCigaretteQueue.signal();
} finally {
lock.unlock();
}
}
private static void sendBreakfast() {
lock.lock();
try {
log.debug("送早餐来了");
hasBreakfast = true;
waitbreakfastQueue.signal();
} finally {
lock.unlock();
}
}
4.18. 同步模式之顺序控制
4.18.1. wait¬ify
package com.bloom.concurrent.chapter4;
import lombok.extern.slf4j.Slf4j;
/**
* @description:
* @author: teago
* @time: 2020/5/24 16:50
*/
@Slf4j
public class Demo01 {
static Object o = new Object();
static boolean checked = false;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (o) {
while (!checked) {
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("1");
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (o) {
log.debug("2");
checked = true;
o.notify();
}
}, "t2");
t1.start();
t2.start();
}
}
4.18.2. ReentrantLock
package com.bloom.concurrent.chapter4;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* @description:
* @author: teago
* @time: 2020/5/24 16:54
*/
@Slf4j
public class Demo2 {
static ReentrantLock reentrantLock = new ReentrantLock();
static Condition condition = reentrantLock.newCondition() ;
static boolean checked = false ;
public static void main(String[] args) {
new Thread(()->{
reentrantLock.lock();
try{
while (!checked){
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("1");
}finally {
reentrantLock.unlock();
}
},"t1").start();
new Thread(()->{
reentrantLock.lock();
try{
log.debug("2");
checked = true ;
condition.signal();
}finally {
reentrantLock.unlock();
}
},"t2").start();
}
}
4.18.3. park&unpark
package com.bloom.concurrent.chapter4;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.LockSupport;
/**
* @description:
* @author: teago
* @time: 2020/5/24 16:56
*/
@Slf4j
public class Demo03 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
LockSupport.park();
log.debug("1");
}, "t1");
Thread t2 = new Thread(() -> {
log.debug("2");
LockSupport.unpark(t1);
}, "t2");
t1.start();
t2.start();
}
}
4.19. 同步模式之交替输出
线程1输出A5次,线程2输出B5次,线程3输出C5次,现在要求输出“abcabcabcabcabc”怎么实现。
4.19.1. wait/notify
package com.bloom.concurrent.chapter4;
import lombok.extern.slf4j.Slf4j;
/**
* @description:
* @author: teago
* @time: 2020/5/24 17:01
*/
@Slf4j
public class Demo04 {
public static void main(String[] args) {
SyncWaitNotify syncWaitNotify = new SyncWaitNotify(1, 5);
new Thread(() -> {
syncWaitNotify.print(1, 2, "a");
}).start();
new Thread(() -> {
syncWaitNotify.print(2, 3, "b");
}).start();
new Thread(() -> {
syncWaitNotify.print(3, 1, "c");
}).start();
}
}
class SyncWaitNotify {
private int flag;
private int loopNumber;
public SyncWaitNotify(int flag, int loopNumber) {
this.flag = flag;
this.loopNumber = loopNumber;
}
public void print(int waitFlag, int nextFlag, String str) {
for (int i = 0; i < loopNumber; i++) {
synchronized (this) {
while (this.flag != waitFlag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print(str);
flag = nextFlag;
this.notifyAll();
}
}
}
}
4.19.2. await/signal
package com.bloom.concurrent.chapter4;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* @description:
* @author: teago
* @time: 2020/5/24 17:11
*/
public class Demo05 {
public static void main(String[] args) {
SyncWaitNotify syncWaitNotify = new SyncWaitNotify(1, 5);
new Thread(() -> {
syncWaitNotify.print(1, 2, "a");
}).start();
new Thread(() -> {
syncWaitNotify.print(2, 3, "b");
}).start();
new Thread(() -> {
syncWaitNotify.print(3, 1, "c");
}).start();
}
}
@Slf4j
class AwaitSignal extends ReentrantLock {
public void start(Condition first) {
this.lock();
try {
log.debug("start");
first.signal();
} finally {
this.unlock();
}
}
public void print(String str, Condition current, Condition next) {
for (int i = 0; i < loopNumber; i++) {
this.lock();
try {
current.await();
log.debug(str);
next.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
this.unlock();
}
}
}
// 循环次数
private int loopNumber;
public AwaitSignal(int loopNumber) {
this.loopNumber = loopNumber;
}
}
Demo
4.19.3. park/unpark
package com.bloom.concurrent.chapter4;
import java.util.concurrent.locks.LockSupport;
/**
* @description:
* @author: teago
* @time: 2020/5/24 17:45
*/
public class Demo06 {
public static void main(String[] args) {
SyncPark syncPark = new SyncPark(5);
Thread t1 = new Thread(() -> {
syncPark.print("a");
});
Thread t2 = new Thread(() -> {
syncPark.print("b");
});
Thread t3 = new Thread(() -> {
syncPark.print("c\n");
});
syncPark.setThreads(t1, t2, t3);
syncPark.start();
}
}
class SyncPark {
private int loopNumber;
private Thread[] threads;
public SyncPark(int loopNumber) {
this.loopNumber = loopNumber;
}
public void setThreads(Thread... threads) {
this.threads = threads;
}
public void print(String str) {
for (int i = 0; i < loopNumber; i++) {
LockSupport.park();
System.out.print(str);
LockSupport.unpark(nextThread());
}
}
private Thread nextThread() {
Thread current = Thread.currentThread();
int index = 0;
for (int i = 0; i < threads.length; i++) {
if(threads[i] == current) {
index = i;
break;
}
}
if(index < threads.length - 1) {
return threads[index+1];
} else {
return threads[0];
}
}
public void start() {
for (Thread thread : threads) {
thread.start();
}
LockSupport.unpark(threads[0]);
}
}
4.20. 本章小结
本章我们需要重点掌握的是
- 分析多线程访问共享资源时,哪些代码片段属于临界区
- 使用 synchronized 互斥解决临界区的线程安全问题
- 掌握 synchronized 锁对象语法
- 掌握 synchronzied 加载成员方法和静态方法语法
- 掌握 wait/notify 同步方法
- 使用 lock 互斥解决临界区的线程安全问题
- 掌握 lock 的使用细节:可打断、锁超时、公平锁、条件变量
- 学会分析变量的线程安全性、掌握常见线程安全类的使用
- 了解线程活跃性问题:死锁、活锁、饥饿
- 应用方面
- 互斥:使用 synchronized 或 Lock 达到共享资源互斥效果
- 同步:使用 wait/notify 或 Lock 的条件变量来达到线程间通信效果
- 原理方面
- monitor、synchronized 、wait/notify 原理
- synchronized 进阶原理
- park & unpark 原理
- 模式方面
- 同步模式之保护性暂停
- 异步模式之生产者消费者
- 同步模式之顺序控制












浙公网安备 33010602011771号