在再有人问你Java内存模型是什么,就把这篇文章发给他。中咱们曾经介绍过,Java语言为了解决并发编程中存在的原子性、可见性和有序性问题,提供了一系列和并发处理相关的关键字,好比synchronized
、volatile
、final
、concurren包
等。html
在《深刻理解Java虚拟机》中,有这样一段话:java
synchronized
关键字在须要原子性、可见性和有序性这三种特性的时候均可以做为其中一种解决方案,看起来是“万能”的。的确,大部分并发控制操做都能使用synchronized来完成。程序员
海明威在他的《午后之死》说过的:“冰山运动之雄伟壮观,是由于他只有八分之一在水面上。”对于程序员来讲,synchronized
只是个关键字而已,用起来很简单。之因此咱们能够在处理多线程问题时能够不用考虑太多,就是由于这个关键字帮咱们屏蔽了不少细节。算法
那么,本文就围绕synchronized
展开,主要介绍synchronized
的用法、synchronized
的原理,以及synchronized
是如何提供原子性、可见性和有序性保障的等。编程
synchronized
是Java提供的一个并发控制的关键字。主要有两种用法,分别是同步方法和同步代码块。也就是说,synchronized
既能够修饰方法也能够修饰代码块。多线程
/**
* @author Hollis 18/08/04.
*/
public class SynchronizedDemo {
//同步方法
public synchronized void doSth(){
System.out.println("Hello World");
}
//同步代码块
public void doSth1(){
synchronized (SynchronizedDemo.class){
System.out.println("Hello World");
}
}
}
复制代码
被synchronized
修饰的代码块及方法,在同一时间,只能被单个线程访问。并发
synchronized
,是Java中用于解决并发状况下数据同步访问的一个很重要的关键字。当咱们想要保证一个共享资源在同一时间只会被一个线程访问到时,咱们能够在代码中使用synchronized
关键字对类或者对象加锁。oracle
在深刻理解多线程(一)——Synchronized的实现原理中我曾经介绍过其实现原理,为了保证知识的完整性,这里再简单介绍一下,详细的内容请去原文阅读。jvm
咱们对上面的代码进行反编译,能够获得以下代码:优化
public synchronized void doSth();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
public void doSth1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: ldc #5 // class com/hollis/SynchronizedTest
2: dup
3: astore_1
4: monitorenter
5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #3 // String Hello World
10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: return
复制代码
经过反编译后代码能够看出:对于同步方法,JVM采用ACC_SYNCHRONIZED
标记符来实现同步。 对于同步代码块。JVM采用monitorenter
、monitorexit
两个指令来实现同步。
在The Java® Virtual Machine Specification中有关于同步方法和同步代码块的实现原理的介绍,我翻译成中文以下:
方法级的同步是隐式的。同步方法的常量池中会有一个
ACC_SYNCHRONIZED
标志。当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED
,若是有设置,则须要先得到监视器锁,而后开始执行方法,方法执行以后再释放监视器锁。这时若是其余线程来请求执行方法,会由于没法得到监视器锁而被阻断住。值得注意的是,若是在方法执行过程当中,发生了异常,而且方法内部并无处理该异常,那么在异常被抛到方法外面以前监视器锁会被自动释放。同步代码块使用
monitorenter
和monitorexit
两个指令实现。能够把执行monitorenter
指令理解为加锁,执行monitorexit
理解为释放锁。 每一个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程得到锁(执行monitorenter
)后,该计数器自增变为 1 ,当同一个线程再次得到该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行monitorexit
指令)的时候,计数器再自减。当计数器为0的时候。锁将被释放,其余线程即可以得到锁。
不管是ACC_SYNCHRONIZED
仍是monitorenter
、monitorexit
都是基于Monitor实现的,在Java虚拟机(HotSpot)中,Monitor是基于C++实现的,由ObjectMonitor实现。
ObjectMonitor类中提供了几个方法,如enter
、exit
、wait
、notify
、notifyAll
等。sychronized
加锁的时候,会调用objectMonitor的enter方法,解锁的时候会调用exit方法。(关于Monitor详见深刻理解多线程(四)—— Moniter的实现原理)
原子性是指一个操做是不可中断的,要所有执行完成,要不就都不执行。
咱们在Java的并发编程中的多线程问题究竟是怎么回事儿?中分析过:线程是CPU调度的基本单位。CPU有时间片的概念,会根据不一样的调度算法进行线程调度。当一个线程得到时间片以后开始执行,在时间片耗尽以后,就会失去CPU使用权。因此在多线程场景下,因为时间片在线程间轮换,就会发生原子性问题。
在Java中,为了保证原子性,提供了两个高级的字节码指令monitorenter
和monitorexit
。前面中,介绍过,这两个字节码指令,在Java中对应的关键字就是synchronized
。
经过monitorenter
和monitorexit
指令,能够保证被synchronized
修饰的代码在同一时间只能被一个线程访问,在锁未释放以前,没法被其余线程访问到。所以,在Java中可使用synchronized
来保证方法和代码块内的操做是原子性的。
线程1在执行
monitorenter
指令的时候,会对Monitor进行加锁,加锁后其余线程没法得到锁,除非线程1主动解锁。即便在执行过程当中,因为某种缘由,好比CPU时间片用完,线程1放弃了CPU,可是,他并无进行解锁。而因为synchronized
的锁是可重入的,下一个时间片仍是只能被他本身获取到,仍是会继续执行代码。直到全部代码执行完。这就保证了原子性。
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其余线程可以当即看获得修改的值。
咱们在再有人问你Java内存模型是什么,就把这篇文章发给他。中分析过:Java内存模型规定了全部的变量都存储在主内存中,每条线程还有本身的工做内存,线程的工做内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的全部操做都必须在工做内存中进行,而不能直接读写主内存。不一样的线程之间也没法直接访问对方工做内存中的变量,线程间变量的传递均须要本身的工做内存和主存之间进行数据同步进行。因此,就可能出现线程1改了某个变量的值,可是线程2不可见的状况。
前面咱们介绍过,被synchronized
修饰的代码,在开始执行时会加锁,执行完成后会进行解锁。而为了保证可见性,有一条规则是这样的:对一个变量解锁以前,必须先把此变量同步回主存中。这样解锁后,后续线程就能够访问到被修改后的值。
因此,synchronized关键字锁住的对象,其值是具备可见性的。
有序性即程序执行的顺序按照代码的前后顺序执行。
咱们在再有人问你Java内存模型是什么,就把这篇文章发给他。中分析过:除了引入了时间片之外,因为处理器优化和指令重排等,CPU还可能对输入代码进行乱序执行,好比load->add->save 有可能被优化成load->save->add 。这就是可能存在有序性问题。
这里须要注意的是,synchronized
是没法禁止指令重排和处理器优化的。也就是说,synchronized
没法避免上述提到的问题。
那么,为何还说synchronized
也提供了有序性保证呢?
这就要再把有序性的概念扩展一下了。Java程序中自然的有序性能够总结为一句话:若是在本线程内观察,全部操做都是自然有序的。若是在一个线程中观察另外一个线程,全部操做都是无序的。
以上这句话也是《深刻理解Java虚拟机》中的原句,可是怎么理解呢?周志明并无详细的解释。这里我简单扩展一下,这其实和as-if-serial语义
有关。
as-if-serial
语义的意思指:无论怎么重排序(编译器和处理器为了提升并行度),单线程程序的执行结果都不能被改变。编译器和处理器不管如何优化,都必须遵照as-if-serial
语义。
这里不对as-if-serial语义
详细展开了,简单说就是,as-if-serial语义
保证了单线程中,指令重排是有必定的限制的,而只要编译器和处理器都遵照了这个语义,那么就能够认为单线程程序是按照顺序执行的。固然,实际上仍是有重排的,只不过咱们无须关心这种重排的干扰。
因此呢,因为synchronized
修饰的代码,同一时间只能被同一线程访问。那么也就是单线程执行的。因此,能够保证其有序性。
前面介绍了synchronized
的用法、原理以及对并发编程的做用。是一个很好用的关键字。
synchronized
实际上是借助Monitor实现的,在加锁时会调用objectMonitor的enter
方法,解锁的时候会调用exit
方法。事实上,只有在JDK1.6以前,synchronized的实现才会直接调用ObjectMonitor的enter
和exit
,这种锁被称之为重量级锁。
因此,在JDK1.6中出现对锁进行了不少的优化,进而出现轻量级锁,偏向锁,锁消除,适应性自旋锁,锁粗化(自旋锁在1.4就有,只不过默认的是关闭的,jdk1.6是默认开启的),这些操做都是为了在线程之间更高效的共享数据 ,解决竞争问题。
关于自旋锁、锁粗化和锁消除能够参考深刻理解多线程(五)—— Java虚拟机的锁优化技术,关于轻量级锁和偏向锁,已经在排期规划中,我后面会有文章单独介绍,将独家发布在个人博客(http://www.hollischuang.com)和公众号(Hollis)中,敬请期待。
好啦,关于synchronized
关键字,咱们介绍了其用法、原理、以及如何保证的原子性、顺序性和可见性,同时也扩展的留下了锁优化相关的资料及思考。后面咱们会继续介绍volatile
关键字以及他和synchronized
的区别等。敬请期待。