一、简介

JMM(Java Memory Model,Java内存模型)规范定义Java的共享内存模型,但共享内存模型随之带来的就是共享变量的线程安全问题,对JMM的理解可以参看《Java 内存模型》这篇文章。

为了保证多线程对共享变量的互斥操作,Java提供了两大类型的方案,即阻塞式和非阻塞式的解决方案。阻塞的方式有Synchronized关键字和Lock锁,非阻塞的方式使用原子变量(CAS+自旋)。

而在Java中,互斥和同步都可以通过Synchronized来实现,但它们还是有区别的:

互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码

同步是由于线程执行的先后顺序不同,需要一个线程等待其他线程运行到某个点然后再唤起线程

临界区

一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区,其共享资源为临界资源

竞态条件

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

Synchronized是Java提供的一种原子性的内置锁,Java中的每个对象都可以把它当作一个同步锁来使用,所以Synchronized也称为对象锁,它的实现依赖于Java对象,这些 Java 内置的使用者看不到的锁被称为内置锁,也叫作监视器锁。

Synchronized基于Monitor机制实现,依赖底层操作系统的互斥原语Mutex(互斥量),它是一个重量级锁,性能较低。为此,JVM内置锁在JDK1.5之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、偏向锁(Biased Lock)、轻量级锁(Ligthweight Lock)、自适应性自旋(Adaptive Spining)等技术来减少锁操作的开销,内置锁的并发性能已经基本与Lock持平。

下面是Open JDK官方对Synchronized的描述:

The Java® Language Specification
Each object is associated with a monitor (§17.1), which is used by synchronized methods (§8.4.3) and the synchronized statement (§14.19) to provide control over concurrent access to state by multiple threads (§17 (Threads and Locks)).
The Java® Virtual Machine Specification
The Java Virtual Machine supports synchronization of both methods and sequences of instructions within a method by a single synchronization construct: the monitor.

Java虚拟机通过一个同步结构支持方法和方法中的指令序列的同步:monitor

二、基本使用

Synchronized可以用来修饰方法,也可以用来修饰代码块

代码实例:

public synchronized  void increment(){}synchronized (this){//TODO
}

修饰方法时,既可以修饰实例方法,也可以修饰静态方法,但方法的访问限制符只能为public|protected|private

[public|protected|private] [static] synchronized void increment(){}

修饰代码块时,可以是类实例对象,也可以是Class对象,也可以是任意实例对象Object

synchronized ([InstanceObject|Class|AnyObject]){}

synchronized加锁方式与锁对象的关系如下:

上面我们说了synchronized是对象锁,那么它在锁实例对象和Class对象有什么区别呢?结合下面的代码来理解

class User{private String userName;private int age;private String address;public synchronized void setUserName(String userName){this.userName = userName;}public synchronized void setAddress(String address){this.address = address;}public void setAge(int age){synchronized (User.class){this.age = age;}}
}

上面的User类中,提供了三个同步方法,setUserName()setAddress()是两个实例方法,所以锁的是实例对象,而setAge()方法中的同步代码块锁的是User类对象。

假设现在有User1和User2两个对象,User1在调用setUserName()方法的时候,User2也可以调用setUserName()方法,因为它们是不同的实例对象,它们之间并不存在同步关系。但如果User1在调用setUserName()方法时,它没法同时去调用setAddress()方法,会被阻塞,直到setUserName()方法执行完成后才会再去执行setAddress()方法

而对于setAge()方法,因为它锁是User类对象,每个类只有一个类对象,所以,当User1调用setAge()方法时,User2再去调用setAge()会被阻塞,直到User1完成调用,User2才被唤醒去执行setAge()方法

三、底层原理

3.1 字节码指令

synchronized可以用于方法和代码块,我们可以从字节码指令序列来看分别对应什么指令(idea可以安装jclasslib Bytecode Viewer插件来查看字节码指令)

下面是部分Java方法的访问标志:

当我们在方法上使用synchronized关键字时,对应的字节码如下:

上图中可以看到,increment()方法对应的访问标志为0x0021,而publicsynchronized对应的访问标志分别为0x00010x0020,正好就是这两个访问标志的和。

由此可见,同步方法是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现

当我们在代码块使用synchronized时,对应的字节码如下:

同步代码块是通过moniterentermoniterexit来实现的,这两个指令的执行是JVM通过调用操作系统的互斥原语Mutex来实现的,被阻塞的线程被挂起、等待重新调度,会导致“用户态”和“内核态”两个态之间的来回切换,对性能有较大的影响。

