synchronized使用及原理解析
修饰静态方法、实例方法、代码块
Synchronized修饰静态方法,对类对象进行加锁,是类锁。
Synchronized修饰实例方法,对方法所属对象进行加锁,是对象锁。
Synchronized修饰代码块时,对一段代码块进行加锁,是对象锁。
/**
* synchronized示例
* 1、修饰静态方法
* 2、修饰实例方法
* 3、修饰代码块
*/
public class SyncDemo2 {
private static int num = 0; /**
* 修饰静态方法
*/
public static synchronized void count1() {
for (int i = 0; i < 100000000; i++) {
num++;
}
} /**
* 修饰实例方法
*/
public synchronized void count2() {
for (int i = 0; i < 100000000; i++) {
num++;
}
} /**
* 修饰代码块
* 效果与修饰静态方法相同
*/
public void count3() {
synchronized(SyncDemo2.class) {
for (int i = 0; i < 100000000; i++) {
num++;
}
}
} /**
* 修饰代码块
* 效果与修饰实例方法相同
*/
public void count4() {
synchronized(this) {
for (int i = 0; i < 100000000; i++) {
num++;
}
}
} public static void main(String[] args) {
//两个线程运行一个类的两个对象,运行类的静态方法count1,
//产生同步,num=200000000 //两个线程运行一个类的两个对象,运行类的实例方法count2
//因为调用的是不同的对象,并未产生同步,num<=200000000
SyncDemo2 syncDemo1 = new SyncDemo2();
SyncDemo2 syncDemo2 = new SyncDemo2(); //两个线程运行一个对象,运行类的实例方法count2
//因为调用的是同一个对象,产生同步,num=200000000
//SyncDemo2 syncDemo3 = new SyncDemo2();
//syncDemo1 = syncDemo3;
//syncDemo2 = syncDemo3; //启动两个线程进行运算
Thread thread1 = new Thread(new ThreadDemo(syncDemo1));
Thread thread2 = new Thread(new ThreadDemo(syncDemo2));
thread1.start();
thread2.start(); try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(SyncDemo2.num);
}
} class ThreadDemo implements Runnable {
SyncDemo2 syncDemo2;
public ThreadDemo(SyncDemo2 syncDemo2){
this.syncDemo2 = syncDemo2;
} @Override
public void run() {
//syncDemo2.count1();
//syncDemo2.count2();
syncDemo2.count3();
//syncDemo2.count4();
}
}
Synchronized底层实现原理
Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现,无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。在 Java 语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。同步方法并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的。
锁是加在对象上的,无论是类对象还是实例对象。每个对象主要由一个对象头、实例变量、填充数据三部分组成,结构如图:

synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字来存储对象头(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark Word 和 Class Metadata Address 组成,其结构说明如下:

其中Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等以下是32位JVM的Mark Word默认存储结构:

由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,如32位JVM下,除了上述列出的Mark Word默认存储结构外,还有如下可能变化的结构:

Synchronized属于结构中的重量级锁,锁标识位为10,其中指针指向的是monitor对象的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)。
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
结构中几个重要的字段要关注,_count、_owner、_EntryList、_WaitSet。
count用来记录线程进入加锁代码的次数。
owner记录当前持有锁的线程,即持有ObjectMonitor对象的线程。
EntryList是想要持有锁的线程的集合。
WaitSet 是加锁对象调用wait()方法后,等待被唤醒的线程的集合。
每个等待锁的线程都会被封装成ObjectWaiter对象,当多个线程同时访问一段同步代码(临界区)时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程,_owner指向持有ObjectMonitor对象的线程。同时monitor中的计数器count加1。
若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。
若当前线程执行完毕也将释放monitor并复位变量的值,以便其他线程进入获取monitor(锁)。

