栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

共享模型之管程

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

共享模型之管程

目录
  • 1.1共享带来的问题
    • 临界区 Critical Section
    • 竞态条件 Race Condition
  • 1.2synchronized 解决方案
  • 1.3 方法上的 synchronized
    • 所谓的“线程八锁”
  • 1.4 变量的线程安全分析
    • 成员变量和静态变量是否线程安全?
    • 局部变量是否线程安全?
    • 局部变量线程安全分析
    • 常见线程安全类
  • 1.5 Monitor 概念
    • Java 对象头
    • Monitor 原理
    • synchronized 原理
    • synchronized 原理进阶
      • 1. 轻量级锁
      • 2. 锁膨胀
      • 3. 自旋优化
      • 4. 偏向锁
        • 撤销 - 调用对象 hashCode
        • 撤销 - 其它线程使用对象
        • 撤销 - 调用 wait/notify
        • 批量重偏向
        • 批量撤销
      • 5. 锁消除和锁粗化
  • 1.6 wait/notify
    • wait/notify原理
    • API 介绍
    • wait notify 的正确姿势

1.1共享带来的问题

java的体现
两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

static int counter = 0;
public static void main(String[] args) throws InterruptedException {
	 Thread t1 = new Thread(() -> {
	 for (int i = 0; i < 5000; i++) {
	 	counter++;
	 }
	 }, "t1");
	 Thread t2 = new Thread(() -> {
	 for (int i = 0; i < 5000; i++) {
		 counter--;
	 }
	 }, "t2");
	 t1.start();
	 t2.start();
	 t1.join();
	 t2.join();
	 log.debug("{}",counter);
}

问题分析
以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析
例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i

而对应 i-- 也是类似:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i

而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:

如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:

但多线程下这 8 行代码可能交错运行:
出现负数的情况:

出现正数的情况:

临界区 Critical Section
  • 一个程序运行多个线程本身是没有问题的
  • 问题出在多个线程访问共享资源
    • 多个线程读共享资源其实也没有问题
    • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

例如,下面代码中的临界区

static int counter = 0;//临界资源
static void increment()
// 临界区
{
 counter++;
}
static void decrement()
// 临界区
{
 counter--;
}
竞态条件 Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

1.2synchronized 解决方案

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案:原子变量

使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

注意
虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

synchronized
语法

synchronized(对象) // 线程1, 线程2(blocked)
{
 临界区
}

解决

static int counter = 0;
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
	 Thread t1 = new Thread(() -> {
	 for (int i = 0; i < 5000; i++) {
		 synchronized (room) {
		 counter++;
		 }
	 }
	 }, "t1");
	 Thread t2 = new Thread(() -> {
	 for (int i = 0; i < 5000; i++) {
		 synchronized (room) {
		 counter--;
	 }
	 }
	 }, "t2");
	 t1.start();
	 t2.start();
	 t1.join();
	 t2.join();
	 log.debug("{}",counter);
	}

synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断

面向对象改进
把需要保护的共享变量放入一个类

class Room {
 int value = 0;
 public void increment() {
	 synchronized (this) {
		 value++;
	 }
 }
 public void decrement() {
	 synchronized (this) {
	 	value--;
	 }
 }
 public int get() {
	 synchronized (this) {
		 return value;
	 }
 }
}
@Slf4j
public class Test1 {

 public static void main(String[] args) throws InterruptedException {
	 Room room = new Room();
	 Thread t1 = new Thread(() -> {
	 for (int j = 0; j < 5000; j++) {
	 	room.increment();
	  }
	 }, "t1");
	 Thread t2 = new Thread(() -> {
	 for (int j = 0; j < 5000; j++) {
		 room.decrement();
	 }
	 }, "t2");
	 t1.start();
	 t2.start();
	 t1.join();
	 t2.join();
	 log.debug("count: {}" , room.get());
 }
}
1.3 方法上的 synchronized
class Test{
	 public synchronized void test() {
	
	 }
}
等价于
class Test{
	 public void test() {
		 synchronized(this) {
		
		 }
	 }
}
class Test{
	 public synchronized static void test() {
	 }
}
	等价于
