线程安全问题

  • 线程不安全:

    • 当多线程并发访问临界资源时(可共享的对象),如果破坏原子操作,可能会造成数据不一致。
    • 临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可以保证其正确性。
    • 原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可打乱或缺省。

同步方式(1)

  • 同步代码块:
synchronized(临界资源对象){ //对临界资源加锁
//代码(原子操作)
}

​ 注:

每个对象都有一个互斥锁标记,用来分配给线程的。

​ 只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步代码块。

​ 线程退出同步代码块时,会释放响应的互斥锁标记。

线程的状态(阻塞)

同步方式(2)

  • 同步方法:
synchronized 返回值类型 方法名称(形参列表0){ //对当前对象(this)加锁
//代码(原子操作)
}

注:只有拥有对象互斥锁标记的线程,才能进入该对象加锁的同步方法中。

​ 线程退出同步方法时,会释放相应的互斥锁标记。

特殊情况1

这种情况下方法由static修饰,因此synchronized锁的对象是类对象.(所谓类对象就是Java虚拟机中的一个模板对象,全局只有一个)

class Number{
public static synchronized void a(){
sleep(1);
System.out.Println("1");
}
public static synchronized void b(){
System.out.Println("2");
}
} public static void main(String[] args){
Number n1 = new Number();
new Thread(() -> {n1.a();}).start();
new Thread(() -> {n1.b();}).start();
}

特殊情况2

这个时候b方法没有被static修饰,因此synchronized锁的对象是 this,也就是创建的实例对象,因此两个线程是同时执行

class Number{
public static synchronized void a(){
sleep(1);
System.out.Println("1");
}
public synchronized void b(){
System.out.Println("2");
}
} public static void main(String[] args){
Number n1 = new Number();
new Thread(() -> {n1.a();}).start();
new Thread(() -> {n1.b();}).start();
}

特殊情况3

与上面情况相似,a方法被static修饰,锁的是类对象,b方法锁的是 this 实例对象,这个时候创建两个实例对象,会同时执行 a,b方法

class Number{
public static synchronized void a(){
sleep(1);
System.out.Println("1");
}
public synchronized void b(){
System.out.Println("2");
}
} public static void main(String[] args){
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> {n1.a();}).start();
new Thread(() -> {n2.b();}).start();
}

特殊情况4

这时候 a,b方法都被static修饰,锁的是类对象,因此即使创建了两个实例对象,方法访问的依然是同一个对象即全局唯一的类对象,这时候会先执行a或先执行b

class Number{
public static synchronized void a(){
sleep(1);
System.out.Println("1");
}
public static synchronized void b(){
System.out.Println("2");
}
} public static void main(String[] args){
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> {n1.a();}).start();
new Thread(() -> {n2.b();}).start();
}

同步规则

  • 注意:

    • 只有在调用包含同步代码块的方法,或者同步方法时,才需要对象的锁标记。
    • 如果调用不包含同步代码块的方法,或者普通方法时,则不需要锁标记,可直接调用。
  • 已知JDK中线程安全的类:
    • StringBuffer
    • Vector
    • Hashtable
    • 以上类中的公开方法,均为synchronize修饰的同步方法。

Java对象头

以32位虚拟机为例

普通对象

  • Klass Word包含了对象的类元信息

数组对象

其中Mark Word结构为

synchronized运行原理

Monitor

下图的Monitor被翻译为监视器或管程(由操作系统提供)

每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针(即上图中的Heavyweight Locked指向monitor)

  • 刚开始Monitor中Owner为null
  • 当Thread-2执行到synchronized(obj)就会将Monitor的所有者Owner置为Thread-2,Monitor中只能有一个Owner
  • 在Thread-2上锁的过程中,如果Thread-3,Thread-4,Thread-5也来执行synchronized(obj),就会进入EntryList BLOCKED
  • Thread-2执行完同步代码块的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争是非公平的
  • 图中WaitSet中的Thread-0,Thread-1是之前获得过锁,但条件不满足进入WAITING状态的线程

注意:

  • Synchronized必须是进入同一个对象的monitor才会有上述效果
  • 不加synchronized的对象不会关联监视器,不遵从上述规则

为什么说重量级锁开销大呢

主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。

这就是说为什么重量级线程开销很大的。


synchronized锁优化

轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(无竞争),那么可以使用轻量级锁来优化

轻量级锁对使用者是透明的,即语法仍然是synchronized

例如:

假设有两个方法同步块,利用同一个对象加锁

