java synchronized的用法及原理详解

目录
  • 为什么要用synchronized
  • 使用方式
  • 字节码语义
  • 对象锁(monitor)
  • 锁升级过程

为什么要用synchronized

相信大家对于这个问题一定都有自己的答案,这里我还是要啰嗦一下,我们来看下面这段车站售票的代码:

/**
 * 车站开两个窗口同时售票
 */
public class TicketDemo {

    public static void main(String[] args) {
        TrainStation station = new TrainStation();
        // 开启两个线程同时进行售票
        new Thread(station, "A").start();
        new Thread(station, "B").start();
    }
}

class TrainStation implements Runnable {
    private volatile int ticket = 10;
    @Override
    public void run() {
        while (ticket > 0) {
            System.out.println("线程" + Thread.currentThread().getName() + "售出" + ticket + "号票");
            ticket = ticket - 1;
        }
    }
}

上面这段代码是没有做考虑线程安全问题的,执行这段代码可能会出现下面的运行结果:

可以看出,两个线程都买出了10号票,这在实际业务场景中是绝对不能出现的。(你去坐火车有个大哥说你占了他的座,让你滚,还说你是票贩子,你气不气)

那因为有这种问题的存在,我们应该怎么解决呢?synchronized就是为了解决这种多线程共享数据安全问题的。

使用方式

synchronized的使用方式主要以下三种。

同步代码块

public static void main(String[] args) {
    String str = "hello world";
    synchronized (str) {
        System.out.println(str);
    }
}

同步实例方法

class TrainStation implements Runnable {
    private volatile int ticket = 100;

    // 关键字直接写在实例方法签名上
    public synchronized void sale() {
        while (ticket > 0) {
            System.out.println("线程" + Thread.currentThread().getName() + "售出" + ticket + "号票");
            ticket = ticket - 1;
        }
    }

    @Override
    public void run() {
        sale();
    }
}

同步静态方法

class TrainStation implements Runnable {
    // 注意这里ticket变量声明为static的,因为静态方法只能访问静态变量
    private volatile static int ticket = 100;

    // 也可以直接放在静态方法的签名上
    public static synchronized void sale() {
        while (ticket > 0) {
            System.out.println("线程" + Thread.currentThread().getName() + "售出" + ticket + "号票");
            ticket = ticket - 1;
        }
    }
    @Override
    public void run() {
        sale();
    }
}

字节码语义

通过程序运行,我们发现通过synchronized关键字确实可以保证线程安全,那计算机到底是怎么保证的呢?这个关键字背后到底做了些什么?我们可以看一下java代码编译后的class文件。首先来看同步代码块编译后的class。通过javap -v 名称可以查看字节码文件:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: ldc           #2                  // String hello world
         2: astore_1
         3: aload_1
         4: dup
         5: astore_2
         6: monitorenter			// 监视器进入
         7: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        10: aload_1
        11: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        14: aload_2
        15: monitorexit				// 监视器退出
        16: goto          24
        19: astore_3
        20: aload_2
        21: monitorexit
        22: aload_3
        23: athrow
        24: return

注意看第6行和第15行,这两个指令是增加synchronized代码块之后才会出现的,monitor是一个对象的监视器,monitorenter代表这段指令的执行要先拿到对象的监视器之后,才能接着往下执行,而monitorexit代表执行完synchronized代码块之后要从对象监视器中退出,也就是要释放。所以这个对象监视器也就是我们所说的锁,获取锁就是获取这个对象监视器的所有权。

接下来我们在看看synchronized修饰实例方法时的字节码文件是什么样的。

 public synchronized void sale();
    descriptor: ()V
	//方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field ticket:I
	// 省略其他无关字节码

可以看到synchronized修饰实例方法上之后不会再有monitorentermonitorexit指令,而是直接在这个方法上增加一个ACC_SYNCHRONIZED的flag。当程序在运行时,调用sale()方法时,会检查该方法是否有ACC_SYNCHRONIZED访问标识,如果有,则表明该方法是同步方法,这时候还行线程会先尝试去获取该方法对应的监视器(monitor)对象,如果获取成功,则继续执行该sale()方法,在执行期间,任何其他线程都不能再获取该方法监视器的使用权,知道该方法执行完毕或者抛出异常,才会释放,其他线程可以重新获得该监视器。

那么synchronized修饰静态方法的字节码文件是什么样呢?

public static synchronized void sale();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=0, args_size=0
         0: getstatic     #2                  // Field ticket:I
      // 省略其他无关字节码

可以看出synchronized修饰静态方法和实例方法没有区别,都是增加一个ACC_SYNCHRONIZED的flag,静态方法只是比实例方法多一个ACC_STATIC标识代表这个方法是静态的。