class Test{
	 public static void test() {
		 synchronized(Test.class) {
	
	 	}
 	}
}

不加 synchronized 的方法
不加 synchronzied 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的)

所谓的“线程八锁”

其实就是考察 synchronized 锁住的是哪个对象
情况1:12 或 21

@Slf4j(topic = "c.Number")
class Number{
	 public synchronized void a() {
	 	log.debug("1");
	 }
	 public synchronized void b() {
	 	log.debug("2");
	 }
}
public static void main(String[] args) {
	 Number n1 = new Number();
	 new Thread(()->{ n1.a(); }).start();
	 new Thread(()->{ n1.b(); }).start();
}

情况2:1s后12,或 2 1s后 1

@Slf4j(topic = "c.Number")
class Number{
 public synchronized void a() {
	 sleep(1);
	 log.debug("1");
 }
 public synchronized void b() {
 	log.debug("2");
 }
}
public static void main(String[] args) {
	 Number n1 = new Number();
	 new Thread(()->{ n1.a(); }).start();
	 new Thread(()->{ n1.b(); }).start();
}

情况3:3 1s 12 或 23 1s 1 或 32 1s 1

@Slf4j(topic = "c.Number")
class Number{
 public synchronized void a() {
	 sleep(1);
	 log.debug("1");
 }
 public synchronized void b() {
	 log.debug("2");
 }
 public void c() {
 	log.debug("3");
 }
}
public static void main(String[] args) {
	 Number n1 = new Number();
	 new Thread(()->{ n1.a(); }).start();
	 new Thread(()->{ n1.b(); }).start();
	 new Thread(()->{ n1.c(); }).start();
}

情况4:2 1s 后 1

@Slf4j(topic = "c.Number")
class Number{
	 public synchronized void a() {
		 sleep(1);
		 log.debug("1");
	 }
	 public synchronized void b() {
		 log.debug("2");
	 }
}
public static void main(String[] args) {
	 Number n1 = new Number();
	 Number n2 = new Number();
	 new Thread(()->{ n1.a(); }).start();
	 new Thread(()->{ n2.b(); }).start();
}

情况5:2 1s 后 1

@Slf4j(topic = "c.Number")
class Number{
 public static synchronized void a() {
	 sleep(1);
	 log.debug("1");
 }
 public synchronized void b() {
 	log.debug("2");
 }
 }
public static void main(String[] args) {
	 Number n1 = new Number();
	 new Thread(()->{ n1.a(); }).start();
	 new Thread(()->{ n1.b(); }).start();
}

情况6:1s 后12, 或 2 1s后 1

@Slf4j(topic = "c.Number")
class Number{
	 public static synchronized void a() {
		 sleep(1);
		 log.debug("1");
	 }
	 public static synchronized void b() {
		 log.debug("2");
	 }
}
public static void main(String[] args) {
	 Number n1 = new Number();
	 new Thread(()->{ n1.a(); }).start();
	 new Thread(()->{ n1.b(); }).start();
}

情况7:2 1s 后 1

@Slf4j(topic = "c.Number")
class Number{
	 public static synchronized void a() {
		 sleep(1);
		 log.debug("1");
	 }
	 public synchronized void b() {
		 log.debug("2");
	 }
}
public static void main(String[] args) {
 Number n1 = new Number();
 Number n2 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n2.b(); }).start();
}

情况8:1s 后12, 或 2 1s后 1

class Number{
 public static synchronized void a() {
	 sleep(1);
	 log.debug("1");
 }
 public static synchronized void b() {
	 log.debug("2");
 }
}
public static void main(String[] args) {
	 Number n1 = new Number();
	 Number n2 = new Number();
	 new Thread(()->{ n1.a(); }).start();
	 new Thread(()->{ n2.b(); }).start();
}
1.4 变量的线程安全分析 成员变量和静态变量是否线程安全?
  • 如果它们没有共享,则线程安全
  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量是否线程安全?
  • 局部变量是线程安全的
  • 但局部变量引用的对象则未必
    • 如果该对象没有逃离方法的作用范围,它是线程安全的
    • 如果该对象逃离方法的作用范围,需要考虑线程安全
