JUC并发编程与源码分析(5)

JUC并发编程与源码分析

一、ThreadLocal

1.1 是什么?

在这里插入图片描述
翻译:
此类提供线程局部变量。 这些变量与它们的普通对应变量的不同之处在于,访问一个(通过其 get 或 set 方法)的每个线程都有自己的、独立初始化的变量副本。 ThreadLocal 实例通常是希望将状态与线程相关联的类中的私有静态字段(例如,用户 ID 或事务 ID)。

1.2 能干嘛?

实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不麻烦别人,不和其他人共享,人人有份,人各一份),
主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题。
在这里插入图片描述

1.3 api介绍

在这里插入图片描述

1.4 入门

案例

需求:
三个售票员卖完50张票务,总量完成即可

class MovieTicket {
    
    
    int number = 50;

    public synchronized void saleTicket() {
    
    
        if(number >0) {
    
    
            System.out.println(Thread.currentThread().getName()+"\t"+"---卖出第: "+(number--));
        }else{
    
    
            System.out.println("----卖光了");
        }
    }
}

/**
 * 1  三个售票员卖完50张票务,总量完成即可,吃大锅饭,售票员每个月固定月薪
 */
public class ThreadLocalDemo {
    
    
    public static void main(String[] args) {
    
    
        MovieTicket movieTicket = new MovieTicket();


        for (int i = 1; i <=3; i++) {
    
    
            new Thread(() -> {
    
    
                for (int j = 1; j <=20; j++) {
    
    
                    movieTicket.saleTicket();
                }
            },String.valueOf(i)).start();
        }
    }
}
  • 需求变更,不参加总和计算,各凭销售本事提成,按照出单数各自统计,比如某找房软件,每个中介销售都有自己的销售额指标,自己专属自己的

在这里插入图片描述


class House {
    
    
    private String houseName;

    ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
    public void saleHouse() {
    
    
        Integer value = threadLocal.get();
        ++value;
        threadLocal.set(value);
    }

    ThreadLocal<Integer> threadLocal2 = ThreadLocal.withInitial(() -> 100);
    public void saleHouse2() {
    
    
        Integer value = threadLocal2.get();
        ++value;
        threadLocal2.set(value);
    }
}

/**
 * 2  各个销售自己动手,丰衣足食
 */
public class ThreadLocalDemo {
    
    
    public static void main(String[] args) {
    
    
        House house = new House();

        new Thread(() -> {
    
    
            try
            {
    
    
                for (int j = 1; j <=3; j++) {
    
    
                    house.saleHouse();
                    house.saleHouse2();
                }
                System.out.println(Thread.currentThread().getName()+"\t"+"---卖出: "+house.threadLocal.get());
                System.out.println(Thread.currentThread().getName()+"\t"+"---卖出: "+house.threadLocal2.get());
            }catch (Exception e){
    
    
                e.printStackTrace();
            }finally {
    
    
                house.threadLocal.remove();
                house.threadLocal2.remove();
            }
        },"t1").start();

        new Thread(() -> {
    
    
            try
            {
    
    
                for (int j = 1; j <=5; j++) {
    
    
                    house.saleHouse();
                }
                System.out.println(Thread.currentThread().getName()+"\t"+"---卖出: "+house.threadLocal.get());
            }catch (Exception e){
    
    
                e.printStackTrace();
            }finally {
    
    
                house.threadLocal.remove();
            }
        },"t2").start();

        new Thread(() -> {
    
    
            try
            {
    
    
                for (int j = 1; j <=8; j++) {
    
    
                    house.saleHouse();
                }
                System.out.println(Thread.currentThread().getName()+"\t"+"---卖出: "+house.threadLocal.get());
            }catch (Exception e){
    
    
                e.printStackTrace();
            }finally {
    
    
                house.threadLocal.remove();
            }
        },"t3").start();


        System.out.println(Thread.currentThread().getName()+"\t"+"---卖出: "+house.threadLocal.get());


        House bighouse = new House();

        new Thread(() -> {
    
    
            bighouse.saleHouse();
        },"t1").start();

        new Thread(() -> {
    
    
            bighouse.saleHouse();
        },"t2").start();

    }
}

1.5 ThreadLocal源码分析

Thread,ThreadLocal,ThreadLocalMap 关系

Thread和ThreadLocal

在这里插入图片描述

ThreadLocal和ThreadLocalMap

在这里插入图片描述

三者总概括

在这里插入图片描述
在这里插入图片描述

小总结

近似的可以理解为:
ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以ThreadLocal为Key),不过是经过了两层包装的ThreadLocal对象:
在这里插入图片描述
JVM内部维护了一个线程版的Map<Thread,T>(通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中),每个线程要用到这个T的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量,人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量。

