管程

什么是管程

管程是由局部于自己的若干公共变量及其说明和所有访问这些公共变量的过程所组成的软件模块。

管程的属性

  1. 共享性:管程可被系统范围内的进程互斥访问,属于共享资源
  2. 安全性:管程的局部变量只能由管程的过程访问,不允许进程或其它管程直接访问,管程也不能访问非局部于它的变量。
  3. 互斥性:多个进程对管程的访问是互斥的。任一时刻,管程中只能有一个活跃进程。
  4. 封装性:管程内的数据结构是私有的,只能在管程内使用,管程内的过程也只能使用管程内的数据结构。进程通过调用管程的过程使用临界资源。管程在Java中已实现。

共享带来的问题

有一个静态变量 count,有一个线程循环n次对它做自增操作,另一个线程循环n次对它做自减操作,最后结果不一定是0.(如果一直是0,可以把n设置为较大的值)

static int count = 0;
  private static final Logger logger = LoggerFactory.getLogger(Test1.class);

  public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
      for (int i = 0; i < 2000; i++) {
        count++;
      }
    }, "t1");
    Thread t2 = new Thread(() -> {
      for (int i = 0; i < 2000; i++) {
        count--;
      }
    }, "t2");
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    logger.debug("结果:{}",count);
  }

输出:

18:24:13.768 [main] DEBUG com.fly.n2.Test1 - 结果:104

原因

count++对应的字节码如下:

0 getstatic #12 <com/fly/n2/Test1.count : I>//获取静态变量 count 的值
3 iconst_1//加载一个常量1
4 iadd//自增
5 putstatic #12 <com/fly/n2/Test1.count : I>//将修改过的值赋给 count

count--对应的字节码如下:

0 getstatic #12 <com/fly/n2/Test1.count : I>//获取静态变量 count 的值
3 iconst_1//加载一个常量1
4 isub//自减
5 putstatic #12 <com/fly/n2/Test1.count : I>/将修改过的值赋给 count

如果是多线程执行以上代码可能交错运行

临界区

  1. 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
  2. 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

竞态条件 Race Condition

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

synchronized

  1. 可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。
  2. 当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而,当一个线程访问object的一个加锁代码块时,另一个线程仍可以访问该object中的非加锁代码块。
  3. 语法:
synchronized (对象) {
          临界区
        }

解决:

 static int count = 0;
  static final Object lock = new Object();
  private static final Logger logger = LoggerFactory.getLogger(Test1.class);

  public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
      for (int i = 0; i < 2000; i++) {
        synchronized (lock) {
          count++;
        }
      }
    }, "t1");
    Thread t2 = new Thread(() -> {
      for (int i = 0; i < 2000; i++) {
        synchronized (lock) {
          count--;
        }
      }
    }, "t2");
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    logger.debug("结果:{}",count);
  }

输出:

12:13:32.611 [main] DEBUG com.fly.n2.Test1 - 结果:0

变量的线程安全分析

一 成员变量和静态变量是否线程安全?

  1. 如果它们没有共享,则线程安全
  2. 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
    2.1 如果只有读操作,则线程安全
    2.2 如果有读写操作,则这段代码是临界区,需要考虑线程安全
    二 局部变量是否线程安全?
  3. 局部变量是线程安全的
  4. 但局部变量引用的对象则未必
    2.1 如果该对象没有逃离方法的作用访问,它是线程安全的
    2.2 如果该对象逃离方法的作用范围,需要考虑线程安全

常见线程安全类

  1. String、Integer、StringBuffer、Random、Vector、Hashtable、java.util.concurrent 包下的类
  2. 它们的每个方法是原子的
  3. 它们多个方法的组合不是原子的

Monitor

Java对象头

以32位虚拟机为例
普通对象

数组对象

Mark Word 结构:

64 位虚拟机 Mark Word:

Monitor 原理

  1. Monitor 被翻译为监视器或管程
  2. 每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
  3. Monitor 结构如下:
  4. 刚开始 Monitor 中 Owner 为 null
  5. 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner
  6. 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入EntryList BLOCKED
  7. Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
  8. 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程
  9. synchronized 必须是进入同一个对象的 monitor 才有上述的效果

synchronized原理

public void method3() {
    synchronized (lock) {
      count++;
    }
  }

对应字节码如下:

 0 getstatic #15 <com/fly/n2/Test1.lock : Ljava/lang/Object;>  //获得 lock 的引用
 3 dup  //复制 lock的引用
 4 astore_1  //lock-->临时变量 slot1
 5 monitorenter  //将 lock对象 MarkWord 置为 Monitor 指针
 6 getstatic #12 <com/fly/n2/Test1.count : I>  //count
 9 iconst_1  //准备常数1
10 iadd  //+1
11 putstatic #12 <com/fly/n2/Test1.count : I>  //-->count
14 aload_1  //获取 lock引用
15 monitorexit  // 将 lock对象 MarkWord 重置, 唤醒 EntryList
16 goto 24 (+8)
19 astore_2  //异常对象 e --> slot 2
20 aload_1  //加载 lock 引用
21 monitorexit  //将 lock对象 MarkWord 重置, 唤醒 EntryList
22 aload_2  //加载异常对象
23 athrow  //抛出异常 e
24 return

