Java并发机制的底层实现原理
1、volatile的应用
volatile要保证共享变量的可见性。java规范中对volatile的定义如下:Java编程语言允许线程访问共享变量,为了确保共享变量能够被准确和一致地更新,线程应该用排他锁单独获得这个变量。计算机系统可以使用缓存一致性模型来保证volatile的可见性(例如MESI协议)。
值得注意的是volatile只保证可见性,而不保证原子性,例如i++操作是一个复合读写操作,其读是原子的,写是原子的,但复合读写就不一定是原子的。假定一个volatile变量i的初始值是0,线程一和和线程二都执行一个i++操作,执行结果可能为1。
2、synchronized的实现原理和应用
synchronized又称重量级锁。
利用synchronized实现同步的基础:每个对象都可以当作一个锁,具体表现为以下三种形式:
1、对于普通同步方法,锁是当前实例对象;
2、对于静态同步方法,锁是当前类的Class对象;
3、对于同步方法块,是synchronized后面配置的那个对象。
JVM基于进入和退出Monitor对象(monitorenter和monitorexit)的方法来实现方法同步和代码块同步。
synchronized用的锁是存储在对象头里的(Mark word中存储对象的hashcode和锁信息)。
Java SE 1.6为了减少锁的代价,引入了偏向锁和轻量级锁。所以锁一共有四种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。
由于获取锁的往往是同一个进程,所以引入偏向锁。线程第一次访问同步块时,会在对象头和栈帧的锁记录里存储偏向锁的线程ID,以后该线程使用该锁时不需要进行CAS操作来加锁和解锁,只需检测对象头中存储的偏向锁线程ID与当前线程ID是否一致即可。偏向锁使用了一种只有竞争发生时才释放锁的机制,当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会交出偏向锁。
轻量级锁机制下,在进入同步区之前,先把对象头的Mark word字段复制到锁记录当中,然后线程会试图使用CAS将对象头的mark word修改为换为指向锁记录的指针,操作成功获得锁,失败则表示有其他线程竞争锁,当前线程将使用自旋的方式竞争锁。如果轻量级锁长时间得不到响应,将会膨胀为重量级锁并阻塞,迫使持有锁的线程交出锁并唤醒阻塞线程。
下面是三种锁的比较:
锁 | 优点 | 缺点 | 适用场景 |
偏向锁 | 加锁和解锁不需要额外的开销,和执行非同步方法仅有纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的撤销开销 | 仅有一个线程访问同步块 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,自旋会消耗CPU | 追求响应速率 |
重量级锁 | 线程竞争不自旋,不损耗cpu | 线程阻塞,响应时间慢 | 追求吞吐率 |
3、原子操作的实现原理
对于处理机而言,主要借助于总线锁和缓存锁两个机制来实现原子性。
在java中主要使用CAS和锁来实现原子性。事实上锁的底层使用了CAS操作,当一个线程想进入同步块时使用循环CAS的方式获取锁,退出同步区时用CAS操作释放锁。