static final Object obj = new Object();
public static void method1(){
synchronized(obj){
//同步块A
method2();
}
public static void method1(){
synchronized(obj){
//同步块B
}
}
  • 当执行到method1()时,会创建一个锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word

MarkWord的结构:

  • 接下来会让锁记录中的Object reference指向锁对象,并尝试用cas替换Object的Mark word, 将 Mark Word的值存入锁记录

  • 如果cas替换成功,对象头中存储了锁记录地址和状态 00,表示由该线程给对象加锁,将来解锁的时候再将Mark word 恢复回去

  • 如果cas失败,有两种情况

    • 如果是其他线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程
    • 如果是自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数

发现object已经加锁了,但是锁的地址是自己,那么再添加一条Lock Record作为重入的计数

  • 当退出synchronized代码块(解锁)时,如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数-1

  • 当退出synchronized代码块(解锁)时,锁记录的值不为null,这时使用cas将Mark Word的值恢复给对象头

    • 成功,则解锁成功
    • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

锁膨胀

如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

static Object obj = new Object();

public static void method1(){

    synchronized(obj){

        //同步代码块
}
}
  • 当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁

  • 这时Thread-1加轻量级锁失败,进入锁膨胀流程

    • 即为Object对象申请Monitor锁,让Object指向重量级锁地址
    • 然后自己进入Monitor的EntryList BLOCKED

  • 当Thread-0退出同步代码块解锁时,使用cas将Mark Word的值恢复给对象头,失败

    • 这时会进入重量级解锁流程,即按照Monitor地址找到Minitor对象,设置Owner为null,唤醒EntryList中BLOCKED线程

自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经推出了同步块,释放了锁),这时当前线程就可以避免阻塞.

自旋重试成功的情况:

注意:

​ 只有多核cpu下,自旋优化才有意义,因为单核下没有多余cpu可以给其他线程用来自旋了

自旋重试失败的情况:

  • 在Java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋
  • Java7之后不能控制是否开启自旋功能

偏向锁

轻量级锁在没有竞争时,每次重入仍然需要执行CAS操作

Java6中引入了偏向锁来进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归线程所有

例如:

