首页 > 编程语言 >Java线程同步总结

Java线程同步总结

时间:2023-01-03 19:56:33浏览次数:36  
标签:同步 Java synchronized CAS lock 加锁 volatile 线程

线程同步的关键是保证临界资源访问的原子性和可见性。
一般的解决方案是使用volatile(保证可见性、不一定保证原子性)修饰共享变量,或是加锁(直接保证原子性和可见性)进行线程同步

1. volatile

1.1 可见性实现原理:

汇编层面会向volatile修饰的变量加关键字前缀lock,这个lock前缀的作用有二:

  1. lock前缀修饰变量的修改不会在缓存停留,会直接回写主存,
  2. 其他cpu会在总线上嗅探lock修饰的变量是否修改,若变化,直接置cpu本地缓存为不可用,下次使用需要从主存中读取最新的值

1.2 禁止指令重排序:

包括禁止在编译阶段优化(javac优化),以及执行阶段优化(处理器流水线统筹优化)

1.3 为什么不保证原子性

在编译过程中,一条Java代码语句可能被编译为多条JVM指令码

  • 如自增语句 i++ 会被编译为 1.load(读变量i的值)、2.increment(变量值加一)、3.store(写回) 三条指令码。
  • 用volatile修饰之后,执行store指令时会回写主存,同时其他线程load时需要从主存读取最新的值
  • 可能出现的问题:初始值volatile int i = x,两线程同时执行 i++,预期结果为i == x+2。先同时load i 的值为x,然后自增为x+1,再写回,由于一个线程store的时候,另一个线程已经load过了,所以导致两个线程都store x的值为x+1,而不是预期的x+2

证明:

public class VolatileTest {
    volatile static int cnt = 0;
    volatile static boolean signal = true;
    public static void main(String[] args) throws InterruptedException {
        VolatileTest main = new VolatileTest();
        main.test1();
        Thread.sleep(2000);
        signal = false;
        System.out.println(cnt);
        // 第一次 54055473
        // 第二次 50769994
        // 第三次 57593069
    }

    private void test1() {
        Runnable runnable = () -> {
            while (signal) {
                cnt++;
            }
        };

        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
    }
}

1.4 常见面试问题:

  1. 什么时候只使用volatile,不使用锁就可以做线程同步
  2. 两个线程,一个用volatile修饰的共享变量i,初始值为0,两个线程分别做50次i++,最后i的值会是100么?
  3. 举一个场景:不使用volatile,不保证可见性后可能出现的问题

2. 锁相关

2.1 synchronized和lock的区别

  1. 关键字 vs 接口
    • synchronized是一个JVM提供的关键字
    • lock是jdk中提供的接口,包括多个方法如 lock() unlock() tryLock()...
  2. 加锁方式
    • synchronized是隐式加锁
    • lock是显式加锁,需要手动调用lock()和unlock()
  3. 作用位置
    • synchronized可在方法、静态方法、代码块上加锁
    • lock仅支持在代码块上加锁
  4. 加锁的方式
    • synchronized仅支持阻塞加锁
    • lock支持非阻塞加锁,如tryLock(),如果获取不到直接返回,不阻塞
    • lock支持设置阻塞等待锁的时间,如tryLock(long time),阻塞time后获取不到锁就唤醒
    • lock支持等待锁过程中可响应中断,如lockInterruptibly()
  5. 锁类型
    • synchronized是非公平锁
    • lock的实现类如reentrantlock可选公平非公平
  6. 底层实现
    • synchronized基于c++的ObjectMonitor实现,其中包含一个同步队列 + 一个等待队列
    • lock基于AQS实现,包含一个同步队列 + 多个等待队列(通过newCondition() 创建)
  7. 等待唤醒机制
    • synchronized 通过Object.wait() 和 Object.notify() 完成等待唤醒
    • lock 通过创建的Condition对象的 Condition.await() 和 Condition.signal() 方法完成等待和唤醒
  8. 个性化定制
    • synchronized无法定制化
    • lock采用模版方法模式,易于根据个人需求定制化

2.2 synchronized锁升级过程

