在上一篇博客中,我“蜻蜓点水”般的介绍了下Java内存模型,在这一篇博客,我将带着大家看下Synchronized关键字的那些事,其实把Synchronized关键字放到上一篇博客中去介绍,也是符合 “Java内存模型”这个标题的,因为Synchronized关键字和Java内存模型有着密不可分的关系。但是这样,上一节的内容就太多了。同样的,这一节的内容也相当多。

好了,废话不多说,让我们开始吧,

Synchronized基本使用

首先从一个最简单的例子开始看:

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了。

Synchronized与原子性

为什么会出现这样的问题呢,可能就有一小部分人不知道其中的原因了。

这和Java的内存模型有关系:在Java的内存模型中,保证并发安全的三大特性是 原子性,可见性,有序性。导致这问题出现的原因 便是 num++ 不是原子性操作,它至少有三个操作:

1.把i读取出来

2.做自增计算

3.把值写回i

让我们设想有这样的一个场景:

当num=5

  1. A线程执行到num++这一步,读到了num的值为5(因为还没进行自增操作)。

  2. B线程也执行到了num++这一步,读到了num的值还是为5(因为A线程中的num还没有来得及进行自增操作)。

  3. A线程中的num终于进行了自增操作,num为6。

  4. B线程的num也进行了自增操作,num也为6。

可能光用文字描述,还是有点懵,所以我画了一张图来帮助大家理解:

结合文字和图片,应该就可以理解了。

可以看出来,虽然执行了两次自增操作,但是实际的效果只是自增了一次。

所以在第一段代码中,运行的结果并不是1000,而是比1000小的数字。

对于在多线程环境中,出现奇怪的结果或者情况,我们也称为“线程不安全”。

而第二段代码,就是通过Synchronized关键字,把test方法串行化执行了,也就是 A线程执行完test方法,B线程才可以执行test方法。两个线程是互斥的。这样就保证了线程的安全性,最后的结果就是1000。如果从Java内存模型的角度来说,就是保证了操作的“原子性”。

Synchronized几种使用方法

上面的例子是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的对象。

JConsole探究Synchronized关键字

我们需要用到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的互斥性。关于线程的状态,这篇不会深入,以后可能会介绍这方面的知识。

因为我是一边写博客,一边执行各种操作的,所以速度上有些跟不上,导致截图和描述不同,大家可以自己去试试。

javap探究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) {
}
}

锁与Monitor

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。

当然,锁的重入性是针对于同一个线程来说。

Synchronized与有序性,可见性

在上一篇中,我们简单的介绍了指令重排,知道了三大特性之一的有序性,但是介绍的太简单。这一次,我们把上一次的内容补充下。

其实,指令重排分为两种:

  1. 编译器重排
  2. 运行时CPU指令排序

为什么编译器和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与单例模式

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进行的优化,有机会,会再抽出一节的内容来讲讲这个。

