Synchronized是Java中很是重要的一个关键字。java
1. 起源数据库
事务的产生老是会有特定的缘由,下面这段代码就做为引出Synchronized的引子编程
public class SynchronizedDemo implements Runnable { private static int count = 0; public static void main(String[] args) { for (int i = 0; i < 10; i++) { Thread thread = new Thread(new SynchronizedDemo()); thread.start(); } try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("result: " + count); } @Override public void run() { for (int i = 0; i < 1000000; i++) count++; } }
10个线程同时操做count,每一个线程作一百万次count自增操做,最后出来的结果不必定是一千万,并且每次运行的结果还不同。安全
2. Synchronized实现原理多线程
实现原理:JVM经过进入、退出对象监视器(Monitor)来实现对方法、同步块的同步。并发
看下面一段代码ide
public class SynchronizedDemo { public static void main(String[] args) { synchronized (SynchronizedDemo.class) { } method(); } private static void method() { } }
编译以后,用javap -v SynchronizedDemo.class 查看字节码文件:post
该图能够看出,任意线程对Object的访问,首先要得到Object的监视器,若是获取失败,该线程就进入同步状态,线程状态变为BLOCKED,当Object的监视器占有者释放后,在同步队列中得线程就会有机会从新获取该监视器。性能
3. 锁获取和锁释放的内存语义public class MonitorDemo { private int a = 0; public synchronized void writer() { // 1 a++; // 2 } // 3 public synchronized void reader() { // 4 int i = a; // 5 } // 6 }
从上图能够看出,线程A会首先先从主内存中读取共享变量a=0的值而后将该变量拷贝到本身的本地内存,进行加一操做后,再将该值刷新到主内存,整个过程即为线程A 加锁-->执行临界区代码-->释放锁相对应的内存语义。测试
线程B获取锁的时候一样会从主内存中共享变量a的值,这个时候就是最新的值1,而后将该值拷贝到线程B的工做内存中去,释放锁的时候一样会重写到主内存中。
从总体上来看,线程A的执行结果(a=1)对线程B是可见的,实现原理为:释放锁的时候会将值刷新到主内存中,其余线程获取锁时会强制从主内存中获取最新的值。
从横向来看,这就像线程A经过主内存中的共享变量和线程B进行通讯,A 告诉 B 咱们俩的共享数据如今为1啦,这种线程间的通讯机制正好吻合java的内存模型正好是共享内存的并发模型结构。
使用锁时,线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,因此当前线程获取到锁的时候同时也会阻塞其余线程获取该锁。而CAS操做(又称为无锁操做)是一种乐观锁策略,它假设全部线程访问共享资源的时候不会出现冲突,既然不会出现冲突天然而然就不会阻塞其余线程的操做。所以,线程就不会出现阻塞停顿的状态。那么,若是出现冲突了怎么办?无锁操做是使用**CAS(compare and swap)**又叫作比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操做直到没有冲突为止。
CAS比较交换的过程能够通俗的理解为CAS(V,O,N),包含三个值分别为:V 内存地址存放的实际值;O 预期的值(旧值);N 更新的新值。当V和O相同时,也就是说旧值和内存中实际的值相同代表该值没有被其余线程更改过,即该旧值O就是目前来讲最新的值了,天然而然能够将新值N赋值给V。反之,V和O不相同,代表该值已经被其余线程改过了则该旧值O不是最新版本的值了,因此不能将新值N赋给V,返回V便可。当多个线程使用CAS操做一个变量是,只有一个线程会成功,并成功更新,其他会失败。失败的线程会从新尝试,固然也能够选择挂起线程
CAS的实现须要硬件指令集的支撑,在JDK1.5后虚拟机才可使用处理器提供的CMPXCHG指令实现。
1. ABA问题 由于CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。好比一个旧值A变为了成B,而后再变成A,恰好在作CAS时检查发现旧值并无变化依然为A,可是实际上的确发生了变化。解决方案能够沿袭数据库中经常使用的乐观锁方式,添加一个版本号能够解决。原来的变化路径A->B->A就变成了1A->2B->3C。java这么优秀的语言,固然在java 1.5后的atomic包中提供了AtomicStampedReference来解决ABA问题,解决思路就是这样的。
2. 自旋时间过长
使用CAS时非阻塞同步,也就是说不会将线程挂起,会自旋(无非就是一个死循环)进行下一次尝试,若是这里自旋时间过长对性能是很大的消耗。若是JVM能支持处理器提供的pause指令,那么在效率上会有必定的提高。
3. 只能保证一个共享变量的原子操做
当对一个共享变量执行操做时CAS能保证其原子性,若是对多个共享变量进行操做,CAS就不能保证其原子性。有一个解决方案是利用对象整合多个共享变量,即一个类中的成员变量就是这几个共享变量。而后将这个对象作CAS操做就能够保证其原子性。atomic中提供了AtomicReference来保证引用对象之间的原子性。
如图在Mark Word会默认存放hasdcode,年龄值以及锁标志位等信息。
Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争状况逐渐升级。锁能够升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提升得到锁和释放锁的效率。对象的MarkWord变化为下图:
3.2 偏向锁
HotSpot的做者通过研究发现,大多数状况下,锁不只不存在多线程竞争,并且老是由同一线程屡次得到,为了让线程得到锁的代价更低而引入了偏向锁。
下图线程1展现了偏向锁获取的过程,线程2展现了偏向锁撤销的过程。
如何关闭偏向锁