扫描二维码关注公众号,回复: 13495716 查看本文章

1.6 ThreadLocal内存泄漏问题

什么是内存泄漏?

不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。

回顾ThreadLocalMap

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

强软弱虚四大引用

整体架构

在这里插入图片描述
Java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
在这里插入图片描述

强引用

当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收。

强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。

对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,
一般认为就是可以被垃圾收集的了(当然具体回收时机还是要看垃圾收集策略)。

软引用

软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。

对于只有软引用的对象来说,

  • 当系统内存充足时它 不会 被回收,
  • 当系统内存不足时它 会 被回收。

软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!

弱引用

弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短,

对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。

虚引用

虚引用需要java.lang.ref.PhantomReference类来实现。

顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列 (ReferenceQueue)联合使用。

虚引用的主要作用是跟踪对象被垃圾回收的状态。 仅仅是提供了一种确保对象被 finalize以后,做某些事情的机制。 PhantomReference的get方法总是返回null,因此无法访问对应的引用对象。

其意义在于:说明一个对象已经进入finalization阶段,可以被gc回收,用来实现比finalization机制更灵活的回收操作。

换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。

GCRoots和四大引用小总结

在这里插入图片描述

为什么要用弱引用?

public void function01()
{
    
    
    ThreadLocal tl = new ThreadLocal<Integer>();    //line1
    tl.set(2021);                                   //line2
    tl.get();                                       //line3
}

在这里插入图片描述
在这里插入图片描述

当function01方法执行完毕后,栈帧销毁强引用 tl 也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象
若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏;
若这个key引用是弱引用大概率会减少内存泄漏的问题(还有一个key为null的雷)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null
在这里插入图片描述

1 当我们为threadLocal变量赋值,实际上就是当前的Entry(threadLocal实例为key,值为value)往这个threadLocalMap中存放。Entry中的key是弱引用,当threadLocal外部强引用被置为null(tl=null),那么系统 GC 的时候,根据可达性分析,这个threadLocal实例就没有任何一条链路能够引用到它,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

2当然,如果当前thread运行结束,threadLocal,threadLocalMap,Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收。

3 但在实际使用中我们有时候会用线程池去维护我们的线程,比如在Executors.newFixedThreadPool()时创建线程的时候,为了复用线程是不会结束的,所以threadLocal内存泄漏就值得我们小心

虽然弱引用,保证了key指向的ThreadLocal对象能被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现key为null时才会去回收整个entry、value,因此弱引用不能100%保证内存不泄露。我们要在不使用某个ThreadLocal对象后,手动调用remoev方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。

set、get方法会去检查所有键为null的Entry对象

  • set()
    在这里插入图片描述
    在这里插入图片描述

  • get()
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  • remove()
    在这里插入图片描述

结论

从前面的set,getEntry,remove方法看出,在threadLocal的生命周期里,针对threadLocal存在的内存泄漏的问题,
都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法清理掉key为null的脏entry。

最佳实践

用完记得手动remove

总结

  • ThreadLocal 并不解决线程间共享数据的问题
  • ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
  • ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
  • 每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题
  • ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题
  • 都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏,属于安全加固的方法

二、Java对象内存布局

  • Object object = new Object()谈谈你对这句话的理解?
  • 一般而言JDK8按照默认情况下,new一个对象占多少内存空间

2.1 对象在堆内存的布局

在这里插入图片描述
在这里插入图片描述

2.1.1 对象头

对象标记Mark Word

它保存什么?

在这里插入图片描述
在这里插入图片描述
在64位系统中,Mark Word占了8个字节,类型指针占了8个字节,一共是16个字节
在这里插入图片描述

默认存储对象的HashCode、分代年龄和锁标志位等信息。
这些信息都是与对象自身定义无关的数据,所以MarkWord被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。
它会根据对象的状态复用自己的存储空间,也就是说在运行期间MarkWord里存储的数据会随着锁标志位的变化而变化。

类元信息

在这里插入图片描述
对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

对象头多大

在64位系统中,Mark Word占了8个字节,类型指针占了8个字节,一共是16个字节。

2.1.2 实例数据

存放类的属性(Field)数据信息,包括父类的属性信息,
如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。

2.1.3 对齐填充

虚拟机要求对象起始地址必须是8字节的整数倍。
填充数据不是必须存在的,仅仅是为了字节对齐
这部分内存按8字节补充对齐。

2.2 官网

2.3 对象头的MarkWord

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

oop.hpp

在这里插入图片描述

markOop.hpp

