Java并发编程深入理解之Synchronized的使用及底层原理详解 上

 更新时间:2021年09月23日 15:09:05   作者:没头脑遇到不高兴  
在并发编程中存在线程安全问题,主要原因有:1.存在共享数据 2.多线程共同操作共享数据。关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized可以保证一个线程的变化可见(可见性),即可以代替volatile

一、线程安全问题

1、临界资源

多线程编程中,有可能会出现多个线程同时访问同一个共享、可变资源的情况,这个资源我们称之其为临界资源;这种资源可能是:对象、变量、文件等。

  1. 共享:资源可以由多个线程同时访问
  2. 可变:资源可以在其生命周期内被修改

2、线程安全问题

当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的,否则就是非线程安全的。

3、如何解决线程安全问题

互斥同步(Mutual Exclusion & Synchronization)是一种最常见也是最主要的并发正确性保障手段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条(或者是一些,当使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)都是常见的互斥实现方式。

在Java里面,最基本的互斥同步手段就是synchronized关键字,另外还有从JDK1.5开始引入了JUC里面的Lock接口,其中用的比较多的就是ReentrantLock,后面也会进行介绍。

二、synchronized使用介绍

synchronized是JVM内置的,是可重入的,其使用方法有三种:加在static修饰的静态方法上,加在普通方法上,同步代码块三种方式。

  1. 加在静态方法上(public synchronized static void test()),锁的是当前类的Class对象
  2. 加在实例方法上(public synchronized void test()),锁的是当前对象
  3. synchronized同步代码块(synchronized(object) {......}),锁的是synchronized后面括号里面的对象

从上面可以看出synchronized锁的其实都是对象。

三、synchronized实现原理

1、synchronized底层指令:monitorenter和monitorexit

synchronized是基于JVM内置锁实现,通过内部对象Object Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低。当然,JVM内置锁在1.5之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销,内置锁的并发性能已经基本与Lock持平。

注意:只有synchronized锁升级为重量级锁时才会用到Object Monitor(监视器锁)。

synchronized关键字被编译成字节码后会被翻译成monitorenter 和monitorexit 两条指令分别在同步块逻辑代码的起始位置与结束位置。

public class TestSynchronized {
    private Object obj = new Object();
    public void testLock() {
        synchronized (obj) {
            System.out.println("获取了锁");
        }
    }
}

我们通过javap -c TestSynchronized.class将上面代码的class文件进行反汇编,可以看到如下所示:我们看到了monitorenter 和monitorexit 两条指令,但是monitorexit却出现了两次,原因如下:

  • 第一个monitorexit指令是同步代码块正常释放锁的一个标志;
  • 如果同步代码块中出现Exception或者Error,则会调用第二个monitorexit指令来保证释放锁
public void testLock();
    Code:
       0: aload_0
       1: getfield      #3                  // Field obj:Ljava/lang/Object;
       4: dup
       5: astore_1
       6: monitorenter
       7: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      10: ldc           #5                  // String 鑾峰彇浜嗛攣
      12: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      15: aload_1
      16: monitorexit
      17: goto          25
      20: astore_2
      21: aload_1
      22: monitorexit
      23: aload_2
      24: athrow
      25: return
    Exception table:
       from    to  target type
           7    17    20   any
          20    23    20   any

2、Object Monitor(监视器锁)机制

上面提到了,只有synchronized锁升级为重量级锁时才会用到Object Monitor(监视器锁)。我们看一下Object Monitor的实现机制是什么?查看OpenJDK源码可以看到Object Monitor由C++语言实现,打开JDK源码目录 “jdk\hotspot\src\share\vm\runtime“可以看到objectMonitor.hpp,这个就是监视器锁的实现,截取一段代码如下:

ObjectMonitor() {
	_header       = NULL; //对象头
	_count        = 0;	//记录加锁次数,锁重入时用到
	_waiters      = 0, //当前有多少处于wait状态的thread
	_recursions   = 0; //记录锁的重入次数
	_object       = NULL;
	_owner        = 0; //指向持有ObjectMonitor对象的线程
	_WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
	_WaitSetLock  = 0 ;
	_Responsible  = NULL ;
	_succ         = NULL ;
	_cxq          = NULL ;
	FreeNext      = NULL ;
	_EntryList    = NULL ;//处于等待加锁block状态的线程,会被加入到该列表
	_SpinFreq     = 0 ;
	_SpinClock    = 0 ;
	OwnerIsThread = 0 ;
	_previous_owner_tid = 0;
}

其中几个比较重要的字段:

  • _header 对象头,前面说过synchronized锁升级为重量级锁之后才会用到objectMonitor,这时候对象头的Mark word会有一个指向重量级锁Monitor的指针
  •  _count 线程获取锁的次数,每加锁一次该值加1。
  • _waiters 当前有多少处于wait状态的thread
  • _recursions 锁的重入次数 
  • _owner 指向持有ObjectMonitor对象的线程地址。 
  • _WaitSet 存放调用wait方法,而进入等待状态的线程的队列。
  • _EntryList 处于等待加锁block状态的线程,会被加入到该列表

ObjectMonitor的加锁解锁过程如下图所示,ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装成ObjectWaiter对象);整个monitor运行的机制过程如下:

  • _owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合
  • 当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程的同时,monitor中的计数器count加1,
  • 若已经获取锁的线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。
  • 若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。

下节将会介绍一下synchronized的锁优化和锁升级过程

到此这篇关于Java并发编程深入理解之Synchronized的使用及底层原理详解 上的文章就介绍到这了,更多相关Java 并发编程 Synchronized内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java复合语句的使用方法详解

    Java复合语句的使用方法详解

    这篇文章主要介绍了Java编程中复合语句,结合相关的具体实例介绍了其用法,需要的朋友可以参考下
    2017-09-09
  • 浅谈collection标签的oftype属性能否为java.util.Map

    浅谈collection标签的oftype属性能否为java.util.Map

    这篇文章主要介绍了collection标签的oftype属性能否为java.util.Map,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-02-02
  • Java C++题解eetcode940不同的子序列 II

    Java C++题解eetcode940不同的子序列 II

    这篇文章主要为大家介绍了Java C++题解eetcode940不同的子序列 II实现示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-10-10
  • SpringBoot集成Druid的实例代码

    SpringBoot集成Druid的实例代码

    这篇文章主要介绍了SpringBoot集成Druid的实例代码,有依赖和配置相关内容,本文通过实例代码给大家介绍的非常详细,需要的朋友可以参考下
    2021-12-12
  • Java的RocketMq水平扩展及负载均衡详解

    Java的RocketMq水平扩展及负载均衡详解

    这篇文章主要介绍了Java的RocketMq水平扩展及负载均衡详解,RocketMQ是一个分布式具有高度可扩展性的消息中间件,本文旨在探索在broker端,生产端,以及消费端是如何做到横向扩展以及负载均衡的,需要的朋友可以参考下
    2024-01-01
  • Java Scala实现数据库增删查改操作详解

    Java Scala实现数据库增删查改操作详解

    这篇文章主要介绍了Java Scala实现数据库增删查改操作,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2023-04-04
  • Spring整合MyBatis图示过程解析

    Spring整合MyBatis图示过程解析

    这篇文章主要介绍了Spring整合MyBatis图示过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-11-11
  • 浅谈一下Java中集合的迭代方式

    浅谈一下Java中集合的迭代方式

    这篇文章主要介绍了浅谈一下Java中集合的迭代方式,可以帮助我们学习,理解函数式编程,需要的朋友可以参考下
    2023-04-04
  • Java for each实现机制代码原理解析

    Java for each实现机制代码原理解析

    这篇文章主要介绍了Java for each实现机制代码原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-06-06
  • java数据结构与算法之桶排序实现方法详解

    java数据结构与算法之桶排序实现方法详解

    这篇文章主要介绍了java数据结构与算法之桶排序实现方法,结合具体实例形式详细分析了桶排序的概念、原理、实现方法与相关操作技巧,需要的朋友可以参考下
    2017-05-05

最新评论