异常表:

这样无论怎么都会释放锁

轻量级锁

在没有多线程竞争的情况下,可以使用轻量级锁来优化。语法:synchronized

static final Object obj = new Object();
public static void method1() {
 synchronized( obj ) {
 // 同步块 A
 method2();
 }
}
public static void method2() {
 synchronized( obj ) {
 // 同步块 B
 }
}
  1. 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word

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

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

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

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

  6. 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
    6.1 成功,则解锁成功
    6.2 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

锁膨胀

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

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

  2. 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
    2.1 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
    2.2 然后自己进入 Monitor 的 EntryList BLOCKED

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

自旋优化

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

偏向锁

Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

wait notify

  1. API介绍
    1.1 obj.wait() 让进入 object 监视器的线程到 waitSet 等待
    1.2 obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
    1.3 obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒
  2. 它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法
private static final Object object = new Object();
  private static final Logger logger = LoggerFactory.getLogger(Test2.class);

  public static void main(String[] args) throws InterruptedException {
    new Thread(() -> {
      synchronized (object) {
        logger.debug("开始执行");
        try {
          object.wait();  //让线程在obj上一直等待下去
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        logger.debug("===========");
      }
    },"t1").start();

    new Thread(() -> {
      synchronized (object) {
        logger.debug("开始执行");
        try {
          object.wait();  //让线程在obj上一直等待下去
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        logger.debug("===========");
      }
    },"t2").start();
    Thread.sleep(2000);
    logger.debug("唤醒 obj 上其他线程");
    synchronized (object) {
      object.notify();  // 唤醒obj上一个线程
//      object.notifyAll(); // 唤醒obj上所有等待线程
    }
  }

输出:

21:40:22.068 [t1] DEBUG com.fly.n2.Test2 - 开始执行
21:40:22.071 [t2] DEBUG com.fly.n2.Test2 - 开始执行
21:40:24.067 [main] DEBUG com.fly.n2.Test2 - 唤醒 obj 上其他线程
21:40:24.067 [t1] DEBUG com.fly.n2.Test2 - ===========
21:41:35.699 [t1] DEBUG com.fly.n2.Test2 - 开始执行
21:41:35.703 [t2] DEBUG com.fly.n2.Test2 - 开始执行
21:41:37.697 [main] DEBUG com.fly.n2.Test2 - 唤醒 obj 上其他线程
21:41:37.697 [t1] DEBUG com.fly.n2.Test2 - ===========
21:41:37.697 [t2] DEBUG com.fly.n2.Test2 - ===========
  1. wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到notify 为止
  2. wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify

使用

private static final Object lock = new Object();
  private static final Logger log = LoggerFactory.getLogger(Test3.class);
  private static boolean condition1 = false;
  private static boolean condition2 = false;

  public static void main(String[] args) throws InterruptedException {
    new Thread(() -> {
      synchronized (lock) {
        log.debug("电脑到了没?:{}",condition1);
        while (!condition1) {
          log.debug("电脑没到,先休息");
          try {
            lock.wait();
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          log.debug("电脑到了没?:{}",condition1);
        }
        log.debug("电脑到了,开始工作");
      }
    },"t1").start();

    new Thread(() -> {
      synchronized (lock) {
        log.debug("工具到了没?:{}",condition2);
        while (!condition2) {
          log.debug("工具没到,先休息");
          try {
            lock.wait();
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          log.debug("工具到了没?:{}",condition2);
        }
        log.debug("工具到了,开始工作");
      }
    },"t2").start();
    Thread.sleep(1000);

    new Thread(() -> {
      try {
        Thread.sleep(500);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      synchronized (lock) {
        condition1 = true;
        log.debug("电脑到了====");
        lock.notifyAll();
      }
    },"送电脑的").start();

    new Thread(() -> {
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      synchronized (lock) {
        condition2 = true;
        log.debug("工具到了===");
        lock.notifyAll();
      }
    },"送工具的").start();
  }

输出:

22:17:19.633 [t1] DEBUG com.fly.n2.Test3 - 电脑到了没?:false
22:17:19.639 [t1] DEBUG com.fly.n2.Test3 - 电脑没到,先休息
22:17:19.640 [t2] DEBUG com.fly.n2.Test3 - 工具到了没?:false
22:17:19.640 [t2] DEBUG com.fly.n2.Test3 - 工具没到,先休息
22:17:21.128 [送电脑的] DEBUG com.fly.n2.Test3 - 电脑到了====
22:17:21.128 [t1] DEBUG com.fly.n2.Test3 - 电脑到了没?:true
22:17:21.128 [t1] DEBUG com.fly.n2.Test3 - 电脑到了,开始工作
22:17:21.128 [t2] DEBUG com.fly.n2.Test3 - 工具到了没?:false
22:17:21.128 [t2] DEBUG com.fly.n2.Test3 - 工具没到,先休息
22:17:21.629 [送工具的] DEBUG com.fly.n2.Test3 - 工具到了===
22:17:21.629 [t2] DEBUG com.fly.n2.Test3 - 工具到了没?:true
22:17:21.629 [t2] DEBUG com.fly.n2.Test3 - 工具到了,开始工作

wait notify 原理

  1. Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
  2. BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
  3. BLOCKED 线程会在 Owner 线程释放锁时唤醒
  4. WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争

park、unpark

先 park

private static final Logger log = LoggerFactory.getLogger(Test4.class);

  public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
      try {
        Thread.sleep(500);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      log.debug("t1...park");
      LockSupport.park();
      log.debug("t1...un park");
    }, "t1");
    t1.start();
    Thread.sleep(1000);
    log.debug("un park");
    LockSupport.unpark(t1);
  }

输出:

11:43:22.941 [t1] DEBUG com.fly.n2.Test4 - t1...park
11:43:23.441 [main] DEBUG com.fly.n2.Test4 - un park
11:43:23.441 [t1] DEBUG com.fly.n2.Test4 - t1...un park

先 unpark

 Thread t1 = new Thread(() -> {
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      log.debug("t1...park");
      LockSupport.park();
      log.debug("t1...un park");
    }, "t1");
    t1.start();
    Thread.sleep(500);
    log.debug("un park");
    LockSupport.unpark(t1);

输出:

11:47:42.581 [main] DEBUG com.fly.n2.Test4 - un park
11:47:43.081 [t1] DEBUG com.fly.n2.Test4 - t1...park
11:47:43.081 [t1] DEBUG com.fly.n2.Test4 - t1...un park

ReentrantLock

特点

相对于 synchronized 它具备如下特点:

  1. 可中断
  2. 可以设置超时时间
  3. 可以设置为公平锁
  4. 支持多个条件变量
  5. 与 synchronized 一样,都支持可重入

基本语法

// 获取锁
reentrantLock.lock();
try {
 // 临界区
} finally {
 // 释放锁
 reentrantLock.unlock();
}

可重入

可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

private static final ReentrantLock lock = new ReentrantLock();
  private static final Logger log = LoggerFactory.getLogger(Test5.class);

  public static void main(String[] args) {
    method1();
  }

  public static void method1() {
    lock.lock();
    try {
      log.debug("execute method1");
      method2();
    } finally {
      lock.unlock();
    }
  }

  public static void method2() {
    lock.lock();
    try {
      log.debug("execute method2");
      method3();
    } finally {
      lock.unlock();
    }
  }
  public static void method3() {
    lock.lock();
    try {
      log.debug("execute method3");
    } finally {
      lock.unlock();
    }
  }

输出:

16:51:01.042 [main] DEBUG com.fly.n2.Test5 - execute method1
16:51:01.045 [main] DEBUG com.fly.n2.Test5 - execute method2
16:51:01.045 [main] DEBUG com.fly.n2.Test5 - execute method3

可打断

 private static final ReentrantLock lock = new ReentrantLock();
  private static final Logger log = LoggerFactory.getLogger(Test6.class);

  public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
      log.debug("启动...");
      try {
        lock.lockInterruptibly();
      } catch (InterruptedException e) {
        e.printStackTrace();
        log.debug("等锁的过程中被打断");
        return;
      }
      try {
        log.debug("获得了锁");
      } finally {
        lock.unlock();
      }
    }, "t1");
    lock.lock();
    log.debug("获得了锁");
    t1.start();
    try {
      Thread.sleep(1000);
      t1.interrupt();
      log.debug("执行打断");
    } finally {
      lock.unlock();
    }
  }