在这里插入图片描述

在这里插入图片描述

markword(64位)分布图,锁升级就是对象标记MarkWord里面标志位的变化

在这里插入图片描述

2.4 Object obj = new Object()

JOL证明

pom

<!--
官网:http://openjdk.java.net/projects/code-tools/jol/
定位:分析对象在JVM的大小和分布
-->
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

测试

package com.atguigu.juc.senior.inner.object;

import org.openjdk.jol.vm.VM;


public class MyObject{
    
    
    public static void main(String[] args){
    
    
        //VM的细节详细情况
        System.out.println(VM.current().details());
        //所有的对象分配的字节都是8的整数倍。
        System.out.println(VM.current().objectAlignment());
    }
}

在这里插入图片描述

public class ObjectHeadDemo {
    
    
    public static void main(String[] args) {
    
    

        Object object = new Object();

        //引入了JOL,直接使用
        System.out.println(ClassLayout.parseInstance(object).toPrintable());

        //java5之前 只有重量级锁
        new Thread(() -> {
    
    
            synchronized (object){
    
    
                System.out.println("----hello juc");
            }
        },"t1").start();
    }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

GC年龄采用4位bit存储,最大为15,例如MaxTenuringThreshold参数默认值就是15

参数说明

  • java -XX:+PrintCommandLineFlags -version
默认开启压缩说明
  • -XX:+UseCompressedClassPointers
    在这里插入图片描述
    在这里插入图片描述
  • 上述表示开启了类型指针的压缩,以节约空间,假如不加压缩?
手动关闭压缩再看看
  • -XX:-UseCompressedClassPointers
    在这里插入图片描述
  • 对象包含实例数据测试
    在这里插入图片描述

在这里插入图片描述

三、Synchronized与锁升级

在这里插入图片描述

  • 用锁能够实现数据的安全性,但是会带来性能下降。
  • 无锁能够基于线程并行提升程序性能,但是会带来安全性下降
    在这里插入图片描述

3.1 Synchronized的性能变化

java5以前,只有Synchronized,这个是操作系统级别的重量级操作

  • java5以前,只有Synchronized,这个是操作系统级别的重量级操作

Java5之前,用户态和内核态之间的切换

在这里插入图片描述
java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。

在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,挂起线程和恢复线程都需要转入内核态去完成,阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长”,时间成本相对较高,这也是为什么早期的synchronized效率低的原因
Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁

为什么每一个对象都可以成为一个锁?

markOop.hpp

在这里插入图片描述
Monitor可以理解为一种同步工具,也可理解为一种同步机制,常常被描述为一个Java对象。Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。

在这里插入图片描述
Monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的转换,成本非常高。

Monitor(监视器锁)

在这里插入图片描述
Mutex Lock
Monitor是在jvm底层实现的,底层代码是c++。本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的转换,状态转换需要耗费很多的处理器时间成本非常高。所以synchronized是Java语言中的一个重量级操作。

Monitor与java对象以及线程是如何关联 ?
1.如果一个java对象被某个线程锁住,则该java对象的Mark Word字段中LockWord指向monitor的起始地址
2.Monitor的Owner字段会存放拥有相关联对象锁的线程id

Mutex Lock 的切换需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间。

java6开始,优化Synchronized

  • Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁
  • 需要有个逐步升级的过程,别一开始就捅到重量级锁

3.2 Synchronized锁种类及升级步骤

  • 案例: 买票50张

多线程访问情况,3种

  • 只有一个线程来访问,有且唯一Only One
  • 有2个线程A、B来交替访问
  • 竞争激烈,多个线程来访问

升级流程

  • synchronized用的锁是存在Java对象头里的Mark Word中锁升级功能主要依赖MarkWord中锁标志位和释放偏向锁标志位
  • 64位标记图

在这里插入图片描述

无锁

public class MyObject
{
    
    
    public static void main(String[] args)
    {
    
    
        Object o = new Object();

        System.out.println("10进制hash码:"+o.hashCode());
        System.out.println("16进制hash码:"+Integer.toHexString(o.hashCode()));
        System.out.println("2进制hash码:"+Integer.toBinaryString(o.hashCode()));

        System.out.println( ClassLayout.parseInstance(o).toPrintable());
    }
}

在这里插入图片描述
在这里插入图片描述

偏向锁

当一段同步代码一直被同一个线程多次访问,由于只有一个线程那么该线程在后续访问时便会自动获得锁

Hotspot 的作者经过研究发现,大多数情况下:

多线程的情况下,锁不仅不存在多线程竞争,还存在锁由同一线程多次获得的情况

偏向锁就是在这种情况下出现的,它的出现是为了解决只有在一个线程执行同步时提高性能

64位标记图