主要关注升级条件,以及如何争抢和释放锁

  1. 无锁

    • 此时对象头markword中锁标识位为001,表示对象未被加锁
  2. 偏向锁

    • 升级为偏向锁
      • 线程CAS修改锁标志位从001 -> 101,若成功,则占有偏向锁,此时线程将其id写入对象头markword的hashcode区域,便于后续判断锁是否偏向该线程。(同时要注意,如果代码中有调用对象的hashcode方法,该对象无法做为偏向锁使用)
      • 若CAS失败,表明出现争抢,直接升级为轻量级锁
    • 争抢锁
      • 若锁标志位为101,接着检查锁偏向的线程id是否为当前线程id,若是则直接获取成功,若不是则意味有竞争
    • 竞争
      • 竞争线程发现锁标志位为101,将markword中记录的对应锁偏向线程执行到安全点后挂起,并检查线程状态
      • 若线程未执行临界区资源,则进行线程重偏向(依然是CAS争抢)
      • 若线程仍在执行临界区资源,则升级为轻量级锁
  3. 轻量级锁

    • 争抢锁
      • 通过CAS将对象头中的markword拷贝到栈帧中的lockrecord,并覆盖为lockrecord的地址,同时lockrecord中也存有对象的地址
      • 重入:每一次重入,栈帧中就压入一个空lockrecord,这样只有最后一次弹出,才会使用非空的lockrecord释放锁
    • 释放
      • CAS将lockrecord中的markword还原回去
      • 若CAS失败,说明已升级为重量级锁,直接将对象头还原到monitor中防止丢失,同时进行重量级锁释放流程
    • 竞争
      • 线程自旋CAS尝试获取锁(这里需要再深入了解)
      • 若自旋次数过多或竞争线程过多,升级为重量级锁。线程修改锁标志位为重量级锁,并将markword设置为指向monitor的指针
  4. 重量级锁

    • ObjectMonitor
      • 一个c++实现的对象,java基于该对象实现重量级锁
      • 包含一个重入计数器,同步队列entryList,等待队列waitSet,竞争队列cxq等
    • 争抢锁
      • 执行monitorenter指令,线程CAS尝试把monitor的重入计数器从0变为1
      • 若成功则代表占有锁,此时锁重入次数为1,线程把monitor的owner设置为自己
      • 若失败,加入同步队列排队
    • 等待
      • 持有锁的线程主动调用wait()方法,则释放monitor,线程进入waitSet,若被notify()唤醒,加入entryList
      • 比如生产者消费者持有同一个锁,多个消费者线程持有锁后发现没有可消费的东西,就进入等待,生产者线程持有锁,生产后再通过notify()方法唤醒消费者线程从waitSet到entryList
    • 释放
      • 执行monitorexit指令,计数器减一,若减完不为0,表示有重入,若为0则表示释放锁,此时唤醒entryList头部线程
      • 唤醒的头部线程和外部线程一同CAS争抢锁,这也体现出synchronized的非公平性

2.3 什么时候可以仅使用volatile而不使用锁做线程同步

1)满足共享变量修改的原子性

  • 变量修改时仅使用简单赋值语句。简单赋值语句是原子性的,如 x = 10,对应指令码只有一条 store x, 10;
    2)变量读多写少
  • 若是写多读少,应当使用锁,因为使用volatile会导致总线风暴(大量锁总线事件)

2.4 常见面试问题:

  1. synchronized的非公平性体现在哪里
  2. reentrantlock的公平和非公平区别在哪里,实现上的区别是什么
  3. 一个线程访问synchronized临界资源、等待、被唤醒、继续执行的详细过程

标签:同步,Java,synchronized,CAS,lock,加锁,volatile,线程
From: https://www.cnblogs.com/yanch01/p/17023216.html

相关文章

  • Java【封装一个新闻类,包含标题和内容属性】
    题目:1、封装一个新闻类,包含标题和内容属性,提供get、set方法,重写toString方法,打印对象时只打印标题;(10分)2、只提供一个带参数的构造器,实例化对象时,只初始化标题;并且实例化......
  • element Ui VUE 前端实现同步调用后端接口,并等待响应后,在操作下一步
    我这里是使用文件上传的场景,主要关键字awaitasync进行同步阻塞,然后,就可以在循环中,等待响应后,在进行调用如果不等待,则前端会一次性将循环体遍历完,请求直接占满,导致其......
  • JavaScript 的数据是如何回收的
    因为数据是存储在栈和堆两种内存空间中的,所以接下来我们就来分别介绍“栈中的垃圾数据”和“堆中的垃圾数据”是如何回收的。调用栈中的数据是如何回收的当一个函数执行......
  • [clickhouse]同步MySQL
    前言clickhouse的查询速度非常快,而且兼容大部分MySQL的sql语法,因此一般将clickhouse作为MySQL的读库。本文提供两种clickhouse同步MySQL的方式clickhouse版本:21.2.4.6......
  • argocd 同步策略--忽略某个配置同步
    背景当我在k8s中用cronhpa+hpa实现定时pod扩容的同时还能兼容平时的hpa弹性伸缩。cronhpa通过修改hpa的minReplicas与maxReplicas,从而实现pod的伸缩。但是,当hpa......
  • 24.Java程序员的经典错误
    1.使用Objects.equals比较对象是JDK7提供的一种方法,可以快速实现对象的比较,有效避免烦人的空指针检查。但是这种方法很容易用错,例如:1LonglongValue=123L;2System......
  • Mysql主从同步配置
    一、主数据库的配置1.my.cnf(Linux)/my.ini(Windows)在配置文件参数选项[mysqld]下面添加如下内容log_bin=mysql-binserver_id=1innodb_flush_log_at_trx_commit=......
  • 重学 Java 设计模式-结构型模式-适配器模式
    重学Java设计模式-结构型模式-适配器模式内容摘自:添加链接描述适配器模式介绍图片来自:https://refactoringguru.cn/design-patterns/adapter(opensnewwindow)适......
  • systemd自启动java程序
    一、背景条件1.Linux系统是Debian82.Java程序是test.jar,安装路径是/home/test/test.jar二、编写java的启动脚本startTest.sh#!/bin/shjava-jar/home/test/test......
  • 4. 识别线程
    识别线程线程表示类型为std::id可以通过两种方式进行检索第一种可以通过std::thread的对象成员函数get_id()来直接获取第二种是在当前线程中调用std::get_id()vo......