03 | 互斥锁(上):解决原子性问题
03 | 互斥锁(上):解决原子性问题
讲述:王宝令
时长12:58大小11.85M
简易锁模型
改进后的锁模型
Java 语言提供的锁技术:synchronized
用 synchronized 解决 count+=1 问题
锁和受保护资源的关系
总结
课后思考
赞 79
提建议
精选留言(238)
- 好牙2019-03-05加锁本质就是在锁对象的对象头中写入当前线程id,但是new object每次在内存中都是新对象,所以加锁无效。
作者回复: synchronized的实现都知道了,厉害!
共 21 条评论507 - w1sl1y2019-03-05经过JVM逃逸分析的优化后,这个sync代码直接会被优化掉,所以在运行时该代码块是无锁的
作者回复: 👍厉害
共 12 条评论262 - sbwei🚴2019-03-24最后的思考题: 多把锁保护同一个资源,就像一个厕所坑位,有N多门可以进去,没有丝毫保护效果,管理员一看,还不如把门都撤了,弄成开放式(编译器代码优化)😂。共 14 条评论154
- 老杨同志2019-03-05两把不同的锁,不能保护临界资源。而且这种new出来只在一个地方使用的对象,其它线程不能对它解锁,这个锁会被编译器优化掉。和没有syncronized代码块效果是相同的
作者回复: 实在是太厉害了!!!
共 8 条评论110 - zyl2019-03-05sync锁的对象monitor指针指向一个ObjectMonitor对象,所有线程加入他的entrylist里面,去cas抢锁,更改state加1拿锁,执行完代码,释放锁state减1,和aqs机制差不多,只是所有线程不阻塞,cas抢锁,没有队列,属于非公平锁。 wait的时候,线程进waitset休眠,等待notify唤醒
作者回复: sync的优化都知道了,厉害啊
共 9 条评论104 - 宝爸学学学2019-05-15我觉得评论区学到的更多啊,你们真的是来学习的吗 :D共 7 条评论93
- 王大王2019-03-05Get方法加锁不是为了解决原子性问题,这个读操作本身就是原子性的,是为了实现不能线程间addone方法的操作结果对get方法可见,那么value变量加volitile也可以实现同样效果吗?
作者回复: 是的,并发包里的原子类都是靠它实现的
共 9 条评论74 - 探索无止境2019-03-05不能,因为new了,所以不是同一把锁。老师您好,我对那 synchronized的理解是这样,它并不能改变CPU时间片切换的特点,只是当其他线程要访问这个资源时,发现锁还未释放,所以只能在外面等待,不知道理解是否正确
作者回复: 理解正确!
共 4 条评论59 - 石头剪刀布2019-03-08老师说:现实世界里,我们可以用多把锁来保护同一个资源,但在并发领域是不行的。 不能用两把锁锁定同一个资源吗? 如下代码: 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吗?很符合我们的原则。我怎么没想到呢?
共 19 条评论38 - 别皱眉2019-03-17相信很多人跟我一样会碰到这个问题,评论里也看到有人在问,内容有点长,辛苦老师帮忙大家分析下了 哈哈 --------------------------------------------------------- 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操作之后,概率就更低了。
共 7 条评论29 - 老焦2019-06-20有同学说get方法不用sync也能保证可见性,这是对的。但如果真的这么做了,原子性就可能会被打破。sync并不保证线程不被中断。如果在写高低两个双字的中间写线程被中断,而读线程被调度执行,因为读没有尝试加锁,所以可以读到写了一半的结果。这种情况都不用考虑多核,单核都会出现原子性问题。所以谨慎起见还是给get加上sync保险点。
作者回复: 👍
共 5 条评论25 - 别皱眉2019-03-13老师,我觉得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获取的数据不是最新的 您看我说的对吗?还是说具体场景有不同的需求,有些还是允许这点延迟的? 本人大三,请前辈多指教😁😁谢谢展开
作者回复: 我觉得你这个才是正道,并发问题小心还躲不过呢,哪里敢冒险啊!没想到还有学生看这个专栏,有前途👍
共 3 条评论25 - 大南瓜2019-03-05沙发,并不能,不是同一把锁
作者回复: 为快点赞
18 - 别皱眉2019-03-13老师,我对您对成华的回答有点疑问 ------------------------------------------------------------ 陈华: 我理解get方法不需要加synchroized关键字,也可以保证可见性。 因为 对 value的写有被 synchroized 修饰,addOne()方法结束后,会强制其他CPU缓存失效,从新从内存读取最新值! class SafeCalc { long value = 0L; long get() { return value; } synchronized void addOne() { value += 1; } } 2019-03-07作者回复 说的对,从实现上看是这样。但是hb没有这样的要求 ------------------------------------------------------------ 会强制其他CPU缓存失效,从新从内存读取最新值?如果陈华说的是正确的,那get方法就不用加synchronized就可以保证可见性了? 但您文章里说的是get方法不加锁可见性是无法保证的展开
作者回复: 按照规范是不能保证的,具体实现现在可以,这两个不矛盾,但是我们不能依赖没有承诺的实现,它可以随时改,规范就不可以随时改。 我们这里就采取遵循规范。
13 - 小和尚笨南北2019-03-05不正确 使用锁保护资源时,对资源的所有操作应该使用同一个锁,这样才能起到保护的作用。 课后题中每个线程对资源的操作都是用的是各自的锁,不存在互斥和竞争的情况。 这就相当于有一个房间,每个人过来都安装一个门,每个人都有自己门的钥匙,大家都可以随意出入这个房间。 由于每个线程都可以随时进入方法,所以存在原子性问题; 但是因为每次都有加锁和解锁的操作,unlock操作会使其他缓存的变量失效,需要重新从主内存中加载变量的值,所以可以解决可见性问题。 如有错误,请老师指正。展开
作者回复: 比喻很生动
12 - 陈华2019-03-07我理解get方法不需要加synchroized关键字,也可以保证可见性。 因为 对 value的写有被 synchroized 修饰,addOne()方法结束后,会强制其他CPU缓存失效,从新从内存读取最新值! class SafeCalc { long value = 0L; long get() { return value; } synchronized void addOne() { value += 1; } }展开
作者回复: 你说的对,从实现上看是这样。但是hb没有这样的要求
共 11 条评论11 - 空白2019-06-28在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。 如果同步块所使用的锁对象通过这种分析被证实只能够被一个线程访问,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这个取消同步的过程就叫同步省略,也叫锁消除。展开8
- 彻头撤尾2019-03-31别皱眉同学,我特意把你的代码考下来了,run方法里什么都不加 就是死循环,加一个变量b==2作为循环出口,线程b也可以正常退出的啊!!!!线程可见性问题应该描述的是变量被修改的这一瞬间其他线程可见性问题吧?你加不加打印语句,加不加同步代码块都不会影响线程b的正常结束吧?只要变量最新值刷到主内存中,线程b 就可见然后就终止了.
作者回复: 感谢热心同学的回复!!
共 3 条评论6 - 侯大虎2019-03-30老师,有个小问题 class锁锁的是该类的所有实例,和this不应该是同一把锁吗(this不就是这个类的实例吗)?
作者回复: 没有包含关系,就像公交卡和单次票一样,都能坐车
共 4 条评论6 - 别皱眉2019-03-17老师,我有几个问题比较疑惑😄 --------------------------------------------- 问题1: synchronized void test() { //操作1 value = value + 1; //业务方法.... //操作2 value = value + 2; } 为保证原子性,也就是value中间状态对外不可见,我觉得操作1完成后不会将最新值刷回内存,而是到解锁后才会将在synchronized块中操作的数据刷回内存! -------------------------------------------------- 问题2: 有些同学说将value变量加volitile也可以实现同样效果. 我觉得不行,可见性保证了,原子性却会被破坏。理由如下: 基于问题1,假设操作1完成后不会将最新值刷回内存,那如果此时value变量加volitile上后,操作1完成后那岂不是会将这个中间值value存入内存?如果真是这样,原子性是保证不了的。 ---------------------------------------------------- 问题3: volatile int x = 0:告诉编译器,对这个变量的读写,不能使用 CPU 缓存,必须从内存中读取或者写入。 Synchronized即保证可见性,又保证原子性。 那要保证可见性,在Synchronized块中是不是第一次读取到变量value时将直接从内存读,解锁时,将最新值刷回内存。 希望老师帮忙分析下 谢谢🙏🙏🙏展开
作者回复: 1. synchronized 能保证互斥,所以操作1完成后刷入内存也没问题。如果你同步代码块里要操作10亿个共享变量,它不放内存放哪里呢?缓存早就爆表了。 2. 同意 3. 同意,另外volatile还会遵循hb规则。Synchronized解锁后会强刷缓存。
5