在前面章节,全面概括了并发三大特性,其中可见、有序性还是较为容易理解,并在前面章节都有对其做过场景理解说明,此篇单独对原子性做场景理解;
把一个或者多个操作在 CPU 执行的过程中不被中断的特性;Java内存模型中,直接保证了原子性变量操作【read,load,use,assign,store,wirte】,在应用中,可以大致认定基本类型操作读写具备原子性的,除了【long,double】,如果应用场景需要一个更大范围的原子操作,那么就有lock,unlock来保证。 但这并不是唯一的,比如还提供了,隐式的字节符码指令monitorenter和monitorexit,对应关键字就是synchronized。
从特性中明显分析的出来,保证原子性的根本:就是禁止CPU的中断,换句话来讲就是线程切换;
以long基本类型操作读写为例说明: 总所周知,pc机器分32位和64位,long的字符节是64位的,如果在32位机器上执行写操作,那么得分俩次写操作执行,第一段32位,第二段32位。 那么就有可能出现 明明已经进行的写入操作,但读出来并不是预期值; 为什么会产生此bug呢? 在多核情况下,如有线程A在cpu1,B在cpu2上同时在运行,此时cpu中断了,只能保证cpu上的线程连续执行,不能让保证同一时刻,只有一个线程执行。如果线程a和b同时写入第一段32位字符,就可能产生问题了。
同一时刻下保证只有一个线程执行 就可以保证原子性,称为互斥;
在上述的问题中,已经把问题和答案公布,那么解决的思路就是如何来建立互斥,实现上述的论点;
对,你猜到了,就是用Lock【锁】 :
对于一段有风险的代码块,可在 代码块进行 lock()---->代码块执行[互斥区]----->unlock()
不论任意线程需要执行时,先尝试进行加锁,获取到锁后,方可进行互斥区的操作,最后解锁。 于此同时,其他线程在在还未解锁时。都需要在互斥区钱等着获取锁,否者一直等着;
用个生活中的例子理解下: 每天早上上班后,在上厕所高峰期时,坑位 就是咱这互斥区,进坑为为加锁,出坑开门为解锁。 咋一看,没毛病就是这样的,但细细一想不对,咱们锁的到底是啥? 所以得在进一步细化这个互斥的步骤;
接着上面的问题细化,lock到底是锁什么东西呢? 总得有个事物,比如我家里的门锁,锁的自己的家,总不能去锁其他人家的吧。 那么锁和资源就有一种关系存在了,改进锁的方式:
create资源保护锁LR---->lock()---->代码块执行[互斥区]------>互斥区中圈出受保护资源----->unlock()
那么这个流程中 LR锁 对应资源关系 就是 互斥区中的受保护资源,不然又得出现自家门锁,锁其他人家了。
在Java中提供了synchronized 关键字 和 Lock锁的实现,俩种方式各有特点。后面例子先用synchronized讲述,可以用来修饰方法,代码块,对象;例如:
class LockTest {
Object obj = new Object();
//锁非静态方法------当修饰非静态方法的时候,锁定的是当前实例对象 this。
synchronized void test1() {}
//锁静态方法------当修饰静态方法的时候,锁定的是当前类的 Class 对象,即LockTest
synchronized static void test2() {}
//锁对象,锁代码块
void test3() {
synchronized(obj) {}
}
}
不用奇怪,没找到lock和unlock是因为在编译时,隐时帮助自动加上,切记lock和unlock必定是成对出现,不然肯定会造成很大问题。
class Calc{
long a = 0L;
long read(){
return a;
}
synchronized void calc(){
a += 1;
}
}
calc()是符合 Happens-Before规则中的 “管程锁定规则”,因此是具备可见性,在则被synchronized修饰后,能保证在同一时刻下只有一个线程执行,也可以保证原子操作。如果在多线程的情况下循环执行calc()100次,不出意外 a已经等于100;
在看read方法,经过calc方法后,其a变量对read是可见的么? 答案是否定,套用下BF规则,是否没有一个满足的。 那么解决的办法也有挺多的, 最简单的对read方法进行加锁,用synchronize修饰即满足管程锁定规则:对一个锁解锁后,后续对这个锁加锁的是可见的。
class Calc{
long a = 0L;
synchronized long read(){
return a;
}
synchronized void calc(){
a += 1;
}
}
这就回到上述说的互斥锁的模型上了, 这里锁的资源都是this,不论哪个线程执行read和calc方法时,都是用this锁进行保护的,那么这样一来 a 也对read具备了可见。
这个单独拿出来说,是因为这个误区大多从这不经意间开始,否者锁是有了,但我家锁,锁到别人家,这不茬批了。 对于这个关系正解应该是:受保护资源和锁之间的关联关系是 N:1 的关系 ,也就是说,家里的门锁,可以保护电视,冰箱,洗衣机等等,对于家里面的物件不是单独上锁了。
基于上述例子做下小改动,将calc编程静态方法,那此时并发还会安全么? 各自锁的资源又是啥?
class Calc{
long a = 0L;
synchronized long read(){
return a;
}
synchronized static void calc(){
a += 1;
}
}
首先并发肯定不是安全的,在“解决原子的技术”篇幅,有说明,对于静态和非静态方法加锁后的所资源实惠变化的, read锁住的是this,calc锁的是Calc.class这个对象,那么此时俩个方法并有互斥了,肯定是存在并发安全问题。由此可见虽然表面上都是加了锁,但是使用方式不对,是很可能在此造成安全问题。
通过原子特性,引出来互斥锁的特性。 但同时也告诉我们,盲目的加锁有可能还是会造成并发不安全,如果想要真正正确使用锁,那么就得有这种锁与资源的风险意识。在平时开发过程中,这个点往往是最容易搞混淆的点。深入分析锁 定的对象和受保护资源的关系,综合考虑受保护资源的访问路径,多方面考量才能用好互 斥锁。
Java中还有其他的锁实现,此篇是用synchronize为例,但是使用的原理本质都是一致的。只是锁的实现方式不同,功能不同罢了。