局部变量线程安全分析
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

局部变量的引用稍有不同
先看一个成员变量的例子

class ThreadUnsafe {
 ArrayList list = new ArrayList<>();
 public void method1(int loopNumber) {
	 for (int i = 0; i < loopNumber; i++) {
		 // { 临界区, 会产生竞态条件
		 method2();
		 method3();
	  	// } 临界区
	 }
 }
 private void method2() {
	 list.add("1");
 }
 private void method3() {
 	list.remove(0);
 }
}

执行

static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
 ThreadUnsafe test = new ThreadUnsafe();
 for (int i = 0; i < THREAD_NUMBER; i++) {
	 new Thread(() -> {
	 test.method1(LOOP_NUMBER);
 }, "Thread" + i).start();
 }
}

其中一种情况是,如果线程2 还未 add,线程1 remove 就会报错:

Exception in thread “Thread1” java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:657)
at java.util.ArrayList.remove(ArrayList.java:496)
at cn.itcast.n6.ThreadUnsafe.method3(TestThreadSafe.java:35)
at cn.itcast.n6.ThreadUnsafe.method1(TestThreadSafe.java:26)
at cn.itcast.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14)
at java.lang.Thread.run(Thread.java:748)

分析:

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

将 list 修改为局部变量

class ThreadSafe {
 public final void method1(int loopNumber) {
 ArrayList list = new ArrayList<>();
 for (int i = 0; i < loopNumber; i++) {
 method2(list);
 method3(list);
 }
 }
 private void method2(ArrayList list) {
 list.add("1");
 }
 private void method3(ArrayList list) {
 list.remove(0);
 }
}

那么就不会有上述问题了
分析:

  • list 是局部变量,每个线程调用时会创建其不同实例,没有共享
  • 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象
  • method3 的参数分析与 method2 相同

方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?

  • 情况1:有其它线程调用 method2 和 method3
  • 情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,即
class ThreadSafe {
 public final void method1(int loopNumber) {
 ArrayList list = new ArrayList<>();
 for (int i = 0; i < loopNumber; i++) {
	 method2(list);
	 method3(list);
 }
 }
 private void method2(ArrayList list) {
 	list.add("1");
  }
 private void method3(ArrayList list) {
 	list.remove(0);
 }
}
class ThreadSafeSubClass extends ThreadSafe{
 @Override
 public void method3(ArrayList list) {
	 new Thread(() -> {
	 	list.remove(0);
	 }).start();
 }
}

从这个例子可以看出 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);
}


不可变类线程安全性
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?

public class Immutable{
 private int value = 0;
 public Immutable(int value){
 this.value = value;
 }
 public int getValue(){
 return this.value;
 }
}

如果想增加一个增加的方法呢?

public class Immutable{
	 private int value = 0;
	 public Immutable(int value){
	 this.value = value;
 }
 public int getValue(){
	 return this.value;
 }

 public Immutable add(int v){
	 return new Immutable(this.value + v);
 }
}

实例分析
例1:

public class MyServlet extends HttpServlet {
 // 是否安全?不安全
 Map map = new HashMap<>();
 // 是否安全?安全
 String S1 = "...";
 // 是否安全?安全
 final String S2 = "...";
 // 是否安全?不安全
 Date D1 = new Date();
 // 是否安全?不安全,只是引用不可变,调用修改方法时线程不安全
 final Date D2 = new Date();

 public void doGet(HttpServletRequest request, HttpServletResponse response) {
  // 使用上述变量
 }
}

例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++;
 }
}

例3:

@Aspect
@Component
public class MyAspect {
 // 是否安全?不安全,spring默认单例,即使是多例也不安全,因为before和after方法可能访问的对象不一致,最好用环绕通知加局部变量解决
 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));
 }
}

例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 = ?";
 // 是否安全,安全,Connection是局部变量
 try (Connection conn = DriverManager.getConnection("","","")){
 // ...
 } catch (Exception e) {
 // ...
 }
 }
}

