Java 并发编程实战
王宝令
资深架构师
72485 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 51 讲
学习攻略 (1讲)
Java 并发编程实战
15
15
1.0x
00:00/00:00
登录|注册

03 | 互斥锁(上):解决原子性问题

使用synchronized修饰代码块
synchronized
深入分析
临界区的代码
静态变量
N:1的关系
addOne()方法
get()方法
SafeCalc类
锁定的对象
加锁和解锁操作
修饰代码块
修饰方法
synchronized关键字
解锁操作
加锁操作
创建锁
受保护的资源
解锁
加锁
临界区
多核场景
线程切换
课后思考
总结
锁和受保护资源的关系
用synchronized解决count+=1问题
Java语言提供的锁技术:synchronized
改进后的锁模型
简易锁模型
原子性问题
互斥锁

该思维导图由 AI 生成,仅供参考

第一篇文章中我们提到,一个或者多个操作在 CPU 执行的过程中不被中断的特性,称为“原子性”。理解这个特性有助于你分析并发编程 Bug 出现的原因,例如利用它可以分析出 long 型变量在 32 位机器上读写可能出现的诡异 Bug,明明已经把变量成功写入内存,重新读出来却不是自己写入的。
那原子性问题到底该如何解决呢?
你已经知道,原子性问题的源头是线程切换如果能够禁用线程切换那不就能解决这个问题了吗?而操作系统做线程切换是依赖 CPU 中断的,所以禁止 CPU 发生中断就能够禁止线程切换。
在早期单核 CPU 时代,这个方案的确是可行的,而且也有很多应用案例,但是并不适合多核场景。这里我们以 32 位 CPU 上执行 long 型变量的写操作为例来说明这个问题,long 型变量是 64 位,在 32 位 CPU 上执行写操作会被拆分成两次写操作(写高 32 位和写低 32 位,如下图所示)。
在单核 CPU 场景下,同一时刻只有一个线程执行,禁止 CPU 中断,意味着操作系统不会重新调度线程,也就是禁止了线程切换,获得 CPU 使用权的线程就可以不间断地执行,所以两次写操作一定是:要么都被执行,要么都没有被执行,具有原子性。
但是在多核场景下,同一时刻,有可能有两个线程同时在执行,一个线程执行在 CPU-1 上,一个线程执行在 CPU-2 上,此时禁止 CPU 中断,只能保证 CPU 上的线程连续执行,并不能保证同一时刻只有一个线程执行,如果这两个线程同时写 long 型变量高 32 位的话,那就有可能出现我们开头提及的诡异 Bug 了。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文深入介绍了互斥锁的原子性问题及解决方案,以及Java语言提供的锁技术。作者首先讨论了在多核CPU场景下,禁止CPU中断无法保证原子性,因此需要互斥来保证对共享变量的修改是互斥的。文章提出了简易锁模型和改进后的锁模型,强调了锁和受保护资源之间的对应关系。此外,文章还介绍了Java语言提供的锁技术synchronized,包括修饰非静态方法、静态方法和代码块时锁定的对象。通过深入分析锁定的对象和受保护资源的关系,读者可以更好地理解互斥锁的使用。总的来说,本文为读者提供了深入了解互斥锁的原子性问题及解决方案的机会,同时也介绍了Java语言提供的锁技术,为读者提供了全面的知识视角。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Java 并发编程实战》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(242)

  • 最新
  • 精选
  • 好牙
    加锁本质就是在锁对象的对象头中写入当前线程id,但是new object每次在内存中都是新对象,所以加锁无效。

    作者回复: synchronized的实现都知道了,厉害!

    2019-03-05
    22
    527
  • w1sl1y
    经过JVM逃逸分析的优化后,这个sync代码直接会被优化掉,所以在运行时该代码块是无锁的

    作者回复: 👍厉害

    2019-03-05
    14
    274
  • 老杨同志
    两把不同的锁,不能保护临界资源。而且这种new出来只在一个地方使用的对象,其它线程不能对它解锁,这个锁会被编译器优化掉。和没有syncronized代码块效果是相同的

    作者回复: 实在是太厉害了!!!

    2019-03-05
    8
    118
  • zyl
    sync锁的对象monitor指针指向一个ObjectMonitor对象,所有线程加入他的entrylist里面,去cas抢锁,更改state加1拿锁,执行完代码,释放锁state减1,和aqs机制差不多,只是所有线程不阻塞,cas抢锁,没有队列,属于非公平锁。 wait的时候,线程进waitset休眠,等待notify唤醒

    作者回复: sync的优化都知道了,厉害啊

    2019-03-05
    9
    108
  • 王大王
    Get方法加锁不是为了解决原子性问题,这个读操作本身就是原子性的,是为了实现不能线程间addone方法的操作结果对get方法可见,那么value变量加volitile也可以实现同样效果吗?

    作者回复: 是的,并发包里的原子类都是靠它实现的

    2019-03-05
    10
    76
  • 探索无止境
    不能,因为new了,所以不是同一把锁。老师您好,我对那 synchronized的理解是这样,它并不能改变CPU时间片切换的特点,只是当其他线程要访问这个资源时,发现锁还未释放,所以只能在外面等待,不知道理解是否正确

    作者回复: 理解正确!

    2019-03-05
    4
    60
  • 石头剪刀布
    老师说:现实世界里,我们可以用多把锁来保护同一个资源,但在并发领域是不行的。 不能用两把锁锁定同一个资源吗? 如下代码: public class X { private Object lock1 = new Object(); private Object lock2 = new Object(); private int value = 0; private void addOne() { synchronized (lock1) { synchronized (lock2) { value += 1; } } } private int get() { synchronized (lock1) { synchronized (lock2) { return value; } } } } 虽然说这样做没有实际意义,但是也不会导致死锁或者其他不好的结果吧?请老师指导,谢谢。

    作者回复: 你这么优秀,我该怎么指导呢?你这不是用lock1 保护 lock2,lock2保护value吗?很符合我们的原则。我怎么没想到呢?

    2019-03-08
    19
    38
  • 别皱眉
    相信很多人跟我一样会碰到这个问题,评论里也看到有人在问,内容有点长,辛苦老师帮忙大家分析下了 哈哈   --------------------------------------------------------- public class A implements Runnable {     public Integer b = 1;       @Override     public void run() {        System.out.println("A is begin!");        while (true) { System.out.println("a");            // System.out.println(b);            if (b.equals(2))               break;        }          System.out.println("A is finish!");     }       public static void main(String[] args) {        A a = new A();        //线程A        new Thread(a).start();        try {            Thread.sleep(1000);        } catch (InterruptedException e) {            e.printStackTrace();        }        a.b = 2;     } }   我们知道这个程序会出现可见性问题。 但是在while内加上System.out.println(b)后 当主线程修改b的值后 线程A居然能够取得最新值 可见性问题得到解决 System.out.println(b)的实现如下     public void println(String x) {         synchronized (this) {             print(x);             newLine();         }     }   Doug Lea大神的Concurrent Programming in Java一书中有这样一个片段来描述synchronized这个关键字:   这里英文就不放出来了 字数超过两千…… 这篇文章也有提及https://www.jianshu.com/p/3c06ffbf0d52   简单翻译一下:从本质上来说,当线程释放一个锁时会强制性的将工作内存中之前所有的写操作都刷新到主内存中去,而获取一个锁则会强制性的加载可访问到的值到线程工作内存中来。虽然锁操作只对同步方法和同步代码块这一块起到作用,但是影响的却是线程执行操作所使用的所有字段。 也就是说当调用System.out.println("a")时当前线程的缓存会被重新刷新过,所以才能够读到这个值最新值  --------------------------------------------------------- 然后问题来了 问题1: 首先上面的说法不知道是不是真的是这样。 然后我在下面加了System.out.println(b) 结果打印出来的是旧值,但是下面的b.equals(2)却能通过 这里没弄明白 我觉得应该是编译器进行了优化?因为现在大三能力不够,还没学会看class文件 没法验证   问题2: 网上找了一些文章 有些人的说法是:打印是IO操作,而IO操作会引起线程的切换,线程切换会导致线程原本的缓存失效,从而也会读取到修改后的值。   我尝试着将打印换成File file = new File("D://1.txt");这句代码,程序也能够正常的结束。当然,在这里也可以尝试将将打印替换成synchronized(A.class){ }这句空同步代码块,发现程序也能够正常结束。   这里有个问题就是 线程切换时会把之前操作的相关数据保存到内存里,切换回来后会把内存里的数据重新加载到寄存器里吗,这样说的话 就算切换也是获取不到修改后的值的,不知道是什么做到能够读到这个修改后的值的?   问题3: 是不是 线程执行过程中,操作系统会随机性的把缓存刷到内存 线程结束后一定会把缓存里的数据刷到内存  --------------------------------------------------------- 在评论里好多大神 能学到好多东西😄😄

    作者回复: 1. println的代码里锁的this指的是你的控制台,这个锁跟你的代码没关系,而且println里也没有写操作,所以println不会导致强刷缓存。 我觉得是因为println产生了IO,IO相对CPU来说,太慢,所以这个期间大概率的会把缓存的值写入内存。也有可能这个线程被调度到了其他的CPU上,压根没有缓存,所以只能从内存取数。你调用sleep,效果应该也差不多。 2. 线程切换显然不足以保证可见性,保证的可见性只能靠hb规则。 3. 线程结束后,不一定会强刷缓存。否则Join的规则就没必要了 并发问题本来就是小概率的事件,尤其有了IO操作之后,概率就更低了。

    2019-03-17
    11
    31
  • 老焦
    有同学说get方法不用sync也能保证可见性,这是对的。但如果真的这么做了,原子性就可能会被打破。sync并不保证线程不被中断。如果在写高低两个双字的中间写线程被中断,而读线程被调度执行,因为读没有尝试加锁,所以可以读到写了一半的结果。这种情况都不用考虑多核,单核都会出现原子性问题。所以谨慎起见还是给get加上sync保险点。

    作者回复: 👍

    2019-06-20
    5
    26
  • 别皱眉
    老师,我觉得get方法有必要用加锁来保证可见性的另一个理由如下: class SafeCalc { long value = 0L; synchronized long get() { return value; } synchronized void add(int i) { // 业务代码....假如这里比较耗时 value += i; } } 假如线程A执行add方法 当方法还没执行完 线程B执行get方法 如果get方法没有加锁 因为此时A正在修改这个数据 B获取的数据不是最新的 您看我说的对吗?还是说具体场景有不同的需求,有些还是允许这点延迟的? 本人大三,请前辈多指教😁😁谢谢

    作者回复: 我觉得你这个才是正道,并发问题小心还躲不过呢,哪里敢冒险啊!没想到还有学生看这个专栏,有前途👍

    2019-03-13
    3
    26
收起评论
显示
设置
留言
99+
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部