static final Object obj = new Object();
public static void method1(){
synchronized(obj){
//同步块A
method2();
}
public static void method1(){
synchronized(obj){
//同步块B
method3();
}
public static void method3(){
synchronized(obj){
//同步块C
}
}


偏向状态

一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,Mark Word值为0x05即最后3位为101,这时它的thread,epoch,age都为0
  • 偏向锁是默认延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数-XX:BiasedLockingStartupDelay=0来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,Mark Word值为0x01即最后3位位001,这时它的hashcode,age都为0,第一次用到hashcode时才会赋值
  • 同时可以通过添加VM参数:-XX:-UseBiasedLocking来禁用偏向锁
  • 如果说在加锁之前调用了对象的hashCode()方法,此时会禁用这个对象的偏向锁
    • 因为哈希码用的时候才产生,默认是0;只有第一次调用对象的hashcode,对象的哈希码才会产生,才在对象头的Mark Word里填充哈希码,只有当Mark Word处于正常状态时才能填充hashCode,因此会禁用掉Biased(偏向锁)
  • 当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁

批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏内T2,重偏向会重置对象的Thread ID

当撤销偏向锁阈值超过20次后,JVM会认为自己是不是偏向错了,于是会再给这些对象加锁时重新偏向至加锁线程

批量撤销

当撤销偏向锁阈值超过40次之后,JVM就会觉得自己确实偏向错了,根本就不该偏向.于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的

锁清除

我们知道Java程序是一边执行一边编译的,在JVM即时编译过程中会去扫描这些代码,做逃逸分析,看看那些对象是线程安全的,那么对于这些对象的访问就不用做同步操作,减少锁带来的性能损失。

逃逸分析具体有对线程的逃逸分析和对方法的逃逸分析

拿方法逃逸举例,JVM检测某个对象在方法中被定义之后,会不会作为参数被外部方法引用

​ 如果不会,表示该对象是线程安全的,那对象不会逃逸到方法之外,那么对该变量的访问就可以不做同步操作。

事实上JVM就是这么做,在程序中对于一些不存在竞争的代码段,减少同步操作可以提升执行速度,用逃逸分析来得出那些不存在竞争的代码段,然后消除一些锁操作。

逃逸分析可以用参数-XX:+EscapeAnalysis来开启,然后参数-XX:+EliminateLocks来开启锁消除

锁粗化

大部分情况下,我们对一段程序加锁需要尽量缩小范围,因为如果范围太大,执行时间也会增加。但是也有特殊情况,我们看看下面这种情况。

public class LockDemo {
public void fun() {
for (int i = 0; i < 10000; ++i) {
synchronized(LockDemo.class) {
System.out.println("test...");
}
}
}
}

上面的for循环整个过程都在频繁加锁和释放锁,非常耗费性能,因此JIT将会把加锁的过程优化到for循环外面,如下面的代码所示,这就是锁粗化的过程。

public class LockDemo {
public void fun() {
synchronized(LockDemo.class) {
for (int i = 0; i < 10000; ++i) {
System.out.println("test...");
}
}
}
}

synchronized运行原理以及优化的更多相关文章

  1. synchronized实现原理及其优化-(自旋锁,偏向锁,轻量锁,重量锁)

    1.synchronized概述: synchronized修饰的方法或代码块相当于并发中的临界区,即在同一时刻jvm只允许一个线程进入执行.synchronized是通过锁机制实现同一时刻只允许一个 ...

  2. synchronized原理及优化,(自旋锁,锁消除,锁粗化,偏向锁,轻量级锁)

    偏向锁:不占用CPU自旋锁:占用CPU.代码执行成本比较低且线程数少时,可以使用 .不经过OS.内核态,效率偏低 理解Java对象头与Monitor 在JVM中,对象在内存中的布局分为三块区域:对象头 ...

  3. Synchronized用法原理和锁优化升级过程(面试)

    简介 多线程一直是面试中的重点和难点,无论你现在处于啥级别段位,对synchronized关键字的学习避免不了,这是我的心得体会.下面咱们以面试的思维来对synchronized做一个系统的描述,如果 ...

  4. synchronized实现原理

    线程安全是并发编程中的重要关注点,应该注意到的是,造成线程安全问题的主要诱因有两点,一是存在共享数据(也称临界资源),二是存在多条线程共同操作共享数据.因此为了解决这个问题,我们可能需要这样一个方案, ...

  5. jdk源码剖析二: 对象内存布局、synchronized终极原理

    很多人一提到锁,自然第一个想到了synchronized,但一直不懂源码实现,现特地追踪到C++层来剥开synchronized的面纱. 网上的很多描述大都不全,让人看了不够爽,看完本章,你将彻底了解 ...

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

    深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoader) 深入 ...

  7. 深入理解并发编程之----synchronized实现原理

    版权声明:本文为博主原创文章,请尊重原创,未经博主允许禁止转载,保留追究权 https://blog.csdn.net/javazejian/article/details/72828483 [版权申 ...

  8. Java并发(五):synchronized实现原理

    一.synchronized用法 Java中的同步块用synchronized标记. 同步块在Java中是同步在某个对象上(监视器对象). 所有同步在一个对象上的同步块在同时只能被一个线程进入并执行操 ...

  9. synchronized底层原理

    synchronized底层语义原理 Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现. 在 Java 语言中,同步用的最多的地方可能是被 syn ...

随机推荐

  1. python3 base64

    import base64s='hello world'bytes_by_s=s.encode() #将字符串编码-->字节码,b64_encode_bytes=base64.b64encode ...

  2. k8s多集群配置管理平台

    k8s多集群配置管理平台 临时集群特性 模拟生产环境 整体环境说明 内网:10.17.1.44 [root@localhost account-server]# kubectl get nodes N ...

  3. CSS3常见动画

    一.是什么 CSS动画(CSS Animations)是为层叠样式表建议的允许可扩展标记语言(XML)元素使用CSS的动画的模块 即指元素从一种样式逐渐过渡为另一种样式的过程 常见的动画效果有很多,如 ...

  4. Leedcode算法专题训练(二分查找)

    二分查找实现 非常详细的解释,简单但是细节很重要 https://www.cnblogs.com/kyoner/p/11080078.html 正常实现 Input : [1,2,3,4,5] key ...

  5. Buuctf刷题:部分

    get_started_3dsctf_2016 关键词:ROP链.栈溢出.mprotect()函数 可参考文章(优质): https://www.cnblogs.com/lyxf/p/12113401 ...

  6. Object.assign()和解构赋值:给对象赋值的两种方法

    一.Object.assign()方法给对象赋值 Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象.它将返回目标对象. 拷贝的是属性值 如果目标对象中的属性 ...

  7. 让你的Windows/Linux玩上Switch!

    1 前言 某天在Github上面看到了两个Switch的模拟器: yuzu Ryujinx 于是就想动手想尝试一下在Linux上面玩上Switch. 本文首先简单介绍一下两个模拟器,接着是两个模拟器的 ...

  8. 认识Git并了解Git的基本知识

    目录 认识Git 版本控制 版本控制的发展史 安装Git Git的核心概念 Git的使用原理 Git的工作流程 Git的基本流程 Git与SVN的区别 Git的基本使用 初始化Git 创建一个Git仓 ...

  9. JDBC_09_Statement 和 PreparedStatement

    Statement 和 PreparedStatement * Statement数据库操作对象存在SQL注入问题,而PreparedStatement预编译数据库操作对象解决了SQL注入问题. * ...

  10. CPF 入门教程 - 控件布局(六)

    CPF netcore跨平台桌面UI框架 系列教程 CPF 入门教程(一) CPF 入门教程 - 数据绑定和命令绑定(二) CPF 入门教程 - 样式和动画(三) CPF 入门教程 - 绘图(四) C ...