  • 通过CAS方式修改markword中的线程ID
    在这里插入图片描述

偏向锁的持有

理论落地:
在实际应用运行过程中发现,“锁总是同一个线程持有,很少发生竞争”,也就是说锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程
那么只需要在锁第一次被拥有的时候,记录下偏向线程ID。这样偏向线程就一直持有着锁(后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁)。
如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程ID与当前线程ID是否一致,如果一致直接进入同步。无需每次加锁解锁都去CAS更新对象头。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
假如不一致意味着发生了竞争,锁已经不是总是偏向于同一个线程了,这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。

技术实现:
一个synchronized方法被一个线程抢到了锁时,那这个方法所在的对象就会在其所在的Mark Word中将偏向锁修改状态位,同时还
会有占用前54位来存储线程指针作为标识。若该线程再次访问同一个synchronized方法时,该线程只需去对象头的Mark Word 中去判断一下是否有偏向锁指向本身的ID,无需再进入 Monitor 去竞争对象了。

细化案例细化案例Account对象举例说明

偏向锁的操作不用直接捅到操作系统,不涉及用户到内核转换,不必要直接升级为最高级,我们以一个account对象的“对象头”为例,
在这里插入图片描述
假如有一个线程执行到synchronized代码块的时候,JVM使用CAS操作把线程指针ID记录到Mark Word当中,并修改标偏向标示,标示当前线程就获得该锁。锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。
在这里插入图片描述
这时线程获得了锁,可以执行同步代码块。当该线程第二次到达同步代码块时会判断此时持有锁的线程是否还是自己(持有锁的线程ID也在对象头里),JVM通过account对象的Mark Word判断:当前线程ID还在,说明还持有着这个对象的锁,就可以继续进入临界区工作。由于之前没有释放锁,这里也就不需要重新加锁。 如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

结论:JVM不用和操作系统协商设置Mutex(争取内核),它只需要记录下线程ID就标示自己获得了当前锁,不用操作系统接入。
上述就是偏向锁:在没有其他线程竞争的时候,一直偏向偏心当前线程,当前线程可以一直执行。

偏向锁JVM命令

  • java -XX:+PrintFlagsInitial |grep BiasedLock
    在这里插入图片描述

Code演示

  • 默认
public class MyObject
{
    
    
    public static void main(String[] args)
    {
    
    
        Object o = new Object();

        new Thread(() -> {
    
    
            synchronized (o){
    
    
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        },"t1").start();
    }
}

  • 会发现偏向锁没有效果,是因为偏向锁默认有4秒延迟
    在这里插入图片描述
  • 手动指定jvm参数
    在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

  • 手动指定延时参数,启用该功能, 再次测试
  • -XX:BiasedLockingStartupDelay=0
    在这里插入图片描述

偏向锁的撤销

  • 当有另外线程逐步来竞争锁的时候,就不能再使用偏向锁了,要升级为轻量级锁
  • 竞争线程尝试CAS更新对象头失败,会等待到全局安全点(此时不会执行任何代码)撤销偏向锁。

偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。
撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行:

① 第一个线程正在执行synchronized方法(处于同步块),它还没有执行完,其它线程来抢夺,该偏向锁会被取消掉并出现锁升级
此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。
② 第一个线程执行完成synchronized方法(退出同步块),则将对象头设置成无锁状态并撤销偏向锁,重新偏向 。
在这里插入图片描述

总体步骤如下

在这里插入图片描述

轻量级锁

主要作用

  • 有线程来参与锁的竞争,但是获取锁的冲突时间极短
  • 本质就是自旋锁

64位标记图

在这里插入图片描述

轻量级锁的获取

轻量级锁是为了在线程近乎交替执行同步块时提高性能。
主要目的: 在没有多线程竞争的前提下,通过CAS减少重量级锁使用操作系统互斥量产生的性能消耗,说白了先自旋再阻塞。
升级时机: 当关闭偏向锁功能或多线程竞争偏向锁会导致偏向锁升级为轻量级锁

假如线程A已经拿到锁,这时线程B又来抢该对象的锁,由于该对象的锁已经被线程A拿到,当前该锁已是偏向锁了。
而线程B在争抢时发现对象头Mark Word中的线程ID不是线程B自己的线程ID(而是线程A),那线程B就会进行CAS操作希望能获得锁。
此时线程B操作中有两种情况:
如果锁获取成功,直接替换Mark Word中的线程ID为B自己的ID(A → B),重新偏向于其他线程(即将偏向锁交给其他线程,相当于当前线程"被"释放了锁),该锁会保持偏向锁状态,A线程Over,B线程上位;
在这里插入图片描述
如果锁获取失败,则偏向锁升级为轻量级锁,此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程B会进入自旋等待获得该轻量级锁。
在这里插入图片描述

Code演示

