本篇博文将介绍java并发底层的实现原理,我们知道java实现的并发操作最后肯定是由我们的CPU完成的,中间经历了将java源码编译成.class文件,然后进行加载,然后虚拟机执行引擎进行执行,解释为汇编语言,然后转为操作系统指令,然后转为1,0,最后CPU进行识别执行。
提到java的并发,我们不由的就会想到java中常见的键字:volatile和synchronized,我们接下来就会从这两个关机字展开分析:
- volatile的底层实现原理
- synchronized的实现原理和应用
volatile
说到volatile,在java的面试中面试官可是最喜欢问的问题了。看到它我们首先想到的便是保持线程间的可见性,是一个轻量级的synchronized,在一些情况下它可以代替synchronized。
volatile的作用:
一个被volatie修饰的变量,java内存模型会保证所有的线程看见的变量值是一致的。
volatile的工作原理:
我们可以定义一个volatile变量,并对他进行赋值,并通过工具来获取jit编译器生成的汇编指令,我们会发现在对volatile变量进行写操作时,会多出一条指令:以lock为前缀的指令:
lock为前缀的指令在多核处理器下回引发两件事情:
①将当前处理器缓存行的数据回写到内存中。
②这个回写内存的操作会使得在其他cpu里缓存了改内存地址的数据无效。
当我们知道了以上两点,我们就不难理解volatie变量的机制了。
在多处理器下,为了保证各个处理器的缓存是一致的,会实现缓存一致性协议,每个处理器通过嗅探在总线上的传播的数据来检查自己缓存的值是不是过期了。
synchronized
想到多线程的并发,其实我第一个想到的便是这个synchronized,翻译过来为同步,我们都知道它是一个重量级锁,当对一个方法或者代码块使用它时,当一个线程获得了这个锁,那么其它的线程就会陷入挂起状态,在java中也就表现为sleep状态,我们都知道线程的挂起和运行时要转入操作系统的内核态的(与内核态对应的便是用户态),这样特别浪费cpu资源,所以这个重量级锁是名副其实的!
但是,java SE 1.6过后java的维护团队对它进行了一系列的优化(这些优化后面一一讲述),他也就没那么“重”了,以前还有优势的可重入锁也变得没那么有优势了(ReentrantLock)。
一下我们就下列几个方面讲述synchronized:
- 利用synchronized实现同步的基础
- synchronized是如何实现锁的
- 偏向锁,轻量级锁(自旋锁),重量级锁
- 锁的升级
- java如何实现原子操作
①利用synchronized实现同步的基础:
我们在开发中或者java的源码中都能看见synchronized的身影,例如HashTable,StringBuilder等地方,常见有两种方式:
Ⅰ丶同步方法
同步方法只需要在方法前加上synchronized便可,当一个线程执行它的时候其他线程便会陷入等待,直到它释放锁。对方法使用又可以分为两种:对普通同步方法和对静态方法,它们之间的差别是加锁的对象不同,普通方法加锁的位置是当前的对象,而静态方法加锁的位置是当前类的Class对象。
Ⅱ丶同步方法块
同步方法块加锁的是Synchronized后括号里配置的对象,这个对象可以是一个值以及任何一个变量或者对象。
②synchronized是如何实现锁的:
在jvm的规范中可以看到synchronized在jvm中的实现原理,jvm基于进入和退出Monitor对象来实现同步方法和代码块的同步,代码块是使用monitorenter和monitorexit指令来实现的,而同步方法jvm规范里没有具体给出,但是我相信具体的原理应该相差不大,无非是将java源码编译为class文件,在class字节码文件中对使用synchronized的方法进行一个标记,在字节码引擎执行这个方法的时候会对这个方法进行同步处理。
③偏向锁,轻量级锁(自旋锁),重量级锁:
在讲锁之前我们需要知道java对象头,java的对象头:
synchronized使用的锁是存储在java对象头里的,java对象头里面有32bit/64bit(视操作系统的位数而定)长度的MarkWord 里面存储了对象的hashCode和锁的信息等,在MarkWord中有2bit的空间来表示锁的状态00,01,10,11,分别表示轻量级锁,偏向锁,重量级锁,GC标记。
偏向锁:偏向锁也就人称它为偏心锁,从名字我们就可以看出来,它是一个偏向某一个线程的锁。
在实际的开发中,我们发现多线程并发,大多数执行同步方法的都是同一个线程,出现多个线程争抢一个方法的概率比较低,所以重复的获取锁和释放锁就会产生大量的资源浪费,所以为了让线程获得锁的代价更低引入了偏向锁,当一个线程访问一个同步块并获得锁时,会在对象头和线程的栈帧中的锁记录中存储偏向锁的线程ID,以后该线程进入和退出同步块时不需要进行CAS操作来进行加锁和解锁,只需要简单的查看对象头的MarkWord里是否还存有指向当前的偏向锁(在MarkWord中每个对象还有一个偏向锁标志位用来表示当前对象是否支持偏向锁,我们可以使用jvm参数来设定偏向锁)。
关于偏向锁的释放,偏向锁使用了等到存在竞争时才释放锁的机制,所以当有其他线程尝试竞争偏向锁的时候持有偏向锁的线程才会释放锁。
注意:在java6,7中偏向锁是默认启动的
轻量级锁:
轻量级锁就是在执行同步块之前,jvm会在当前线程的栈帧中创建用于存储锁记录的的空间,并将对象头中的MarkWord复制到里面,然后线程将尝试将对象头内的MarkWord替换为指向锁记录的指针,如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便自旋来获得锁。
④锁的升级:
当前线程如果无法试用上面的方法获得锁,那么表示当前的锁存在竞争,锁就会升级为重量级锁。
轻量级锁和偏向锁的区别:
轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,而偏向锁就是在无竞争的情况下把整个同步都去除,连CAS操作都不做!
⑤ java如何实现原子操作:
在了解java是如何实现原子操作之前,我们要知道处理器是如何实现原子操作的:
处理器一般分为两种方法执行原子操作:缓存加锁和总线加锁,其中缓存加锁比较优秀而总线加锁则比较消耗资源。(关于两种加锁的方式我们这里不做过多解释,具体在操作系统中有详细的讲解)
java使用(大多数情况下)循环CAS实现原子操作,但是使用CAS实现原子操作也会出现下面的一些经典的问题:
一)ABA问题
jdk中提供AtomicStampedReference类来解决(提供检查预期引用和预期标志)
二)循环时间长开销大
无法解决,这个是循环的通病
三)只能保证一个共享变量的原子操作
jdk中提供一个AtomicReference来解决,将多个共享变量放置在一个类中进行CAS操作。
以上为自己学习的笔记和一些自己的感想,以便博主以后查阅,也供大家参考如果错误和侵权还请联系我,改正,侵删!
参考书籍:《并发编程的艺术》