(三)共享模型之管程

news2024/11/18 15:16:40

线程安全问题

案例

两个线程对初始值为 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);
    }
}

输出:
image.png
image.png
image.png

问题分析

以上的结果可能是正数、负数、零?为什么呢?因为 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 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:
image.png
如果是单线程,以上 8 行代码是顺序执行(不会交错),没有问题:
image.png
但多线程下这 8 行代码可能交错运行:
出现负数的情况:
image.png
线程 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 等方法

出现正数的情况:
image.png

临界区 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);
    }
}

输出:
image.png

对于 synchronized 的理解

image.png
可以做这样的类比:

  • synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门),房间只能一次进入一人进行计算,线程 t1、t2 想象成两个人
  • 当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门,拿走了钥匙,在门内执行 counter++ 代码
  • 这时候如果 t2 也运行到了 **synchronized (room) 时,**它发现门被锁住了,只能在门外等待,发生了上下文切换,阻塞住了。
  • 这中间即使 t1 的 CPU 时间片不幸用完了,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去),这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才能开门进入。
  • 当 t1 执行完 synchronized{} 块内的代码,这时候才从 room 房间出来,并解开门上的锁,唤醒 t2 线程把钥匙给它。t2 线程这时才能进入 room 房间,锁住了门拿上钥匙,执行它的 count-- 代码。

用图来表示:
image.png

对于 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());
    }
}

输出:
image.png

方法上的 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

如图:
image.png
假设有线程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 就会报错:
image.png
分析:

  • 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
  • method3 与 method2 分析相同

image.png
两个线程 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 相同

image.png
方法访问修饰符带来的思考,如果把 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 变量属于同一个变量,为共享资源,因此是会产生线程安全问题的。

image.png
:::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);
}

不是线程安全的。
image.png
假设有两个线程同时执行上述代码,线程 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;
        }
    }
}

测试结果:
image.png
是会发生线程安全问题的。
分析:
想解决线程安全问题,需要先判断出哪些代码属于临界区。根据临界区的定义“一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区。”可知,下面的代码,即 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 关键字,进行了同步处理,所以不存在线程安全问题。
image.png
那需不需要考虑 window.sell() 和 sellCount.add() 这两个方法的组合的线程安全问题呢?答案也是不需要的,因为 window 和 sellCount 是两个不同的共享变量。跟下面的情况时不一样。
image.png
上述案例是对一个同共享变量 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);
        }
    }
}

运行结果:
image.png
总金额加起来不等于 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 后的运行结果:
image.png
可以看出结果依然不对。想要解决这个问题,必须要用 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);
        }
    }
}

image.png

Monitor 概念

对象头

:::success
一个 Java 对象由对象头,实例数据和对齐填充组成,其中对象头是极其重要的,对象头是由对象标记 Mark Word 和类型指针组成,其中又以 Mark Word 最重要,对于 Mark Word 它占 8 个字节也就是64位(64位虚拟机)!
:::
以 32 位虚拟机为例:
普通对象
image.png
Klass Word 指向了该对象所属的类对象,比如 Student.class 或者 Teacher.class
数组对象
image.png
其中 Mark Word 结构为
image.png
hashcode:每个对象都有一个属于自己的 hash 码。
age:表示分代年龄,如果对象超过了一定的年龄(默认为 15,4bit【2 的 4 次方 】),就会从幸存者区晋升至老年代。
biased_lock:是否是偏向锁
最后两位:代表对象的加锁状态
64 位虚拟机 Mark Word
image.png

Monitor 工作原理

image.png

  • 刚开始 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。

image.png

  • 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word(对象标记),将 Mark Word 的值存入锁记录。

image.png

  • 如果 cas 替换成功,对象头中存储了锁记录地址和状态 00,表示由该线程给对象加锁,这时图示如下:、
    • 00 表示轻量级锁
    • 前 30 位表示锁记录的地址
    • image.png

image.png

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

image.png

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

image.png

  • 当退出 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 已经对该对象加了轻量级锁

image.png

  • 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
    • 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
    • 然后自己进入 Monitor 的 EntryList BLOCKED
    • (Thread-1 申请轻量级锁失败后,需要进入阻塞状态进行等待,轻量级锁没有阻塞,只能申请重量级锁 Monitor)

image.png

  • 当 Thread-0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级锁解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程。

image.png
“10”表示重量级锁,前 30 位为 Monitor 对象地址。

自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞(阻塞需要切换上下文,比较耗费性能)。
自旋重试成功的情况:
image.png
自旋重试失败的情况:image.png
:::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
    }
}

image.png
image.png
当 m2 线程再次调用 synchronized(obj) 时,会去检查对象 obj 的 Mark Word 中的 ThreadID 是否是当前线程调用的。而不是像轻量级锁那样,每次都需要用锁记录去替换 Mark Word,然后重新生成新的锁记录。