例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 {
 // 是否安全,不安全,多个线程共享conn
 private Connection conn = null;
 public void update() throws SQLException {
 String sql = "update user set password = ? where username = ?";
 conn = DriverManager.getConnection("","","");
 // ...
 conn.close();
 }
}

例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 {
 // 是否安全,安全,因为userDao是局部变量,多例
 private Connection = null;
 public void update() throws SQLException {
	 String sql = "update user set password = ? where username = ?";
	 conn = DriverManager.getConnection("","","");
	 // ...
	 conn.close();
 }
}

例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();
 }
}
1.5 Monitor 概念 Java 对象头

以 32 位虚拟机为例

普通对象

数组对象

其中 Mark Word 结构为:

64 位虚拟机 Mark Word

Monitor 原理

Monitor 被翻译为监视器或管程
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
Monitor 结构如下

  • 刚开始 Monitor 中 Owner 为 null
  • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner
  • 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入EntryList BLOCKED
  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争时是非公平的
  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲wait-notify 时会分析

注意:

  • synchronized 必须是进入同一个对象的 monitor 才有上述的效果
  • 不加 synchronized 的对象不会关联监视器,不遵从以上规则
synchronized 原理
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
 synchronized (lock) {
 counter++;
 }
}

对应的字节码为

public static void main(java.lang.String[]);
 descriptor: ([Ljava/lang/String;)V
 flags: ACC_PUBLIC, ACC_STATIC
 Code:
 stack=2, locals=3, args_size=1
 0: getstatic #2 // <- lock引用 (synchronized开始)
 3: dup//复制
 4: astore_1 // lock引用 -> slot 1
 5: monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针
 6: getstatic #3 // <- i
 9: iconst_1 // 准备常数 1
 10: iadd // +1
 11: putstatic #3 // -> i
 14: aload_1 // <- lock引用
 15: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
 16: goto 24
 19: astore_2 // e -> slot 2
 20: aload_1 // <- lock引用
 21: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
 22: aload_2 // <- slot 2 (e)
 23: athrow // throw e
 24: return
 Exception table:
 from to target type
 6 16 19 any
 19 22 19 any
 LineNumberTable:
 line 8: 0
 line 9: 6
 line 10: 14
 line 11: 24
 LocalVariableTable:
 Start Length Slot Name Signature
 0 25 0 args [Ljava/lang/String;
 StackMapTable: number_of_entries = 2
 frame_type = 255 
 offset_delta = 19
 locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
 stack = [ class java/lang/Throwable ]
 frame_type = 250 
 offset_delta = 4

注意

  • 方法级别的 synchronized 不会在字节码指令中有所体现,而是通过flags:ACC_SYNCHRONIZED体现


synchronized 原理进阶 1. 轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized
假设有两个方法同步块,利用同一个对象加锁

static final Object obj = new Object();
public static void method1() {
 synchronized( obj ) {
 // 同步块 A
 method2();
 }
}
public static void method2() {
 synchronized( obj ) {
 // 同步块 B
 }
}

创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word

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

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

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


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


当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头

  • 成功,则解锁成功
  • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
2. 锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

static Object obj = new Object();
public static void method1() {
 synchronized( obj ) {
 // 同步块
 }
}
  • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

  • 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
    • 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
    • 然后自己进入 Monitor 的 EntryList BLOCKED
  • 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程
3. 自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
自旋重试成功的情况

自旋重试失败的情况

  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
  • Java 7 之后不能控制是否开启自旋功能
4. 偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 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
 }
}


偏向状态
回忆一下对象头格式

一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的thread、epoch、age 都为 0
  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -
    XX:BiasedLockingStartupDelay=0 来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值

1) 测试延迟特性
2) 测试偏向锁