以上的同步代码块,同步方法中都提到对象监视器这个概念,那么三种同步方式使用的对象监视器具体是哪个对象呢?

同步代码块的对象监视器就是使用的我们synchronized(str)中的str,也就是我们括号中指定的对象。而我们在开发中增加同步代码块的目的是为了多个线程同一时间只能有一个线程持有监视器,所以这个对象的指定一定要是多个线程共享的对象,不能直接在括号中new一个对象,这样不能做到互斥,也就不能保证安全。

同步实例方法的对象监视器是当前这个实例,也就是this。

同步静态方法的对象监视器是当前这个静态方法所在类的Class对象,我们都知道Java中每个类在运行过程中也会用一个对象表示,就是这个类的对象,每个类有且仅有一个。

对象锁(monitor)

上面说了线程要进入同步代码块需要先获取到对象监视器,也就是对象锁,那在开始说之前我们先来了解下在Java中一个对象都由哪些东西组成。

这里先问大家一个问题,Object obj = new Object()这段代码在JVM中是怎样的一个内存分布?

想必了解过JVM知识的同学应该都知道,new Object()会在堆内存中创建一个对象,Object obj是栈内存中的一个引用,这个引用指向堆中的对象。那么怎么知道堆内存中的对象到底由哪些内容组成呢?这里给大家介绍一个工具叫JOL(Java Object Layout)Java对象布局。可以通过maven在项目中直接引入。

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

引入之后在代码中可以打印出对象的内存分布。

public static void main(String[] args) {
    Object obj = new Object();
    // parseInstance将对象解析,toPrintable让解析后的结果可输出
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}

输出后的结果如下:

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

从结果上可以看出,这个obj对象主要分4部分,每部分的SIZE=4代表4个字节,前三行是对象头object header,最后一行的4个字节是为了保证一个对象的大小能是8的整数倍。

我们再来看看对于一个加了锁的对象,打印出来有什么不一样?

