在上一篇博客中,我“走马观花”般的介绍了下Java内存模型,在这一篇博客,我将带着你们看下Synchronized关键字的那些事,其实把Synchronized关键字放到上一篇博客中去介绍,也是符合 “Java内存模型”这个标题的,由于Synchronized关键字和Java内存模型有着密不可分的关系。可是这样,上一节的内容就太多了。一样的,这一节的内容也至关多。java
好了,废话很少说,让咱们开始吧,程序员
首先从一个最简单的例子开始看:面试
public class Main {
private int num = 0;
private void test() {
for (int i = 0; i < 50; i++) {
try {
TimeUnit.MILLISECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
num++;
}
}
public static void main(String[] args) {
Main main = new Main();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
main.test();
}).start();
}
try {
TimeUnit.SECONDS.sleep(5);
System.out.println(main.num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
复制代码
Main方法中开启了20个线程,每一个线程执行50次的累加操做,最后打印出来的应该是50*20,也就是1000,可是每次打印出来的都不是1000,而是比1000小的数字。相信这个例子,你们早就烂熟于心了,对解决方案也是手到擒来:编程
public class Main {
private int num = 0;
private synchronized void test() {
for (int i = 0; i < 50; i++) {
try {
TimeUnit.MILLISECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
num++;
}
}
public static void main(String[] args) {
Main main = new Main();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
main.test();
}).start();
}
try {
TimeUnit.SECONDS.sleep(5);
System.out.println(main.num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
复制代码
只要在test方法上加一个synchronized关键字,就OK了。安全
为何会出现这样的问题呢,可能就有一小部分人不知道其中的缘由了。bash
这和Java的内存模型有关系:在Java的内存模型中,保证并发安全的三大特性是 原子性,可见性,有序性。致使这问题出现的缘由 即是 num++ 不是原子性操做,它至少有三个操做:多线程
让咱们设想有这样的一个场景:并发
当num=5app
A线程执行到num++这一步,读到了num的值为5(由于还没进行自增操做)。工具
B线程也执行到了num++这一步,读到了num的值仍是为5(由于A线程中的num尚未来得及进行自增操做)。
A线程中的num终于进行了自增操做,num为6。
B线程的num也进行了自增操做,num也为6。
可能光用文字描述,仍是有点懵,因此我画了一张图来帮助你们理解:
结合文字和图片,应该就能够理解了。
能够看出来,虽然执行了两次自增操做,可是实际的效果只是自增了一次。
因此在第一段代码中,运行的结果并非1000,而是比1000小的数字。
对于在多线程环境中,出现奇怪的结果或者状况,咱们也称为“线程不安全”。
而第二段代码,就是经过Synchronized关键字,把test方法串行化执行了,也就是 A线程执行完test方法,B线程才能够执行test方法。两个线程是互斥的。这样就保证了线程的安全性,最后的结果就是1000。若是从Java内存模型的角度来讲,就是保证了操做的“原子性”。
上面的例子是Synchronized关键字的使用方式之一,此时,synchronized标记的是类的实例方法,锁对象是类的实例对象。固然还有其余使用方式:
private static synchronized void test() {
for (int i = 0; i < 10; i++) {
try {
TimeUnit.MILLISECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(num++);
}
}
复制代码
此时,synchronized标记的是类的静态方法,锁对象是类。
以上两种,是直接标记在方法上。
还能够包裹代码块:
private void test() {
synchronized (Main.class) {
for (int i = 0; i < 10; i++) {
try {
TimeUnit.MILLISECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(num++);
}
}
}
复制代码
此时锁的对象是 类。
private void test() {
synchronized (this) {
for (int i = 0; i < 10; i++) {
try {
TimeUnit.MILLISECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(num++);
}
}
}
复制代码
此时锁的对象是类的实例对象。
private Object object = new Object();
private void test() {
synchronized (object) {
for (int i = 0; i < 10; i++) {
try {
TimeUnit.MILLISECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(num++);
}
}
}
复制代码
此时,锁对象是Object的对象。
咱们须要用到JDK自带的一个工具:JConsole,它位于JDK的bin目录下。
为了让观察更加方便,咱们须要给线程起一个名字,每一个线程内sleep的时间稍微长一点:
public class Main {
private synchronized void test() {
try {
TimeUnit.SECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Main main = new Main();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
main.test();
}, "Hello,Thread " + i).start();
}
}
}
复制代码
咱们先启动项目,而后打开JConsole,找到你项目的进程,就能够链接上去了。
能够看到,5个线程已经显示在JConsole里面了:
点击某个线程,能够看到关于线程的一些信息:
其中四个线程都处于BLOCKED,只有一个处于TIME_WAITING,说明只有一个线程得到了锁,并在TIME_WAITING,其他的线程都没有得到锁,没有进入到方法,说明了Synchronized的互斥性。关于线程的状态,这篇不会深刻,之后可能会介绍这方面的知识。
由于我是一边写博客,一边执行各类操做的,因此速度上有些跟不上,致使截图和描述不一样,你们能够本身去试试。
为了把问题简单化,让你们看的清楚,我只保留synchronized相关的代码:
public class Main {
public static void main(String[] args) {
synchronized (Main.class) {
}
}
}
复制代码
编译后,用javap命令查看字节码文件:
javap -v Main.class
复制代码
用红圈圈出来的就是添加synchronized后带来的命令了。执行同步代码块,先是调用monitorenter命令,执行完毕后,再调用monitorexit命令,为何会有两个monitorexit呢,一个是正常执行办法后的monitorexit,一个是发生异常后的monitorexit。
synchronized标记方法会是什么状况呢?
public class Main {
public synchronized void Hello(){
System.out.println("Hellol");
}
public static void main(String[] args) {
}
}
复制代码
JVM为每一个对象都分配了一个monitor,syncrhoized就是利用monitor来实现加锁,解锁。同一时刻,只有一个线程能够得到monitor,而且执行被包裹的代码块或者方法,其余线程只能等待monitor释放,整个过程是互斥的。monitor拥有一个计数器,当线程获取monitor后,计数器便会+1,释放monitor后,计数器便会-1。那么为何会是+1,-1 的操做,而不是“得到monitor,计数器=1,释放monitor后,计数器=0”呢?这就涉及到 锁的重入性了。咱们仍是经过一段简单的代码来看:
public static void main(String[] args) {
synchronized (Main.class){
System.out.println("第一个synchronized");
synchronized (Main.class){
System.out.println("第二个synchronized");
}
}
}
复制代码
结果:
主线程获取了类锁,打印出 “第一个synchronized”,紧接着主线程又获取了类锁,打印出“第二个synchronized”。
问题来了,第一个类锁明明尚未释放,下面又获取了这个类锁。若是没有“锁的重入性”,这里应该只会打印出 “第一个synchronized”,而后程序就死锁了,由于它会一直等待释放第一个类锁,可是却永远等不到那一刻。
这也就是解释了为何会是“当线程获取monitor后,计数器便会+1,释放monitor后,计数器便会-1“这样的设计。只有当计数器=0,才表明monitor已经被释放。第二个线程才能再次获取monitor。
固然,锁的重入性是针对于同一个线程来讲。
在上一篇中,咱们简单的介绍了指令重排,知道了三大特性之一的有序性,可是介绍的太简单。这一次,咱们把上一次的内容补充下。
其实,指令重排分为两种:
为何编译器和CPU会作“指令重排”这个“吃力不讨好”的事情呢?固然是为了效率。
指令重排会遵照两个规则:即 self-if-serial 和 happens-before。
咱们来举一个例子:
int a=1;//1
int b=5;//2
int c=a+b;//3
复制代码
这结果显而易见:c=6。
可是这段代码真正交给CPU去执行是按照什么顺序呢,大部分人会认为 ”从上到下"。是的,从你们开始学编程第一天就被灌输了这个思想,可是这仅仅是一个幻觉,真正交给CPU执行,多是 先执行第二行,而后再执行第一行,最后是第三行。由于第一行和第二行,哪一行先运行,并不影响最终的结果,可是第三行的执行顺序就不能改变了,由于数据存在依懒性。若是改变了第三行的执行顺序,那不乱套了。
编译器,CPU会在不影响单线程程序最终执行的结果的状况下进行“指令重排”。
这就是“ self-if-serial”规则。
这个规则就给程序员造给一种假象,在单线程中,代码都是从上到下执行的,却不知,编译器和CPU其实在背后偷偷的作了不少事情,而作这些事情的目的只有一个“提升执行的速度”。
在单线程中,咱们可能并不须要关心指令重排,由于不管背后进行了多么翻天覆地的“指令重排”都不会影响到最终的执行结果,可是self-if-serial是针对于单线程的,对于多线程,会有第二个规则:happens-before。
happens-before用来表述两个操做之间的关系。若是A happens-before B,也就表明A发生在B以前。
因为两个操做可能处于不一样的线程,happens-before规定,若是一个线程A happens-before另一个线程B,那么A对B可见,正是因为这个规定,咱们说Synchronized保证了线程的“可见性”。Synchronized具体是怎么作的呢?当咱们得到锁的时候,执行同步代码,线程会被强制从主内存中读取数据,先把主内存的数据复制到本地内存,而后在本地内存进行修改,在释放锁的时候,会把数据写回主内存。
而Synchronized的同步特性,显而易见的保证了“有序性”。
总结一下,Synchronized既能够保证“原子性”,又能够保证“可见性”,还能够保证“有序性”。
Synchronized最经典的应用之一就是 懒汉式单例模式 了,以下:
public class Main {
private static Main main;
private Main() {
}
public static Main getInstance() {
if (main == null) {
synchronized (Main.class) {
if (main == null) {
main = new Main();
}
}
}
return main;
}
}
复制代码
相信这代码,你们已经熟悉的不能再熟悉了,可是在极端状况下,可能会产生意想不到的状况,这个时候,Synchronized的好基友Volatile就出现了,这是咱们下一节中要讲的内容。
Synchronized能够说是每次面试一定会出现的问题,平时在多线程开发的时候也会用到,可是真正要理解透彻,仍是有不小难度。虽然说Synchronized的互斥性,很影响性能,Java也提供了很多更好用的的并发工具,可是Synchronized是并发开发的基础,因此值得花点时间去好好研究。
好了,本节的内容到这里结束了,文章已经至关长了,可是还有一大块东西没有讲:JDK1.6对Synchronized进行的优化,有机会,会再抽出一节的内容来说讲这个。