偏向状态

回忆一下对象头格式
image.png
一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,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();
    }

}
  1. 测试延迟特性
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 {

}

运行结果如下:
image.png
偏向锁默认是延迟的,不会在程序启动时立即生效。由于对象 d 已经创建成功,即使等待 4 秒后,它的偏向锁也不会再发生改变,所以需要重新创建对象进行验证。
最后 3 位为 101 表示偏向锁开启。
使用 VM 参数:-XX:BiasedLockingStartupDaley=0 来禁用延迟。
image.png
运行结果:
image.png
禁用成功。

  1. ** 测试偏向锁**
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 {

}

测试结果:
image.png
数据的后三位表示加锁信息,分别表示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 仍存储于对象头中
:::

  1. 禁用偏向锁

在上面测试代码运行时添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁
测试结果:
image.png
在禁用掉偏向锁后,从打印出的 Mark Word 信息可以看出,synchronized 中变成了轻量级锁。轻量级锁的前 62 位为 Lock Record(锁记录)的地址。解锁后,又恢复为未加锁状态。

:::success
优先级:
有偏向锁时用偏向锁。如果有其他线程也使用了同一个对象进行加锁,撤销偏向锁,升级为轻量级锁。如果有其他线程来竞争锁,就会膨胀为重量级锁。
偏向锁 → 轻量级锁 → 重量级锁
:::

撤销偏向锁
  1. 撤销-调用对象 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 {

}

运行结果:
image.png
正常状态对象一开始是没有 hashCode 的,第一次调用 hashCode() 才生成
调用了对象的 hashcode,但偏向锁的对象 Mark Word 中存储的是线程 ID,如果调用 hashcode 会导致偏向锁被撤销。(偏向锁中没有其他的位置可以用来保存 hashcode,当调用 hashCode 方法时,只能撤销偏向锁)

  • 轻量级锁会在锁记录中记录 hashcode
  • 重量级锁会在 Monitor 中 记录 hashcode

在调用 hashCode 后使用偏向锁,记得去掉 -XX:-UseBiasedLocking
image.png

  1. 撤销-其他线程使用对象

当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁。

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 对象的时间能够错开,而不会产生竞争锁对象,从而引发重量级锁。
image.png
t2 线程释放锁对象后,d 对象已经是不可偏向的状态了。

  1. 撤销-调用 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 {

}

运行结果:
image.png
状态由 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
执行结果:
image.png
去除锁消除功能后,打包:java -XX:-EliminateLocks -jar benchmarks.jar
image.png
锁粗化
对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化,这不同于之前讲的细分锁的粒度。

wait notify 原理

image.png

  • 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 的执行结果:
image.png
notifyAll 的执行结果:
image.png
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 [小南] - 有烟没?【false18:30:01.718 [小南] - 没有烟,休息会!
18:30:01.718 [小女] - 有外卖没?【false18:30:01.718 [小女] - 没有外卖,休息会!
18:30:02.718 [送外卖的] - 外卖到了
18:30:02.718 [小女] - 有外卖没?【true18:30:02.718 [小女] - 可以开始干活了
18:30:02.718 [小南] - 没有烟,休息会!
总结
synchronized(lock) {
     while(条件不成立) {
         lock.wait();
     }
 // 干活
}
//另一个线程
synchronized(lock) {
     lock.notifyAll();
}

同步模式之保护性暂停

定义

即 Guarded Suspension,用在一个线程等待另一个线程的执行结果
要点:

  • 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
  • 如果有结果不断从一个线程到另一个线程,那么可以使用消息队列(见生产者/消费者)
  • JDK 中,join 的实现、Future 的实现,采用的就是此模式
  • 因为要等待另一方的结果,因此归类到同步模式

image.png

实现

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();
    }
}

执行结果:
image.png

带超时版的 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();
    }
}

测试结果:
image.png

测试超时

@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();
    }
}

测试结果:
image.png

测试虚假唤醒

@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();
    }
}

测试结果:
image.png

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

image.png
图中 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();
        }
    }
}

测试结果:
image.png

异步模式之生产者/消费者

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(() -> {});
    }
}

输出:
image.png
队列容量为 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();
    }
}

输出:image.png
因为只有三个生产者,所以消费者在消费完三条消息后进入了等待

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);
    }
}

输出:
image.png
先 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);
    }
}

输出:
image.png

特点

与 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 仅会补充一份备用干粮

image.png

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

image.png

  1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
  2. 唤醒 _cond 条件变量中的 Thread_0
  3. Thread_0 恢复运行
  4. 设置 _counter 为 0

image.png

  1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
  2. 当前线程调用 Unsafe.park() 方法
  3. 检查 _counter,本情况为 1,这时线程无需阻塞,继续运行
  4. 设置 _counter 为 0

