使用 Synchronized 关键字来解决并发问题是最简单的一种方式,我们只需要使用它修饰需要被并发处理的代码块、方法或字段属性,虚拟机自动为它加锁和释放锁,并将不能获得锁的线程阻塞在相应的阻塞队列上。

基本使用

我们在上篇文章介绍线程的基本概念时,提到了多线程的好处,能够最大化 CPU 使用效率、更友好交互等等,但是也提出了它带来的问题,比如竞态条件、内存可见性问题。

我们引用上篇文章中的一个案例:

一百个线程随机地为 count 加一,由于自增操作非原子性,多线程之间不正常的访问导致 count 最终的值不确定,始终得不到预期的结果。

使用 synchronized 即刻就能解决,看代码:

代码稍作修改,现在的程序无论你运行多少次,或者你增大并发量,最后 count 的值总是正确的 100 。

大概什么意思呢?

我们的 JAVA 中,对于每个对象都有一把『内置锁』,而 synchronized 中的代码在被线程执行之前,会去尝试获取一个对象的锁,如果成功,就进入并顺利执行代码,否则将会被阻塞在该对象上。

除此之外,synchronized 除了可以修饰代码块,还可以直接修饰在方法上,例如:

public synchronized void addCount(){......}
public static synchronized void addCount(){......}

这是两种不同的使用方式,前一种是使用 synchronized 修饰的实例方法,那么 synchronized 使用的就是当前方法调用时所属的那个实例的『内置锁』。也就是说,addCount 方法调用前会去尝试获取调用实例对象的锁。

而后一种 addCount 方法是一个静态方法,所以 synchronized 使用的就是 addCount 所属的类对象的锁。

synchronized 的使用方式还是很简单的,什么时候加锁,什么时候释放锁都不需要我们操心,被 JVM 封装好了,下面我们就来简单看看 JVM 是如何实现这种间接锁机制的。

基本实现原理

我们先看一段简单的代码:

public class TestAxiom {
private int count; @Test
public void test() throws InterruptedException {
synchronized (this){
count++;
}
}
}

这是一段非常简单的代码,使用 synchronized 修饰代码块,保护 count++ 操作。现在我们反编译一下:

可以看到,在执行 count++ 指令之前,编译器加了一条 monitorenter 指令,count++ 指令执行结束时又加了一条 monitorexit 指令。准确意义上来说,这就是两条加锁的释放锁的指令,具体细节我们稍后再看。

除此之外,我们的 synchronized 方法在反编译后并没有这两条指令,但是编译器却在方法表的 flags 属性中设置了一个标志位 ACC_SYNCHRONIZED。

这样,每个线程在调用该方法之前都会检查这个状态位是否为 1,如果状态为 1 说明这是一个同步方法,需要首先执行 monitorenter 指令去尝试获取当前实例对象的内置锁,并在方法执行结束执行 monitorexit 指令去释放锁。

其实本质上是一样的,只是 synchronized 方法是一种隐式的实现。下面我们来看一看这个内置锁的具体细节。

Java 中一个对象主要由以下三种类型数据组成:

  • 对象头:也称 Mark Word,主要存储的对象的 hash 值以及相关锁信息。
  • 实例数据:保存的当前对象的数据,包括父类属性信息等。
  • 填充数据:这部分是应 JVM 要求,每个对象的起始地址必须是 8 的倍数,所以如果当前对象不足 8 的倍数字节时用于字节填充。

我们的『内置锁』在对象头里面,而 Mark Word 的一个基本结构是这样的:

先不去管什么是,轻量锁,重量锁,偏向锁,自旋锁,这是虚拟机一种锁优化机制,通过锁膨胀来优化性能,这一点的细节我们以后再介绍,你先把它们统一理解为一把锁。

其中,每把锁会有一个标志位用于区分锁类型,和一个指向锁记录的指针,也就是说锁指针会关联另一种结构,Monitor Record。

Owner 字段存储的是拥有当前锁的线程唯一标识号,当某个线程拥有了该锁之后就会把自己的线程号写入这个字段中。如果某个线程发现这里的 Owner 字段不是 null 也不是自己的线程号,那么它将会被阻塞在 Monitor 的阻塞队列上直至某个线程走出同步代码块并发起唤醒操作。