  • 如果关闭偏向锁,就可以直接进入轻量级锁
  • -XX:-UseBiasedLocking
    在这里插入图片描述

步骤流程如图所示

在这里插入图片描述

自旋达到一定次数和程度

java6之前
  • 默认启用,默认情况下自旋的次数是 10 次
  • 或者自旋线程数超过cpu核数一半
  • -XX:PreBlockSpin=10来修改
Java6之后
  • 自适应
  • 自适应意味着自旋的次数不是固定不变的
  • 根据同一个锁上一次自旋的时间以及拥有锁线程的状态来决定。

轻量锁与偏向锁的区别和不同

  • 争夺轻量级锁失败时,自旋尝试抢占锁
  • 轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁

重量级锁

  • 有大量的线程参与锁的竞争,冲突性很高

锁标志位

在这里插入图片描述

Code演示

在这里插入图片描述

总结

  • 各种锁优缺点、synchronized锁升级和实现原理
    在这里插入图片描述
    synchronized锁升级过程总结:一句话,就是先自旋,不行再阻塞。
    实际上是把之前的悲观锁(重量级锁)变成在一定条件下使用偏向锁以及使用轻量级(自旋锁CAS)的形式

synchronized在修饰方法和代码块在字节码上实现方式有很大差异,但是内部实现还是基于对象头的MarkWord来实现的。
JDK1.6之前synchronized使用的是重量级锁,JDK1.6之后进行了优化,拥有了无锁->偏向锁->轻量级锁->重量级锁的升级过程,而不是无论什么情况都使用重量级锁。

偏向锁:适用于单线程适用的情况,在不存在锁竞争的时候进入同步方法/代码块则使用偏向锁。
轻量级锁:适用于竞争较不激烈的情况(这和乐观锁的使用范围类似), 存在竞争时升级为轻量级锁,轻量级锁采用的是自旋锁,如果同步方法/代码块执行时间很短的话,采用轻量级锁虽然会占用cpu资源但是相对比使用重量级锁还是更高效。
重量级锁:适用于竞争激烈的情况,如果同步方法/代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就需要升级为重量级锁。

3.3 JIT编译器对锁的优化

  • JIT(Just In Time Compiler),一般翻译为即时编译器

锁消除

/**
 * 锁消除
 * 从JIT角度看相当于无视它,synchronized (o)不存在了,这个锁对象并没有被共用扩散到其它线程使用,
 * 极端的说就是根本没有加这个锁对象的底层机器码,消除了锁的使用
 */
public class LockClearUPDemo
{
    
    
    static Object objectLock = new Object();//正常的

    public void m1()
    {
    
    
        //锁消除,JIT会无视它,synchronized(对象锁)不存在了。不正常的
        Object o = new Object();

        synchronized (o)
        {
    
    
            System.out.println("-----hello LockClearUPDemo"+"\t"+o.hashCode()+"\t"+objectLock.hashCode());
        }
    }

    public static void main(String[] args)
    {
    
    
        LockClearUPDemo demo = new LockClearUPDemo();

        for (int i = 1; i <=10; i++) {
    
    
            new Thread(() -> {
    
    
                demo.m1();
            },String.valueOf(i)).start();
        }
    }
}
 
 

锁粗化

/**
 * 锁粗化
 * 假如方法中首尾相接,前后相邻的都是同一个锁对象,那JIT编译器就会把这几个synchronized块合并成一个大块,
 * 加粗加大范围,一次申请锁使用即可,避免次次的申请和释放锁,提升了性能
 */
public class LockBigDemo
{
    
    
    static Object objectLock = new Object();


    public static void main(String[] args)
    {
    
    
        new Thread(() -> {
    
    
            synchronized (objectLock) {
    
    
                System.out.println("11111");
            }
            synchronized (objectLock) {
    
    
                System.out.println("22222");
            }
            synchronized (objectLock) {
    
    
                System.out.println("33333");
            }
        },"a").start();

        new Thread(() -> {
    
    
            synchronized (objectLock) {
    
    
                System.out.println("44444");
            }
            synchronized (objectLock) {
    
    
                System.out.println("55555");
            }
            synchronized (objectLock) {
    
    
                System.out.println("66666");
            }
        },"b").start();

    }
}
 
 

猜你喜欢

转载自blog.csdn.net/qq_43478625/article/details/121584140