线程同步-synchronized-基础

Synchronized-基础

基本概念

:::primary

基本概念是对 synchronized 的所有内容的总结,如果发现存在难以理解的地方可以先查看后面的笔记

:::

[基本内容]{.label .info}

  • synchronized 定义:[采用的 悲观锁 的思想,是一种独占锁或者说排他锁]{.red}

  • synchronized 作用:[给共享变量添加排他锁从而确保 每次仅有一个线程 可以访问该共享变量,确保线程安全]{.red}

  • synchronized 过程分析:

    • [存在线程成功获取共享变量的锁之后,其余线程就会 发生上下文切换进入阻塞队列等待]{.red}

      • [持有锁的线程被 分配的时间片可能不足 以支撑执行完临界区中所有代码]{.pink}
      • [此时线程会发生上下文切换 但是依然持有锁,其余线程依然处于阻塞队列中无法获取锁]{.pink}
      • [持有锁的线程需要等待处理器再次分配给自己时间片,从而执行完临界区中所有代码,才能够释放锁]{.pink}
    • [持有锁的线程释放锁之后,阻塞队列中的线程 再次发生上下文切换开始竞争锁的所有权]{.red}

    • 竞争成功的线程可以获取到共享变量的锁,其余线程再次进入阻塞队列等待

  • wait 作用:[持有锁的线程执行所需的条件没有得到的满足时,主动释放锁进入等待队列等待,让其余线程执行]{.red}

  • notify / notifyAll 作用:[等待队列中的线程执行所需的条件得到满足,从而被唤醒去竞争锁的使用权]{.red}

[底层原理]{.label .danger}

  • 实现原理:
    • [synchronized + wait + notify 机制就是基于操作系统提出的 管程 这个概念实现的]{.red}
    • [synchronized + wait + notify 都是抽象数据结构管程的组成部分]{.red}
  • 编译器实现:
    • [编译器在编译生成字节码的时候会将 synchronized 转换成 monitorenter、monitorexit 两条字节码指令]{.red}
    • [这两条字节码指令确保共享变量是线程独占的,并且在修改之后对其他线程是可见的]{.red}
    • [这两条字节码指令的底层实现是由 HotSpot 虚拟机中的 ObjectMonitor 对象实现的]{.red}
      • [这个对象负责管理锁、锁的拥有者、等待队列、阻塞队列等信息]{.pink}

:::info

这里只是简单讲述 synchronized 关键字的底层实现,详细内容查看Synchronized 底层原理

:::

[性能分析]{.label .success}

  • 核心:[synchronized 锁]{.red}

  • [每次没能成功获取锁的线程需要执行上下文切换进入阻塞队列,而上下文切换会陷入内核态造成很大的开销]{.red}

  • [没有竞争的情况下线程依然需要获取锁,也就是会修改监视器对象的信息,这种开销显然是没有必要的]{.red}

    • [没有竞争的情况下产生的获取监视器对象的开销已经在 JDK 6 之后被相应的优化措施解决了]{.pink}

[优化措施]{.label .primary}

  • 前提:所有的优化措施都是基于没有线程竞争的情况,从而减少没有竞争的情况下同步的开销
  • [轻量级锁 + 偏向锁(批量重偏向、批量撤销)]{.pink}
  • [自旋锁 + 锁消除 + 锁粗化]{.pink}

[其余细节]{.label .warning}

  • [synchronized 锁完美解决了所有并发问题:原子性问题、可见性问题、有序性问题]{.red}

    • 不过 synchronized 只是采用内存屏障禁止同步代码块外的代码和同步代码块内的代码进行指令重排
    • 而同步代码内部的代码依然是可以随意进行指令重排的,因为在单线程下指令重排的结果依然是正确的
  • [synchronized 锁是非公平锁,并且不可以被中断,不可以设置超时等待]{.red}

  • [synchronized 锁可以对共享变量添加,也可以对方法添加]{.red}

    • [synchronized 锁给实例方法添加的本质是给对象实例 this 添加锁]{.pink}
    • [synchronized 锁给静态方法添加的本质是给 Class 对象添加锁]{.pink}
  • [synchronized 只能给引用类型的变量上锁,基本数据类型是不可以上锁的,并且锁住的引用类型不能为空]{.red}

  • [不同线程中 synchronized 锁住对象必须相同,才能够确保共享变量是线程独占的]{.red}

基本使用