moniterentermoniterexit可能并不是成对出现的,但moniterexit一定要多过moniterenter,因为要考虑执行异常的情况,也要通过moniterenter来释放锁。上面代码块的执行完第5行没有问题就跳转到14行,直接return了,出现异常的时候,才会去执行11行的moniterexit

3.2 Monitor

Monitor,直译为“监视器”,而操作系统领域一般翻译为“管程”。管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发。在Java 1.5之前,Java语言提供的唯一并发语言就是管程,Java 1.5之后提供的SDK并发包也是以管程为基础的。除了Java之外,C/C++、C#等高级语言也都是支持管程的。synchronized关键字和wait()、notify()、notifyAll()这三个方法是Java中实现管程技术的组成部分。

MESA

在管程的发展史上,先后出现过三种不同的管程模型,分别是Hasen模型、Hoare模型和MESA模型。

现在广泛使用的是MESA模型,它的基本结构如下图所示:

在MESA模型中,最主要的就是入口等待队列和条件变量等待队列,而每个条件变量都对应一个条件队列。

当多个线程访问共享变量时,只允许一个线程进入,其他线程在入口等待队列进行排队等待,保证了线程间的互斥。

在线程执行的过程中,当前线程可能需要等待其他线程的计算结果(条件),这个时候,当前线程就会进入到条件队列进行等待,当其他线程运行完得到计算结果之后,会唤醒条件队列的线程,这时,条件等待队列的线程会再次进入到入口等待队列中。条件变量和条件等待队列的作用是解决线程之间的同步问题。

wait()方法的正确使用姿势

对于MESA管程来说,有一个编程范式:

while(条件不满足){wait();
}

由于线程被唤醒的时候和获取到锁执行的时间是不一致的,被唤醒的线程在入口队列中经过排队等候重新获取到锁时,可能条件又不满足了,所以需要循环检验条件。MESA模型的wait()方法还有一个超时参数,为了避免线程进入等待队列永久阻塞。

notify()和notifyAll()的使用

满足以下条件时,可以使用notify(),其余情况尽量使用notifyAll()

  • 所有等待线程拥有相同的等待条件
  • 所有等待线程被唤醒后,执行相同的操作
  • 只需要唤醒一个线程

Java内置管程Synchronized

Java参考了MESA模型,语言内置的管程(synchronized)对MESA模型进行了精简,在MESA模型中,条件变量可以有多个,而Java语言内置的管程中只有一个条件变量。模型如下图所示:

Monitor机制在Java中实现

java.lang.Object类定义了wait()notify()notifyAll()方法,所以在Java中,所有对象都可以作为锁对象,这些方法的具体实现,依赖于ObjectMonitor,这是JVM内部基于C++实现的一套机制。

ObjectMonitor其主要数据结构如下(hosspot源码ObjectMonitor.hpp)

ObjectMonitor() {_header       = NULL; //对象头  markOop_count        = 0;  _waiters      = 0,   _recursions   = 0;   // 锁的重入次数 _object       = NULL;  //存储锁对象_owner        = NULL;  // 标识拥有该monitor的线程(当前获取锁的线程) _WaitSet      = NULL;  // 等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点_WaitSetLock  = 0 ;    _Responsible  = NULL ;_succ         = NULL ;_cxq          = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构)FreeNext      = NULL ;_EntryList    = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失败的线程)_SpinFreq     = 0 ;_SpinClock    = 0 ;OwnerIsThread = 0 ;_previous_owner_tid = 0;
}

_WaitSet是一个条件等待队列,所有调用wait()方法的线程都会放入到该队列中,_csq_EntryList是两个入口等待队列,不同的是,_cxq是一个栈结构,而_EntryList是一个链表结构,ObjectMonitor中线程锁流转图如下:

当外部线程竞争时,会把竞争的线程插入到_cxq的头部,而释放锁时,根据策略会有所不同,默认策略(QMode=0)是:如果_EntryList为空,则把_cxq中的元素按照原有顺序插入到_EntryList中,并唤醒第一个线程,也就是当_EntryList为空时,是后来的线程先获取锁(非公平锁);如果_EntryList不为空,直接从_EntryList中唤醒锁

注:Synchronized只有处于重量级锁状态时,才会有ObjectMonitor对象,但Synchronized还有偏向锁、轻量级锁以及无锁这三种锁状态,这些锁以及锁膨胀过程会在后面的文章介绍。