输出:

17:00:32.868 [main] DEBUG com.fly.n2.Test6 - 获得了锁
17:00:32.872 [t1] DEBUG com.fly.n2.Test6 - 启动...
17:00:33.872 [main] DEBUG com.fly.n2.Test6 - 执行打断
17:00:33.873 [t1] DEBUG com.fly.n2.Test6 - 等锁的过程中被打断
java.lang.InterruptedException
	at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:944)
	at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1263)
	at java.base/java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:317)
	at com.fly.n2.Test6.lambda$main$0(Test6.java:17)
	at java.base/java.lang.Thread.run(Thread.java:834)

注意:如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断

公平锁

  1. ReentrantLock 默认是不公平的
  2. Creates an instance of ReentrantLock with the given fairness policy.
    Params:
    fair – true if this lock should use a fair ordering policy
  3. 创建:private static final ReentrantLock lock = new ReentrantLock(true);

条件变量

  1. ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的
  2. 使用要点:
    2.1 await 前需要获得锁
    2.2 await 执行后,会释放锁,进入 conditionObject 等待
    2.3 await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
    2.4 竞争 lock 锁成功后,从 await 后继续执行
private static final ReentrantLock lock = new ReentrantLock();
  Condition condition = lock.newCondition();
posted @   翻蹄亮掌一皮鞋  阅读(517)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)
点击右上角即可分享
微信分享提示