synchronized

  • synchronized

    1. 使用方式:给共享变量添加排他锁:synchronized(object)

      • 演示代码

        private Object object;
        // 给共享变量上锁
        public void method(){
        // 其余代码
        synchronized(object){
        // 临界区: 对共享变量进行操作
        }
        // 其余代码
        }
      • 细节:

        • [给共享变量上锁可以避免给不需要同步的代码块上锁,从而提高加锁的效率]{.pink}

        • [只有引用类型的共享变量可以上锁,基本数据类型可以借助引用类型变量保护自身]{.red}

          // 用于上锁的变量
          private Object lock;
          // 共享变量: 不可以直接上锁
          private int count;
          public void method(){
          // 其余代码
          synchronized(lock){
          // 保护基本数据类型的变量
          count++;
          }
          // 其余代码
          }
    2. 使用方式:给方法添加排他锁

      • 演示代码(两种添加方式)

        • [给虚方法(实例方法)上锁的实质是对类对象上锁]{.red}

          class ThreadSynchronized{
          // 给实例方法上锁
          public synchronized void method(){

          }
          // 等价于锁住类相应的实例
          public void method(){
          synchronized(this){
          }
          }
          }
        • [给静态方法上锁的实质是对类的 Class 对象上锁]{.red}

          class ThreadSynchronized{
          // 给静态方法上锁
          public synchronized static void method(){

          }
          // 等价于锁住类对应的 Class对象
          public static void method(){
          synchronized(ThreadSynchronized.class){
          }
          }
          }
      • 细节:[给整个方法上锁会导致锁住不需要同步的代码块从而降低线程的效率]{.pink}

    3. 解决线程安全问题:原子性问题

      // 线程同步方案: synchronized 关键字
      @Slf4j(topic = "c.syn")
      public class ThreadSynchronized{
      // 共享变量
      private int count;

      // 基本数据类型无法上锁, 所以可以给方法上锁或者直接给 this 上锁
      private synchronized void increment(){
      count++;
      }

      private synchronized void decrement(){
      count--;
      }

      private synchronized int getCount(){
      return count;
      }

      public static void main(String[] args) throws InterruptedException{
      ThreadSynchronized obj = new ThreadSynchronized();
      // 执行递增操作的线程
      Thread t1 = new Thread(() ->{
      for (int i = 0; i < 1000; i++){
      obj.increment();
      }
      }, "t1");
      // 执行递减操作的线程
      Thread t2 = new Thread(() ->{
      for (int i = 0; i < 1000; i++){
      obj.decrement();
      }
      }, "t2");
      // 启动线程
      t1.start();
      t2.start();
      // 无论这个测试代码执行多少次, 最后得到结果一定是固定且正确的
      log.debug("count = {}", obj.getCount();
      }
      }

wait

  • 作用:[调用该方法的线程会 主动放弃共享变量 的锁,进入等待队列等待被唤醒]{.red}

  • 应用场景:线程发现自己正在执行的任务缺少相应的资源,无法继续正常执行下去,所以会先挂起自己等待条件满足

  • 特点:

    • [线程会从 RUNNABLE 状态变为 WAIT 或者 TIMED_WAITING 状态]{.red}
    • [线程调用 wait 方法之后会主动释放锁,以便其他线程能够获取]{.red}
    • [wait 方法被调用之前必须先对共享变量上锁,否则会抛出 IllegalMonitorStateException]{.pink}
    • [Object 类中提供了该方法,所以引用类型的共享变量都具有 wait 方法]{.blue}
    • [notify \ notify、interrupt 方法可以打断线程的等待过程,并且抛出 InterruptedException 异常]{.pink}
  • 使用方式:

    1. wait():[线程会无限期等待其余线程唤醒自己]{.aqua}

    2. wait(long timeout) :[线程会在有限的时间内等待其余线程唤醒自己,超过时间之后就不会继续等待]{.aqua}

      private Object object;
      // 给共享变量上锁
      public static void main(String[] args){
      new Thread(()->{
      // 其余代码
      synchronized(object){
      // 线程发现正在执行的任务缺少相应资源, 挂起自己
      try{
      // 线程会持续等待直到有人唤醒自己
      object.wait()
      }catch (Exception e){}
      }
      }).start();
      }

notify / notifyAll

  • 作用:[正在运行的线程唤醒等待队列中等待的线程]{.red}

  • 应用场景:正在运行的线程提供了等待队列中的线程需要的资源,所以将这些等待线程唤醒

  • 特点:

    • [线程调用 notify / notifyAll 方法 不会主动释放锁]{.red}
      • [这也就意味着线程只能够在即将释放锁的时候调用 notify / notifyAll 方法]{.pink}
      • [否则在没有释放锁的情况下唤醒等待线程,被唤醒的线程也只能陷入阻塞]{.pink}
    • [没有任何方法可以指定唤醒某个线程]{.red}
      • Java 采用的是内核级线程 + 抢占式调度的算法,所有线程的调度都由 OS 管理
      • C# 等采用协程的语言才是可以控制线程的
    • [被唤醒的线程是从此前释放锁的位置开始执行而不是从头开始]{.pink}
    • [Object 类中提供了该方法,所以引用类型的共享变量都具有 notify / notifyAll 方法]{.blue}
  • 使用方式:

    1. notify:[线程调用 notify 方法将会 随机唤醒 一个线程]{.aqua}

    2. notifyAll:[线程调用 notifyAll 将会 唤醒所有 等待线程]{.aqua}

      private Object object;
      // 给共享变量上锁
      public static void main(String[] args){
      new Thread(()->{
      // 其余代码
      synchronized(object){
      // 线程发现正在执行的任务缺少相应资源, 挂起自己
      try{
      // 线程会持续等待直到有人唤醒自己
      object.wait()
      }catch (Exception e){}
      }
      }).start();

      Time.Unit.SECONDS.sleep(1);
      // 主线程获取锁之后唤醒等待队列中的线程
      synchronized (LOCK){
      // 唤醒所有线程
      LOCK.notifyAll();
      }
      }

虚假唤醒

  • 问题描述:[线程没有其他任何线程 唤醒或者被中断或者超时,该线程从 WAIT 状态变为 RUNNABLE 状态]{.blue}

    • 这里的唤醒应该指的是其余线程想要唤醒的不是这个线程,所以说该线程没有被唤醒
  • 问题演示:

    1. Thread-1、Thread-2 需要 flag、status 变量为 true 时才可正常执行任务
    2. 两个线程在获取到对象锁之后分别检查变量是否满足,不满足的情况下就会进入等待队列中等待
    3. 主线程在子线程释放锁之后获取对象锁,并且在即将离开之前唤醒所有线程
    // 虚假唤醒问题
    @Slf4j(topic = "c.fake")
    public class ThreadFakeNotify
    {
    private static final Object LOCK = new Object();
    // 第一个线程正常执行需要满足的条件
    private static boolean flag = false;
    // 第二个线程正常执行需要满足的条件
    private static boolean status= false;
    // 为了节省篇幅将去除异常的代码块
    public static void main(String[] args) throws InterruptedException
    {
    new Thread(()->{
    synchronized (LOCK){
    // 先检查运行所需的条件是否满足
    if (!flag){
    log.debug("线程未能够获得执行所需的条件, 进入等待队列...");
    LOCK.wait();
    }
    // 唤醒之后检查是否满足条件
    if (flag)
    log.debug("执行任务...");
    }
    }).start();

    new Thread(()->{
    synchronized (LOCK){
    // 先检查运行所需的条件是否满足
    if (!status){
    log.debug("线程未能够获得执行所需的条件, 进入等待队列...");
    LOCK.wait();
    }
    if (status)
    log.debug("执行任务...");
    }
    }).start();

    TimeUnit.SECONDS.sleep(1);
    new Thread(()->{
    synchronized (LOCK){
    // 由于资源限制仅满足一个条件
    status = true;
    // 唤醒所有线程
    LOCK.notifyAll();
    }
    }).start();
    }
    }
  • 核心原因:[每个锁对象仅存在唯一的条件变量,也就是只有一个等待队列]{.red}

    1. [所有持有该锁对象的线程在调用 wait 方法之后都会进入相同的等待队列]{.pink}
    2. [其余线程调用 notifyAll 方法会唤醒所有线程,无论线程需要的条件是否满足]{.pink}
    3. [被唤醒的线程不会检查条件变量是否满足,而是直接从此前等待的位置继续执行]{.pink}
    4. [从而导致线程在条件没有满足的情况下,什么都无法完成就结束了]{.pink}
  • 正确使用方式:[采用循环判断的方式而不是条件判断]{.aqua}

    • 每次线程被唤醒都会检查自己的条件是否满足
      • 如果条件不满足就会继续调用 wait 方法进行等待
      • 如果条件满足就执行正常任务
    new Thread(()->{
    synchronized (LOCK){
    // 先检查运行所需的条件是否满足
    while (!flag){
    log.debug("线程未能够获得执行所需的条件, 进入等待队列...");
    LOCK.wait();
    }
    // 唤醒之后检查是否满足条件
    if (flag)
    log.debug("执行任务...");
    }
    }).start();

:::primary

① 在 if 块中使用 wait 方法是非常危险的,因为一旦线程被唤醒并得到锁,就不会再判断if条件而是执行if语句块外的代码

② 在 while 块中则是会在唤醒之后继续判断条件是否成立,这样就避免被虚假唤醒的危险

:::

八锁问题

:::info

如果你认为自己对 Synchronized 关键的使用掌握的很好的话,其实这八个问题都很简单,没有必要看

:::

  • 前言:[线程八锁问题其实非常简单,考察的是你是否能够清楚分辨锁住的到底是那个对象和线程执行的先后顺序]{.aqua}

  • 问题

    +++warning 观察下列情况,分析两个线程锁住的对象是否相同,线程之间是否会因为锁而互斥

    两个线程锁住都是 this 引用,而 this 指向的是相同的对象,既然锁住的对象相同那么必然就会产生互斥

    +++

    @Slf4j(topic = "c.syn")
    public class ThreadSynchronizedTest
    {
    public synchronized void method1(){
    log.debug("方法一...");
    }

    public synchronized void method2(){
    log.debug("方法二...");
    }


    // 测试方法
    public static void main(String[] args)
    {
    ThreadSynchronizedTest test = new ThreadSynchronizedTest();
    new Thread(test::method1, "t1").start();
    new Thread(test::method2, "t2").start();
    }
    }

    +++warning 在第一题的基础上让其中一个线程睡眠固定时间,试分析会出现的情况

    ① sleep 方法不会释放线程持有的锁

    ② 其中一个线程需要等待另一个线程固定时间+运行时间才可以获取锁]

    +++

    @Slf4j(topic = "c.syn")
    public class ThreadSynchronizedTest
    {
    public synchronized void method1(){
    // 省略捕获异常的代码...
    TimeUnit.sleep(1)
    log.debug("方法一...");
    }

    public synchronized void method2(){
    log.debug("方法二...");
    }


    // 测试方法
    public static void main(String[] args)
    {
    ThreadSynchronizedTest test = new ThreadSynchronizedTest();
    new Thread(test::method1, "t1").start();
    new Thread(test::method2, "t2").start();
    }
    }

    +++warning 观察下列情况,试分析线程的执行的先后顺序

    ① 线程-1和线程-2之间的执行顺序取决于谁先抢到锁,先抢到的先执行

    ② 线程-3执行的方法没有上锁,执行的时机依靠虚拟机的调度

    ③ 线程1和线程-3:线程-3会先于线程-1执行结束,无论线程-1是否抢到锁,因为线程-1需要睡眠1秒,而线程的调度时间是纳 秒级别的,线程-1执行结束前线程-3早就被调度执行

    ④ 线程-2和线程-3:如果线程-2没有先抢到锁,那么就会晚于线程-3执行,因为线程-1需要睡眠1秒;如果线程-2先抢到锁,那么线程-2和线程-3的执行顺序就是随机的

    ⑤ 结果:3 1 2,3 2 1,2 3 1

    +++

    // 线程八锁问题: 明确到底锁住的是哪个对象
    @Slf4j(topic = "c.syn")
    public class ThreadSynchronizedTest
    {
    public synchronized void method1(){
    // 省略捕获异常的代码...
    TimeUnit.sleep(1)
    log.debug("方法一...");
    }
    public synchronized void method2(){
    log.debug("方法二...");
    }
    // 没有上锁的方法
    public void method3(){

    }
    // 测试方法
    public static void main(String[] args)
    {
    ThreadSynchronizedTest test = new ThreadSynchronizedTest();
    new Thread(test::method1, "t1").start();
    new Thread(test::method2, "t2").start();
    new Thread(test::method3, "t3").start();
    }
    }

    +++warning 观察下面的代码,试分析两个线程锁住的是否为同一个对象,是否产生互斥

    ① 两者锁住的不是相同的对象,虽然 synchronized 锁住的都是 this 引用,但是两个 this 指向的却是不同的对象

    ② 锁住既然不是相同的对象,那么也就不会产生互斥,两个线程可以并发或者并行输出

    +++

    @Slf4j(topic = "c.syn")
    public class ThreadSynchronizedTest
    {
    public synchronized void method1(){
    log.debug("方法一...");
    }
    public synchronized void method2(){
    log.debug("方法二...");
    }

    // 测试方法
    public static void main(String[] args){
    // 两个不同的对象
    ThreadSynchronizedTest test1 = new ThreadSynchronizedTest();
    ThreadSynchronizedTest test2 = new ThreadSynchronizedTest();
    new Thread(test1::method1, "t1").start();
    new Thread(test2::method2, "t2").start();
    }
    }

    +++warning 观察下面的代码,试分析两个线程锁住的是否为同一个对象,是否产生互斥

    前者锁住的是静态方法,锁住的是类对应的 Class 对象;后者锁住的是实例方法,锁住的是类对应的实例对象

    +++

    @Slf4j(topic = "c.syn")
    public class ThreadSynchronizedTest
    {
    public static synchronized void method1(){
    log.debug("方法一...");
    }
    public synchronized void method2(){
    log.debug("方法二...");
    }

    // 测试方法
    public static void main(String[] args){
    // 两个不同的对象
    ThreadSynchronizedTest test = new ThreadSynchronizedTest();
    // 对象调用静态方法是可以的, 只不过不推荐
    new Thread(()->{test.method1();}, "t1").start();
    new Thread(test::method2, "t2").start();
    }
    }

    +++warning 观察下面的代码,试分析两个线程锁住的是否为同一个对象,是否产生互斥

    两者锁住都是类对应的 Class 对象

    +++

    @Slf4j(topic = "c.syn")
    public class ThreadSynchronizedTest
    {
    public static synchronized void method1(){
    log.debug("方法一...");
    }
    public static synchronized void method2(){
    log.debug("方法二...");
    }

    // 测试方法
    public static void main(String[] args){
    // 两个不同的对象
    ThreadSynchronizedTest test = new ThreadSynchronizedTest();
    // 对象调用静态方法是可以的, 只不过不推荐
    new Thread(()->{test.method1();}, "t1").start();
    new Thread(()->{test.method2();}, "t2").start();
    }
    }

    +++warning 观察下面的代码,试分析两个线程锁住的是否为同一个对象,是否产生互斥

    前者锁住的依然是 Class 对象,后者锁住的依然是实例对象,只不过实例是 test2 而不是 test 1

    +++

    @Slf4j(topic = "c.syn")
    public class ThreadSynchronizedTest
    {
    public static synchronized void method1(){
    log.debug("方法一...");
    }
    public synchronized void method2(){
    log.debug("方法二...");
    }

    // 测试方法
    public static void main(String[] args){
    // 两个不同的对象
    ThreadSynchronizedTest test1 = new ThreadSynchronizedTest();
    ThreadSynchronizedTest test2 = new ThreadSynchronizedTest();
    // 对象调用静态方法是可以的, 只不过不推荐
    new Thread(()->{test1.method1();}, "t1").start();
    new Thread(()->{test2.method2();}, "t2").start();
    }
    }

    +++warning 观察下面的代码,试分析两个线程锁住的是否为同一个对象,是否产生互斥

    虚拟机会为每个类都维护唯一的 Class 对象,两者锁住的都是相同的 Class 对象,尽管实例对象是不同

    +++

    @Slf4j(topic = "c.syn")
    public class ThreadSynchronizedTest
    {
    public static synchronized void method1(){
    log.debug("方法一...");
    }
    public static synchronized void method2(){
    log.debug("方法二...");
    }

    // 测试方法
    public static void main(String[] args){
    // 两个不同的对象
    ThreadSynchronizedTest test1 = new ThreadSynchronizedTest();
    ThreadSynchronizedTest test2 = new ThreadSynchronizedTest();
    // 对象调用静态方法是可以的, 只不过不推荐
    new Thread(()->{test1.method1();}, "t1").start();
    new Thread(()->{test2.method2();}, "t2").start();
    }
    }
  • 总结:帮助初学者熟悉 synchronized 机制使用的题目,个人觉得意义不是很大

Author: Fuyusakaiori
Link: http://example.com/2021/10/28/juc/多线程基础/线程同步-synchronized-基础/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.