public static void main(String[] args) {
    Object obj = new Object();
    synchronized (obj){
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
}

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 58 f7 19 01 (01011000 11110111 00011001 00000001) (18478936)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以很明显的看到,最前面的8个字节发生了变化,也就是Mark Word变了。所以给对象加锁,实际就是改变对象的Mark Word。

Mark Word中的这8个字节具有不同的含义,为了让这64个bit能表示更多信息,JVM将最后2位设置为标记位,不同标记位下的Mark word含义如下:

其中最后两位的锁标记位,不同值代表不同含义。

biased_lock lock 状态
0 00 无锁态(NEW)
0 01 偏向锁
1 01 偏向锁
0 00 轻量级锁
0 10 重量级锁
0 11 GC标记

biased_lock标记该对象是否启用偏向锁,1代表启用偏向锁,0代表未启用。

age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。

identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。

thread:持有偏向锁的线程ID。

epoch:偏向时间戳。

ptr_to_lock_record:指向栈中锁记录的指针。

ptr_to_heavyweight_monitor:指向管程Monitor的指针。

锁升级过程

既然会有无锁,偏向锁,轻量级锁,重量级锁,那么这些锁是怎么样一个升级过程呢,我们来看一下。

新建

从前面讲到对象头的结构和我们上面打印出来的对象内存分布,可以看出新创建的一个对象,它的标记位是00,偏向锁标记(biased_lock)也是0,表示该对象是无锁态。

偏向锁

偏向锁是指当一段同步代码被同一个线程所访问时,不存在其他线程的竞争时,那么该线程在以后访问时便会自动获得锁,从而降低获取锁带来的消耗,提高性能。

当一个线程访问同步代码块并获取锁时,会在 Mark Word 里存储线程 ID。在线程进入和退出同步块时不再通过 CAS 操作来加锁和解锁,而是检测 Mark Word 里是否存储着指向当前线程的偏向锁。轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令即可。

轻量级锁

轻量级锁是指当锁是偏向锁的时候,有其他线程来竞争,但是该锁正在被其他线程访问,那么就会升级为轻量级锁。或者还有一种情况就是关闭JVM的偏向锁开关,那么一开始锁对象就会被标记位轻量级锁。

轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。

在进入同步代码时,如果对象锁状态符合升级轻量级锁的条件,虚拟机会在当前想要竞争锁的线程的栈帧中开辟一个Lock Record空间,并将锁对象的Mark Word拷贝到Lock Record空间中。

然后虚拟机会使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record中的owner指针指向对象的Mark Word。

如果操作成功,则表示当前线程获得锁,如果失败则表示其他线程持有该锁,当前线程会尝试使用自旋的方式来重新获取。

轻量级锁解锁时,会使用CAS操作将Lock Record替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

重量级锁

重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。是依赖于底层操作系统的Mutex实现,Mutex也叫互斥锁。也就是说重量级锁会让锁从用户态切换到内核态,将线程的调度交给操作系统,性能相比会很低。

整个锁升级的过程通过下面这张图能更全面的展示。

到此这篇关于java synchronized的用法及原理详解的文章就介绍到这了,更多相关java synchronized内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java并发之synchronized实现原理深入理解

    目录 synchronized的三种应用方式 synchronized作用于实例方法 synchronized作用于静态方法 synchronized同步代码块 synchronized底层语义原理 理解Java对象头与Monitor synchronized代码块底层原理 synchronized方法底层原理 Java虚拟机对synchronized的优化 偏向锁 轻量级锁 自旋锁 锁消除 关于synchronized 可能需要了解的关键点 synchronized的可重入性 线程中断与syn

  • JAVAsynchronized原理详解

    目录 1.synchronized的作用 2.synchronized的语法 3.Monitor原理 4.synchronized的原理 4.1偏向锁 4.2轻量级锁 4.3锁膨胀 4.4重量级锁 4.5自旋锁 4.6锁消除 4.7锁粗化 5.锁升级过程 总结 1.synchronized的作用 为了避免临界区的竞态条件发生,有多种手段可以达到目的. 阻塞式的解决方案:synchronized,Lock 非阻塞式的解决方案:原子变量 synchronized,即俗称的[对象锁],它采用互斥的方式

  • 死磕 java同步系列之synchronized解析

    问题 (1)synchronized的特性? (2)synchronized的实现原理? (3)synchronized是否可重入? (4)synchronized是否是公平锁? (5)synchronized的优化? (6)synchronized的五种使用方式? 简介 synchronized关键字是Java里面最基本的同步手段,它经过编译之后,会在同步块的前后分别生成 monitorenter 和 monitorexit 字节码指令,这两个字节码指令都需要一个引用类型的参数来指明要锁定和解

  • [java并发编程之深入理解]Synchronized的使用

    1.为什么要使用synchronized 在并发编程中存在线程安全问题,主要原因有:1.存在共享数据 2.多线程共同操作共享数据.关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized可以保证一个线程的变化可见(可见性),即可以代替volatile. 2.实现原理 synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性 3.synchronized的三种应

  • Java多线程之synchronized关键字的使用

    一.使用在非静态方法上 public synchronized void syzDemo(){ System.out.println(System.currentTimeMillis()); System.out.println("进入synchronized锁:syzDemo"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } 二.使用在静态方法上 publi

  • Java的Synchronized关键字学习指南(全面 & 详细)

    前言 在Java中,有一个常被忽略 但 非常重要的关键字Synchronized今天,我将详细讲解 Java关键字Synchronized的所有知识,希望你们会喜欢 目录 1. 定义 Java中的1个关键字 2. 作用 保证同一时刻最多只有1个线程执行 被Synchronized修饰的方法 / 代码 其他线程 必须等待当前线程执行完该方法 / 代码块后才能执行该方法 / 代码块 3. 应用场景 保证线程安全,解决多线程中的并发同步问题(实现的是阻塞型并发),具体场景如下: 修饰 实例方法 / 代

  • 详解Java中的锁Lock和synchronized

    一.Lock接口 1.Lock接口和synchronized内置锁 a)synchronized:Java提供的内置锁机制,Java中的每个对象都可以用作一个实现同步的锁(内置锁或者监视器Monitor),线程在进入同步代码块之前需要或者这把锁,在退出同步代码块会释放锁.而synchronized这种内置锁实际上是互斥的,即没把锁最多只能由一个线程持有. b)Lock接口:Lock接口提供了与synchronized相似的同步功能,和synchronized(隐式的获取和释放锁,主要体现在线程进

  • Java中关键字synchronized的使用方法详解

    synchronized是Java里的一个关键字,起到的一个效果是"监视器锁"~~,它的功能就是保证操作的原子性,同时禁止指令重排序和保证内存的可见性! public class TestDemo { static class Counter{ public int count = 0; public void add(){ count++; } } public static void main(String[] args) throws InterruptedException {

  • java synchronized 锁机制原理详解

    目录 前言: 1.synchronized 的作用: 2.synchronized 底层语义原理: 3. synchronized 的显式同步与隐式同步: 3.1.synchronized 代码块底层原理: 3.2.synchronized 方法底层原理: 4.JVM 对 synchronized 锁的优化: 4.1.锁升级:偏向锁->轻量级锁->自旋锁->重量级锁 4.1.1.synchronized 的 Mark word 标志位: 4.1.2.锁升级过程: 4.2.锁消除: 4.3

  • java synchronized的用法及原理详解

    目录 为什么要用synchronized 使用方式 字节码语义 对象锁(monitor) 锁升级过程 为什么要用synchronized 相信大家对于这个问题一定都有自己的答案,这里我还是要啰嗦一下,我们来看下面这段车站售票的代码: /** * 车站开两个窗口同时售票 */ public class TicketDemo { public static void main(String[] args) { TrainStation station = new TrainStation(); //

  • Java synchronized与CAS使用方式详解

    目录 引言 synchronized synchronized的三种使用方式 synchronized的底层原理 JDK1.6对synchronized的优化 synchronized的等待唤醒机制 CAS 引言 上一篇文章中我们说过,volatile通过lock指令保证了可见性.有序性以及“部分”原子性.但在大部分并发问题中,都需要保证操作的原子性,volatile并不具有该功能,这时就需要通过其他手段来达到线程安全的目的,在Java编程中,我们可以通过锁.synchronized关键字,以及

  • Java 中This用法的实例详解

     Java 中This用法的实例详解 用类名定义一个变量的时候,定义的只是一个引用,外面可以通过这个引用来访问这个类里面的属性和方法. 那们类里面是够也应该有一个引用来访问自己的属性和方法纳? 呵呵,Java提供了一个很好的东西,就是 this 对象,它可以在类里面来引用这个类的属性和方法.先来个简单的例子: public class ThisDemo { String name="Mick"; public void print(String name){ System.out.pr

  • Java线程池FutureTask实现原理详解

    前言 线程池可以并发执行多个任务,有些时候,我们可能想要跟踪任务的执行结果,甚至在一定时间内,如果任务没有执行完成,我们可能还想要取消任务的执行,为了支持这一特性,ThreadPoolExecutor提供了 FutureTask 用于追踪任务的执行和取消.本篇介绍FutureTask的实现原理. 类视图 为了更好的理解FutureTask的实现原理,这里先提供几个重要接口和类的结构,如下图所示: RunnableAdapter ThreadPoolExecutor提供了submit接口用于提交任

  • python super用法及原理详解

    这篇文章主要介绍了python super用法及原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 概念 super作为python的内建函数.主要作用如下: 允许我们避免使用基类 跟随多重继承来使用 实例 在单个继承的场景下,一般使用super来调用基类来实现: 下面是一个例子: class Mammal(object): def __init__(self, mammalName): print(mammalName, 'is a wa

  • Java对称加密工作模式原理详解

    这篇文章主要介绍了Java对称加密工作模式原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 对称加密又分为分组加密和序列密码. 分组密码,也叫块加密(block cyphers),一次加密明文中的一个块.是将明文按一定的位长分组,明文组经过加密运算得到密文组,密文组经过解密运算(加密运算的逆运算),还原成明文组. 序列密码,也叫流加密(stream cyphers),一次加密明文中的一个位.是指利用少量的密钥(制乱元素)通过某种复杂的运算

  • Java正则表达式Pattern和Matcher原理详解

    这篇文章主要介绍了Java正则表达式Pattern和Matcher原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 基本使用 Scanner中的使用正则表达式 //Scanner 支持的分组 Scanner cin=new Scanner("red a bbc").useDelimiter("\\s*a\\s*"); System.out.println(cin.next()); System.out.pri

  • java为什么需要虚拟机jvm原理详解

    目录 JVM的快速理解 曾几何时,我们还是初识Hello World的时候,我们哪曾知道,Java这门神奇的语言,在执行我们的代码的时候,不是直接将我们所编写的Java代码交付给操作系统底层进行解析编译,而是采用了JDK来对Java代码进行编译,编译成dotClass文件后,将dotClass文件转交至JRE中.(dotClass其实就是.class) jre也就是JavaRunTimeEnvironment,java运行环境,因为在这里,存在着Java的秘密武器,也就是JVM!Jvm是组成JR

  • Java中的 HTTP 协议原理详解

    目录 前言 1.HTTP 特点 2.HTTP 组成 2.1 请求对象 2.1.1 请求行 2.1.2 请求报头 2.1.3 空行 2.1.4 请求正文 2.2 响应对象 2.2.1 状态行 2.2.2 响应报头 2.2.3 空行 2.2.4 响应正文 总结 前言 HTTP(Hyper Text Transfer Protocol)超文本传输协议,下文简称 HTTP,它的作用是用于实现服务器端和客户端的数据传输的.它可以传输任意的数据类型,如文本.HTML.图片.文件.声音等类型. 简单来说,HT

  • swift语言Codable 用法及原理详解

    目录 Codable Codable 的用法 JSON 和 模型的相互转换 解码(JSON Data -> Model): 编码(Model -> JSON Data): Codable 支持的数据类型 基础数据类型 Date 嵌套对象 枚举 自定义 CodingKeys Codable 的原理 Decodable 协议 Container 核心原理分析(Container <--> JSON) JSONDecoder 的解码过程 编译器帮我们做了什么? 默认值问题 属性包装器 @

随机推荐