并发机制的底层实现
JAVA并发机制的底层实现
volatile
在并发编程中最容易出现的是”数据竞争“,多个线程对共享变量进行操作时,一个线程对数据进行了修改,而其它进程却不知道。这时,我们可以用volatitle来解决这个问题。从下面几个方面来理解volatile的原理。
Cache
Cache被翻译为高速缓存,它位于CPU内部,是CPU和内存的缓冲带。因为CPU的工作频率比内存要高的多,所以CPU的运行速度往往受制于内存。Cache是一个高速设备,我们可以先把数据从内存中装入到Cache中,CPU直接从Cache中取数据,这样可以提高CPU的运行速率。
问题
Cache引入虽然提高了执行速率,但是会引起Cache的不一致性问题。比如双核CPU,A核和B核都有自己的Cache,在这两个核上各跑一个线程,这两个线程会对同一个变量进行操作,则它们会将这个变量加入各自的Cache中。假如A线程在某一时刻修改了变量的值,它也仅仅只是在自己的Cache中修改,并不会将该值写回内存中,更不会通知另一个线程了。
解决方案
在共享变量前加volatile关键字可以解决Cache一致性问题。java代码最后要翻译成机器指令,当对一个被volatile修饰的变量做写操作时(写操作机器指令),会在该操作指令后面加一条额外的指令---Lock指令。
该指令在多核处理器下会引发两件事情
- 当前处理器修改的数据写会到系统中内存中
- 这个写回操作会使其它CPU里面缓存的对应数据无效
思考
volatie是否能实现锁的功能呢?当然不是。它只是提供了内存的可见性而已。假如两个线程都同时对同一个变量修改,那么最后的变量仍然存在一致性问题
下面的程序会退出吗?
import java.util.concurrent.TimeUnit;
public class Test {
private static boolean bChanged = true;
public static void main(String[] args) throws InterruptedException {
new Thread() {
@Override
public void run() {
while (bChanged);
System.exit(0);
}
}.start();
TimeUnit.SECONDS.sleep(1);
while (true) {
bChanged = false;
}
}
}
synchronized
Java中的每个对象都可以作为锁,具体为一些3种形式:
- 对于普通的同步,锁是当前实例对象
- 对于静态方法的同步,锁是当前类的Class对象
- 对于同步块,锁是synchronized括号里配置的对象
实现方式
synchroized是在JVM里面实现的,JVM基于获取和退出Monitor来实现方法同步和代码块同步的,但是两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步使用的是另外的一种方式实现的。任何一个对象都有一个monitor与之关联,当一个monitor被劫持后,它将处于锁定状态。线程执行到monitor指令时,将会尝试获取对象对应的monitor所有权,即获取对象的锁。每个对象都有个对象头,在对象头中有个锁标志位来标志锁的状态。
锁的升级和对比
Java中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在java中锁总共有四种状态:无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态。这几个状态会随着竞争的情况逐渐升级。
偏向锁
偏向锁是为了让线程获取锁的代价更低才引入的。假如对象O处于无锁状态,A线程要执行同步代码块,它先检查对象头中是否存储其ID,如果没有(这时一定是没有的),则使用CAS技术将填充自己的ID并将该对象设为偏置状态。然后在执行同步代码块(退出同步代码块后并不会将对象复原到无锁状态)。如果线程A再次进入同步代码块,则查看对象头中是否有自己的ID(这时一定有的),则直接执行同步代码块的内容。假如在某个时刻,线程2也要访问同步代码块,这时会出现锁竞争,如果竞争成功(A进程已死),则将线程ID设置为当前线程ID。如果竞争失败,则获得偏向锁的线程将被挂起,偏向锁将升级为轻量级锁,然后挂起的线程继续执行。
特点:适用于只有一个线程访问同步块的场景。如果一旦出现竞争,则会上升为轻量级锁。
轻量级锁
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,则自旋获取锁,当自旋获取锁仍然失败时,表示存在其他线程竞争锁(两条或两条以上的线程竞争同一个锁),则轻量级锁会膨胀成重量级锁。
特点:线程不会阻塞,减少了上下文切换,但是有可能会空耗CPU。
重量级锁
重量级锁就是synchronize,它不会引起线程自旋,不消耗CPU。
下面的线程执行完毕有先后顺吗
public class Test {
synchronized static void test1() {
// TODO sleep 3s
}
synchronized void test2() {
// TODO sleep 3s
}
public static void main(String[] args) {
final Test t = new Test();
new Thread(new Runnable() {
@Override
public void run() {
t.test1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
t.test2();
}
}).start();
}
}
Java中的原子操作
CAS:比较和交换,在进行CAS操作时需要输入两个数值,一个旧值(期望操作前的值),一个新值。在操作期间先比较旧值有没有发生变化,如果没有变化,才交换新值,发生了变化则不交换。
使用CAS实现原子操作
AtomicInteger atomicI = new AtomicInteger(0)
private void safeCount() {
for (;;) {
int i = atomicI.get();
boolean suc = atomicI.compareAndSet(i, i+1);
if (suc) {
break;
}
}
}
上面的代码我们要对Integer进行加1操作。先用get()方法获取初值,然后通过compareAndSet(CAS)来修改(i表示原始值,i+1是修改后的值),如果返回值为true表示修改成功,否则不断循环修改。