修饰静态方法、实例方法、代码块

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使用及原理解析的更多相关文章

  1. Volley 实现原理解析(转)

    Volley 实现原理解析 转自:http://blog.csdn.net/fengqiaoyebo2008/article/details/42963915 1. 功能介绍 1.1. Volley ...

  2. 超详细的Guava RateLimiter限流原理解析

    超详细的Guava RateLimiter限流原理解析  mp.weixin.qq.com 点击上方“方志朋”,选择“置顶或者星标” 你的关注意义重大! 限流是保护高并发系统的三把利器之一,另外两个是 ...

  3. Android进阶:七、Retrofit2.0原理解析之最简流程【下】

    紧接上文Android进阶:七.Retrofit2.0原理解析之最简流程[上] 一.请求参数整理 我们定义的接口已经被实现,但是我们还是不知道我们注解的请求方式,参数类型等是如何发起网络请求的呢? 这 ...

  4. [置顶] 滴滴插件化框架VirtualAPK原理解析(一)之插件Activity管理

    上周末,滴滴与360都开源了各自的插件化框架,VirtualAPK与RePlugin,作为一个插件化方面的狂热研究者,在周末就迫不及待的下载了Virtualapk框架来进行研究,本篇博客带来的是Vir ...

  5. Spring IOC设计原理解析:本文乃学习整理参考而来

    Spring IOC设计原理解析:本文乃学习整理参考而来 一. 什么是Ioc/DI? 二. Spring IOC体系结构 (1) BeanFactory (2) BeanDefinition 三. I ...

  6. ButterKnife 原理解析

    一.使用方法 1.添加依赖. implementation 'com.jakewharton:butterknife:8.8.1' annotationProcessor 'com.jakewhart ...

  7. Java并发包JUC核心原理解析

    CS-LogN思维导图:记录CS基础 面试题 开源地址:https://github.com/FISHers6/CS-LogN JUC 分类 线程管理 线程池相关类 Executor.Executor ...

  8. Java volatile 关键字底层实现原理解析

    本文转载自Java volatile 关键字底层实现原理解析 导语 在Java多线程并发编程中,volatile关键词扮演着重要角色,它是轻量级的synchronized,在多处理器开发中保证了共享变 ...

  9. [原][Docker]特性与原理解析

    Docker特性与原理解析 文章假设你已经熟悉了Docker的基本命令和基本知识 首先看看Docker提供了哪些特性: 交互式Shell:Docker可以分配一个虚拟终端并关联到任何容器的标准输入上, ...

随机推荐

  1. 分享几个你可能不知道的交互式Git 命令

    摘要:本文中讲述的几个交互式 Git 命令可以帮助你将文件的特定部分组合成提交. 本文分享自华为云社区<Git你有可能不知道交互式暂存>,作者:龙哥手记. 本节中的几个交互式 Git 命令 ...

  2. string 字符串的操作 大全类的使用

    Array.Sort(vv, string.CompareOrdinal); //ASCII排序 string[] words = { "The", "1quick&qu ...

  3. parquet和orc选型以及压缩格式

    Hive表压缩功能 除了直接配置MapReduce压缩功能外,Hive的ORC表和Parquet表直接支持表的压缩属性. 但支持的压缩格式有限,ORC表支持None.Zlib.Snappy压缩,默认为 ...

  4. 数据分析logistic回归与时间序列

    logistics回归 1.影响关系研究是所有研究中最为常见的. 2.当y是定量数据时,线性回归可以用来分析影响关系. 3.如果现在想对某件事情发生的概率进行预估,比如一件衣服的是否有人想购买? 这里 ...

  5. Python 内存管理与垃圾回收

    Python 内存管理与垃圾回收 参考文献:https://pythonav.com/wiki/detail/6/88/ 引用计数器为主标记清除和分代回收为辅 + 缓存机制 1.1 大管家refcha ...

  6. JZ-051-构建乘积数组

    构建乘积数组 题目描述 给定一个数组A[0,1,...,n-1],请构建一个数组B[0,1,...,n-1],其中B中的元素B[i]=A[0]A[1]...A[i-1]A[i+1]...A[n-1]. ...

  7. Linux CentOS7.X- 添加用户

    1.创建用户 useradd username 其中,username是要创建用户的用户名(root使用): 2.设置密码 passwd username 其中,username是指定要修改密码的用户 ...

  8. 基于idea做java程序的本地k8s调试-skaffold(二)

    上一篇讲完了java代码发到本机minikube中run,这篇来讲讲minkube中进行debug(idea下) 话说,上篇是把pigx基础infra微服务都发到了minikube中,这些微服务是ru ...

  9. laravel 访问器 和修改器的使用

    对于访问器我是这样定义的,就是将数据库中的数据被访问时可以变成我们想要的数据类型(例如:数据库中的时间字段是int类型,要将她变成data(Y-m-d H:i:s),格式类型) 参看博客 https: ...

  10. php 23种设计模式 - 设计模式简介以及分类

    一. Introduction[介绍] 设计模式:提供了一种广泛的可重用的方式来解决我们日常编程中常常遇见的问题.设计模式并不一定就是一个类库或者第三方框架,它们更多的表现为一种思想并且广泛地应用在系 ...