总结一下,被 synchronized 修饰的代码块或者方法在编译器会被额外插入两条指令,monitorenter 会去检查对象头锁信息,对应到一个 Monitor 结构,如果该结构的 Owner 字段已经被占用了,那么当前线程将会被阻塞在 Monitor 的一个阻塞队列上,直到占有锁的线程释放了锁并唤起一波新的锁竞争。

synchronized 的几个特性

1、可重入性

一个对象往往有多个方法,这些方法有的是同步的,有的是非同步的,那么如果一个线程已经获得了某个对象的锁并进入了其某个同步方法,而这个同步方法中还需要调用同一实例的另一个同步方法,是否需要重新竞争锁?

这对于某些锁来说,是需要重新竞争锁的,但是我们的 synchronized 是「可重入的」,也就是说,如果当前线程获得了某个对象的锁,那么该对象的所有方法都是可以无需竞争锁式调用的。

原因也很简单,monitorenter 指令找到 Monitor,查看了 Owner 字段的值等于当前线程的线程号,于是将 Nest 字段增加一,表示当前线程多次持有该对象的锁,每调用一次 monitorexit 都会减一 Nest 的值。

2、内存可见性

引用上篇文章的一个例子:

线程 ThreadTwo 不停的监听 flag 的值,而我们主线程对 flag 进行了修改,由于内存可见性,ThreadTwo 看不见,于是程序一直死循环。

某种意义上,synchronized 是可以解决这类内存可见性问题的,修改代码如下:

主线程先获得 obj 的内置锁,然后启动 ThreadTwo 线程,该线程由于获取不到 obj 的锁而被阻塞,也就是它知道已经有其他线程在操作共享变量,所以等到自己获得锁的时候一定要从内存重新读一下共享变量。

而我们的主线程会在释放锁的时候将私有工作内存中所有的全局变量的值刷新到内存空间,这样其实就实现了多线程之间的内存可见性。

当然有一点大家要注意,synchronized 修饰的代码块会在释放锁的时候刷新自己更改过的全局变量,但是另一个线程要想看见,必须也从内存中重新读才行。而一般情况下,不是你加了 synchronized 线程就会从内存中读数据的,而只有它在竞争某把锁失败后,得知有其他线程正在修改共享变量,这样的前提下等到自己拥有锁之后才会重新去刷内存数据。

你也可以试试,让 ThreadTwo 线程不去竞争 obj 这把锁,而随便给它一个对象,结果依然会是死循环,flag 的值只会是 ThreadTwo 刚启动时从内存读入的初始数据的缓存版。

但是说实话,解决内存可见性而使用 synchronized 代价太高,需要加锁和释放锁,甚至还需要阻塞和唤醒线程,我们一般使用关键字 volatile 直接修饰在变量上就可以了,这样对于该变量的读取和修改都是直接映射内存的,不经过线程本地私有工作内存的。

关于 synchronized 关键字我们暂时先介绍到这,后续还会涉及到它的,我们还要介绍近几个 JDK 版本对于 synchronized 的优化细节,包括自旋锁,偏向锁,重量级锁之间的锁膨胀机制,也是这种优化使得现在的 synchronized 性能不输于 Lock。


文章中的所有代码、图片、文件都云存储在我的 GitHub 上:

(https://github.com/SingleYam/overview_java)

欢迎关注微信公众号:OneJavaCoder,所有文章都将同步在公众号上。

使用 Synchronized 关键字的更多相关文章

  1. 从JAVA看C#中volatile和synchronized关键字的作用

    最近一直在想C#中 volatile关键字到底是用来干什么的?查了很多.NET的文章都是说用volatile修饰的变量可以让多线程同时修改,这是什么鬼... 然后查到了下面这篇JAVA中关于volat ...

  2. Java多线程系列--“基础篇”04之 synchronized关键字

    概要 本章,会对synchronized关键字进行介绍.涉及到的内容包括:1. synchronized原理2. synchronized基本规则3. synchronized方法 和 synchro ...

  3. java中synchronized关键字的用法

    在java编程中,经常需要用到同步,而用得最多的也许是synchronized关键字了,下面看看这个关键字的用法. 因为synchronized关键字涉及到锁的概念,所以先来了解一些相关的锁知识. j ...

  4. 深入理解java中的synchronized关键字

    synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程A每次运行到这个方法时,都要检查有没有其它正在用这个方法的线程B(或者C D等),有的话要等正在使用这个方法的线程B(或者C D ...

  5. synchronized关键字,Lock接口以及可重入锁ReentrantLock

    多线程环境下,必须考虑线程同步的问题,这是因为多个线程同时访问变量或者资源时会有线程争用,比如A线程读取了一个变量,B线程也读取了这个变量,然后他们同时对这个变量做了修改,写回到内存中,由于是同时做修 ...

  6. Java synchronized关键字用法(清晰易懂)

    本篇随笔主要介绍 java 中 synchronized 关键字常用法,主要有以下四个方面: 1.实例方法同步 2.静态方法同步 3.实例方法中同步块 4.静态方法中同步块 我觉得在学习synchro ...

  7. volatile与synchronized关键字

    volatile关键字相信了解Java多线程的读者都很清楚它的作用.volatile关键字用于声明简单类型变量,如int.float.boolean等数据类型.如果这些简单数据类型声明为volatil ...

  8. Java 多线程 —— synchronized关键字

    java 多线程 目录: Java 多线程——基础知识 Java 多线程 —— synchronized关键字 java 多线程——一个定时调度的例子 java 多线程——quartz 定时调度的例子 ...

  9. 【深入比较ThreadLocal模式与synchronized关键字】

    [深入比较ThreadLocal模式与synchronized关键字]ThreadLocal模式与synchronized关键字都是用于处理多线程并发访问变量的问题.只是两者处理问题的角度和思路不同. ...

  10. java synchronized关键字浅探

    synchronized 是 java 多线程编程中用于使线程之间的操作串行化的关键字.这种措施类似于数据库中使用排他锁实现并发控制,但是有所不同的是,数据库中是对数据对象加锁,而 java 则是对将 ...

随机推荐

  1. 洛谷[LnOI2019]长脖子鹿省选模拟赛 简要题解

    传送门 听说比赛的时候T4T4T4标程锅了??? WTF换我时间我要写T3啊 于是在T4T4T4调半天无果的情况下260pts260pts260pts收场真的是tcltcltcl. T1 快速多项式变 ...

  2. 作业 -- 几道简单的Python题

    1.编写程序,要求生成10240个随机[0,512)之间的整数,并统计每个元素出现的次数. 2.编写程序,要求当用户输入一个列表和两个整数作为下标时,程序可以使用切片获取并输出列表中截取两个下标之间的 ...

  3. MAC帧和PPP帧区别

  4. C++ Error C2664:无法将参数 1 从“const char [9]”转换为“LPCWSTR”解决方案

    问题出现 编译平台:VS2013     Windows 出现地方:在使用LoadLibrary( )函数动态链接DLL文件时出现的一个问题 Eg.   在使用 UNICODE字符的工程中,  HIN ...

  5. 支持Linux,嗅探和注入功能的网卡

    支持的WiFi USB 以下是已知可以很好地支持Linux,嗅探和注入功能,外部天线(可以替换)和强大的TX功率以及良好的RX灵敏度的Wifi卡的列表 TP-LINK TL-WN722N(仅限卷1) ...

  6. 使用 shell 在多服务器上批量操作

    需求 日常工作中,我们常需要同时在多台服务器上执行同样的命令,如对比日志.检查服务等.这就需要我们有服务器批量操作的能力. 两年前写过一篇文章,shell实现SSH自动登陆 使用 shell 的 ex ...

  7. Nerd的畅销产品

    感谢陈大先生花费时间一起细致地讨论这个课题,促成此文. <Nerd的套现ATM机>中已经谈到,对于nerd来讲最畅销的产品是“考试通过”.本文将继续这个话题的展开讨论,更深入细致地分析这个 ...

  8. 吴恩达机器学习笔记50-主成分分析算法(PCA Algorithm)

    PCA 减少

  9. hbase概念解析

    hbase是一种nosql数据库.是一个高可靠,高性能,面向列,可伸缩,实时读取的分布式数据库. hbase一般由行键,时间戳,列族,列,表格单元,行组成. 行一般由一个行键和一个或多个具有关联关系值 ...

  10. 《Java 多线程编程核心技术》- 笔记

    作为业务开发人员,能够在工作中用到的技术其实不多.虽然平时老是说什么,多线程,并发,注入,攻击!但是在实际工作中,这些东西不见得用得上.因为,我们用的框架已经把这些事做掉了. 比如web开发,外面有大 ...