Synchronized的那些事的更多相关文章

  1. Java synchronized那点事

    前言 请看上篇:Java 对象头那点事 文章中的源码都有不同程度缩减,来源于openjdk8的开源代码(tag:jdk8-b120). 锁粗化过程 偏向锁 ①:markword中保存的线程ID是自己且 ...

  2. 关于java的Synchronized,你可能需要知道这些(上)

    对于使用java同学,synchronized是再熟悉不过了.synchronized是实现线程同步的基本手段,然而底层实现还是通过锁机制来保证,对于被synchronized修饰的区域每次只有一个线 ...

  3. Java同步工具类总结

    先谈谈闭锁和栅栏的区别: 1.关键区别在于,所有线程必须同时到达栅栏位置,才能继续执行. 2.闭锁用于等待某一个事件的发生,举例:CountDownLatch中await方法等待计数器为零时,所有事件 ...

  4. 【转载】synchronized 与 Lock 的那点事

    最近在做一个监控系统,该系统主要包括对数据实时分析和存储两个部分,由于并发量比较高,所以不可避免的使用到了一些并发的知识.为了实现这些要求,后台使用一个队列作为缓存,对于请求只管往缓存里写数据.同时启 ...

  5. synchronized 与 Lock 的那点事

    最近在做一个监控系统,该系统主要包括对数据实时分析和存储两个部分,由于并发量比较高,所以不可避免的使用到了一些并发的知识.为了实现这些要求,后台使用一个队列作为缓存,对于请求只管往缓存里写数据.同时启 ...

  6. Android 谈谈封装那些事 --BaseActivity 和 BaseFragment(二)

      1.前言 昨天谈了BaseActivity的封装,Android谈谈封装那些事--BaseActivity和BaseFragment(一)有很多小伙伴提了很多建议,比如: 通用标题栏可以自定义Vi ...

  7. 【Java并发编程实战】-----synchronized

    在我们的实际应用当中可能经常会遇到这样一个场景:多个线程读或者.写相同的数据,访问相同的文件等等.对于这种情况如果我们不加以控制,是非常容易导致错误的.在java中,为了解决这个问题,引入临界区概念. ...

  8. Synchronized

    1. 在编写一个类时,如果该类中的代码可能运行与多线程环境下,就要考虑同步问题了. 会同时被多个线程访问的资源,就是竞争资源,也称为竞争条件.对于多线程共享的资源我们必须进行同步,以避免一个线程的改动 ...

  9. Java编程中“为了性能”需做的26件事

    1.尽量在合适的场合使用单例 使用单例可以减轻加载的负担,缩短加载的时间,提高加载的效率,但并不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面: (1)控制资源的使用,通过线程同步来控制 ...

随机推荐

  1. 「JOISC 2017 Day 3」幽深府邸

    题解: 和hnoi2018day2t1基本一样 我想了半小时想出了一个很麻烦的做法 写了之后发现假掉了 刚开始想的是 先预处理出每个门要打开至少要在左边的哪个点$L[]$,右边的哪个点$R[]$ 对每 ...

  2. 运行报错:java.io.IOException: invalid constant type: 15

    jdk,tomcat更新到jdk1.8与 tomcat8 运行报错:java.io.IOException: invalid constant type: 15 pom.xml文件中更新javassi ...

  3. 提高在word编辑公式的效率,及快捷键、对齐、编号问题

    1.     Word中编辑公式简介(重点看) https://jacobz.top/2017-08/WordMath/ 2.     快捷键 https://wenku.baidu.com/view ...

  4. 利用kibana插件对Elasticsearch进行映射

    映射(mapping) 映射是创建索引的时候,可以预先定义字段的类型以及相关属性 Elasticsearch会根据JSON源数据的基础类型去猜测你想要的字段映射.将输入的数据变成可搜索的索引项.Map ...

  5. tomcat部署项目启动采坑之UnknownHostException

    在一台新服务器上,把war包部署在tomcat上,很普通的很简单的一个活,但我踩到一个大坑. 需要组件tomcat8,mysql5.7,mosqquito1.5,centos7,war包,把组件都装好 ...

  6. BZOJ.4151.[AMPPZ2014]The Cave(结论)

    BZOJ 不是很懂他们为什么都要DFS三次.于是稳拿Rank1 qwq. (三道题两个Rank1一个Rank3效率是不是有点高qwq?) 记以\(1\)为根DFS时每个点的深度是\(dep_i\).对 ...

  7. 转发 Learning Go — from zero to hero

    原文:https://medium.freecodecamp.org/learning-go-from-zero-to-hero-d2a3223b3d86 Learning Go — from zer ...

  8. SpringMVC+Mybatis+MySQL8遇到的问题

    搭建SpringMVC+Mybatis+MySQL8过程中遇到的坑. 1.数据库驱动要使用新版本,我的和mysql保持一致. 查看mysql版本:MySQL\bin>mysql -V 配置对应版 ...

  9. oracle中文乱码问题解决

    中文乱码问题解决:1.查看服务器端编码select userenv('language') from dual;我实际查到的结果为:AMERICAN_AMERICA.ZHS16GBK2.执行语句 se ...

  10. python | Elasticsearch-dsl常用方法总结(join为案例)

    Elasticsearch DSL是一个高级库,其目的是帮助编写和运行针对Elasticsearch的查询.它建立在官方低级客户端(elasticsearch-py)之上. 它提供了一种更方便和习惯的 ...