// 添加虚拟机参数 -XX:BiasedLockingStartupDelay=0
public static void main(String[] args) throws IOException {
	 Dog d = new Dog();
	 ClassLayout classLayout = ClassLayout.parseInstance(d);
	 new Thread(() -> {
	 log.debug("synchronized 前");
	 System.out.println(classLayout.toPrintableSimple(true));
	 synchronized (d) {
		 log.debug("synchronized 中");
		 System.out.println(classLayout.toPrintableSimple(true));
	 }
	 log.debug("synchronized 后");
	 System.out.println(classLayout.toPrintableSimple(true));
	 }, "t1").start();
}

输出

11:08:58.117 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
11:08:58.121 c.TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101
11:08:58.121 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101 

注意
处于偏向锁的对象解锁后,线程 id 仍存储于对象头中

3)测试禁用
在上面测试代码运行时在添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁
输出

11:13:10.018 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
11:13:10.021 c.TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00100000 00010100 11110011 10001000
11:13:10.021 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 

4) 测试 hashCode

  • 正常状态对象一开始是没有 hashCode 的,第一次调用才生成
撤销 - 调用对象 hashCode

调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销

  • 轻量级锁会在锁记录中记录 hashCode
  • 重量级锁会在 Monitor 中记录 hashCode
    在调用 hashCode 后使用偏向锁,记得去掉 -XX:-UseBiasedLocking
    输出
11:22:10.386 c.TestBiased [main] - 调用 hashCode:1778535015
11:22:10.391 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001
11:22:10.393 c.TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00100000 11000011 11110011 01101000
11:22:10.393 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001
撤销 - 其它线程使用对象

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

private static void test2() throws InterruptedException {
 Dog d = new Dog();
 Thread t1 = new Thread(() -> {
 synchronized (d) {
	 log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
 }
 synchronized (TestBiased.class) {
	 TestBiased.class.notify();
 }
 // 如果不用 wait/notify 使用 join 必须打开下面的注释
 // 因为:t1 线程不能结束,否则底层线程可能被 jvm 重用作为 t2 线程,底层线程 id 是一样的
 
 }, "t1");
 t1.start();
 Thread t2 = new Thread(() -> {
 synchronized (TestBiased.class) {
 try {
	 TestBiased.class.wait();
 } catch (InterruptedException e) {
	 e.printStackTrace();
 }
 }
 log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
 synchronized (d) {
 	log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
 }
 log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
 }, "t2");
 t2.start();
}

输出

[t1] - 00000000 00000000 00000000 00000000 00011111 01000001 00010000 00000101
[t2] - 00000000 00000000 00000000 00000000 00011111 01000001 00010000 00000101
[t2] - 00000000 00000000 00000000 00000000 00011111 10110101 11110000 01000000
[t2] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
撤销 - 调用 wait/notify

会膨胀为重量级锁

public static void main(String[] args) throws InterruptedException {
 Dog d = new Dog();
 Thread t1 = new Thread(() -> {
	 log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
	 synchronized (d) {
	 	log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
	 try {
	 	d.wait();
	 } catch (InterruptedException e) {
	 	e.printStackTrace();
	 }
	 log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
 	 }
 }, "t1");
 t1.start();
 new Thread(() -> {
 try {
 	Thread.sleep(6000);
 } catch (InterruptedException e) {
 	e.printStackTrace();
 }
 synchronized (d) {
 	log.debug("notify");
 	d.notify();
 }
 }, "t2").start();
}

输出

[t1] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
[t1] - 00000000 00000000 00000000 00000000 00011111 10110011 11111000 00000101
[t2] - notify
[t1] - 00000000 00000000 00000000 00000000 00011100 11010100 00001101 11001010
批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID
当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程

private static void test3() throws InterruptedException {
 Vector 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" + ClassLayout.parseInstance(d).toPrintableSimple(true));
 }
 }
 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" + ClassLayout.parseInstance(d).toPrintableSimple(true));
 synchronized (d) {
 log.debug(i + "t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
 }
 log.debug(i + "t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
 }
 }, "t2");
 t2.start();
}

输出

