一、互斥与锁
我知道原子性问题是由线程切换带来的,最直接的解决当地就是禁止线程切换,操作系统做线程切换需要依赖CPU的中断机制,所以说,禁止CPU发生中断就能够禁止线程切换。
注:这种方案在单核CPU上是可行的,但是并不适合多核CPU。
场景:在32位多核CPU上执行long型变量的写操作,long型变量是64位的,在32位CPU上执行写操作会被拆分成两次写操作(分别是写高32位和写低32位)
-
单核场景下:同一时刻只有一个线程执行,禁止CPU中断,如果一个线程获取到CPU资源,就可以一直执行下去,直到线程结束为止。在这个线程中,对于long型变量的两次写操作,要么都被执行,要么都没有被执行,两次写操作具有原子性,不会出现写入的数据和读取的数据不一致的情况。
-
多核场景下:同一时刻,可能有两个甚至更多的线程在同时执行。假设有两个线程分别是线程A和线程B,线程A执行在CPU-01上,线程B执行在CPU-02上,此时,禁用CPU中断,只能保证在每个CPU上执行的线程是连续的,并不能保证同一时刻只有一个线程执行,如果线程A和线程B同时写long型变量的高32位的话,那么,就有可能出现诡异的Bug问题,也就是说,明明已经将变量成功写入内存了,但是重新读取出来的数据却不是自己写入的!!
从上述例子看,如果我们能够保证对共享变量的修改是同一时刻只有一个线程执行的,那么,无论是单核CPU还是多核CPU,就都能保证原子性了,我们称之为互斥。提及互斥,我们第一时间想到的应该是Lock锁、synchronized关键字了
二、锁模型
-
简易锁模型
加锁操作:lock()
临界区:一段代码
解锁操作:unlock()
-
我们把一段需要互斥执行的代码称为临界区。线程在进入临界区之前,首先尝试加锁lock(),如果成功,则进入临界区,此时我们称这个线程持有锁;否则呢就等待,直到持有锁的线程解锁;持有锁的线程执行完临界区的代码后,执行解锁unlock()。
-
这里会有几个问题
-
锁的是什么?是一段代码吗?
-
我们加锁是在保护什么?
-
-
-
改进后的锁模型
-
回答上面的问题
-
保护的是在代码执行过程中,临界区中的变量资源不被其他其他线程访问,从而达到互斥的效果
-
锁的是临界区中的变量资源R,因此要为 R 创建一个对应的锁 LR,需要处理资源 R 的时候先加锁,处理完之后解锁
-
-
下面是改进后的模型,一个资源必须和锁对应,不能用 A 锁去锁 B 资源
创建保护资源R的锁:LR
加锁操作:lock()
临界区:一段代码
变量资源:R
临界区:一段代码
解锁操作:unlock()
-
三、JAVA提供的锁技术
-
synchronized
-
synchronized关键字可以用来修饰方法,也可以用来修饰代码块,它的使用示例基本上都是下面这个样子:
class X { // 修饰非静态方法 synchronized void foo() { // 临界区 } // 修饰静态方法 synchronized static void bar() { // 临界区 } // 修饰代码块 Object obj = new Object(); void baz() { synchronized(obj) { // 临界区 } } }
-
参照锁模型,会有点奇怪,有几个疑问?
-
加锁lock()和解锁unlock()在哪里呢?
两个操作是被Java默默加上的,Java编译器会在synchronized修饰的方法或代码块前后自动加上加锁lock()和解锁unlock(),这样做的好处就是加锁lock()和解锁unlock()一定是成对出现的,毕竟忘记解锁unlock()可是个致命的Bug(意味着其他线程只能死等下去了)。
-
synchronized里的加锁,锁的对象是什么?
-
当修饰静态方法的时候,锁定的是当前类的Class对象,在上面的例子中就是Class X
-
当修饰非静态方法的时候,锁定的是当前实例对象this
对于上面的例子,synchronized修饰静态方法相当于: class X { // 修饰静态方法 synchronized(X.class) static void bar() { // 临界区 } } 修饰非静态方法,相当于: class X { // 修饰非静态方法 synchronized(this) void foo() { // 临界区 }
-
-
-
-
lock
-
lock是一个接口,一般使用 ReentrantLock 类作为锁。在加锁和解锁处需要通过 lock() 和 unlock() 显示指出。所以一般会在 finally 块中写 unlock() 以防死锁。
class X { //创建锁 private static Lock lock = new ReentrantLock(); public static void main(String[] args) { //加锁 numLock.lock(); try { //临界区代码 } finally { //释放锁 lock.unlock(); } } }
-
四、尝试解决count+=1问题
-
前面文章中提到过的count+=1存在的并发问题
public class DemoOne { private int x = 0; public static void main(String[] args) throws InterruptedException { DemoOne demoOne = new DemoOne(); Thread threadOne = new Thread(() -> { demoOne.addx(); }); Thread threadTwo = new Thread(() -> { demoOne.addx(); }); threadOne.start(); threadTwo.start(); threadOne.join(); threadTwo.join(); demoOne.soutX(); } private void addx(){ int idx = 0; while(idx++ < 10000) { x += 1; } } private void soutX(){ System.out.println(this.x); } } print:14973
-
使用synchronized修饰的addx()临界区
public class DemoOne { private volatile int x = 0; public static void main(String[] args) throws InterruptedException { DemoOne demoOne = new DemoOne(); Thread threadOne = new Thread(() -> { demoOne.addx(); }); Thread threadTwo = new Thread(() -> { demoOne.addx(); }); threadOne.start(); threadTwo.start(); threadOne.join(); threadTwo.join(); demoOne.soutX(); } private synchronized void addx(){ int idx = 0; while(idx++ < 10000) { x += 1; } } private void soutX(){ System.out.println(this.x); } } print:20000
-
查看执行结果,问题被解决了。但我们忽略了soutX()方法,x变量对soutX()方法是可见的吗?
public class DemoOne { private volatile int x = 0; public static void main(String[] args) throws InterruptedException { DemoOne demoOne = new DemoOne(); Thread threadOne = new Thread(() -> { demoOne.addx(); demoOne.soutX(); }); Thread threadTwo = new Thread(() -> { demoOne.addx(); demoOne.soutX(); }); threadOne.start(); threadTwo.start(); threadOne.join(); threadTwo.join(); demoOne.soutX(); } private synchronized void addx(){ int idx = 0; while(idx++ < 10000) { x += 1; } } private void soutX(){ System.out.println(this.x); } } print:1 print:1 print:3 print:5 print:5 ... print:20000
-
我们在在执行addx()方法后,执行soutX()方法,发现打印结果是错乱了,就是因为没有保证x变量对soutX()方法可见性,解决方法很简单对soutX()方法也添加synchronized声明
public class DemoOne { private volatile int x = 0; public static void main(String[] args) throws InterruptedException { DemoOne demoOne = new DemoOne(); Thread threadOne = new Thread(() -> { demoOne.addx(); demoOne.soutX(); }); Thread threadTwo = new Thread(() -> { demoOne.addx(); demoOne.soutX(); }); threadOne.start(); threadTwo.start(); threadOne.join(); threadTwo.join(); demoOne.soutX(); } private synchronized void addx(){ int idx = 0; while(idx++ < 10000) { x += 1; } } private synchronized void soutX(){ System.out.println(this.x); } } print:1 print:2 print:3 print:4 print:5 ... print:20000
-
上面的代码转换为我们提到的锁模型
创建保护资源R的锁:LR
加锁操作:lock()
-
临界区:addx()
变量资源:R
临界区:addx()
-
临界区:soutX()
变量资源:R
临界区:soutX()
解锁操作:unlock()
-
五、锁和受保护资源的关系
-
受保护资源和锁之间的关联关系非常重要,他们的关系是怎样的呢?一个合理的关系是:受保护资源和锁之间的关联关系是N:1的关系。
-
拿球赛门票的管理来类比,就是一个座位,我们只能用一张票来保护,如果多发了重复的票,那就要打架了。不过倒是可以用同一把锁来保护多个资源,这个对应到现实世界就是我们所谓的“包场”了。