重新理解线程状态转换

image.png
假设有线程 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
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

情况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();
    }
}

结果
image.png
改进

@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);
            }
        }
    }
}

执行结果
image.png
将锁的粒度细分

  • 好处,是可以增强并发度
  • 坏处,如果以线程需要同时获得多把锁,就容易发生死锁

活跃性

死锁

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
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 来进行排查

哲学家就餐问题

image.png
有五位哲学家,围坐在圆桌旁。

  • 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
  • 吃饭时要用两根筷子吃,桌上共有 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();
    }
}

执行不多会,就执行不下去了
image.png

名称: 阿基米德
状态: 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 调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题
线程饥饿的例子:
先来看看使用顺序加锁的方式解决之前的死锁问题
image.png
顺序加锁的解决方案
image.png
代码调整:

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 调度执行,产生饥饿现象。
image.png

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();
        }
    }
}

输出image.png
注意如果是不可中断模式,那么即使使用了 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();
        }
    }
}

输出
image.png

锁超时

立刻失败

@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();
        }
    }
}

输出
image.png

超时失败

@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();
        }
    }
}

输出
image.png

使用 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();
    }
}

输出
image.png

同步模式之顺序控制

固定顺序

比如,必须先 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 原理
  • 模式方面
    • 同步模式之保护性暂停
    • 异步模式之生产者消费者
    • 同步模式之顺序控制

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1896419.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

NFT音乐版权系统的技术难点

NFT音乐版权系统是指利用区块链技术和NFT技术来管理和交易音乐版权的系统。该系统涉及多项技术&#xff0c;其开发和应用存在以下技术难点。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 1. 音乐作品的数字版权确权 音乐作品的数字…

《向量数据库指南》——Milvus Cloud查询增强如何提升 RAG Pipeline 效果?

查询增强 1.假设性问题 这一方法首先使用LLM为每一条文档块生成一些假设问题,这些文档块可以回答与之对应的假设问题。在RAG阶段,进行一个query-to-query的搜索,先搜索到相关的假设问题,然后找到对应的文档块,再通过它们生成最后的回答。 这里的query-to-query指的是embed…

7月开强化,考研数学保底110分真的很难吗?

七月开始强化是正常时间 不要因为进度而感到焦虑&#xff0c;因为进度是很主观的因素&#xff0c;无法衡量学习效果&#xff01;所以&#xff0c;不要和比人比进度&#xff0c;也不要赶进度&#xff0c;一步一个脚印&#xff0c;踏踏实实的学习才是王道&#xff01; 虽然我不…

MicroCap声学仿真介绍(一)

MicroCap是一款基于电路分析的仿真软件&#xff0c;基于集总参数的电力声的转换&#xff0c;我们可以用这个软件完成常用的声学仿真工作。目前这个软件已经开源了&#xff0c;免费使用&#xff0c;最新版已经到MC12&#xff0c;但里面不自带声学库&#xff0c;后来找到了MC8&am…

react v18 less使用(craco)

方案一、弹出配置&#xff08;不推荐&#xff09; 安装依赖&#xff1a;yarn add less less-loader 首先 执行 yarn eject 弹出配置项文件&#xff08;注意&#xff1a;弹出配置不可逆&#xff01;&#xff09; 在 config 文件夹中 找到 webpack.config.js&#xff0c;在如图…

Vue 详情实战涉及从项目初始化到功能实现、测试及部署的整个过程