[t1] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 1 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 2 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 3 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 4 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 5 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 6 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 7 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 8 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 9 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 10 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 11 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 12 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 13 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 14 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 15 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 16 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 17 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 18 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - ===============>
[t2] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 0 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 0 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 1 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 1 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 1 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 2 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 2 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 2 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 3 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 3 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 3 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 4 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 4 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 4 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 5 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 5 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 5 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 6 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 6 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 6 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 7 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 7 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 7 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 8 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 8 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 8 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 9 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 9 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 9 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 10 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 10 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 10 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 11 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 11 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 11 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 12 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 12 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 12 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 13 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 13 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 13 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 14 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 14 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 14 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 15 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 15 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 15 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 16 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 16 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 16 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 17 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 17 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 17 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 18 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 18 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 18 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
批量撤销

当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的

static Thread t1,t2,t3;
private static void test4() throws InterruptedException {
 Vector 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" + ClassLayout.parseInstance(d).toPrintableSimple(true));
	 }
 }
 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" + ClassLayout.parseInstance(d).toPrintableSimple(true));
		synchronized (d) {
		 	log.debug(i + "t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
		 }
	 	log.debug(i + "t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
	 }
 	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" + ClassLayout.parseInstance(d).toPrintableSimple(true));
		 synchronized (d) {
		 log.debug(i + "t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
		 }
		 log.debug(i + "t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
	 }
 }, "t3");
 t3.start();
 t3.join();
 log.debug(ClassLayout.parseInstance(new Dog()).toPrintableSimple(true));
}

参考资料
https://github.com/farmerjohngit/myblog/issues/12
https://www.cnblogs.com/LemonFive/p/11246086.html
https://www.cnblogs.com/LemonFive/p/11248248.html
偏向锁论文

5. 锁消除和锁粗化

锁消除

@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
 public void b() throws Exception {
 	Object o = new Object();
 	synchronized (o) {
	 x++;
  	}
 }
}

锁消除:虚拟机的运行时编译器在运行时如果检测到一些要求同步的代码上不可能发生共享数据竞争,则会去掉这些锁。
消除无意义的锁获取和释放,可以提高程序运行性能。
锁粗化:对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化,这不同于之前讲的细分锁的粒度。
参数设置:-XX:+EliminateLocks。

1.6 wait/notify wait/notify原理

  • Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
  • BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
  • BLOCKED 线程会在 Owner 线程释放锁时唤醒
  • WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争
API 介绍
  • obj.wait() 让进入 object 监视器的线程到 waitSet 等待
  • obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
  • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒

它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法

final static Object obj = new Object();
public static void main(String[] args) {
 new Thread(() -> {
	 synchronized (obj) {
	 	log.debug("执行....");
	 try {
	 	obj.wait(); // 让线程在obj上一直等待下去
	 } catch (InterruptedException e) {
	 	e.printStackTrace();
	 }
	 log.debug("其它代码....");
	 }
 }).start();
 new Thread(() -> {
	 synchronized (obj) {
	 	log.debug("执行....");
	 try {
	 	obj.wait(); // 让线程在obj上一直等待下去
	 } catch (InterruptedException e) {
	 	e.printStackTrace();
	 }
	 log.debug("其它代码....");
	 }
 }).start();
 // 主线程两秒后执行
 sleep(2);
 log.debug("唤醒 obj 上其它线程");
 synchronized (obj) {
 	obj.notify(); // 唤醒obj上一个线程
 // obj.notifyAll(); // 唤醒obj上所有等待线程
 }
}

notify 的一种结果

20:00:53.096 [Thread-0] c.TestWaitNotify - 执行....
20:00:53.099 [Thread-1] c.TestWaitNotify - 执行....
20:00:55.096 [main] c.TestWaitNotify - 唤醒 obj 上其它线程
20:00:55.096 [Thread-0] c.TestWaitNotify - 其它代码....

notifyAll 的结果

19:58:15.457 [Thread-0] c.TestWaitNotify - 执行....
19:58:15.460 [Thread-1] c.TestWaitNotify - 执行....
19:58:17.456 [main] c.TestWaitNotify - 唤醒 obj 上其它线程
19:58:17.456 [Thread-1] c.TestWaitNotify - 其它代码....
19:58:17.456 [Thread-0] c.TestWaitNotify - 其它代码.... 

wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到notify 为止
wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify

wait notify 的正确姿势

开始之前先看看
sleep(long n) 和 wait(long n) 的区别

  1. sleep 是 Thread 方法,而 wait 是 Object 的方法
  2. sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
  3. sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
  4. 它们状态 TIMED_WAITING

step 1

static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;

思考下面的解决方案好不好,为什么?

new Thread(() -> {
 synchronized (room) {
 log.debug("有烟没?[{}]", hasCigarette);
 if (!hasCigarette) {
 log.debug("没烟,先歇会!");
 sleep(2);
 }
 log.debug("有烟没?[{}]", hasCigarette);
 if (hasCigarette) {
 log.debug("可以开始干活了");
 }
 }
}, "小南").start();
for (int i = 0; i < 5; i++) {
 new Thread(() -> {
 synchronized (room) {
 log.debug("可以开始干活了");
 }
 }, "其它人").start();
 }
sleep(1);
new Thread(() -> {
 // 这里能不能加 synchronized (room)?
 hasCigarette = true;
 log.debug("烟到了噢!");
}, "送烟的").start();

输出

20:49:49.883 [小南] c.TestCorrectPosture - 有烟没?[false]
20:49:49.887 [小南] c.TestCorrectPosture - 没烟,先歇会!
20:49:50.882 [送烟的] c.TestCorrectPosture - 烟到了噢!
20:49:51.887 [小南] c.TestCorrectPosture - 有烟没?[true]
20:49:51.887 [小南] c.TestCorrectPosture - 可以开始干活了
20:49:51.887 [其它人] c.TestCorrectPosture - 可以开始干活了
20:49:51.887 [其它人] c.TestCorrectPosture - 可以开始干活了
20:49:51.888 [其它人] c.TestCorrectPosture - 可以开始干活了
20:49:51.888 [其它人] c.TestCorrectPosture - 可以开始干活了
20:49:51.888 [其它人] c.TestCorrectPosture - 可以开始干活了
  • 其它干活的线程,都要一直阻塞,效率太低
  • 小南线程必须睡足 2s 后才能醒来,就算烟提前送到,也无法立刻醒来
  • 加了 synchronized (room) 后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main 没加
  • synchronized 就好像 main 线程是翻窗户进来的
  • 解决方法,使用 wait - notify 机制

step 2
思考下面的实现行吗,为什么?

