上文提到了并发产生的原因,那么JAVA给出的解决方案是什么呢,我们来一起探讨一下。
一、解决缓存导致的并发问题
针对于这个问题,原因主要是各个缓存之间的数据可见性的问题。那么解决这个问题的最简单粗暴的思路就是禁用缓存。
试想一下,内存只有一个区域且对任何CPU都是可见的,如果给某个数据打上个标签,让这种类型的数据不进缓存里面,读写操作都直接在内存上操作。那么是不是这个可见性的问题就解决了?
Volatile的原理就是这个意思。用volatile修饰对象,就等于告诉编译器,对这个变量的读写,不能使用 CPU 缓存,必须从内存中读取或者写入。
二、解决原子性问题
原子性问题的产生原因在于线程切换,线程切换是操作系统上控制的,这个就不是一个JAVA或者说是JAVA虚拟机能控制得住的事情了。既然不能阻止他发送线程切换,那我们如果在某些会产生线程安全问题的地方加一个限制,这个限制就是当运行到这段代码的时候,最多有且只能有一个线程完全的执行完成后,其他线程才能排队执行,是不是就解决问题了呢?
举个例子现实中的例子,一座独木桥,桥身只能承受一个人的重量,且每次只能允许一个人通过。就算桥的主人赋予了某个人优先通过的权利(即发生了线程切换),但是由于前一个人还在桥上没走完。这个被予以优先通过权利的人也得等和正在桥上的人走完才能继续过桥。
同一时刻只有一个线程执行的特性,就是互斥性。只要保证对对象的修改是互斥的,我们就能保证他的原子性了。
JAVA在实现互斥的方式就是上锁。在JAVA里面,锁的类型可以分为两类,分别是显示锁和隐式锁。显示锁就是各种Lock对象,包括经常看到的可重入的ReentrantLock,读写锁ReentrantReadWriteLock等等。隐式锁指的就是synchronized关键字了。
针对显示锁跟隐式锁的优劣:
显示锁的优点在于锁的加锁和解锁都是认为控制,灵活度较好,劣势就在于便捷性较差了。
隐式锁的优点就在于用着方便,加锁解锁都是默认给定好了的,不存在加了锁后忘记解锁的情况。劣势就在于由于不能人为操控解锁,一些需要提前解锁的场景就没法用syn关键字实现了。
三、 解决编译优化带来的问题
编译优化这个就很底层了,如果完全禁用编译优化,那有点捡了芝麻丢了西瓜的感觉。那么JAVA是如何解决这个问题的呢,答案就是Happens-Before 规则。引用一下别人的话:
Happens-Before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 Happens-Before 规则。
Happens-Before 翻译过来也就是 “发生在什么之前”,在JAVA里面的含义也就是,如果 A Happens-Before B (即是A和B 满足Happens-Before 规则) ,也就是A 发生在B之前,那么A对B来说就是可见的。
下面简述并记录一下Happens-Before 规则:
1、程序顺序规则:一个线程中的每个操作,happens-before于随后该线程中的任意后续操作
2、监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的获取
3、volatile变量规则:对一个volatile域的写,happens-before于对这个变量的读 4、传递性:如果A
happens-before B,B happens-before C,那么A happens-before C
5、start规则:如果线程A执行线程B的start方法,那么线程A的ThreadB.start()happens-before于线程B的任意操作
6、join规则:如果线程A执行线程B的join方法,那么线程B的任意操作happens-before于线程A从TreadB.join()方法成功返回。