(图摘自:https://blog.csdn.net/javazejian/article/details/72828483)
Synchronized在jvm字节码上的体现
我们以之前的例子为例,使用javac编译代码,然后使用javap进行反编译。

反编译后部分片段如下图:

对于使用synchronized修饰的方法,反编译后字节码中会有ACC_SYNCHRONIZED关键字。

而synchronized修饰的代码块中,在代码块的前后会有monitorenter、monitorexit关键字,此处的字节码中有两个monitorexit是因为我们有try-catch语句块,有两个出口。
Synchronized与等待唤醒
等待唤醒是指调用对象的wait、notify、notifyAll方法。调用这三个方法时,对象必须被synchronized修饰,因为这三个方法在执行时,必须获得当前对象的监视器monitor对象。
另外,与sleep方法不同的是wait方法调用完成后,线程将被暂停,但wait方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行。而sleep方法只让线程休眠并不释放锁。notify/notifyAll方法调用后,并不会马上释放监视器锁,而是在相应的synchronized代码块或synchronized方法执行结束后才自动释放锁。
Synchronized的可重入与中断
重入
当多个线程请求同一个临界资源,执行到同一个临界区时会产生互斥,未获得资源的线程会阻塞。而当一个已获得临界资源的线程再次请求此资源时并不会发生阻塞,仍能获取到资源、进入临界区,这就是重入。Synchronized是可重入的。
中断
在Thread类中与线程中断相关的方法有三个:
/**
* Interrupt设置一个线程为中断状态
* Interrupt操作的线程处于sleep,wait,join 阻塞等状态的时候,清除“中断”状态,抛出一个InterruptedException
* Interrupt操作的线程在可中断通道上因调用某个阻塞的 I/O 操作(serverSocketChannel. accept()、socketChannel.connect、socketChannel.open、
* socketChannel.read、socketChannel.write、fileChannel.read、fileChannel.write),会抛出一个ClosedByInterruptException
**/
public void interrupt();
/**
* 判断线程是否处于“中断”状态,然后将“中断”状态清除
**/
public static boolean interrupted();
/**
* 判断线程是否处于“中断”状态
**/
public boolean isInterrupted();
在实际使用中,当线程正处于调用sleep、wait、join方法后,调用interrupt会清除线程中断状态,并抛出异常。而当线程已进入临界区、正在执行,则需要isInterrupted()或interrupted()与interrupt()配合使用中断执行中的线程。
Sychronized修饰的方法、代码块被多个线程请求时,调用中断。正在执行的线程响应中断。正在阻塞的线程、执行中的线程都会标记中断状态,但阻塞的线程不会立刻处理中断,而是在进入临界区后再响应。
示例:中断对执行synchronized方法线程的影响
import java.util.concurrent.TimeUnit; /**
* 示例:中断对执行synchronized方法线程的影响
* 正在执行的线程响应中断
* 正在阻塞的线程、执行中的线程都会标记中断状态,
* 但阻塞的线程不会立刻处理中断,而是在进入临界区后再响应。
*/
public class SyncDemo3 {
public static boolean flag = true; public static synchronized void m1() {
System.out.println(Thread.currentThread().getName() + " hold resource!");
while (flag) {
if (!Thread.currentThread().isInterrupted()) {
//不用sleep,因为sleep会对中断抛出异常
Thread.yield();
} else {
System.out.println(Thread.currentThread().getName() + " interrupted and release !");
return;
}
}
} public static void main(String[] args) {
SyncDemo3 syncDemo1 = new SyncDemo3();
SyncDemo3 syncDemo2 = new SyncDemo3();
//启动两个线程
Thread thread1 = new Thread(new ThreadDemo3(syncDemo1), "thread1");
Thread thread2 = new Thread(new ThreadDemo3(syncDemo2), "thread2");
thread1.start();
//休眠1秒,让thread1获取资源
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} thread2.start();
//休眠1秒
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//thread1中断
thread1.interrupt();
//thread2中断
thread2.interrupt(); if (thread1.isInterrupted()) {
System.out.println("thread1 interrupt!");
}
if (thread2.isInterrupted()) {
System.out.println("thread2 interrupt!");
} //休眠1秒,让thread2获取资源
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} }
} class ThreadDemo3 implements Runnable {
SyncDemo3 syncDemo3; public ThreadDemo3(SyncDemo3 syncDemo3) {
this.syncDemo3 = syncDemo3;
} @Override
public void run() {
syncDemo3.m1();
}
}
JDK6对Synchronized的优化
在JDK6以前synchronized的性能并不高,但在之后进行了优化,我们在之前的Mark Word的结构中可以看到,锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。
偏向锁
偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段。经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。
偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。
所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失。但偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
轻量级锁
若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段,此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。
自旋锁
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。
锁消除
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。
锁粗化
如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展到整个操作序列的外部,这样就只需要加锁一次就够了。
参考:
《实战Java高并发程序设计》 葛一鸣,郭超 著
https://blog.csdn.net/javazejian/article/details/72828483
synchronized使用及原理解析的更多相关文章
- Volley 实现原理解析(转)
Volley 实现原理解析 转自:http://blog.csdn.net/fengqiaoyebo2008/article/details/42963915 1. 功能介绍 1.1. Volley ...
- 超详细的Guava RateLimiter限流原理解析
超详细的Guava RateLimiter限流原理解析 mp.weixin.qq.com 点击上方“方志朋”,选择“置顶或者星标” 你的关注意义重大! 限流是保护高并发系统的三把利器之一,另外两个是 ...
- Android进阶:七、Retrofit2.0原理解析之最简流程【下】
紧接上文Android进阶:七.Retrofit2.0原理解析之最简流程[上] 一.请求参数整理 我们定义的接口已经被实现,但是我们还是不知道我们注解的请求方式,参数类型等是如何发起网络请求的呢? 这 ...
- [置顶]
滴滴插件化框架VirtualAPK原理解析(一)之插件Activity管理
上周末,滴滴与360都开源了各自的插件化框架,VirtualAPK与RePlugin,作为一个插件化方面的狂热研究者,在周末就迫不及待的下载了Virtualapk框架来进行研究,本篇博客带来的是Vir ...
- Spring IOC设计原理解析:本文乃学习整理参考而来
Spring IOC设计原理解析:本文乃学习整理参考而来 一. 什么是Ioc/DI? 二. Spring IOC体系结构 (1) BeanFactory (2) BeanDefinition 三. I ...
- ButterKnife 原理解析
一.使用方法 1.添加依赖. implementation 'com.jakewharton:butterknife:8.8.1' annotationProcessor 'com.jakewhart ...
- Java并发包JUC核心原理解析
CS-LogN思维导图:记录CS基础 面试题 开源地址:https://github.com/FISHers6/CS-LogN JUC 分类 线程管理 线程池相关类 Executor.Executor ...
- Java volatile 关键字底层实现原理解析
本文转载自Java volatile 关键字底层实现原理解析 导语 在Java多线程并发编程中,volatile关键词扮演着重要角色,它是轻量级的synchronized,在多处理器开发中保证了共享变 ...
- [原][Docker]特性与原理解析
Docker特性与原理解析 文章假设你已经熟悉了Docker的基本命令和基本知识 首先看看Docker提供了哪些特性: 交互式Shell:Docker可以分配一个虚拟终端并关联到任何容器的标准输入上, ...
随机推荐
- [题解]Mail.Ru Cup 2018 Round 1 - A. Elevator or Stairs?
[题目] A. Elevator or Stairs? [描述] Masha要从第x层楼去第y层楼找Egor,可以选择爬楼梯或者坐直升电梯.已知爬楼梯每层需要时间t1:坐直升电梯每层需要时间t2,直升 ...
- 报表工具为什么我推荐用Smartbi,数据分析师和初学者都能灵活运用
在很多人入门数据分析师或者投身大数据行业的时候,肯定会接触到报表工具,很多人这时候就会去使用一些Excel插件的报表工具,但是很多报表工具都是需要下载一系列的软件,配置各种复杂的环境.尤其是一些数据分 ...
- Python "爬虫"出发前的装备之二数据先行( Requests 模块)
1. 概念 爬虫不是动物,而是一种计算机程序. 这种程序有自己特定的功能,能按照使用者给定的一系列规则自行浏览万维网并获取需要的信息.此类程序被称为 网络爬虫(web crawler) 或 网络蜘蛛( ...
- 【C# 线程】Thread类 以及使用案例
System.Threading.Thread类 涉及到的类和枚举 Volatile 类Interlocked 类SpinLock 类SpinWait类Barrier 类ThreadLocal< ...
- Linux经典100题及参考答案
转至:https://blog.csdn.net/yaoqiang2011/article/details/11908189 一.单选题 1. cron 后台常驻程序 (daemon) 用于: A. ...
- shell脚本上传sftp文件
转至:https://blog.csdn.net/sxh6365966/article/details/83385711 #!/bin/bash #SFTP配置信息 #用户名 YEARS=`date ...
- 【译】C# 11 特性的早期预览
原文 | Kathleen 翻译 | 郑子铭 Visual Studio 17.1(Visual Studio 2022 Update 1)和 .NET SDK 6.0.200 包含 C# 11 的预 ...
- Chrome:查看用户代理User-Agent
用户代理(User-Agent)是浏览器客户端与服务器交互时的重要信息之一,用于帮助服务器识别请求用户的浏览器类别,以便于网站发送相应的网页数据. 用户代理数据包括:操作系统标识.加密等级标识和浏览器 ...
- JZ-003-从尾到头打印链表
从尾到头打印链表 题目描述 输入一个链表,按链表从尾到头的顺序返回一个ArrayList. 题目链接: 从尾到头打印链表 代码 import java.util.ArrayList; /** * 标题 ...
- Scala语法1
目录 main方法和def 函数 变量,类型转换,字符串分割拼接 文件读写和JDBC 面向对象编程 继承 case类,最常用的 main方法和def 函数 package scala_01 /** * ...