查看: 78|回复: 0

【搞定面试官】- Synchronized如何实现同步?锁优化?(1)

[复制链接]
发表于 2020-2-15 20:53:21 | 显示全部楼层 |阅读模式
媒介

说起Java口试中最高频的知识点非多线程莫属。每每提起多线程都绕不过一个Java关键字——synchronized。我们都知道该关键字可以包管在同一时刻,只有一个线程可以实行某个方法或者某个代码块以包管多线程的安全性。那么,本篇文章我们就来揭开这个synchronized的面纱。
线程安全的实现方法

在具体介绍synchronized之前,我们首先了解一下实现线程安全的不同方式,了解synchronized是怎样实现线程安全的理论底子,做到胸有定见。如今重要有三种线程安全实现方法:互斥同步(阻塞同步)、非阻塞同步以及无需同步的线程安全方案。

  • 互斥同步(Mutual Exclusion & Synchnronization)
互斥同步是指在多个线程并发访问共享数据时,包管共享数据在同一时刻只被一个(或一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区、互斥量和信号量都是重要的互斥实现方式。因此在互斥同步四个字中,互斥是因,同步是果;互斥是方法,同步是目的。
Java中最基本的互斥同步手段就是synchronized,具体怎样实现的互斥同步请继续往下看。
btw,除了synchronized,还有别的一种实现同步的方式,那就是java.util.concurrent包中的重入锁ReentrantLock,具体细节就不细说了,它和synchronized用法几乎一样。只是synchronized是原生语法,而ReentrantLock是JDK提供的API层面的互斥锁。

  • 非阻塞同步
互斥同步重要同步阻塞线程来包管线程安全,因此也被称为阻塞同步。它认为只要不去做正确的同步方式(比方加锁),那就一定会出现问题,无论共享数据是否会出现竞争(悲观锁)。
回来随着硬件指令集的发展,我们有了别的一种选择:先辈行操作,如果没有其他线程争用,那操作就成功了;如果有其他线程争用,产生了辩论,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不必要把线程挂起,所以这种同步方式成为非阻塞同步。

  • 无需同步的线程安全方案
要包管线程安全,并不一定就要进行同步,两者并没有因果关系。如果一个方法本来就不涉及共享数据,那它天然无需任何同步手段去包管正确性,因此会有一些代码天生线程安全。比如可重入代码(Reentrant Code)和线程本地存储(Thread Local Storage)等。
JDK中的synchronized改进

在 JDK1.5 之前,Java 是依靠 Synchronized 关键字实现锁功能来做到线程安全。Synchronized 是 JVM 实现的一种内置锁,锁的获取和释放是由 JVM 隐式实现。
到了 JDK1.5 版本,java.util.concurrent包中新增了 Lock 接口来实现锁功能,它提供了与 Synchronized 关键字类似的同步功能,只是在使用时必要显示获取和释放锁。前边我们提到过,Lock 同步锁是基于 Java 实现的,而 Synchronized 是基于底层操作系统的 Mutex Lock 实现的,每次获取和释放锁操作都会带来用户态和内核态的切换,从而增加系统性能开销。因此,在锁竞争激烈的情况下,Synchronized 同步锁在性能上就表现得非常糟糕,它也常被大家称为重量级锁。特殊是在单个线程重复申请锁的情况下,JDK1.5 版本的 Synchronized 锁性能要比 Lock 的性能差许多。比方,在 Dubbo 基于 Netty 实现的通讯中,消耗端向服务端通讯之后,由于接收返回消息是异步,所以必要一个线程轮询监听返回信息。而在接收消息时,就必要用到锁来确保 request session 的原子性。如果我们这里使用 Synchronized 同步锁,那么每当同一个线程哀求锁资源时,都会发生一次用户态和内核态的切换。
到了 JDK1.6 版本之后,Java 对 Synchronized 同步锁做了充分的优化,甚至在某些场景下,它的性能已经超越了 Lock 同步锁。
synchronized使用方式

Java中万物皆对象,而每一个对象都可以加锁,这是synchronized包管线程安全的底子。

  • 对于同步方法,锁是当前实例对象,即this,对该类其他实例对象无影响。
  • 对于静态同步方法,锁是当前对象的 Class 对象, 影响其他该类的实例化对象。
  • 对于同步方法块,锁是 synchronized括号里配置的对象。
也就是说,我们可以使用synchronized修饰类,类中的方法或者方法块。如下面的代码,分别对应上述三种情形。
  1. public class synchronizedTest implements Runnable {    static synchronizedTest instance=new synchronizedTest();    public void run() {        synchronized(instance){             //同步代码块,对应文章中第3点            //*******        }    }   void synchronized method1() {} //类中的同步方法 对应文章中第1点   void static synchronized method2() {} ////类中静态同步方法 对应文章中第2点}
复制代码
同步方法块

当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出非常时必须释放锁。那么锁存在那里呢?锁里面会存储什么信息呢?我们先来看一段代码以及它的字节码(我这里用的Idea的jclasslib插件)。
  1. package techgo.blog;public class SynchronizedTest {    private int i = 0;    public void fun() {        synchronized (this) {            i ++;        }    }}
复制代码

我们看到monitorenter和monitorexit,之后查阅虚拟机字节码指令表,我们知道这两个字节码操作分别表现获得和释放对象的锁。进入 monitorenter 指令后,线程将持有 Monitor 对象,退出 monitorenter 指令后,线程将释放该 Monitor 对象。以上这是同步方法块的实现方式。
同步方法

对于同步方法来说,如果去查看其字节码,我们会看不到这两个指令,因为同步方法依靠的是方法修饰符上的ACC_SYNCHRONIZED来实现的:
  1.     public synchronized void fun1() {    }
复制代码

当方法调用时,调用指令将会查抄该方法是否被设置 ACC_SYNCHRONIZED 访问标志。如果设置了该标志,实行线程将先持有 Monitor 对象,然后再实行方法。在该方法运行期间,其它线程将无法获取到该 Mointor 对象,当方法实行完成后,再释放该 Monitor 对象。
synchronized锁的实现

synchronized的对象锁,其指针指向的是一个monitor对象(由C++实现)的起始地址。每个对象实例都会有一个 monitor。其中monitor可以与对象一起创建、销毁;亦或者当线程试图获取对象锁时自动生成。必要注意的是monitor不是Java特有的概念,想了解更多monitor的具体介绍可以查看这篇文章
在HotSpot虚拟机中,最终采用ObjectMonitor类实现monitor。
openjdk\hotspot\src\share\vm\runtime\objectMonitor.hpp源码如下:
  1. ObjectMonitor() {    _count        = 0;    _owner        = null;//指向获得ObjectMonitor对象的线程或底子锁    _EntryList    = NULL ;//处于等候锁block状态的线程,会被加入到entry set;    _WaitSet      = NULL;//处于wait状态的线程,会被加入到wait set;    _WaitSetLock  = 0 ;        _header       = NULL;//markOop对象头    _waiters      = 0,//等候线程数    _recursions   = 0;//重入次数    _object       = NULL;//监视器锁寄生的对象。锁不是平白出现的,而是寄托存储于对象中。    _Responsible  = NULL ;    _succ         = NULL ;    _cxq          = NULL ;    FreeNext      = NULL ;    _SpinFreq     = 0 ;    _SpinClock    = 0 ;    OwnerIsThread = 0 ;// _owner is (Thread *) vs SP/BasicLock    _previous_owner_tid = 0;// 监视器前一个拥有者线程的ID  }
复制代码
当多个线程同时访问一段同步代码时,多个线程会先被存放在 ContentionList 和 _EntryList 集合中,处于 block 状态的线程,都会被加入到该列表。接下来当线程获取到对象的 Monitor 时,Monitor 是依靠底层操作系统的 Mutex Lock 来实现互斥的,线程申请 Mutex 成功,则持有该 Mutex,其它线程将无法获取到该 Mutex,竞争失败的线程会再次进入 ContentionList 被挂起。
如果线程调用 wait() 方法,就会释放当前持有的 Mutex,并且该线程会进入 WaitSet 集合中,等候下一次被唤醒。如果当火线程顺利实行完方法,也将释放 Mutex。

继续深入(锁优化)

我们都知道,对象被创建在堆中。并且对象在内存中的存储结构方式可以分为3块区域:对象头、实例数据、对齐填充。

对于对象头来说,重要是包括俩部门信息Mark Word和Klass Point:

  • Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年事、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码便是4字节,也就是32bit),但是如果对象是数组类型,则必要三个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。


  • 另一部门是类型指针Klass Point:JVM通过这个指针来确定这个对象是哪个类的实例。
锁升级功能重要依赖于 Mark Word 中的锁标志位和释放偏向锁标志位,Synchronized 同步锁就是从偏向锁开始的,随着竞争越来越激烈,偏向锁升级到轻量级锁,最终升级到重量级锁。好了今天就先到这了,锁优化的细节还在码字中。。
参考资料
《深入理解Java虚拟机》 第二版
https://blog.csdn.net/wangyadong317/article/details/84065828
https://blog.csdn.net/zjy15203167987/article/details/82531772
https://www.cnblogs.com/JsonShare/p/11433302.html
https://baijiahao.baidu.com/s?id=1612142459503895416&wfr=spider&for=pc
http://cmsblogs.com/?p=2071
https://www.php.cn/java-article-410323.html

本文由博客一文多发平台 OpenWrite 发布!
文章首发:https://zhuanlan.zhihu.com/lovebell
个人公众号:技术Go
您的点赞与支持是作者持续更新的最大动力!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?用户注册

x

相关技术服务需求,请联系管理员和客服QQ:2753533861或QQ:619920289
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

帖子推荐:
客服咨询

QQ:2753533861

服务时间 9:00-22:00

快速回复 返回顶部 返回列表