思考:Synchronized加锁是加在对象上,那么锁对象是如何记录锁状态的?

深入理解Synchronized(一)相关推荐

  1. synchronized()_这篇文章带你彻底理解synchronized关键字

    Synchronized关键字一直是工作和面试中的重点.这篇文章准备彻彻底底的从基础使用到原理缺陷等各个方面来一个分析,这篇文章由于篇幅比较长,但是如果你有时间和耐心,相信会有一个比较大的收获,所以, ...

  2. 深入理解synchronized

    深入理解synchronized 线程安全问题 临界区( Critical Section) 竞态条件( Race Condition ) synchronized 的使用 加锁方式 synchron ...

  3. 理解synchronized的含义

    理解synchronized的含义 1.synchronized就是给当前的NumOps类型的对象添加了一个互斥锁机制:锁只能有一个 2.当有一个线程正在某个同步方法中执行,则其它线程不能进入该对象的 ...

  4. 并发编程之深入理解synchronized

    并发编程之深入理解synchronized 一.java共享内存带来的线程安全问题 1.1 问题分析 1.2 临界区 1.3 竞态条件 二.synchronized使用 2.1 解决之前的共享问题 三 ...

  5. synchronized不能锁静态变量_多线程编程不可错过——彻底理解synchronized

    持续分享互联网研发技术,欢迎关注我.本人是一线架构师,有问题可以沟通. 1. synchronized简介 在学习知识前,我们先来看一个现象: public class SynchronizedDem ...

  6. 让你彻底理解Synchronized

    synchronized简介 在学习知识前,我们先来看一个现象: public class SynchronizedDemo implements Runnable {private static i ...

  7. 深入理解 Synchronized

    同步 synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性 Java中每一个对象都可以作为锁,这是synchronized ...

  8. 深入理解synchronized关键字

    2019独角兽企业重金招聘Python工程师标准>>> synchronized是并发编程中重要的使用工具之一,我们必须学会使用并且掌握它的原理. 概念及作用 JVM自带的关键字,可 ...

  9. synchronized()_深入理解synchronized

    并发编程中用到最多的关键字就是synchronized.下面来探究一下synchronized: synchronized如何使用? synchronized是如何实现同步加锁的,原理是什么? syn ...

  10. 理解Synchronized

    https://www.jianshu.com/p/d53bf830fa09 1. synchronized简介 在学习知识前,我们先来看一个现象: public class Synchronized ...

最新文章

  1. Mybatis-plus常用API全套教程,看完没有不懂的
  2. memcache函数整理
  3. Nature:大脑空间导航研究五十年
  4. java bigdecimal乘法_Java BigDecimal类型的 加减乘除运算
  5. vue el-checkbox循环多个如何选中当前的_一次关于Vue的自我模拟面试
  6. Ajax.NET 作者发布支持Visual Web Developer 2005的模板安装
  7. request的简介和运行环境
  8. leetcode1249. 移除无效的括号(栈)
  9. mongoose 更新元素 DeprecationWarning: collection.update is deprecated. Use updateOne, updateMany
  10. (计算机组成原理)第一章计算机系统概述-第三节:计算机层次结构
  11. 下划线_Python中下划线的5种含义
  12. iPhone 13与12对比图曝光:更厚、更大了
  13. 转化率高的爆款文案都是如何写出来的?
  14. pdf英文转换成html网页,PDF文件转换成html网页文件小方法
  15. 90 后程序员开发“AI 吵架神器”,专治女朋友各种不服!
  16. android 多行排列,安卓简单布局样例_采用LinearLayout实现多列多行展示
  17. table 转义字符 html,HTML转义字符表
  18. html5的geolocation 定位误差大的解决办法
  19. 智能车学习日记【四】————环岛
  20. 绝地求生 Win10 崩溃解决办法 (提示cmd.exe应用程序错误0xc000124)

热门文章

  1. 超声波传感器(CH101ch201) - Ⅰ
  2. 高数 | 【无穷级数】基础阶段总结 注意点、例题
  3. Message的几种简单用法
  4. 请输入课程编号C语言,为什么呢?为什么呢?
  5. ABAQUS导入Mechanical APDL(ANSYS)
  6. Postman测试实践笔记
  7. C++之sprintf函数(itoa函数)
  8. 232/485电平,OC门,OD门,TTL电平,CMOS电平
  9. 音视频框架-webrtc中的网络反馈与控制
  10. java 字符串 压缩_用JAVA实现字符串压缩算法