本人详解 作者:王文峰,参加过 CSDN 2020年度博客之星,《Java王大师王天师》 公众号:JAVA开发王大师,专注于天道酬勤的 Java 开发问题中国国学、传统文化和代码爱好者的程序人生,期待你的关注和支持!本人外号:神秘小峯 山峯 转载说明:务必注明来源(注明:作者:王文峰…

视频提取字幕怎么弄?5个快速获取视频字幕的方法

在忙碌而又充满活力的生活中&#xff0c;我们常常在通勤路上和午休间隙通过视频来获取信息和放松心情。 但有时候&#xff0c;我们想把视频里那些令人难忘的瞬间或关键信息保存下来&#xff0c;方便以后回顾或者分享。然而&#xff0c;手动摘录不仅费时&#xff0c;还容易漏掉…

BugkuCTF-Crypto(1-5)

题&#xff1a;抄错的字符 题目作者: Aman 题目描述:老师让小明抄写一段话&#xff0c;结果粗心的小明把部分数字抄成了字母&#xff0c;还因为强迫症把所有字母都换成大写。你能帮小明恢复并解开答案吗&#xff1a;QWIHBLGZZXJSXZNVBZW 分析&#xff1a; 数字和字符可能的转…

数据库管理-第217期 Oracle的高可用-02(20240704)

数据库管理217期 2024-07-04 数据库管理-第217期 Oracle的高可用-02&#xff08;20240704&#xff09;1 GDS简介2 GDS架构2.1 全局数据服务池2.2 全局数据服务域2.3 全局服务管理2.4 全局数据服务目录2.5 Oracle通知服务 3 GDS简图3.1 负载均衡3.2 只读服务失败转移3.3 多主复制…

7.1 动态规划背包问题综述

动态规划中的背包问题是一类经典的优化问题&#xff0c;主要涉及到在给定的限制条件下&#xff08;如背包容量&#xff09;&#xff0c;如何选择物品集合以达到某种最优目标&#xff08;如价值最大&#xff09;。这类问题通常可以细分为几种类型&#xff0c;包括0-1背包问题、完…

Android的图书交易APP-计算机毕业设计源码25753

摘 要 在数字化与移动互联网迅猛发展的今天&#xff0c;人们对于图书的需求与消费方式也在悄然改变。为了满足广大读者对图书的热爱与追求&#xff0c;我们倾力打造了一款基于Android平台的图书交易APP。这款APP不仅汇聚了海量的图书资源&#xff0c;提供了便捷的交易平台&…

.Net Core + DDD基础分层

基础设施层 基础设施层使用的相关知识&#xff1a;Code First &#xff0c;EF Core&#xff0c;Autofac依赖注入&#xff0c;仓储模式的实现接口&#xff0c;领域服务的实现接口&#xff0c;缓存&#xff0c;以及各种基础工具类 一&#xff0c;Code First&#xff1a;使用Cod…

大学生竞赛管理系统-计算机毕业设计源码37276

大学生竞赛管理系统的设计与实现 摘 要 随着教育信息化的不断发展&#xff0c;大学生竞赛已成为高校教育的重要组成部分。传统的竞赛组织和管理方式存在着诸多问题&#xff0c;如信息不透明、效率低下、管理不便等。为了解决这些问题&#xff0c;提高竞赛组织和管理效率&#x…

新书速览|UML 2.5基础、建模与设计实践

《UML 2.5基础、建模与设计实战》 本书内容 UML是以面向对象图形的方式来描述任何类型的系统&#xff0c;应用领域非常广泛&#xff0c;其中常用的是建立软件系统的模型。《UML 2.5基础、建模与设计实践》基于draw.io开源免费软件&#xff0c;全面讲解UML 2.5的基本概念和建模…

SpringCloud集成Oauth2.0看完这个基本就理解原理了

目录 1.技术栈准备工作 2. 模块架构介绍 3.网关模块&#xff08;gateway&#xff09; 3.1 网关模块(gateway) 3.2 附上主要依赖包 3.3 bootstrap 相关配置 3.4 gateway.yaml 3.5 UserAuthGlobalFiter 全局拦截器配置 4.授权认证模块(auth) 4.1 启用web安全认证&#xff…

移动硬盘传输中断后无法识别:深度解析与数据救援指南

在日常的数据存储与传输过程中&#xff0c;移动硬盘凭借其大容量、便携性成为众多用户的首选。然而&#xff0c;当我们在复制或移动大量数据时遭遇传输中断&#xff0c;随后发现移动硬盘无法被电脑识别&#xff0c;这无疑是一场数据安全的紧急警报。此情此景&#xff0c;不仅影…

RestTemplate、MockMVC、Swagger

rest代码风格 硬编码的部分在实际开发中都是会替换成枚举对象 SpringMVC会自动把json格式的post请求转化为对应接收的 对象 响应请求时&#xff0c;也会自动把 对象转化为 json格式的 RestTemplate 浏览器的地址栏只能提供get请求访问后端&#xff0c;如果要使用post方式发送…

400G SR4和800G SR8光模块在AI集群中的应用

人工智能&#xff08;AI&#xff09;技术的快速发展下&#xff0c;AI集群的计算能力和数据传输需求不断提升。为了满足这一需求&#xff0c;光模块技术也在不断进步。高速率光模块作为新一代高速光通信解决方案&#xff0c;正在逐步应用于AI集群中&#xff0c;为其提供更高效、…

Python函数缺省参数的 “ 坑 ” (与C++对比学习)

我们都知道Python函数的缺省参数可以降低我们调用函数的成本&#xff0c;但是一般我们的缺省参数都是不可变对象&#xff0c;如果是可变对象&#xff0c;我们对其多次调用会发生什么呢&#xff1f; def func(arr[]):arr.append(Hello)print(arr)func() func() func() 这貌似…

phpcms 升级php8.3.8

windows 2008 server 不支持php8.3.8,需升级为windows 2012 1.下载php8.3.8 PHP8.3.9 For Windows: Binaries and sources Releases 2.配置php.ini (1.)在php目录下找到php.ini-development文件&#xff0c;把它复制一份&#xff0c;改名为php.ini (2.)修改php安装目录 根…