线程安全问题
案例
两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?
@Slf4j(topic = "c.ThreadSafe")
public class ThreadSafe {
public static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", counter);
}
}
输出:
问题分析
以上的结果可能是正数、负数、零?为什么呢?因为 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 行代码可能交错运行:
出现负数的情况:
线程 2 在执行完 isub 后得到 i= -1,此时线程 2 还未来得及将计算结果写入到内存当中就发生了上下文切换(线程 2 的时间片用完),线程 1 获取常数 i,由于线程 2 未将结果写入内存,所以 i 的值依然为 0,然后线程 1 做加法运算,得到结果 i = 1,接着又将结果写入到了内存,此时又发生了上下文切换 ,线程 2 得到了 CPU 的使用权。接着执行之前未完成的操作,将结果 i = -1 写入内存,这样就将 i = 1 覆盖了,最终 i 的值就变为了 -1。
上下文切换概念:
因为以下一些原因导致 CPU 不再执行当前的线程,转而执行另一个线程的代码。
- 线程的 CPU 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
出现正数的情况:
临界区 Critical Section
- 一个程序运行多个线程本身是没有问题的
- 问题出在多个线程访问共享资源
- 多个线程读共享资源其实也没有问题
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
- 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
static int counter = 0;
static void increment()
// 临界区
{
counter++;
}
static void decrement()
// 临界区
{
counter--;
}
竞态条件
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。
synchronized 解决方案
应用之互斥
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量
synchronized,俗称【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其他线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
:::info
注意:
虽然 Java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后、顺序不同,需要一个线程等待其他线程运行到某个点
:::
使用
语法:
synchronized(对象){
临界区
}
解决:
@Slf4j(topic = "c.ThreadSafe")
public class ThreadSafe {
public static int counter = 0;
public static Object room = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", counter);
}
}
输出:
对于 synchronized 的理解
可以做这样的类比:
- synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门),房间只能一次进入一人进行计算,线程 t1、t2 想象成两个人
- 当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门,拿走了钥匙,在门内执行 counter++ 代码
- 这时候如果 t2 也运行到了 **synchronized (room) 时,**它发现门被锁住了,只能在门外等待,发生了上下文切换,阻塞住了。
- 这中间即使 t1 的 CPU 时间片不幸用完了,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去),这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才能开门进入。
- 当 t1 执行完 synchronized{} 块内的代码,这时候才从 room 房间出来,并解开门上的锁,唤醒 t2 线程把钥匙给它。t2 线程这时才能进入 room 房间,锁住了门拿上钥匙,执行它的 count-- 代码。
用图来表示:
对于 synchronized 的思考
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
为了加深理解,请思考下面的问题:
- 如果把** synchronized(obj) **放在 for 循环的外面,如何理解?
未将 synchronized(obj) 放在for循环外面前,是针对 count++ 这一句代码做的原子性保护,如果放在 for
循环外面,则是对for循环整体,即 5000次count++ 整体的原子性保护。
- 如果 t1 **synchronized(obj1),而 t2 synchronized(obj2) **会怎样运行?
obj1 和 obj2 可以理解为两个房间,synchronized 在加锁时,锁的是不同房间。若想保护共享资源,必须要 让多个线程锁住同一个对象。
- 如果 t1 **synchronized(obj1),而 t2 **没有加会怎样?如何理解?
t1 线程启动后,获取对象锁成功,而当 t2 线程启动后,由于 t2 线程没有对象锁,也就不会被阻塞住,所以 t2 线程仍然会继续运行。
面向对象改进
创建一个类,将共享资源(变量),以及对共享资源的操作(方法)抽象到该类中。
class Room {
private int counter = 0;
public void increment(){
synchronized (this){
counter++;
}
}
public void decrement(){
synchronized (this){
counter--;
}
}
public int getCounter(){
synchronized (this){
return counter;
}
}
}
修改 ThreadSafe 类
@Slf4j(topic = "c.ThreadSafe")
public class ThreadSafe {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.increment();
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", room.getCounter());
}
}
输出:
方法上的 synchronized
synchronized 加在非静态方法上,锁对象为当前对象 this
class Test{
public synchronized void test() {
}
}
等价于
class Test{
public void test() {
synchronized(this) {
}
}
}
synchronized 加载静态方法上,锁对象为类对象。
class Test{
public synchronized static void test() {
}
}
等价于
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}
变量的线程安全分析
成员变量和静态变量是否线程安全?
- 如果它们没有被共享,则线程安全
- 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量是否线程安全?
- 局部变量是线程安全的
- 但局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用范围,它是线程安全的
- 如果该对象套利方法的作用范围,需要考虑线程安全
局部变量线程安全分析
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
如图:
假设有线程0 和线程 1 两个线程,均要调用 test1() 方法,那么这两个线程就都会创建属于自己的 test1 栈帧,栈帧中的局部变量 i,也是用栈帧中私有的区域来进行存储的。那么在执行 i 的自增时,两个栈帧各做各的,互不干扰。没有共享,也就没有线程安全问题。
局部变量的引用稍有不同
先来看一个成员变量的例子。
package com.atheima.chapter03;
import java.util.ArrayList;
public class ThreadUnsafe {
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 + 1)).start();
}
}
ArrayList<String> list = new ArrayList<String>();
public void method1(int loopNumber) {
for(int i = 0; i < loopNumber; i++) {
// { // 临界区,会产生竞态条件
method2();
method3();
// } 临界区
}
}
private void method3() {
list.add("1");
}
private void method2() {
list.remove(0);
}
}
其中一种情况是,如果线程2 还未 add,线程1 remove 就会报错:
分析:
- 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
- method3 与 method2 分析相同
两个线程 Thread-0 和 Thread-1 创建了属于各自的栈帧,分别调用了 method1,而 method1 又调用了 method2 和 method3。method2 中执行了 list.add 方法,这里的成员变量 list 属于共享资源,即两个线程其实操作的是同一个 list,这样就会发生线程安全问题。
将 list 修改为局部变量
package com.atheima.chapter03;
import java.util.ArrayList;
public class ThreadSafe {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadSafe test = new ThreadSafe();
for(int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + (i + 1)).start();
}
}
public void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<String>();
for(int i = 0; i < loopNumber; i++) {
// { // 临界区,会产生竞态条件
method2(list);
method3(list);
// } 临界区
}
}
private void method3(ArrayList<String> list) {
list.add("1");
}
private void method2(ArrayList<String> list) {
list.remove(0);
}
}
就不会有上述问题了。
分析:
- list 是局部变量,每个线程调用时会创建其不同实例,没有共享
- 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象
- method3 的参数分析与 method2 相同
方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会带来线程安全问题?
- 情况1:由其他线程调用 method2 和 method3
- 这种情况不会产生线程安全问题。这是因为 method1 中的 list 属于局部变量,假设有其他线程直接调用method2 和 method3,那么它们就必须要传入自己的 list 变量,与 method1 中 list 并不是共享资源,所以也就不会产生线程安全问题。
- 情况2:在情况 1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,即
public class ThreadSafe {
public void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<String>();
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();
}
}
- 这种情况是会产生线程安全问题的。这是因为子类 ThreadSafeSubClass 重写了 method3,在 method3 中又重新开启了新的线程,而新线程中的 list 变量,与 method1 中的 list 变量属于同一个变量,为共享资源,因此是会产生线程安全问题的。
:::warning
private 修饰的方法是不允许子类重写的,如果公共方法不想被子类重写,可以添加 final 关键字进行修饰。从这个例子可以看出 private 或 Final 提供【安全】的意义所在。
:::
常见线程安全类
- 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();
- 它们的每个方法是原子的
- 但注意它们多个方法的组合不是原子的,见后面分析
线程安全类方法的组合
分析下面代码是否线程安全?
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
table.put("key", value);
}
不是线程安全的。
假设有两个线程同时执行上述代码,线程 1 在 get(“key”) == null 后,由于时间片用完,发生了线程切换,线程 2 执行了 get(“key”) == null,又执行了 put(“key”, v2) 后发生了线程切换,此时线程 1 继续执行 put(“key”, v1),将线程 2 的值给覆盖了。
不可变类线程安全性
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的。
但是,String 有 replace,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);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
String 类中的 substring 方法实际上如果 beginIndex 大于 0,是会重新创建一个 String 对象的。
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);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
创建新字符串是在原有的字符串基础上,进行了复制,复制好了再复制给新的字符串的 value,根本没有改变原对象的属性。
实例分析
例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) {
// 使用上述变量
}
}
final Date d2 = new Date(); 为什么是线程不安全的?
final 修饰的变量 d2 为不可变的,而变量 d2 中存放的其实是 new Date() 的引用,即 Date 对象在堆中的地址,该地址是不可变的。但是 Date 对象的属性是可以变化的。当有多个线程同时对 d2 中的属性进行操作时,依然是会发生线程安全问题的。
例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++;
}
}
userService 不是线程安全的,这是因为 userService 属于成员变量,是共享资源。当有多个线程同时对 userService 中的属性 count 进行操作时,会发生线程安全问题。
例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));
}
}
本例中注解中没有添加 @Scope,意思即表明全局有且仅有一个实例(每次获取得到的 bean 实例均为同一个)。也就是说,MyAspect 类的 bean 实例是被共享的,其对象的属性 start 自然也就成了共享资源。当有多个对象同时访问 before 和 after 方法时,属性 start 就会存在并发修改问题,所以是线程不安全的。
解决方法:可以将前置通知和后置通知合并成环绕通知,同时将属性 start 改为局部变量。
如果将 @Scope 改为多例呢?这是不行的,这样会导致访问 before 时是 A 对象,访问 after 时是 B 对象,这样就没办法讲结果统计在一起。
例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,没有成员变量,不存在共享资源,而且变量 conn 时局部变量,每个线程一份。所以是线程安全的。
再看 UserServiceImpl,虽然 userDao 是成员变量(共享资源),但它没有可更改的属性,所以也是线程安全的。(没有成员变量的,也就是无状态的,一般都是线程安全的 )
同理 MyServlet 中的成员变量 userService 也是线程安全的。
例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();
}
}
UserDaoImpl:这里的 conn 变量属于成员变量,存在线程安全问题。这就导致 UserServiceImpl 类中的成员变量 userDao 以及 MyServlet 类中的 userService 都成了线程不安全的。
例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 conn = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}
首先 UserDaoImpl 中的 conn 是线程不安全的。但是在 UserServiceImpl 中调用 update 方法时,每次都会重新创建一个 userDao 对象,这就使得 conn 变为了线程安全的。
例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 类使用了 final 来修饰。这就意味着无法被继承,即使有子类继承了 String 类,那么它也就没法修改父类 String 中的方法,规避了多线程安全问题。
习题
卖票练习
测试下面代码是否存在线程安全问题,并尝试改正
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
@Slf4j(topic = "c.ExerciseSell")
public class ExerciseSell {
public static void main(String[] args) {
TicketWindow window = new TicketWindow(2000);
List<Thread> threadList = new ArrayList<Thread>();
// 用于存储卖出去多少张票
List<Integer> sellCount = new Vector<Integer>();
for (int i = 0; i < 2000; i++) {
Thread t = new Thread(() -> {
int count = window.sell(randAmount());
sellCount.add(count);
});
threadList.add(t);
t.start();
}
list.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}); // 等待所有的线程执行结束,再统计结果
// 卖出去的票求和
log.debug("Sell count: {}", sellCount.stream().mapToInt(i -> i).sum());
// 剩余的票数
log.debug("remainder count: {}", window.getCount());
}
static Random random = new Random();
private static int randAmount() {
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;
}
}
}
测试结果:
是会发生线程安全问题的。
分析:
想解决线程安全问题,需要先判断出哪些代码属于临界区。根据临界区的定义“一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区。”可知,下面的代码,即 16、17 行代码属于临界区
int count = window.sell(randAmount());
sellCount.add(count);
其中的 window 和 sellCount 都属于成员变量(共享资源),window 对象调用了方法 sell,在方法 sell 内部存在着对票数的读写操作。(window对象属于共享资源 → window 对象的属性 count(票数)也就成为了共享资源 → sell 方法对共享资源 count 存在读写操作)
下面是 Vector 中的 add 方法,可以看出,在 add 方法内部也存在着对共享资源的读写操作(modCount
以及 elementCount)。但是由于该方法已经添加了 synchronized 关键字,进行了同步处理,所以不存在线程安全问题。
那需不需要考虑 window.sell() 和 sellCount.add() 这两个方法的组合的线程安全问题呢?答案也是不需要的,因为 window 和 sellCount 是两个不同的共享变量。跟下面的情况时不一样。
上述案例是对一个同共享变量 table 的读写操作,所以存在线程安全问题。
所以当前案例只需要每个共享变量的方法中的临界区代码 到安全保护即可,不需要将其综合在一起。
至于变量 threadList,它只在主线程中被使用了,所以不需要考虑线程安全问题。
**解决方案: **
将售票方法 sell 设置为同步的,即给方法 sell 加锁(synchronized)。 synchronized 其实就是对当前对象(同一个售票窗口)进行了加锁。
// 售票窗口
class TicketWindow {
private int count;
public TicketWindow(int count) {
this.count = count;
}
// 获取余票数量
public int getCount() {
return count;
}
// 售票
public synchronized int sell(int amount) {
if(this.count >= amount) {
this.count -= amount;
return amount;
} else {
return 0;
}
}
}
转账练习
测试下面代码是否存在线程安全问题,并尝试改正
import lombok.extern.slf4j.Slf4j;
import java.util.Random;
@Slf4j(topic = "c.ExerciseTransfer")
public class ExerciseTransfer {
public static void main(String[] args) throws InterruptedException {
Account a = new Account(1000);
Account b = new Account(1000);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
a.transfer(b, randomAmount());
}
}, "t1");
Thread t2 = new Thread(() -> {
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);
}
}
}
运行结果:
总金额加起来不等于 2000,发生了线程安全问题
分析:
共享变量:a、b → a 对象的 money 以及 b 对象的 money,既要操作 a 的 money(减少),又要操作 b 的 money(增加)
临界区: transfer 方法,即第 50 行 ~ 第 55 行代码
解决方法:
这样改正行不行,为什么?
public synchronized void transfer(Account target, int amount){
if(this.money > amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
不行。在方法上添加 synchronized 关键字,所持有的锁对象其实是当前对象 this。当对象 a 调用 transfer 方法时,此时的 synchronized 锁的就是 a 对象,但是 target 的 setMoney 其实是对对象 b 的操作,此时的 b 对象是没有锁的。同理 ,当对象 b 调用 transfer 方法时,此时的 synchronized 锁的就是 b 对象,但是 target 的 setMoney 其实是对对象 a 的操作,此时的 a 对象是没有锁的。
这是直接在方法上添加 synchronized 后的运行结果:
可以看出结果依然不对。想要解决这个问题,必须要用 a 的 money 和 b 的 money 共享的锁。而满足这种要求的就是 Account.class 对象,对 transfer 方法做如下修改:
public void transfer(Account target, int amount){
synchronized(Account.class) {
if (this.money > amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}
Monitor 概念
对象头
:::success
一个 Java 对象由对象头,实例数据和对齐填充组成,其中对象头是极其重要的,对象头是由对象标记 Mark Word 和类型指针组成,其中又以 Mark Word 最重要,对于 Mark Word 它占 8 个字节也就是64位(64位虚拟机)!
:::
以 32 位虚拟机为例:
普通对象
Klass Word 指向了该对象所属的类对象,比如 Student.class 或者 Teacher.class
数组对象
其中 Mark Word 结构为
hashcode:每个对象都有一个属于自己的 hash 码。
age:表示分代年龄,如果对象超过了一定的年龄(默认为 15,4bit【2 的 4 次方 】),就会从幸存者区晋升至老年代。
biased_lock:是否是偏向锁
最后两位:代表对象的加锁状态
64 位虚拟机 Mark Word
Monitor 工作原理
-
刚开始 Monitor 中的 Owner 为 null
-
当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 值为 Thread-2,Monitor 中只能有一个 Owner
- 正常情况下,每个对象的 Mark Word 中存储的是哈希码、分代年龄、是否是偏向锁以及锁状态(此时锁状态标记位为 0,表示该对象没有与任何的锁进行关联)等信息。一旦线程执行了 synchronized(obj),就会尝试找一个 monitor, 去与之关联。关联成功后,就会将对象 obj 的锁状态由 01 变为 10,并且会将哈希码、分代年龄以及是否是偏向锁等信息变为一个指向 monitor 对象的指针(ptr_to_heavyweight_monitor:,占用 30 bits)。
-
在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入 EntryList BLOCKED(阻塞状态)
-
Thread-2 执行完同步代码块中的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时候是非公平的
-
图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲 wait-notify 时会分析。
:::success
注意: -
synchronized 必须是进入同一个对象的 monitor 才有上述的效果
-
不加 synchronized 的对象不会关联监视器,不遵从以上规则。
:::
原理之 synchronized
字节码角度
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
System.out.println("ok");
}
}
0: new #2 // new Object
3: dup
4: invokespecial #1 // invokespecial <init>:()V,非虚方法
7: astore_1 // lock引用 -> lock
8: aload_1 // lock (synchronized开始)
9: dup // 一份用来初始化,一份用来引用
10: astore_2 // lock引用 -> slot 2
11: monitorenter // 【将 lock对象 MarkWord 置为 Monitor 指针】
12: getstatic #3 // System.out
15: ldc #4 // "ok"
17: invokevirtual #5 // invokevirtual println:(Ljava/lang/String;)V
20: aload_2 // slot 2(lock引用)
21: monitorexit // 【将 lock对象 MarkWord 重置, 唤醒 EntryList】
22: goto 30
25: astore_3 // any -> slot 3
26: aload_2 // slot 2(lock引用)
27: monitorexit // 【将 lock对象 MarkWord 重置, 唤醒 EntryList】
28: aload_3
29: athrow
30: return
Exception table:
from to target type
12 22 25 any
25 28 25 any
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
8 23 1 lock Ljava/lang/Object;
说明:
- 通过异常 try-catch 机制,确保一定会被解锁
- 方法级别的 synchronized 不会在字节码指令中有所体现
小故事
故事角色
老王 - JVM
小南 - 线程
小女 - 线程
房间 - 对象
房间门上 - 防盗锁 - Monitor
房间门上 - 小南书包 - 轻量级锁
房间门上 - 刻上小南大名 - 偏向锁
批量重刻名 - 一个类的偏向锁撤销到达 20 阈值
不能刻名字 - 批量撤销该类对象的偏向锁,设置该类不可偏向
小南要使用房间保证计算不被其它人干扰(原子性),最初,他用的是防盗锁,当上下文切换时,锁住门。这样, 即使他离开了,别人也进不了门,他的工作就是安全的。
但是,很多情况下没人跟他来竞争房间的使用权。小女是要用房间,但使用的时间上是错开的,小南白天用,小女 晚上用。每次上锁太麻烦了,有没有更简单的办法呢?
小南和小女商量了一下,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因 此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是 自己的,那么就在门外等,并通知对方下次用锁门的方式。
后来,小女回老家了,很长一段时间都不会用这个房间。小南每次还是挂书包,翻书包,虽然比锁门省事了,但仍 然觉得麻烦。
于是,小南干脆在门上刻上了自己的名字:【小南专属房间,其它人勿用】,下次来用房间时,只要名字还在,那 么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦 掉,升级为挂书包的方式。
同学们都放假回老家了,小南就膨胀了,在 20 个房间刻上了自己的名字,想进哪个进哪个。后来他自己放假回老 家了,这时小女回来了(她也要用这些房间),结果就是得一个个地擦掉小南刻的名字,升级为挂书包的方式。老 王觉得这成本有点高,提出了一种批量重刻名的方法,他让小女不用挂书包了,可以直接在门上刻上自己的名字
后来,刻名的现象越来越频繁,老王受不了了:算了,这些房间都不能刻名了,只能挂书包
原理之 synchronized 进阶
轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized。
假设有两个方法同步块,利用同一个对象加锁:
static final Object obj = new Object();
public static void method1() {
synchronized(obj) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized(obj) {
// 同步块 B
}
}
- 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word。
- 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word(对象标记),将 Mark Word 的值存入锁记录。
- 如果 cas 替换成功,对象头中存储了锁记录地址和状态 00,表示由该线程给对象加锁,这时图示如下:、
- 00 表示轻量级锁
- 前 30 位表示锁记录的地址
- 如果 cas 失败,有两种情况
- 如果是其他线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
- 如果是自己执行了 synchronized 锁重入,那么再添加一条 LockRecord 作为重入的计数
- 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
- 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头。
- 成功,则解锁成功
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
CAS定义:
CAS:Compare and Swap 三个字母的缩写。
是什么意思呢?Compare:比较 Swap:交换。所以CAS即为:比较并交换的意思。
需要注意:and 是并的意思。在逻辑运算中,并的意思:只有都成立了才可以执行下面操作。
那么在Java并发中为什么会大量的使用到CAS呢?
那是因为CAS是无锁的一种算法。为了解决多线程并行(并发)情况下使用锁的话,性能消耗的一种机制。
CAS操作流程:
CAS操作包含了三个操作数据。分别是:主内存数据值或主内存位置(V)、线程工作区副本更新前的数据值或者是预期值(A)以及要更新成的新值(B)。
操作流程:
- 线程M在更新的共享变量的时候,会拿着自己工作区变量副本A的值,假设是1,将要更新的值B。假设是2.去更新主内存共享变量V的时候,会先拿着V和A比较。如果V==A的时候,才会将主内存V的值换成B。否则就会重新获取主内存的值,进行自旋操作,知道成功位置。
- 简单理解:我(线程M,工作区值为A)认为主内存V中的共享变量值是(包含)A,如果 V的值是A,那么就将B替换V。如果不是,就不更新V的值,只要告诉我V的最新值。我自己自旋操作,自己玩。
锁膨胀
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
static Object obj = new Object();
public static void method1() {
synchronized (obj) {
// 同步块
}
}
- 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
- 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
- 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
- 然后自己进入 Monitor 的 EntryList BLOCKED
- (Thread-1 申请轻量级锁失败后,需要进入阻塞状态进行等待,轻量级锁没有阻塞,只能申请重量级锁 Monitor)
- 当 Thread-0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级锁解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程。
“10”表示重量级锁,前 30 位为 Monitor 对象地址。
自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞(阻塞需要切换上下文,比较耗费性能)。
自旋重试成功的情况:
自旋重试失败的情况:
:::success
线程 1 执行同步代码块,获取到了 Monitor,此时已经是重量级锁了。线程 1 在执行同步代码块的过程中,线程 2 也开始访问同步代码块,尝试获取 Monitor,但是此时锁对象已经被线程 1 所持有,所以线程 2 获取失败。线程 2 在获取锁对象失败后,并不是立即陷入阻塞状态,进入 EntryList 中等待的,而是会进行多次自旋重试获取锁,如果在自旋重试的过程中,线程 1 执行完同步代码块,并成功释放锁(锁对象为无锁状态,Monitor对象中 Owner 也被置为了 null),那么线程 2 就可以成功获得锁。如果经过一定的自旋次数后,线程 1 一直未释放锁,那么线程 2 才会进入 EntryList 中进行等待。
:::
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势
- 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
- Java 7 之后不能控制是否开启自旋功能。
偏向锁
概念
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头(不再存储重量级锁的指针了,也不再存储锁记录的地址了,而是线程 ID),之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS,以后只要不发生竞争,这个对象就归该线程所有。
例如:
static final Object obj = new Object();
public static void m1() {
synchronized(obj) {
// 同步块 A
m2();
}
}
public static void m2() {
synchronized(obj) {
// 同步块 B
m3();
}
}
public static void m3() {
synchronized(obj) {
// 同步块 C
}
}
当 m2 线程再次调用 synchronized(obj) 时,会去检查对象 obj 的 Mark Word 中的 ThreadID 是否是当前线程调用的。而不是像轻量级锁那样,每次都需要用锁记录去替换 Mark Word,然后重新生成新的锁记录。
偏向状态
回忆一下对象头格式
一个对象创建时:
- 如果开启了偏向锁(默认开启),那么对象创建后,MarkWord 值为 0x05 即最后 3 位为 101,这时它的 thread、epoch、age 都为 0。
- 偏向锁默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数:-XX:BiasedLockingStartupDaley=0 来禁用延迟。
- 如果没有开启偏向锁,那么对象创建后,MarkWord 值为 0x01 即最后 3 位为 001,这时它的 hashcode,age 都为 0,第一次用到 hashcode 时才会赋值。
代码演示上述几种情况:
修改 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>juc</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
</dependencies>
</project>
新增 JolUtils 工具类
import org.openjdk.jol.vm.VM;
import org.openjdk.jol.vm.VirtualMachine;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class JolUtils {
public static String toPrintableSimple(Object o) {
return getHeader64Bit(o);
}
public static void main(String[] args) {
System.out.println( getHeader64Bit(new Object()));
}
public static String getHeader64Bit(Object o) {
VirtualMachine vm = VM.current();
long word = vm.getLong(o, 0);
List<String> list = new ArrayList<>(8);
for (int i = 0; i < 8; i++) {
list.add(toBinary((word >> i * 8) & 0xFF) );
}
Collections.reverse(list);
return String.join(" ",list);
}
// very ineffective, so what?
private static String toBinary(long x) {
StringBuilder s = new StringBuilder(Long.toBinaryString(x));
int deficit = 8 - s.length();
for (int c = 0; c < deficit; c++) {
s.insert(0, "0");
}
return s.toString();
}
}
- 测试延迟特性
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
@Slf4j(topic = "c.TestBiased")
public class TestBiased {
public static void main(String[] args) throws InterruptedException {
Dog d = new Dog();
log.debug(JolUtils.toPrintableSimple(d));
Thread.sleep(4000);
log.debug(JolUtils.toPrintableSimple(new Dog()));
}
}
class Dog {
}
运行结果如下:
偏向锁默认是延迟的,不会在程序启动时立即生效。由于对象 d 已经创建成功,即使等待 4 秒后,它的偏向锁也不会再发生改变,所以需要重新创建对象进行验证。
最后 3 位为 101 表示偏向锁开启。
使用 VM 参数:-XX:BiasedLockingStartupDaley=0 来禁用延迟。
运行结果:
禁用成功。
- ** 测试偏向锁**
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.TestBiased")
public class TestBiased {
public static void main(String[] args) throws InterruptedException {
testBiased();
}
public static void testBiased() {
Dog d = new Dog();
log.debug("synchronized 前:{}", JolUtils.toPrintableSimple(d));
synchronized (d) {
log.debug("synchronized 中:{}", JolUtils.toPrintableSimple(d));
}
log.debug("synchronized 后:{}",JolUtils.toPrintableSimple(d));
}
}
class Dog {
}
测试结果:
数据的后三位表示加锁信息,分别表示biased_lock,lock
biased_lock lock 含义
0 01 无锁
1 01 偏向锁
0 00 轻量级锁
0 10 重量级锁
0 11 GC标记
前 54 位为 Thread ID,是操作系统为线程设置的唯一标识,与 Java 中的 Thread.getId() 获取到的 ID 是不一致的。一个是操作系统层面分配的,一个是 Java 分配的。
在 synchronized 同步代码块执行结束后,线程 ID 也没有发生变化。由于是在主线程中对对象 d 进行的加锁操作,这就导致 d 对象以后就偏向于主线程。所以 d 对象的 Mark Word 头里始终存储的都是主线程的 ID。 除非有其他线程也使 用了 d 对象进行了加锁操作,Mark Word 头中的线程 ID 才会发生改变。
:::success
注意:
处于偏向锁的对象解锁后,线程 id 仍存储于对象头中
:::
- 禁用偏向锁
在上面测试代码运行时添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁
测试结果:
在禁用掉偏向锁后,从打印出的 Mark Word 信息可以看出,synchronized 中变成了轻量级锁。轻量级锁的前 62 位为 Lock Record(锁记录)的地址。解锁后,又恢复为未加锁状态。
:::success
优先级:
有偏向锁时用偏向锁。如果有其他线程也使用了同一个对象进行加锁,撤销偏向锁,升级为轻量级锁。如果有其他线程来竞争锁,就会膨胀为重量级锁。
偏向锁 → 轻量级锁 → 重量级锁
:::
撤销偏向锁
- 撤销-调用对象 hashcode
将偏向锁打开:删除 VM 参数 -XX:-UseBiasedLocking
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.TestBiased")
public class TestBiased {
public static void main(String[] args) throws InterruptedException {
testBiased();
}
public static void testBiased() {
Dog d = new Dog();
d.hashCode();
log.debug("synchronized 前:{}", JolUtils.toPrintableSimple(d));
synchronized (d) {
log.debug("synchronized 中:{}", JolUtils.toPrintableSimple(d));
}
log.debug("synchronized 后:{}",JolUtils.toPrintableSimple(d));
}
}
class Dog {
}
运行结果:
正常状态对象一开始是没有 hashCode 的,第一次调用 hashCode() 才生成
调用了对象的 hashcode,但偏向锁的对象 Mark Word 中存储的是线程 ID,如果调用 hashcode 会导致偏向锁被撤销。(偏向锁中没有其他的位置可以用来保存 hashcode,当调用 hashCode 方法时,只能撤销偏向锁)
- 轻量级锁会在锁记录中记录 hashcode
- 重量级锁会在 Monitor 中 记录 hashcode
在调用 hashCode 后使用偏向锁,记得去掉 -XX:-UseBiasedLocking
- 撤销-其他线程使用对象
当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁。
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.TestBiased")
public class TestBiased {
public static void main(String[] args) throws InterruptedException {
testWithdrawBiased();
}
public static void testWithdrawBiased() {
Dog d = new Dog();
Thread t1 = new Thread(() -> {
log.debug("synchronized 前:{}", JolUtils.toPrintableSimple(d));
synchronized (d) {
log.debug("synchronized 中:{}", JolUtils.toPrintableSimple(d));
}
log.debug("synchronized 后:{}", JolUtils.toPrintableSimple(d));
synchronized (TestBiased.class) {
TestBiased.class.notify(); // d 解锁后,通知 t2 线程继续运行
}
}, "t1");
t1.start();
Thread t2 = new Thread(() -> {
synchronized (TestBiased.class) {
try {
TestBiased.class.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("synchronized 前:{}", JolUtils.toPrintableSimple(d));
synchronized (d) {
log.debug("synchronized 中:{}", JolUtils.toPrintableSimple(d));
}
log.debug("synchronized 后:{}", JolUtils.toPrintableSimple(d));
}, "t2");
t2.start();
}
}
class Dog {
}
使用 wait/notify 的目的是为了能够让两个线程访问 d 对象的时间能够错开,而不会产生竞争锁对象,从而引发重量级锁。
t2 线程释放锁对象后,d 对象已经是不可偏向的状态了。
- 撤销-调用 wait/notify
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.TestBiased")
public class TestBiased {
public static void main(String[] args) throws InterruptedException {
testWaitNotify();
}
public static void testWaitNotify() {
Dog d = new Dog();
Thread t1 = new Thread(() -> {
log.debug(JolUtils.toPrintableSimple(d));
synchronized (d) {
log.debug(JolUtils.toPrintableSimple(d));
try {
d.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(JolUtils.toPrintableSimple(d));
}
}, "t1");
t1.start();
new Thread(() -> {
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (d) {
log.debug("notify");
d.notify();
}
}, "t2").start();
}
}
class Dog {
}
运行结果:
状态由 101 变为了 010,即由偏向锁膨胀为重量级锁。
wait/notify 只有重量级锁有。
批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID。
当撤销偏向锁阈值超过 20 次后,JVM 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程。
private static void test3() throws InterruptedException {
Vector<Dog> list = new Vector<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 30; i++) {
Dog d = new Dog();
list.add(d);
synchronized (d) {
log.debug(i + "\t" + JolUtils.toPrintableSimple(d));
}
}
synchronized (list) {
list.notify();
}
}, "t1");
t1.start();
Thread t2 = new Thread(() -> {
synchronized (list) {
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("===============> ");
for (int i = 0; i < 30; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + JolUtils.toPrintableSimple(d));
synchronized (d) {
log.debug(i + "\t" + JolUtils.toPrintableSimple(d));
}
log.debug(i + "\t" + JolUtils.toPrintableSimple(d));
}
}, "t2");
t2.start();
}
运行结果:
23:14:16.034 [t1] - 0 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.035 [t1] - 1 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.035 [t1] - 2 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.035 [t1] - 3 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.035 [t1] - 4 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.035 [t1] - 5 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.035 [t1] - 6 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.036 [t1] - 7 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.036 [t1] - 8 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.036 [t1] - 9 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.036 [t1] - 10 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.036 [t1] - 11 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.036 [t1] - 12 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.036 [t1] - 13 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.036 [t1] - 14 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.036 [t1] - 15 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.036 [t1] - 16 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.036 [t1] - 17 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.036 [t1] - 18 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.036 [t1] - 19 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.036 [t1] - 20 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.036 [t1] - 21 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.036 [t1] - 22 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.036 [t1] - 23 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.036 [t1] - 24 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.036 [t1] - 25 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.036 [t1] - 26 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.036 [t1] - 27 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.036 [t1] - 28 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.036 [t1] - 29 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.036 [t2] - ===============>
23:14:16.036 [t2] - 0 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.037 [t2] - 0 00000000 00000000 00000000 00000001 01101110 01111100 11101001 01000000
23:14:16.037 [t2] - 0 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
23:14:16.037 [t2] - 1 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.037 [t2] - 1 00000000 00000000 00000000 00000001 01101110 01111100 11101001 01000000
23:14:16.037 [t2] - 1 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
23:14:16.037 [t2] - 2 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.037 [t2] - 2 00000000 00000000 00000000 00000001 01101110 01111100 11101001 01000000
23:14:16.037 [t2] - 2 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
23:14:16.037 [t2] - 3 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.037 [t2] - 3 00000000 00000000 00000000 00000001 01101110 01111100 11101001 01000000
23:14:16.037 [t2] - 3 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
23:14:16.037 [t2] - 4 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.037 [t2] - 4 00000000 00000000 00000000 00000001 01101110 01111100 11101001 01000000
23:14:16.037 [t2] - 4 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
23:14:16.037 [t2] - 5 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.037 [t2] - 5 00000000 00000000 00000000 00000001 01101110 01111100 11101001 01000000
23:14:16.037 [t2] - 5 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
23:14:16.037 [t2] - 6 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.037 [t2] - 6 00000000 00000000 00000000 00000001 01101110 01111100 11101001 01000000
23:14:16.037 [t2] - 6 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
23:14:16.037 [t2] - 7 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.037 [t2] - 7 00000000 00000000 00000000 00000001 01101110 01111100 11101001 01000000
23:14:16.037 [t2] - 7 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
23:14:16.037 [t2] - 8 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.037 [t2] - 8 00000000 00000000 00000000 00000001 01101110 01111100 11101001 01000000
23:14:16.037 [t2] - 8 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
23:14:16.037 [t2] - 9 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.038 [t2] - 9 00000000 00000000 00000000 00000001 01101110 01111100 11101001 01000000
23:14:16.038 [t2] - 9 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
23:14:16.038 [t2] - 10 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.038 [t2] - 10 00000000 00000000 00000000 00000001 01101110 01111100 11101001 01000000
23:14:16.038 [t2] - 10 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
23:14:16.038 [t2] - 11 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.038 [t2] - 11 00000000 00000000 00000000 00000001 01101110 01111100 11101001 01000000
23:14:16.038 [t2] - 11 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
23:14:16.038 [t2] - 12 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.038 [t2] - 12 00000000 00000000 00000000 00000001 01101110 01111100 11101001 01000000
23:14:16.038 [t2] - 12 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
23:14:16.038 [t2] - 13 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.038 [t2] - 13 00000000 00000000 00000000 00000001 01101110 01111100 11101001 01000000
23:14:16.038 [t2] - 13 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
23:14:16.038 [t2] - 14 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.038 [t2] - 14 00000000 00000000 00000000 00000001 01101110 01111100 11101001 01000000
23:14:16.038 [t2] - 14 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
23:14:16.038 [t2] - 15 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.038 [t2] - 15 00000000 00000000 00000000 00000001 01101110 01111100 11101001 01000000
23:14:16.038 [t2] - 15 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
23:14:16.039 [t2] - 16 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.039 [t2] - 16 00000000 00000000 00000000 00000001 01101110 01111100 11101001 01000000
23:14:16.039 [t2] - 16 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
23:14:16.039 [t2] - 17 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.039 [t2] - 17 00000000 00000000 00000000 00000001 01101110 01111100 11101001 01000000
23:14:16.039 [t2] - 17 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
23:14:16.039 [t2] - 18 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.039 [t2] - 18 00000000 00000000 00000000 00000001 01101110 01111100 11101001 01000000
23:14:16.040 [t2] - 18 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
23:14:16.040 [t2] - 19 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.040 [t2] - 19 00000000 00000000 00000000 00000001 00011100 10010101 10110001 00000101
23:14:16.040 [t2] - 19 00000000 00000000 00000000 00000001 00011100 10010101 10110001 00000101
23:14:16.040 [t2] - 20 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.040 [t2] - 20 00000000 00000000 00000000 00000001 00011100 10010101 10110001 00000101
23:14:16.040 [t2] - 20 00000000 00000000 00000000 00000001 00011100 10010101 10110001 00000101
23:14:16.040 [t2] - 21 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.040 [t2] - 21 00000000 00000000 00000000 00000001 00011100 10010101 10110001 00000101
23:14:16.040 [t2] - 21 00000000 00000000 00000000 00000001 00011100 10010101 10110001 00000101
23:14:16.040 [t2] - 22 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.040 [t2] - 22 00000000 00000000 00000000 00000001 00011100 10010101 10110001 00000101
23:14:16.040 [t2] - 22 00000000 00000000 00000000 00000001 00011100 10010101 10110001 00000101
23:14:16.040 [t2] - 23 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.040 [t2] - 23 00000000 00000000 00000000 00000001 00011100 10010101 10110001 00000101
23:14:16.040 [t2] - 23 00000000 00000000 00000000 00000001 00011100 10010101 10110001 00000101
23:14:16.040 [t2] - 24 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.040 [t2] - 24 00000000 00000000 00000000 00000001 00011100 10010101 10110001 00000101
23:14:16.040 [t2] - 24 00000000 00000000 00000000 00000001 00011100 10010101 10110001 00000101
23:14:16.040 [t2] - 25 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.040 [t2] - 25 00000000 00000000 00000000 00000001 00011100 10010101 10110001 00000101
23:14:16.040 [t2] - 25 00000000 00000000 00000000 00000001 00011100 10010101 10110001 00000101
23:14:16.040 [t2] - 26 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.040 [t2] - 26 00000000 00000000 00000000 00000001 00011100 10010101 10110001 00000101
23:14:16.040 [t2] - 26 00000000 00000000 00000000 00000001 00011100 10010101 10110001 00000101
23:14:16.040 [t2] - 27 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.040 [t2] - 27 00000000 00000000 00000000 00000001 00011100 10010101 10110001 00000101
23:14:16.040 [t2] - 27 00000000 00000000 00000000 00000001 00011100 10010101 10110001 00000101
23:14:16.040 [t2] - 28 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.040 [t2] - 28 00000000 00000000 00000000 00000001 00011100 10010101 10110001 00000101
23:14:16.040 [t2] - 28 00000000 00000000 00000000 00000001 00011100 10010101 10110001 00000101
23:14:16.040 [t2] - 29 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.040 [t2] - 29 00000000 00000000 00000000 00000001 00011100 10010101 10110001 00000101
23:14:16.040 [t2] - 29 00000000 00000000 00000000 00000001 00011100 10010101 10110001 00000101
t2 线程刚开始执行 synchronized 前,对象 d 还是偏向于 t1 线程的。可以从线程 ID 看出来。
:::success
23:14:16.036 [t1] - 29 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.036 [t2] - ===============>
23:14:16.036 [t2] - 0 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.037 [t2] - 0 00000000 00000000 00000000 00000001 01101110 01111100 11101001 01000000
23:14:16.037 [t2] - 0 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
:::
t2 线程在执行 synchronized 时,偏向锁被撤销,升级为轻量级锁。
t2 线程执行完 synchronized 同步代码块后,释放锁,锁对象变为不可偏向状态。
t2 线程执行的前 19(注意 序号是从 0 开始的,包括第 19 次)次都是如此,但执行到第 20 次开始,发生了变化。
:::success
23:14:16.039 [t2] - 18 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.039 [t2] - 18 00000000 00000000 00000000 00000001 01101110 01111100 11101001 01000000
23:14:16.040 [t2] - 18 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
23:14:16.040 [t2] - 19 00000000 00000000 00000000 00000001 00011110 10100011 01010000 00000101
23:14:16.040 [t2] - 19 00000000 00000000 00000000 00000001 00011100 10010101 10110001 00000101
23:14:16.040 [t2] - 19 00000000 00000000 00000000 00000001 00011100 10010101 10110001 00000101
:::
t2 线程,第 20 次 synchronized 前,还是偏向于线程 t1 的。
但在 synchronized 时,锁对象就偏向于了 t2 线程。而且在同步代码块执行结束后,偏向状态也没有发生改变。
这种偏向是批量重偏向的,是一次性完成的偏向操作,从第 20 次往后,全部都偏向了 t2 线程。
批量撤销
当撤销偏向锁阈值超过 40 次后,JVM 会这样觉得,自己确实是偏向错了,根本就不该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。
import lombok.extern.slf4j.Slf4j;
import java.util.Vector;
import java.util.concurrent.locks.LockSupport;
@Slf4j(topic = "c.TestBiased")
public class TestBiased {
public static void main(String[] args) throws InterruptedException {
test4();
}
static Thread t1,t2,t3;
private static void test4() throws InterruptedException {
Vector<Dog> list = new Vector<>();
int loopNumber = 39;
t1 = new Thread(() -> {
for (int i = 0; i < loopNumber; i++) {
Dog d = new Dog();
list.add(d);
synchronized (d) {
log.debug(i + "\t" + JolUtils.toPrintableSimple(d));
}
}
LockSupport.unpark(t2);
}, "t1");
t1.start();
t2 = new Thread(() -> {
LockSupport.park();
log.debug("===============> ");
for (int i = 0; i < loopNumber; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + JolUtils.toPrintableSimple(d));
synchronized (d) {
log.debug(i + "\t" + JolUtils.toPrintableSimple(d));
}
log.debug(i + "\t" + JolUtils.toPrintableSimple(d));
}
LockSupport.unpark(t3);
}, "t2");
t2.start();
t3 = new Thread(() -> {
LockSupport.park();
log.debug("===============> ");
for (int i = 0; i < loopNumber; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + JolUtils.toPrintableSimple(d));
synchronized (d) {
log.debug(i + "\t" + JolUtils.toPrintableSimple(d));
}
log.debug(i + "\t" + JolUtils.toPrintableSimple(d));
}
}, "t3");
t3.start();
t3.join();
log.debug(JolUtils.toPrintableSimple(new Dog()));
}
}
class Dog {
}
锁清除
@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations=3)
@Measurement(iterations=5)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyBenchmark {
static int x = 0;
@Benchmark
public void a() throws Exception {
x++;
}
@Benchmark
// JIT 即时编译器会对热点代码进行优化,优化分析时发现局部变量 o 根本不会逃离
// 该方法的作用范围,这就意味着该对象是不可能被共享的,那么对该对象进行加锁也就没有任何意义
// JIT 即时编译器就会将 synchronized 优化掉。
public void b() throws Exception {
Object o = new Object();
synchronized (o) {
x++;
}
}
}
打包:java -jar benchmarks.jar
执行结果:
去除锁消除功能后,打包:java -XX:-EliminateLocks -jar benchmarks.jar
锁粗化
对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化,这不同于之前讲的细分锁的粒度。
wait notify 原理
- Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
- BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
- BLOCKED 线程会在 Owner 线程释放锁时唤醒
- WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味着立刻获得锁,仍需进入 EntryList 重新竞争
API 介绍
obj.wait()
让进入 object 监视器的线程到 waitSet 等待obj.notify()
在 object 上正在 waitSet 等待的线程中挑一个唤醒obj.notifyAll()
让 object 上正在 waitSet 等待的线程全部唤醒
它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法
@Slf4j(topic = "c.Test")
public class Test18 {
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (lock) {
log.debug("执行");
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("其它代码....");
}
}, "t1").start();
new Thread(() -> {
synchronized (lock) {
log.debug("执行");
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("其它代码....");
}
}, "t2").start();
Thread.sleep(2000);
log.debug("唤醒其他线程");
synchronized (lock) {
lock.notify(); // 唤醒lock上一个线程
// lock.notifyAll(); // 唤醒lock上所有等待线程
}
}
}
notify 的执行结果:
notifyAll 的执行结果:
wait()
方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程有机会获得对象的锁。无限等待,直到 notify 为止
wait(long n)
有时限的等待,到 n 毫秒后结束等待,或是被 notify
wait notify 的正确姿势
sleep(long n) 和 wait(long n) 的区别
不同点
- sleep 是 Thread 方法,而 wait 是 Object 的方法
- sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
- sleep 在睡眠的同时,不会释放对象锁,但 wait 在等待的时候会释放掉对象锁
相同点
- 它们的状态均为 TIMED_WAITING
代码演示
step1
@Slf4j(topic = "c.Test")
public class TestWaitNotify {
private static final Object room = new Object();
private static boolean hasCigarette = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?【{}】", hasCigarette);
if(!hasCigarette) {
log.debug("没有烟,休息会!");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了!");
}
}, "其他人").start();
}
Thread.sleep(1000);
new Thread(() -> {
hasCigarette = true;
log.debug("烟到了");
}, "送烟的").start();
}
}
输出:
:::info
17:35:59.272 [小南] - 有烟没?【false】
17:35:59.273 [小南] - 没有烟,休息会!
17:36:00.296 [送烟的] - 烟到了
17:36:01.275 [其他人] - 可以开始干活了!
17:36:01.275 [其他人] - 可以开始干活了!
17:36:01.275 [其他人] - 可以开始干活了!
17:36:01.275 [其他人] - 可以开始干活了!
17:36:01.275 [其他人] - 可以开始干活了!
:::
- 其他干活的线程,都要一直阻塞,效率太低
- 小南贤臣个必须水族 2s 后才能醒来,就算烟提前送到,也无法立刻醒来
- 加了
synchronized(room)
后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main 没加 synchronized 就好像 main 线程是翻窗户进来的 - 解决方法,使用 wait-notify 机制
step2
思考下面的实现行吗,为什么?
@Slf4j(topic = "c.Test")
public class TestWaitNotify {
private static final Object room = new Object();
private static boolean hasCigarette = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?【{}】", hasCigarette);
if(!hasCigarette) {
log.debug("没有烟,休息会!");
try {
room.wait(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了!");
}
}, "其他人").start();
}
Thread.sleep(1000);
new Thread(() -> {
synchronized (room) {
hasCigarette = true;
log.debug("烟到了");
room.notify();
}
}, "送烟的").start();
}
}
输出:
:::info
17:43:02.063 [小南] - 有烟没?【false】
17:43:02.064 [小南] - 没有烟,休息会!
17:43:02.064 [其他人] - 可以开始干活了!
17:43:02.065 [其他人] - 可以开始干活了!
17:43:02.065 [其他人] - 可以开始干活了!
17:43:02.065 [其他人] - 可以开始干活了!
17:43:02.065 [其他人] - 可以开始干活了!
17:43:03.068 [送烟的] - 烟到了
:::
- 解决了其它干活的线程阻塞的问题
- 但如果有其它线程也在等待条件呢?
step3
@Slf4j(topic = "c.Test")
public class TestWaitNotify {
private static final Object room = new Object();
private static boolean hasCigarette = false;
private static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?【{}】", hasCigarette);
if(!hasCigarette) {
log.debug("没有烟,休息会!");
try {
room.wait(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("有烟没?【{}】", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
log.debug("有外卖没?【{}】", hasTakeout);
if(!hasTakeout) {
log.debug("没有外卖,休息会!");
try {
room.wait(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("有外卖没?【{}】", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活");
}
}
}, "小女").start();
Thread.sleep(1000);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了");
room.notify();
}
}, "送外卖的").start();
}
}
输出:
17:50:28.816 [小南] - 有烟没?【false】
17:50:28.818 [小南] - 没有烟,休息会!
17:50:28.818 [小女] - 有外卖没?【false】
17:50:28.818 [小女] - 没有外卖,休息会!
17:50:29.822 [送外卖的] - 外卖到了
17:50:29.822 [小南] - 有烟没?【false】
17:50:29.822 [小南] - 没干成活
17:50:30.819 [小女] - 有外卖没?【true】
17:50:30.819 [小女] - 可以开始干活了
- notify 只能随机唤醒一个 WaitSet 中的线程,这是如果有其他线程也在等待,那么就可能唤醒不了正确的线程,称之为【虚假唤醒】
- 解决方法,改为 notifyAll
step 4
@Slf4j(topic = "c.Test")
public class TestWaitNotify {
private static final Object room = new Object();
private static boolean hasCigarette = false;
private static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?【{}】", hasCigarette);
if(!hasCigarette) {
log.debug("没有烟,休息会!");
try {
room.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("有烟没?【{}】", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
log.debug("有外卖没?【{}】", hasTakeout);
if(!hasTakeout) {
log.debug("没有外卖,休息会!");
try {
room.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("有外卖没?【{}】", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活");
}
}
}, "小女").start();
Thread.sleep(1000);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了");
room.notifyAll();
}
}, "送外卖的").start();
}
}
输出:
:::info
18:25:40.793 [小南] - 有烟没?【false】
18:25:40.795 [小南] - 没有烟,休息会!
18:25:40.795 [小女] - 有外卖没?【false】
18:25:40.795 [小女] - 没有外卖,休息会!
18:25:41.794 [送外卖的] - 外卖到了
18:25:41.794 [小女] - 有外卖没?【true】
18:25:41.794 [小女] - 可以开始干活了
18:25:41.794 [小南] - 有烟没?【false】
18:25:41.794 [小南] - 没干成活
:::
- 用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了
- 解决方法,用 while +wait,当条件不成立,再次 wait
step 5
将 if 改为 while
@Slf4j(topic = "c.Test")
public class TestWaitNotify {
private static final Object room = new Object();
private static boolean hasCigarette = false;
private static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?【{}】", hasCigarette);
while(!hasCigarette) {
log.debug("没有烟,休息会!");
try {
room.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("有烟没?【{}】", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
log.debug("有外卖没?【{}】", hasTakeout);
while(!hasTakeout) {
log.debug("没有外卖,休息会!");
try {
room.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("有外卖没?【{}】", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活");
}
}
}, "小女").start();
Thread.sleep(1000);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了");
room.notifyAll();
}
}, "送外卖的").start();
}
}
输出:
18:30:01.716 [小南] - 有烟没?【false】
18:30:01.718 [小南] - 没有烟,休息会!
18:30:01.718 [小女] - 有外卖没?【false】
18:30:01.718 [小女] - 没有外卖,休息会!
18:30:02.718 [送外卖的] - 外卖到了
18:30:02.718 [小女] - 有外卖没?【true】
18:30:02.718 [小女] - 可以开始干活了
18:30:02.718 [小南] - 没有烟,休息会!
总结
synchronized(lock) {
while(条件不成立) {
lock.wait();
}
// 干活
}
//另一个线程
synchronized(lock) {
lock.notifyAll();
}
同步模式之保护性暂停
定义
即 Guarded Suspension,用在一个线程等待另一个线程的执行结果
要点:
- 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
- 如果有结果不断从一个线程到另一个线程,那么可以使用消息队列(见生产者/消费者)
- JDK 中,join 的实现、Future 的实现,采用的就是此模式
- 因为要等待另一方的结果,因此归类到同步模式
实现
class GuardedObject {
private Object response;
public Object get() {
synchronized (this) {
while(response == null) {
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
return response;
}
}
public void complete(Object response) {
synchronized (this) {
this.response = response;
this.notifyAll();
}
}
}
应用:
一个线程等待另一个线程的执行结果
public class Downloader {
public static List<String> download() throws IOException {
HttpURLConnection conn = (HttpURLConnection) new URL("https://www.baidu.com/").openConnection();
List<String> lines = new ArrayList<>();
try(BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while((line = reader.readLine()) != null) {
lines.add(line);
}
}
return lines;
}
}
@Slf4j(topic = "c.Test")
public class Test20 {
public static void main(String[] args) {
GuardedObject guardedObject = new GuardedObject();
new Thread(() -> {
log.debug("获取结果");
List<String> response = (List<String>) guardedObject.get();
log.debug("文件大小:{}", response.size());
}, "t1").start();
new Thread(() -> {
try {
log.debug("执行下载");
List<String> download = Downloader.download();
guardedObject.complete(download);
} catch (IOException e) {
throw new RuntimeException(e);
}
}, "t2").start();
}
}
执行结果:
带超时版的 GuardedObject
如果要控制超时时间呢?
class GuardedObject {
private Object response;
// timeout 表示需要等多久
public Object get(long timeout) {
synchronized (this) {
// 记录开始时间
long begin = System.currentTimeMillis();
// 经历的时间
long passed = 0;
while(response == null) {
// 这一轮循环应该等待的时间
long waitTime = timeout - passed;
if(waitTime <= 0) {
break;
}
try {
this.wait(waitTime); // 可能存在虚假唤醒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
passed = System.currentTimeMillis() - begin;
}
return response;
}
}
public void complete(Object response) {
synchronized (this) {
this.response = response;
this.notifyAll();
}
}
}
测试没有超时
@Slf4j(topic = "c.Test")
public class Test19 {
public static void main(String[] args) {
GuardedObjectTimeout guardedObject = new GuardedObjectTimeout();
new Thread(() -> {
log.debug("begin");
Object response = guardedObject.get(2000);
log.debug("结果是:{}", response);
}, "t1").start();
new Thread(() -> {
log.debug("begin");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
guardedObject.complete(new Object());
}, "t2").start();
}
}
测试结果:
测试超时
@Slf4j(topic = "c.Test")
public class Test19 {
public static void main(String[] args) {
GuardedObjectTimeout guardedObject = new GuardedObjectTimeout();
new Thread(() -> {
log.debug("begin");
Object response = guardedObject.get(2000);
log.debug("结果是:{}", response);
}, "t1").start();
new Thread(() -> {
log.debug("begin");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
guardedObject.complete(new Object());
}, "t2").start();
}
}
测试结果:
测试虚假唤醒
@Slf4j(topic = "c.Test")
public class Test19 {
public static void main(String[] args) {
GuardedObjectTimeout guardedObject = new GuardedObjectTimeout();
new Thread(() -> {
log.debug("begin");
Object response = guardedObject.get(2000);
log.debug("结果是:{}", response);
}, "t1").start();
new Thread(() -> {
log.debug("begin");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
guardedObject.complete(null); // 虚假唤醒
}, "t2").start();
}
}
测试结果:
join 原理
public final void join() throws InterruptedException {
join(0);
}
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis(); // 记录当前时间
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) { // 如果等待时间为 0,且线程还存活,继续等待
wait(0);
}
} else {
while (isAlive()) { // 如果等待时间不为 0,且线程还存活
long delay = millis - now; // 本轮循环需等待的时间
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base; // 已经等待时间
}
}
}
多任务版 GuardedObject
图中 Futures 就好比居民楼一层的信箱(每个信箱有房间编号),左侧的 t0、t2、t4 就好比等待邮件的居民,右侧的 t1、t3、t5 就好比邮递员
如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类,这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理。
新增 id 用来标识 Guarded Object
class GuardedObject {
private int id;
public int getId() {
return id;
}
public GuardedObject(int id) {
this.id = id;
}
private Object response;
// timeout 表示需要等多久
public Object get(long timeout) {
synchronized (this) {
// 记录开始时间
long begin = System.currentTimeMillis();
// 经历的时间
long passed = 0;
while(response == null) {
// 这一轮循环应该等待的时间
long waitTime = timeout - passed;
if(waitTime <= 0) {
break;
}
try {
this.wait(waitTime); // 可能存在虚假唤醒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
passed = System.currentTimeMillis() - begin;
}
return response;
}
}
public void complete(Object response) {
synchronized (this) {
this.response = response;
this.notifyAll();
}
}
}
中间解耦类
class Mailbox {
private static Map<Integer, GuardedObject> boxes = new Hashtable<>();
private static int id = 0;
public synchronized static int generateId() {
return id++;
}
public static GuardedObject getGuardedObject(int id) { // Hashtable 为线程安全的,所以不需要加 synchronized
return boxes.remove(id);
}
public static GuardedObject createGuardedObject() {
GuardedObject go = new GuardedObject(generateId());
boxes.put(go.getId(), go);
return go;
}
public static Set<Integer> getIds() {
return boxes.keySet();
}
}
业务类
@Slf4j(topic ="c.People")
class People extends Thread {
@Override
public void run() {
// 收信
GuardedObject go = Mailbox.createGuardedObject();
log.debug("开始收信 id:{}", go.getId());
Object mail = go.get(5000);
log.debug("收到信,id:{}, 内容:{}", go.getId(), mail);
}
}
@Slf4j(topic = "c.Postman")
class Postman extends Thread {
private int id;
private String mail;
public Postman(int id, String mail) {
this.id = id;
this.mail = mail;
}
@Override
public void run() {
// 送信
GuardedObject guardedObject = Mailbox.getGuardedObject(id);
log.debug("送信 id:{},送信内容:{}", id, mail);
guardedObject.complete(mail);
}
}
测试类
@Slf4j(topic = "c.Test")
public class Test20 {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
new People().start();
}
Thread.sleep(1000);
for (Integer id : Mailbox.getIds()) {
new Postman(id, "内容" + id).start();
}
}
}
测试结果:
异步模式之生产者/消费者
class Message {
private int id;
private Object message;
public int getId() {
return id;
}
public Object getMessage() {
return message;
}
public Message(int id, Object message) {
this.id = id;
this.message = message;
}
@Override
public String toString() {
return "Message{" +
"id=" + id +
", message=" + message +
'}';
}
}
@Slf4j(topic = "c.MessageQueue")
class MessageQueue {
private LinkedList<Message> queue = new LinkedList<>();
private int capacity;
public MessageQueue(int capacity) {
this.capacity = capacity;
}
public Message take() {
synchronized (queue) {
while(queue.isEmpty()) {
try {
log.debug("队列为空,消费者线程等待");
queue.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
Message message = queue.removeFirst();
log.debug("已消费消息 {}", message);
queue.notifyAll();
return message;
}
}
public void put(Message message) {
synchronized (queue) {
while(queue.size() == capacity) {
try {
log.debug("队列已满,生产者线程等待");
queue.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
queue.addLast(message);
log.debug("已生产消息 {}", message);
queue.notifyAll();
}
}
}
测试没有消费者时:
@Slf4j(topic = "c.Test")
public class Test21 {
public static void main(String[] args) {
MessageQueue messageQueue = new MessageQueue(2);
for (int i = 0; i < 3; i++) {
int id = i;
new Thread(() -> {
messageQueue.put(new Message(id, "值" + id));
}, "生产者" + id).start();
}
new Thread(() -> {});
}
}
输出:
队列容量为 2,有三个生产者,当其中两个生产者生产两条消息后,由于没有消费者从队列中消费消息,导致生产者停止生产。
增加消费者后测试:
@Slf4j(topic = "c.Test")
public class Test21 {
public static void main(String[] args) {
MessageQueue messageQueue = new MessageQueue(2);
for (int i = 0; i < 3; i++) {
int id = i;
new Thread(() -> {
messageQueue.put(new Message(id, "值" + id));
}, "生产者" + id).start();
}
new Thread(() -> {
while(true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
messageQueue.take();
}
}, "消费者").start();
}
}
输出:
因为只有三个生产者,所以消费者在消费完三条消息后进入了等待
Park&Unpark
基本使用
它们是 LockSupport 类中的方法
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象);
先 park 再 unpark
@Slf4j(topic = "c.Test")
public class Test22 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("start...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("park...");
LockSupport.park();
log.debug("resume...");
}, "t1");
t1.start();
Thread.sleep(2000);
log.debug("unpark...");
LockSupport.unpark(t1);
}
}
输出:
先 unpark 再 park
@Slf4j(topic = "c.Test")
public class Test22 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("start...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("park...");
LockSupport.park();
log.debug("resume...");
}, "t1");
t1.start();
Thread.sleep(1000);
log.debug("unpark...");
LockSupport.unpark(t1);
}
}
输出:
特点
与 Object 的 wait & notify 相比
- wait、notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park、unpark 不必
- park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】
- park & unpark 可以先 unpark,而 wait & notify 不能先 notify
park & unpark原理
每个线程都有自己的一个 Parker 对象,由三部分组成_counter
、_cond
、_mutex
,打个比喻
- 线程就像一个旅人,Parker 就像他随身携带的背包,条件变量
_cond
就好比背包中的帐篷。_counter
就好比背包中的备用干粮(0为耗尽,1为充足) - 调用 park 就是要看需不需要停下来歇息
- 如果备用干粮耗尽,那么钻进帐篷歇息
- 如果备用干粮充足,那么不需要停留,继续前进
- 调用 unpark,就好比令干粮充足
- 如果这时线程还在帐篷,就唤醒让他继续前进
- 如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进
- 因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮
- 当前线程调用 Unsafe.park() 方法
- 检查 _counter,本情况为 0,这是,获得 _mutex 互斥锁
- 线程进入 _cond 条件变量阻塞
- 设置 _counter = 0
- 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
- 唤醒 _cond 条件变量中的 Thread_0
- Thread_0 恢复运行
- 设置 _counter 为 0
- 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
- 当前线程调用 Unsafe.park() 方法
- 检查 _counter,本情况为 1,这时线程无需阻塞,继续运行
- 设置 _counter 为 0
重新理解线程状态转换
假设有线程 Thread t
情况1 NEW --> RUNNABLE
- 当调用
t.start()
方法时,由NEW --> RUNNABLE
情况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上所有等待线程 断点
}
}
}
情况3 RUNNABLE <--> WAITING
- 当前线程调用
t.join()
方法时,当前线程从RUNNABLE <--> WAITING
- 注意是当前线程在 t 线程对象的监视器上等待
- t 线程运行结束,或调用了当前线程的
interrupt()
时,当前线程从WAITING <--> RUNNABLE
情况4 RUNNABLE <--> WAITING
- 当前线程调用
LockSupport.park()
方法会让当前线程从RUNNABLE <--> WAITING
- 调用
LockSupport.unpark(目标线程)
或调用了线程的interrupt()
,会让目标线程从WAITING <--> RUNNABLE
情况5 RUNNABLE <--> TIMED_WAITING
- 调用
obj.wait(long n)
方法时,t 线程从RUNNABLE <--> TIMED_WAITING
- t 线程等待时间超过了 n 毫秒,或调用
obj.notify()
,obj.notifyAll()
,t.interrupt()
时- 竞争锁成功,t 线程从
TIMED_WAITING --> RUNNABLE
- 竞争锁失败,t 线程从
TIMED_WAITING --> BLOCKED
- 竞争锁成功,t 线程从
情况6 RUNNABLE <--> TIMED_WAITING
- 当前线程调用
t.join(long n)
方法时,当前线程从RUNNABLE <--> TIMED_WAITING
- 注意是当前线程在 t 线程对象的监视器上等待
- 当前线程等待时间超过了 n 毫秒,或** t 线程运行结束,或调用了当前线程**的
interrupt()
时,当前线程从TIMED_WAITING <--> RUNNABLE
情况7 RUNNABLE <--> TIMED_WAITING
- 当前线程调用
Thread.sleep(long n)
,当前线程从RUNNABLE <--> TIMED_WAITING
- 当前线程等待时间超过了 n 毫秒,当前线程从
TIMED_WAITING <--> RUNNABLE
情况8 RUNNABLE <--> TIMED_WAITING
- 当前线程调用
LockSupport.park(long nanos)
或LockSupport.parkUntil(long millis)
时,当前线程从RUNNABLE <--> TIMED_WAITING
- 调用
LockSupport.unpark(目标线程)
或调用了线程的interrupt()
,或是等待超时,会让目标线程从TIMED_WAITING <--> RUNNABLE
情况9 RUNNABLE <--> BLOCKED
- t 线程用
synchronized(obj)
获取对象锁时如果竞争失败,从RUNNABLE <--> BLOCKED
- 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有
BLOCKED
的线程重新竞争,如果其中 t 线程竞争成功,从BLOCKED <--> RUNNABLE
其他失败的线程仍然BLOCKED
情况9 RUNNABLE <--> TERMINIATED
当前线程所有代码运行完毕,进入TERMINIATED
多把锁
**多把不相干的锁 **
一间大屋子有两个功能:睡觉、学习,互不相干。
现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低
解决方法是准备多个房间(多个对象锁)
例如:
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);
}
}
}
执行
public class TestMultiLock {
public static void main(String[] args) {
BigRoom bigRoom = new BigRoom();
new Thread(() -> {
bigRoom.sleep();
}, "小南").start();
new Thread(() -> {
bigRoom.study();
},"小女").start();
}
}
结果
改进
@Slf4j(topic = "c.BigRoom")
class BigRoom {
private final Object sleep = new Object();
private final Object study = new Object();
public void sleep() {
synchronized (sleep) {
log.debug("sleeping 2 小时");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public void study() {
synchronized (study) {
log.debug("study 1 小时");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
执行结果
将锁的粒度细分
- 好处,是可以增强并发度
- 坏处,如果以线程需要同时获得多把锁,就容易发生死锁
活跃性
死锁
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
t1 线程
获得 A对象
锁,接下来想获取 B对象
的锁 t2 线程
获得 B对象
锁,接下来想获取 A对象
的锁,例:
@Slf4j(topic = "c.TestDeadLock")
public class TestDeadLock {
public static void main(String[] args) {
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A) {
log.debug("lock A");
sleep(1000);
synchronized (B) {
log.debug("lock B");
log.debug("操作...");
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (B) {
log.debug("lock B");
sleep(500);
synchronized (A) {
log.debug("lock A");
log.debug("操作...");
}
}
}, "t2");
t1.start();
t2.start();
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
结果:
13:18:15.272 [t1] - lock A
13:18:15.272 [t2] - lock B
定位死锁
检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁:
MacBook-Pro juc % jps
43216 Launcher
1121
45923 Jps
31655 Launcher
31656 SubjectApplication
24072 st
45867 Launcher
45868 TestDeadLock
liquanpeng@liquanpengdeMacBook-Pro juc % jstack 45868
2024-04-22 13:20:49
Full thread dump OpenJDK 64-Bit Server VM (25.282-b08 mixed mode):
"Attach Listener" #14 daemon prio=9 os_prio=31 tid=0x00000001589c0800 nid=0x3207 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"DestroyJavaVM" #13 prio=5 os_prio=31 tid=0x0000000157921800 nid=0x1003 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"t2" #12 prio=5 os_prio=31 tid=0x00000001589bf000 nid=0x5b03 waiting for monitor entry [0x0000000172296000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.atheima.test.TestDeadLock.lambda$main$1(TestDeadLock.java:34)
- waiting to lock <0x000000076b7cd680> (a java.lang.Object)
- locked <0x000000076b7cd690> (a java.lang.Object)
at com.atheima.test.TestDeadLock$$Lambda$2/875827115.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"t1" #11 prio=5 os_prio=31 tid=0x000000015791e800 nid=0x5903 waiting for monitor entry [0x000000017208a000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.atheima.test.TestDeadLock.lambda$main$0(TestDeadLock.java:23)
- waiting to lock <0x000000076b7cd690> (a java.lang.Object)
- locked <0x000000076b7cd680> (a java.lang.Object)
at com.atheima.test.TestDeadLock$$Lambda$1/451111351.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
// 略去部分输出
Found one Java-level deadlock:
=============================
"t2":
waiting to lock monitor 0x0000000158816a20 (object 0x000000076b7cd680, a java.lang.Object),
which is held by "t1"
"t1":
waiting to lock monitor 0x0000000158818ff0 (object 0x000000076b7cd690, a java.lang.Object),
which is held by "t2"
Java stack information for the threads listed above:
===================================================
"t2":
at com.atheima.test.TestDeadLock.lambda$main$1(TestDeadLock.java:34)
- waiting to lock <0x000000076b7cd680> (a java.lang.Object)
- locked <0x000000076b7cd690> (a java.lang.Object)
at com.atheima.test.TestDeadLock$$Lambda$2/875827115.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"t1":
at com.atheima.test.TestDeadLock.lambda$main$0(TestDeadLock.java:23)
- waiting to lock <0x000000076b7cd690> (a java.lang.Object)
- locked <0x000000076b7cd680> (a java.lang.Object)
at com.atheima.test.TestDeadLock$$Lambda$1/451111351.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
- 避免死锁要注意加锁顺序
- 另外如果由于某个线程进入了死循环,导致其他线程一直等待,对于这种情况 Linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用
top -Hp 进程id
来定位是哪个线程,最后在用 jstack 排查 - 也可以使用 jconsole 来进行排查
哲学家就餐问题
有五位哲学家,围坐在圆桌旁。
- 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
- 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
- 如果筷子被身边的人拿着,自己就得等待
筷子类
class Chopstick {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "Chopstick{" +
"name='" + name + '\'' +
'}';
}
}
哲学家类
@Slf4j(topic = "c.Philosopher")
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) {
synchronized (left) {
synchronized (right) {
eat();
}
}
}
}
private void eat() {
log.debug("吃饭...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
就餐
public class TestPhilosopherProblem {
public static void main(String[] args) {
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();
}
}
执行不多会,就执行不下去了
名称: 阿基米德
状态: com.atheima.test.Chopstick@77ae3291上的BLOCKED, 拥有者: 苏格拉底
总阻止数: 1, 总等待数: 0
堆栈跟踪:
com.atheima.test.Philosopher.run(TestPhilosopherProblem.java:58)
- 已锁定 com.atheima.test.Chopstick@b820676
--------------------------------------------------------------------------
名称: 苏格拉底
状态: com.atheima.test.Chopstick@185b8f25上的BLOCKED, 拥有者: 柏拉图
总阻止数: 2, 总等待数: 5
堆栈跟踪:
com.atheima.test.Philosopher.run(TestPhilosopherProblem.java:58)
- 已锁定 com.atheima.test.Chopstick@77ae3291
--------------------------------------------------------------------------
名称: 柏拉图
状态: com.atheima.test.Chopstick@230f186a上的BLOCKED, 拥有者: 亚里士多德
总阻止数: 2, 总等待数: 0
堆栈跟踪:
com.atheima.test.Philosopher.run(TestPhilosopherProblem.java:58)
- 已锁定 com.atheima.test.Chopstick@185b8f25
--------------------------------------------------------------------------
名称: 亚里士多德
状态: com.atheima.test.Chopstick@185100f9上的BLOCKED, 拥有者: 赫拉克利特
总阻止数: 9, 总等待数: 6
堆栈跟踪:
com.atheima.test.Philosopher.run(TestPhilosopherProblem.java:58)
- 已锁定 com.atheima.test.Chopstick@230f186a
--------------------------------------------------------------------------
名称: 赫拉克利特
状态: com.atheima.test.Chopstick@b820676上的BLOCKED, 拥有者: 阿基米德
总阻止数: 2, 总等待数: 0
堆栈跟踪:
com.atheima.test.Philosopher.run(TestPhilosopherProblem.java:58)
- 已锁定 com.atheima.test.Chopstick@185100f9
这种线程没有按预期结束,执行不下去的情况,归类为【活跃性】问题,除了死锁以外,还有活锁和饥饿者两种情况
活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如:
@Slf4j(topic = "c.TestLiveLock")
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(200);
count--;
log.debug("count: {}", count);
}
}, "t1").start();
new Thread(() -> {
// 期望超过 20 退出循环
while(count < 20) {
sleep(200);
count++;
log.debug("count: {}", count);
}
}, "t2").start();
}
public static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
饥饿
很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题
线程饥饿的例子:
先来看看使用顺序加锁的方式解决之前的死锁问题
顺序加锁的解决方案
代码调整:
public class TestPhilosopherProblem {
public static void main(String[] args) {
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("阿基米德", c1, c5).start();
}
}
将最后一句代码由new Philosopher("阿基米德", c5, c1).start();
改为new Philosopher("阿基米德", c1, c5).start();
让线程“阿基米德”去顺序获取锁。
执行结果不会发生死锁,但是出现了另一个问题,就是线程“赫拉克利特”可能会一直执行,而线程“阿基米德”则一直得不到 CPU 调度执行,产生饥饿现象。
ReentrantLock
相对于 synchronized,它具有如下特点:
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
与 synchronized 一样,都支持可重入
基本语法:
// 获取锁
reentrantLock.lock();
try{
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
可重入
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁。如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。
@Slf4j(topic = "c.TestReetrantLock")
public class TestReetrantLock {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try {
log.debug("enter main");
m1();
} finally {
lock.unlock();
}
}
private static void m1() {
lock.lock();
try {
log.debug("enter m1");
m2();
} finally {
lock.unlock();
}
}
private static void m2() {
lock.lock();
try {
log.debug("enter m2");
} finally {
lock.unlock();
}
}
}
输出:
19:51:40.059 [main] - enter main
19:51:40.060 [main] - enter m1
19:51:40.060 [main] - enter m2
可打断
示例
@Slf4j(topic = "c.Test")
public class Test23 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
// 如果没有竞争那么此方法就会获取 lock 对象锁
// 如果有竞争就进入阻塞队列,可以被其他线程用 interruput 方法打断
log.debug("尝试获取锁");
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("没有获得锁,返回");
return;
}
try {
log.debug("获得锁");
} finally {
log.debug("t1线程释放锁");
lock.unlock();
}
}, "t1");
lock.lock(); // 主线程先获得锁
log.debug("主线程获得锁");
t1.start();
try {
Thread.sleep(1000);
t1.interrupt();
log.debug("执行打断");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
输出
注意如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断
@Slf4j(topic = "c.Test")
public class Test23 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
lock.lock();
try {
log.debug("获得锁");
} finally {
log.debug("t1线程释放锁");
lock.unlock();
}
}, "t1");
lock.lock(); // 主线程先获得锁
log.debug("主线程获得锁");
t1.start();
try {
Thread.sleep(1000);
t1.interrupt();
log.debug("执行打断");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
log.debug("主线程释放锁");
lock.unlock();
}
}
}
输出
锁超时
立刻失败
@Slf4j(topic = "c.Test24")
public class Test24 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("尝试获取锁");
if (!lock.tryLock()) {
log.debug("获取不到锁");
return;
}
try {
log.debug("获得锁");
} finally {
log.debug("t1线程释放锁");
lock.unlock();
}
}, "t1");
lock.lock();
t1.start();
try {
log.debug("主线程获得锁");
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
log.debug("主线程释放锁");
lock.unlock();
}
}
}
输出
超时失败
@Slf4j(topic = "c.Test24")
public class Test24 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("尝试获取锁");
try {
if (!lock.tryLock(1, TimeUnit.SECONDS)) {
log.debug("获取等待 1 秒后失败,返回");
return;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
log.debug("获得锁");
} finally {
log.debug("t1线程释放锁");
lock.unlock();
}
}, "t1");
lock.lock();
t1.start();
try {
log.debug("主线程获得锁");
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
log.debug("主线程释放锁");
lock.unlock();
}
}
}
输出
使用 tryLock 解决哲学家就餐问题
筷子类
class Chopstick extends ReentrantLock {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "Chopstick{" +
"name='" + name + '\'' +
'}';
}
}
哲学家类
@Slf4j(topic = "c.Philosopher")
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("吃饭...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
测试类
public class TestPhilosopherProblem {
public static void main(String[] args) {
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();
}
}
公平锁
ReentrantLock 默认是不公平的
公平锁一般没有必要,会降低并发度,后面分析原理时会讲解
条件变量
synchronized 中也有条件变量,就是我们讲原理时那个 WaitSet 休息室,当条件不满足时进入 WaitSet 等待。
ReetrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比
- synchronized 是那些不满足条件的线程都在一间休息室等消息
- 而 ReetrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室。唤醒时也是按休息室来唤醒
使用要点:
- await 前需要获得锁
- await 执行后,会释放锁,进入 conditionObject 等待
- await 的线程被唤醒(或打断、或超时)后需重新竞争 lock 锁
- 竞争 lock 锁成功后,从 await 后继续执行
@Slf4j(topic = "c.Test25")
public class Test25 {
private static ReentrantLock ROOM = new ReentrantLock();
private static Condition waitCigaretteSet = ROOM.newCondition();
private static Condition waitTakeoutSet = ROOM.newCondition();
private static boolean hasCigarette = false;
private static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
ROOM.lock();
try {
log.debug("有烟没?【{}】", hasCigarette);
while(!hasCigarette) {
log.debug("没有烟,休息会!");
try {
waitCigaretteSet.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("可以开始干活了");
} finally {
ROOM.unlock();
}
}, "小南").start();
new Thread(() -> {
ROOM.lock();
try {
log.debug("有外卖没?【{}】", hasTakeout);
while(!hasTakeout) {
log.debug("没有外卖,休息会!");
try {
waitTakeoutSet.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("可以开始干活了");
} finally {
ROOM.unlock();
}
}, "小女").start();
Thread.sleep(1000);
new Thread(() -> {
ROOM.lock();
try {
hasTakeout = true;
log.debug("外卖到了");
waitTakeoutSet.signal();
} finally {
ROOM.unlock();
}
}, "送外卖的").start();
new Thread(() -> {
ROOM.lock();
try {
hasCigarette = true;
log.debug("烟到了");
waitCigaretteSet.signal();
} finally {
ROOM.unlock();
}
}, "送烟的").start();
}
}
输出
同步模式之顺序控制
固定顺序
比如,必须先 2 后 1 打印
wait notify 版
public class TestWaitNotify {
private static Object obj = new Object();
private static boolean t2Runed = false;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (obj) {
while (!t2Runed) {
try {
obj.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(1);
}
}
}, "t2");
Thread t2 = new Thread(() -> {
synchronized (obj) {
System.out.println(2);
t2Runed = true;
obj.notifyAll();
}
}, "t1");
t1.start();
t2.start();
}
}
注意:t1 线程需要先 start,这样才能获得锁后执行 wait 操作,如果 t2 先 start,t2 执行完后会将 t2Runed 置为 true,导致 t1 无法执行 wait
park unpark 版
可以看到,wait/notify 实现上很麻烦:
- 首先,需要保证先 wait 再 notify,否则 wait 线程永远得不到唤醒,因此使用了【运行标记】来判断该不该 wait
- 第二,如果有些干扰线程错误地 notify 了 wait 线程,条件不满足时还要重新等待,使用了 while 循环来解决此问题
- 最后,唤醒对象上的 wait 线程需要使用 notifyAll,因为【同步对象】上的等待线程可能不止一个
可以使用 LockSupport 类的 park 和 unpark 来简化上面的题目:
public class TestParkUnpark {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
LockSupport.park();
System.out.println(1);
}, "t1");
Thread t2 = new Thread(() -> {
System.out.println(2);
LockSupport.unpark(t1);
}, "t1");
t1.start();
t2.start();
}
}
park 和 unpark 方法比较灵活,他俩谁先调用,谁后调用无所谓,并且是以线程为单位进行【暂停】和【恢复】,不需要【同步对象】和【运行标记】
交替输出
线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次。现在要求输出 abcabcabcabcabc 怎么实现
wait/notify 版
class SyncWaitNotify {
private int flag;
private int loopNumber;
public SyncWaitNotify(int flag, int loopNumber) {
this.flag = flag;
this.loopNumber = loopNumber;
}
public void print(String str, int waitFlag, int nextFlag) {
for (int i = 0; i < loopNumber; i++) {
synchronized (this) {
while(flag != waitFlag) {
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.print(str);
flag = nextFlag;
this.notifyAll();
}
}
}
}
public class TestSyncWaitNotify {
public static void main(String[] args) {
// 设置起始标志
SyncWaitNotify syncWaitNotify = new SyncWaitNotify(1, 5);
new Thread(() -> {
syncWaitNotify.print("a", 1, 2);
}).start();
new Thread(() -> {
syncWaitNotify.print("b", 2, 3);
}).start();
new Thread(() -> {
syncWaitNotify.print("c", 3, 1);
}).start();
}
}
Lock 条件变量版
class AwaitSignal extends ReentrantLock {
private int loopNumber;
public AwaitSignal(int loopNumber) {
this.loopNumber = loopNumber;
}
public void start(Condition first) {
lock();
try {
first.signal();
} finally {
unlock();
}
}
public void print(String str, Condition current, Condition next) {
for (int i = 0; i < loopNumber; i++) {
lock();
try {
current.await();
System.out.print(str);
next.signal();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
unlock();
}
}
}
}
public class TestAwaitSignal {
public static void main(String[] args) {
AwaitSignal as = new AwaitSignal(5);
Condition aWaitSet = as.newCondition();
Condition bWaitSet = as.newCondition();
Condition cWaitSet = as.newCondition();
new Thread(() -> {
as.print("a", aWaitSet, bWaitSet);
}, "t1").start();
new Thread(() -> {
as.print("b", bWaitSet, cWaitSet);
}, "t3").start();
new Thread(() -> {
as.print("c", cWaitSet, aWaitSet);
}, "t3").start();
as.start(aWaitSet);
}
}
注意:
该实现没有考虑 a,b,c 线程都就绪再开始
park/unpark 版
class ParkUnpark {
private int loopNumber;
public ParkUnpark(int loopNumber) {
this.loopNumber = loopNumber;
}
public void print(String str, Thread next) {
for (int i = 0; i < loopNumber; i++) {
LockSupport.park();
System.out.print(str);
LockSupport.unpark(next);
}
}
}
public class TestParkUnpark2 {
static Thread t1;
static Thread t2;
static Thread t3;
public static void main(String[] args) {
ParkUnpark parkUnpark = new ParkUnpark(5);
t1 = new Thread(() -> {
parkUnpark.print("a", t2);
}, "t1");
t2 = new Thread(() -> {
parkUnpark.print("b", t3);
}, "t2");
t3 = new Thread(() -> {
parkUnpark.print("c", t1);
}, "t3");
t1.start();
t2.start();
t3.start();
LockSupport.unpark(t1);
}
}
本章小结
本章我们需要重点掌握的是
- 分析多线程访问共享资源时,哪些代码片段属于临界区
- 使用 synchronized 互斥解决临界区的线程安全问题
- 掌握 synchronized 锁对象语法
- 掌握 synchronzied 加载成员方法和静态方法语法
- 掌握 wait/notify 同步方法
- 使用 lock 互斥解决临界区的线程安全问题
- 掌握 lock 的使用细节:可打断、锁超时、公平锁、条件变量
- 学会分析变量的线程安全性、掌握常见线程安全类的使用
- 了解线程活跃性问题:死锁、活锁、饥饿
- 应用方面
- 互斥:使用 synchronized 或 Lock 达到共享资源互斥效果
- 同步:使用 wait/notify 或 Lock 的条件变量来达到线程间通信效果
- 原理方面
- monitor、synchronized 、wait/notify 原理
- synchronized 进阶原理
- park & unpark 原理
- 模式方面
- 同步模式之保护性暂停
- 异步模式之生产者消费者
- 同步模式之顺序控制