new Thread(() -> {
 synchronized (room) {
 log.debug("有烟没?[{}]", hasCigarette);
 if (!hasCigarette) {
 log.debug("没烟,先歇会!");
 try {
 room.wait(2000);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 }
 log.debug("有烟没?[{}]", hasCigarette);
 if (hasCigarette) {
 log.debug("可以开始干活了");
 }
 }
}, "小南").start();
for (int i = 0; i < 5; i++) {
 new Thread(() -> {
 synchronized (room) {
 log.debug("可以开始干活了");
 }
 }, "其它人").start();
}
sleep(1);
new Thread(() -> {
 synchronized (room) {
 hasCigarette = true;
 log.debug("烟到了噢!");
 room.notify();
 }
}, "送烟的").start();

输出

20:51:42.489 [小南] c.TestCorrectPosture - 有烟没?[false]
20:51:42.493 [小南] c.TestCorrectPosture - 没烟,先歇会!
20:51:42.493 [其它人] c.TestCorrectPosture - 可以开始干活了
20:51:42.493 [其它人] c.TestCorrectPosture - 可以开始干活了
20:51:42.494 [其它人] c.TestCorrectPosture - 可以开始干活了
20:51:42.494 [其它人] c.TestCorrectPosture - 可以开始干活了
20:51:42.494 [其它人] c.TestCorrectPosture - 可以开始干活了
20:51:43.490 [送烟的] c.TestCorrectPosture - 烟到了噢!
20:51:43.490 [小南] c.TestCorrectPosture - 有烟没?[true]
20:51:43.490 [小南] c.TestCorrectPosture - 可以开始干活了
  • 解决了其它干活的线程阻塞的问题
  • 但如果有其它线程也在等待条件呢?

step 3

new Thread(() -> {
 synchronized (room) {
 log.debug("有烟没?[{}]", hasCigarette);
 if (!hasCigarette) {
 log.debug("没烟,先歇会!");
 try {
 room.wait();
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 }
 log.debug("有烟没?[{}]", hasCigarette);
 if (hasCigarette) {
 log.debug("可以开始干活了");
 } else {
 }
 }
}, "小南").start();
new Thread(() -> {
 synchronized (room) {
 Thread thread = Thread.currentThread();
 log.debug("外卖送到没?[{}]", hasTakeout);
 if (!hasTakeout) {
 log.debug("没外卖,先歇会!");
 try {
 room.wait();
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 }
 log.debug("外卖送到没?[{}]", hasTakeout);
 if (hasTakeout) {
 log.debug("可以开始干活了");
 } else {
 log.debug("没干成活...");
 }
 }
}, "小女").start();
sleep(1);
new Thread(() -> {
 synchronized (room) {
 hasTakeout = true;
 log.debug("外卖到了噢!");
 room.notify();
 }
}, "送外卖的").start();

输出

20:53:12.173 [小南] c.TestCorrectPosture - 有烟没?[false]
20:53:12.176 [小南] c.TestCorrectPosture - 没烟,先歇会!
20:53:12.176 [小女] c.TestCorrectPosture - 外卖送到没?[false]
20:53:12.176 [小女] c.TestCorrectPosture - 没外卖,先歇会!
20:53:13.174 [送外卖的] c.TestCorrectPosture - 外卖到了噢!
20:53:13.174 [小南] c.TestCorrectPosture - 有烟没?[false]
20:53:13.174 [小南] c.TestCorrectPosture - 没干成活... 
  • notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,称之为【虚假唤醒】
  • 解决方法,改为 notifyAll

step 4

new Thread(() -> {
 synchronized (room) {
 hasTakeout = true;
 log.debug("外卖到了噢!");
 room.notifyAll();
 }
}, "送外卖的").start();

输出

20:55:23.978 [小南] c.TestCorrectPosture - 有烟没?[false]
20:55:23.982 [小南] c.TestCorrectPosture - 没烟,先歇会!
20:55:23.982 [小女] c.TestCorrectPosture - 外卖送到没?[false]
20:55:23.982 [小女] c.TestCorrectPosture - 没外卖,先歇会!
20:55:24.979 [送外卖的] c.TestCorrectPosture - 外卖到了噢!
20:55:24.979 [小女] c.TestCorrectPosture - 外卖送到没?[true]
20:55:24.980 [小女] c.TestCorrectPosture - 可以开始干活了
20:55:24.980 [小南] c.TestCorrectPosture - 有烟没?[false]
20:55:24.980 [小南] c.TestCorrectPosture - 没干成活...
  • 用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了
  • 解决方法,用 while + wait,当条件不成立,再次 wait

step 5
将 if 改为 while

while (!hasCigarette) {
 log.debug("没烟,先歇会!");
 try {
 room.wait();
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
}

输出

20:58:34.322 [小南] c.TestCorrectPosture - 有烟没?[false]
20:58:34.326 [小南] c.TestCorrectPosture - 没烟,先歇会!
20:58:34.326 [小女] c.TestCorrectPosture - 外卖送到没?[false]
20:58:34.326 [小女] c.TestCorrectPosture - 没外卖,先歇会!
20:58:35.323 [送外卖的] c.TestCorrectPosture - 外卖到了噢!
20:58:35.324 [小女] c.TestCorrectPosture - 外卖送到没?[true]
20:58:35.324 [小女] c.TestCorrectPosture - 可以开始干活了
20:58:35.324 [小南] c.TestCorrectPosture - 没烟,先歇会!
synchronized(lock) {
 while(条件不成立) {
 lock.wait();
 }
 // 干活
}
//另一个线程
synchronized(lock) {
 lock.notifyAll();
}
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/358276.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号