3.java多线程——线程安全

java中的各种锁

下图为java中各种锁的分类情况:
图片来源:https://tech.meituan.com/2018/11/15/java-lock.html
图片:

下表为各种锁的应用关键字
表格来源:https://www.cnblogs.com/lanqingzhou/p/13723584.html

序号锁名称应用
1乐观锁CAS
2悲观锁synchronized、vector、hashtable
3自旋锁CAS
4可重入锁synchronized、Reentrantlock、Lock
5读写锁ReentrantReadWriteLock,CopyOnWriteArrayList、CopyOnWriteArraySet
6公平锁Reentrantlock(true)
7非公平锁synchronized、reentrantlock(false)
8共享锁ReentrantReadWriteLock中读锁
9独占锁synchronized、vector、hashtable、ReentrantReadWriteLock中写锁
10重量级锁synchronized
11轻量级锁锁优化技术
12偏向锁锁优化技术
13分段锁concurrentHashMap
14互斥锁synchronized
15同步锁synchronized
16死锁 相互请求对方的资源
17锁粗化锁优化技术
18锁消除锁优化技术

synchronized

简单案例

先来看下这个案例,用两个线程对num变量执行2千万次+1,看看结果是否为2千万。
类A:

public class A {
    private int num = 0;
    public void increase(){
        num++;
    }
    public int getNum(){
        return num;
    }
}

类LockTest :

public class LockTest {
    public static void main(String[] args) throws InterruptedException {
         A a = new A();
        long start = System.currentTimeMillis();
        Thread t1 = new Thread(()->{
            for(int i=0;i<10000000;i++){
                a.increase();
            }
        });
        t1.start();
        for(int i=0; i < 10000000; i++){
            a.increase();
        }
        t1.join();
        long end = System.currentTimeMillis();
        System.out.println(String.format("%sms", end - start));
        System.out.println(a.getNum());
    }
}

执行上面代码,结果是否为2千万呢?如下:
图片:
这个是线程不安全的典型例子,那么怎么要使得线程安全呢?可以给A#increase()方法加把锁,如下:

// 非静态成员方法上加锁,实际上锁的是方法所属对象
// 静态成员方法加锁,锁的是类.class
public synchronized void increase(){
    num++;
}
// 或者:
public void increase(){
    synchronized (this){
        num++;
    }
}

接下来再执行上面的代码,运行结果如下,发现是我们想要的结果
图片:
总结:
互斥锁、悲观锁、同步锁、重量级锁。
jdk1.6之前没有优化:线程阻塞、上下文切换,操作系统线程调度,特别耗费资源,耗费时间。

锁优化

jdk1.6之后开始对synchronized底层开始做大量优化。下面是synchronized在实际应用中锁升级的过程图。
图片:

  • 无状态:即,没有加锁。
  • 偏向锁:锁优化技术。当一段代码(一个程序)在绝大数时候只有一个线程执行,偶尔会有多线程同时执行的情况。那么把常执行的这个线程id保存到加锁对象的对象头markword里面(例如,上面案例中,会把线程id保存到this对象的markword里面)。每次只要判断当前线程id和加锁对象的对象头markword里面保存的线程id是否一致。若一致,不加锁直接继续执行;若不一致,则加锁。
  • 轻量级锁:一般指的就是cas,锁优化技术。
  • 重量级锁:这里是synchronized(没有优化)。

附加:java对象组成

在分析锁升级之前先看看java的对象组成
java对象内部组成如下图:
图片:

java对象内部共分为3部分:对象头、实例数据、对齐填充位。对象头:
mark word:包含对象的哈希码,锁的一些相关存储;
元数据指针:类对象存在堆里,.class文件(类文件)存在元数据里。类对象的对象头的元数据指针 指向 元数据里面存储的类文件。

各种锁在对象头的mark word存储占位情况。
图片:

锁升级代码演示

先添加个依赖,如下:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.11</version>
</dependency>
(1). 无状态
public static void main(String[] args) throws InterruptedException {
        // User类有两个成员变量:id(Integer), name(String)
        User userTemp = new User();
        System.out.println("无状态(001):" + ClassLayout.parseInstance(userTemp).toPrintable());
    }

运行后控制台打印如下(下面的表格是我截图工具人为画出来的):
图片:

表格第1行为表头
第2~4行为对象头(Object header);size列单位为Byte;
value列对应单元格内容:括号外为4个16进制,括号里面是对应的4个二进制。
第5,6行为实例数据,类User成员变量id、name的初始值都为null
第7行,即最后一行为对齐填充位:填充了4Byte。

这个对象一共占用空间24字节。为什么后面要再填充法4个字节,凑齐24Byte呢?
因为我们当前所用电脑基本都是64位的,在内存里面排列时每8个字节(64位)划分为一行。为了满足最优寻址,对象所占空间大小能被8整除。这里再填充4个字节刚好凑够24个字节在内存里面刚好占3行(3 = 24 / 8)。寻址快。

无状态的(001)体现在这里,顺序刚好从右到左与上面的64位虚拟机锁的占位情况对应上。
图片:

(2). 偏向锁
public static void main(String[] args) throws InterruptedException {
   User userTemp = new User();
   System.out.println("无状态(001):" + ClassLayout.parseInstance(userTemp).toPrintable());
    /*jvm默认延时4s自动开启偏向锁,可通过-XX:BiasedLockingStartupDelay=0 取消延时
    如果不要偏向锁,可通过-XX:-UseBiasedLocking = false 来设置*/
   Thread.sleep(5000);
   User user = new User();
   System.out.println("启用偏向锁(101):" + ClassLayout.parseInstance(user).toPrintable());
   for (int i = 0; i < 2; i++) {
       synchronized (user) {
           System.out.println("使用偏向锁(101)(带线程id):" + ClassLayout.parseInstance(user).toPrintable());
       }
       System.out.println("释放偏向锁(101)(带线程id):" + ClassLayout.parseInstance(user).toPrintable());
   }
}

运行后,控制台部分日志如下:
图片:
上面截图中红色框为锁的标志位,绿色框为线程id存储的地方(没有保存线程id时全为0)

(3). 轻量级锁
public static void main(String[] args) throws InterruptedException {
        User userTemp = new User();
        System.out.println("无状态(001):" + ClassLayout.parseInstance(userTemp).toPrintable());

         /*jvm默认延时4s自动开启偏向锁,可通过-XX:BiasedLockingStartupDelay=0 取消延时
         如果不要偏向锁,可通过-XX:-UseBiasedLocking = false 来设置*/
        Thread.sleep(5000);
        User user = new User();
        System.out.println("启用偏向锁(101):" + ClassLayout.parseInstance(user).toPrintable());

        for (int i = 0; i < 2; i++) {
            synchronized (user) {
                System.out.println("使用偏向锁(101)(带线程id):" + ClassLayout.parseInstance(user).toPrintable());
            }
            System.out.println("释放偏向锁(101)(带线程id):" + ClassLayout.parseInstance(user).toPrintable());
        }

        // 创建了个新线程对user对象加锁
        new Thread(()->{
            synchronized (user){
                System.out.println("轻量级锁(00):" + ClassLayout.parseInstance(user).toPrintable());
            }
            System.out.println("释放轻量级锁(00):" + ClassLayout.parseInstance(user).toPrintable());
        }).start();
}

运行后,控制台部分日志如下:
图片:
上面截图中红色框为 锁状态位00,其他位保存指向栈帧中记录的指针。

(4). 重量级锁

除了主线程外,新创建个线程(名称:线程1)给user对象加锁,休眠3秒,此时,再创建一个线程(名称:线程2)给user对象加锁。多个线程(共3个)竞争锁资源,使用重度锁。

public static void main(String[] args) throws InterruptedException {
    User userTemp = new User();
    System.out.println("无状态(001):" + ClassLayout.parseInstance(userTemp).toPrintable());

     /*jvm默认延时4s自动开启偏向锁,可通过-XX:BiasedLockingStartupDelay=0 取消延时
     如果不要偏向锁,可通过-XX:-UseBiasedLocking = false 来设置*/
    Thread.sleep(5000);
    User user = new User();
    System.out.println("启用偏向锁(101):" + ClassLayout.parseInstance(user).toPrintable());

    for (int i = 0; i < 2; i++) {
        synchronized (user) {
            System.out.println("使用偏向锁(101)(带线程id):" + ClassLayout.parseInstance(user).toPrintable());
        }
        System.out.println("释放偏向锁(101)(带线程id):" + ClassLayout.parseInstance(user).toPrintable());
    }

    // 创建了个新线程对user对象加锁
    new Thread(()->{
        synchronized (user){
            System.out.println("轻量级锁(00):" + ClassLayout.parseInstance(user).toPrintable());
            try{
                System.out.println("睡眠3秒钟=====================");
                Thread.sleep(3000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }, "线程1").start();

    // 再创建个新线程对user对象加锁
    Thread.sleep(1000);
    new Thread(() -> {
        synchronized(user){
            System.out.println("重量级锁(10):" + ClassLayout.parseInstance(user).toPrintable());
        }
    }, "线程2").start();
}

运行后,控制台部分日志如下:
图片:
上面截图中红色框为 锁状态位00,其他位保存指向重量级锁的指针。

锁升级问题

上面示例中,为什么才2,3个线程竞争资源,锁就升级到重量级锁了?
jdk1.8就是这样实现的。后续版本可能会持续优化。

源码看锁升级

我们把之前的A类编译后文件A.class进行反编译。A类代码如下:

public class A {
    private AtomicInteger atomicInteger = new AtomicInteger(0);
    int num = 0;
    public void increase(){
        synchronized (this){
            num++;
        }
    }
    public int getNum(){
        return num;
    }
}

找到A.class文件,执行:javap -c A.class,反汇编代码如下:
图片:

注意:synchronized代码块主要是靠monitorenter和monitorexit这两个原语来实现同步的。
monitor介绍:每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.这里涉及重入锁,如果一个线程获得了monitor,他可以再获取无数次,进入的时候monito+1,退出-1,直到为0,开可以被其他线程获取
如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

源码情况可以参考上面“jvm有序性—>内存屏障—>jvm底层源码”章节。
在src\share\vm\interpreter\interpreterRuntime.cpp找到monitorenter方法如下:
图片: https://uploader.shimo.im/f/pFOjdbEyyKS39XUS.png!thumbnail?accessToken=eyJhbGciOiJIUzI1NiIsImtpZCI6ImRlZmF1bHQiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJhY2Nlc3NfcmVzb3VyY2UiLCJleHAiOjE2MjgyMzgyMDUsImciOiJyR3F5UjNHeGtKNnl2cUQ5IiwiaWF0IjoxNjI4MjM3OTA1LCJ1c2VySWQiOjE0NzE1NTM0fQ.cZUECJqUPuAwkGv5DzgTqFGFq62JR6eRA4ITsFeiyhg

如果使用了偏向锁,在src\share\vm\runtime\synchronizer.cpp文件中。
图片: https://uploader.shimo.im/f/s9bOIyfzqTONHUc7.png!thumbnail?accessToken=eyJhbGciOiJIUzI1NiIsImtpZCI6ImRlZmF1bHQiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJhY2Nlc3NfcmVzb3VyY2UiLCJleHAiOjE2MjgyMzgyMDUsImciOiJyR3F5UjNHeGtKNnl2cUQ5IiwiaWF0IjoxNjI4MjM3OTA1LCJ1c2VySWQiOjE0NzE1NTM0fQ.cZUECJqUPuAwkGv5DzgTqFGFq62JR6eRA4ITsFeiyhg

如果没有使用偏向锁,在src\share\vm\runtime\synchronizer.cpp文件中。
图片:

cas

什么是cas

看上面“synchronized”章节下的"简单案例”部分,除了给A#increase()方法加锁(synchronized)还有什么方法可以保证线程安全呢?看下面代码:

public class A {
    // 原子操作类
    private AtomicInteger num = new AtomicInteger(0);
    public void increase(){
            num.incrementAndGet();
    }
    public int getNum(){
        return num.get();
    }
}

再次执行LockTest #main()方法,运行结果如下:
图片: https://uploader.shimo.im/f/Oaov2NiCpiL7Dp0Y.png!thumbnail?accessToken=eyJhbGciOiJIUzI1NiIsImtpZCI6ImRlZmF1bHQiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJhY2Nlc3NfcmVzb3VyY2UiLCJleHAiOjE2MjgyMzg0OTUsImciOiJyR3F5UjNHeGtKNnl2cUQ5IiwiaWF0IjoxNjI4MjM4MTk1LCJ1c2VySWQiOjE0NzE1NTM0fQ.AOALeZyUo2oQW9AkhhLKLY1eNwQb7tO4shx3zSSUicQ
可见结果不仅是我们期望的,耗费时间也比加锁(synchronized)方式快不少。这个原子操作类AtomicInteger就用到了cas,我们可以点进去看下上面代码块的第5行代码num.incrementAndGet()方法,
图片:
图片:

看到这个方法名称为CompareAndSwapInt。
cas,即compare and swap(set) 比较并交换。

cas机制

cas一般被称为,无锁、自旋锁、乐观锁、轻量级锁。
我们来看下面这段源码Unsafe.class#getAndSetInt()
图片:
上面红色框中是一个do-while循环,用一段代码简单模拟处理逻辑,如下:

while(true){
    // 获取该对象旧值
    int oldValue = atomicInteger.get();
    // 计算得到新值
    int newValue = oldValue + 1;
    // 如果当前引用该对象的地址当前存的值等于旧值,则把新值放进去,跳出循环
    // 如果不等于,则继续循环
    if(atomicInteger.compareAndSet(oldValue, newValue)) {
        break;
    }
}

现在有两个线程在执行。
线程2在修改变量值时,开始执行循环,分别得到旧值、新值。
如果旧值和该对象内存地址里面存的值一样(没有被线程1修改或者ABA问题),则把新值放进去。
如果旧值和该对象内存地址里面存的值不一样(该变量被线程1修改过了),则再次循环。

原子性问题

看上面截图中的源码362行这个关键步骤。事实上这个步骤不是一行代码啊!如果:
线程1 比较旧值和该对象内存地址存的值时,结果一样。 恰好这时
线程2 修改了该对象的值。接着
线程1 把新值赋值给该对象(线程1用新值覆盖了线程2的新值)
这时就发生了线程安全问题,即,比较和交换不是一步完成的。

那么是如何解决的呢?这里我们来看下jvm源码(源码情况可以参考上面“jvm有序性—>内存屏障—>jvm底层源码”章节)。我们一步步去找:

看上面截图中的源码362行。看这个this.compareAndSwapInt()方法的实现,如下:
图片: https://uploader.shimo.im/f/UbCKSYfD32OIvMKZ.png!thumbnail?accessToken=eyJhbGciOiJIUzI1NiIsImtpZCI6ImRlZmF1bHQiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJhY2Nlc3NfcmVzb3VyY2UiLCJleHAiOjE2MjgyMzg3ODUsImciOiJyR3F5UjNHeGtKNnl2cUQ5IiwiaWF0IjoxNjI4MjM4NDg1LCJ1c2VySWQiOjE0NzE1NTM0fQ.Lda4i4kIM1UWFdbzPlkzWzre5q8H7wpZigXDikwkBZw
这个方法是native修饰的,其真正的实现为C/C++,接下来在jvm源码(openjdk)中去找,
src\share\vm\prims\unsafe.cpp文件下的:
图片:
src\os_cpu\linux_x86\vm\atomic_linux_x86.inline.hpp文件里面
Aomic::cmpxchg()方法实现如下:
图片:
还是在该文件下,去看下LOCK_IF_MP()方法的实现,如下:
图片:
当线程1执行到这里获得锁,在释放该锁前。线程2不能获取到该锁去执行。
这里的锁一般是缓存行锁。如果占用内存比较大(超过64Byte),这里的锁会换成总线锁。
给最关键部分代码(比较并交换)加了锁,保证了cas的原子性,保证了线程的安全性。

总结:基于指令前缀lock + cmpxchg硬件原语 实现原子性
参考:《IA-32%20架构软件开发人员手册》7.1.2.2.软件控制的总线加锁 章节

ABA问题

先有两个线程来执行一段代码,都要修改一个变量的值,这个变量的类型为AtomicInteger类
线程1早执行,先读取了变量的值为A;
线程2执行的比较快,这时候把线程2把变量的值A修改为B再改为A;
此时,线程1执行了到比较并交换部分,比较时发现变量的值还是A,接着执行了下去。
这个就是cas的ABA问题。

这个ABA问题在某些场景下可以允许存在,在某些场景下必须杜绝。那么如何处理ABA问题呢?
可以引入版本号的概念,没修改一次对版本号做一次更新操作(恒加或恒减等)。java的rt.jar包为我们提供了两个类:AtomicStampedReference、AtomicMarkableReference。
AtomicStampedReference维护一个版本号,AtomicMarkableReference而是维护一个boolean类型的标记,用法没有AtomicStampedReference灵活。

下面来看示例:
使用AtomicInteger重线ABA问题,如下代码:

public class ABATest1 {
    private static AtomicInteger value = new AtomicInteger(10);

    public static void main(String[] args){
        new Thread(() -> {
            try{
                System.out.println(Thread.currentThread().getName() + "; value的当前值是" + value.get());
                TimeUnit.SECONDS.sleep(2);
                boolean b = value.compareAndSet(10, 12);
                System.out.println(Thread.currentThread().getName() + ":修改成功了吗?" + b + ", 设置的新值是:" + value.get());
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }, "线程1").start();

        new Thread(() -> {
            value.compareAndSet(10, 11);
            value.compareAndSet(11, 10);
            System.out.println(Thread.currentThread().getName() + ": 10->11->10");
        }, "线程2").start();
    }
}

执行结果如下:
图片:
接下来我们是AtomicStampedReference类,规避ABA问题,代码如下:

public class ABATest2 {

    static AtomicStampedReference stampRef = new AtomicStampedReference(10, 1);

    public static void main(String[] args) {
        new Thread(() -> {
            try{
                int stamp = stampRef.getStamp();
                System.out.println(Thread.currentThread().getName() + "第1次查看版本号为:"  + stampRef.getStamp() + ",值为:" + stampRef.getReference());
                TimeUnit.SECONDS.sleep(2);
                boolean b = stampRef.compareAndSet(10, 12, stamp, stampRef.getStamp() + 1);
                System.out.println(Thread.currentThread().getName()
                        + ":修改是否成功?" + b +  ",当前版本是:" + stampRef.getStamp() +  ", 当前实际值是:" + stampRef.getReference());
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }, "线程1").start();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "第1次查看版本号为:"  + stampRef.getStamp() + ",值为:" + stampRef.getReference());
            stampRef.compareAndSet(10, 11, stampRef.getStamp(), stampRef.getStamp() +1);
            System.out.println(Thread.currentThread().getName() + "第2次查看版本号为:"  + stampRef.getStamp()  + ",值为:" + stampRef.getReference());
            stampRef.compareAndSet(11, 10, stampRef.getStamp(), stampRef.getStamp() +1);
            System.out.println(Thread.currentThread().getName() + "第3次查看版本号为:"  + stampRef.getStamp()  + ",值为:" + stampRef.getReference());
        }, "线程2").start();
    }
}

执行结果如下:
图片: https://uploader.shimo.im/f/WsMsUxZiwYPM53Rs.png!thumbnail?accessToken=eyJhbGciOiJIUzI1NiIsImtpZCI6ImRlZmF1bHQiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJhY2Nlc3NfcmVzb3VyY2UiLCJleHAiOjE2MjgyMzkwNzUsImciOiJyR3F5UjNHeGtKNnl2cUQ5IiwiaWF0IjoxNjI4MjM4Nzc1LCJ1c2VySWQiOjE0NzE1NTM0fQ.B4BXALSIzMR_3Y9gJwGqFJccVhYzIOaZtfe7a3b5Zqk
可见使用了AtomicStampedReference类,当线程2修改了变量值后再改回来,线程1去修改时没有成功。杜绝了ABA问题。

分段cas优化

cas的机制是什么?例如,前面使用AtomicInteger?
图片: https://uploader.shimo.im/f/5yKfIwrc9eAuovfQ.png!thumbnail?accessToken=eyJhbGciOiJIUzI1NiIsImtpZCI6ImRlZmF1bHQiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJhY2Nlc3NfcmVzb3VyY2UiLCJleHAiOjE2MjgyMzkwNzUsImciOiJyR3F5UjNHeGtKNnl2cUQ5IiwiaWF0IjoxNjI4MjM4Nzc1LCJ1c2VySWQiOjE0NzE1NTM0fQ.B4BXALSIzMR_3Y9gJwGqFJccVhYzIOaZtfe7a3b5Zqk
如上图,多个线程竞争一个资源处理时,一次只能有一个线程获取资源成功去处理,其他线程处于高度自旋(一直在循环),处理速度慢,性能还有待提升。

这里我们如果使用LongAdder呢,把”什么是cas“章节中代码A类修改下:

public class A {
    LongAdder longAdder = new LongAdder();
    public void increase(){
        longAdder.increment();
    }
    public int getNum(){
        return longAdder.intValue();
    }
}

再次执行LockTest #main()方法(LockTest类见”synchronized“—>”简单案例“章节),运行结果如下:
图片:
这次只用了146ms比”什么是cas“章节运行时间261ms有所减少,可见性能有所提升。那么这个LongAdder类的运行机制怎样的呢?如下:
图片:
多个线程竞争一个资源时,例如,给一个变量增1。线程1首先获取到该变量进行处理,线程2没有获取到该变量(线程1还没有处理完成释放该变量资源)时,就会又产生一个该变量的副本供给线程2去处理。其他线程同样处理。接着会把变量和其所有副本一起去处理得到最终结果。

当然当线程非常多的时候,内存开销会特别大,该用重量级锁还要用。

AQS

概述

简介

AQS指的是AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。

AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

AQS原理:自旋 + lockSupport + cas 实现

问题重现

场景:减商品库存。当前《深入理解Java虚拟机》这本书的库存只有5本了,此时有30个用户在网上下订单去买这本书。

该商品的数据库表shop_order,数据如下:
图片:
后台代码如下

@Controller
public class CustomLockController {
   @GetMapping("/lock")
   @ResponseBody
   public void lockTest() throws Exception {
       int stock = DBUtil.query("select stock from shop_order where id=1");
       if (stock <= 0){
           System.out.println("下单失败,没有库存了");
           return;
       }
       stock--;
       DBUtil.update("update shop_order set stock=" + stock + " where id=1");
       System.out.println("下单成功,当前剩余库存:" + stock);
   }
}

现在用jmeter模拟同时30个人发送请求,请求路径:http://localhost:8080/lock
图片:
查看控制台输出:
图片:
这里发生了线程安全问题,当然解决的方法有很多。上面我们分析了AQS的思想,我们可以试着自己写一个同步器锁,来处理这个问题。

手写同步器锁

自旋 + LockSupport + cas,代码如下:

public class CddLock {

   /**
    * 当前加锁状态:0-未加锁;1-加锁 、 记录加锁次数
    */
   private volatile int state = 0;

   /**
    * 当前持有锁的线程
    */
   private Thread lockHolder;

   /**
    * 线程阻塞队列
    */
   private ConcurrentLinkedDeque<Thread> queue = new ConcurrentLinkedDeque<>();

   public int getState() {
       return state;
   }

   public void setState(int state) {
       this.state = state;
   }

   public Thread getLockHolder() {
       return lockHolder;
   }

   public void setLockHolder(Thread lockHolder) {
       this.lockHolder = lockHolder;
   }

   /**
    * 尝试获取锁
    * @return
    */
   private boolean tryAquire(){
       Thread t = Thread.currentThread();
       int state = getState();
       if(state == 0){
           // 公平锁
           if((queue.size()==0 || queue.peek()==t) && compareAndSwapState(0, 1)){
               setLockHolder(t);
               System.out.println(String.format("Thread-name:%s加锁成功", t.getName()));
               return true;
           }
       }
       return false;
   }

   /**
    * 加锁
    */
   public void lock(){
       // 1. 获取锁-CAS
       if(tryAquire()){
           return;
       }
       Thread current = Thread.currentThread();
       queue.add(current);
       // 2. 没有获取到锁,停留在当前方法: 自旋 + LockSupport + cas
       for(;;){
           if(current==queue.peek() && tryAquire()){
               queue.poll();
               return;
           }
           LockSupport.park(current);
       }
       // 3. 锁被释放后,再次获取锁
   }

   /**
    * 释放锁
    */
   public void unLock(){
       Thread current = Thread.currentThread();
       if(current != lockHolder){
          throw new RuntimeException("你不是持有锁的线程,不可以释放锁");
       }

       int state = getState();
       if(compareAndSwapState(state, 0)){
           System.out.println(String.format("Thread-name:%s释放锁成功", current.getName()));
           setLockHolder(null);
           Thread head = queue.peek();
           if(head != null){
               LockSupport.unpark(head); // 线程被唤醒
           }
       }
   }

   /**
    * 原子操作
    * @param oldValue 旧值
    * @param newValue 新值
    * @return
    */
   public final boolean compareAndSwapState(int oldValue, int newValue){
       return unsafe.compareAndSwapInt(this, stateOffset, oldValue, newValue);
   }

   private static final Unsafe unsafe = GetUnsafeInstance.getUnsafeInstance();

   private static final long stateOffset;

   static {
       try {
           stateOffset = unsafe.objectFieldOffset(CddLock.class.getDeclaredField("state"));
       } catch (NoSuchFieldException e) {
           throw new Error();
       }
   }

   /**
    * @Description 通过反射获取Unsafe类对象
    * @Author cdd
    * @Date  2021/6/21
    * @Vesrion v1.0
    **/
   static class GetUnsafeInstance {
       public static Unsafe getUnsafeInstance() {
           try {
               Class<?> clazz = Unsafe.class;
               Field f = clazz.getDeclaredField("theUnsafe");
               f.setAccessible(true);
               Unsafe unsafe = (Unsafe) f.get(clazz);
               return unsafe;
           } catch (IllegalAccessException e) {
               e.printStackTrace();
           } catch (SecurityException e) {
               e.printStackTrace();
           } catch (NoSuchFieldException e) {
               e.printStackTrace();
           }
           return null;
       }
   }
}

使用我们上面写的同步器锁CddLock,针对上面“问题重现’'章节的案例,修改CustomLockController类,如下:

@Controller
public class CustomLockController {
    // 实例化锁
    CddLock cddLock = new CddLock();

    @GetMapping("/lock")
    @ResponseBody
    public void lockTest() throws Exception {
        cddLock.lock();  // 加锁
        int stock = DBUtil.query("select stock from shop_order where id=1");
        if (stock <= 0){
            System.out.println("下单失败,没有库存了");
            cddLock.unLock();  // 释放锁
            return;
        }
        stock--;
        DBUtil.update("update shop_order set stock=" + stock + " where id=1");
        System.out.println("下单成功成功,当前剩余库存:" + stock);
        cddLock.unLock(); // 释放锁
    }
}

现在把数据库表shop_order表的库存再改为5,用jmeter再次测试,控制打印如下:
图片:

AQS的可重入性

线程能多次获取锁就叫可重入,这里可以用信号量标示,加一次锁给信号量做一次处理(比如:加1操作),释放锁时就做同样次数的反向处理(比如:减1操作)。
不可重入反之。
例如:在上面我们手写的同步器锁CddLock类中的state就是信号量,只有在state为0时才能加锁,加锁后把state改为1,这样就不能多次加锁,就不具备可重入性。
如果去掉”只有在state为0时才能加锁“的前提条件,每次加锁都给state加1,这样就具备可重入性了。

AQS的公平与非公平

这个是否公平体现在:
在使用同步器锁的过程中,可能一个线程获取到锁在执行中,其他线程都发放在队列里面。当这个线程执行结束释放锁时。这时候又有个新线程来了,那么是该哪个线程获取锁呢?
如果是队列里面对头的线程获取到锁,即按照先来后到的顺序获取锁 这个就是公平锁。
如果这个新线程获取锁(反之,没有让先入队列的线程获取锁)的话,这个就是非公平锁。

在实际通常使用ReentrantLock这个可重入同步器锁,该锁的结构图如下:
图片:

两个构造器,如下图:
图片:

源码中内部类FairSync,tryAcquire的实现如下图,红色框中部分体现了公平性。
图片:

源码中内部类NonfairSync,tryAcquire方法调用了父类Sync的tryAcquire方法,如下图:
图片:

源码中Sync类的nonfairTryAcquire()方法实现如下,红框中当前线程直接去获取锁了(就算还有等待时间更久的线程),体现了非公平性。
图片:

ThreadLocal

介绍

官方介绍

java.lang.Thread类上注释说:
ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。
我们可以得知ThreadLocalde的作用是:提供线程内的局部变量,不同线程之间不会干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。

总结:

  • 线程并发: 在多线程并发的场景下
  • 传递数据: 我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量
  • 线程隔离: 每个线程的变量都是独立的,不会互相影响

基本使用

常用方法如下:

方法声明描述
ThreadLocal()创建ThreadLocal对象
public void set( T value)设置当前线程绑定的局部变量
public T get()获取当前线程绑定的局部变量
public void remove()移除当前线程绑定的局部变量

接下来看个案例,有代码如下:

public class MyDemo01 {
    private String content;
    public String getContent() {
        return content;
    }
    public void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args) {
        MyDemo01 demo = new MyDemo01();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    demo.setContent(Thread.currentThread().getName() + "的数据");
                    System.out.print("");
                    System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
                }
            }, "线程"+i);
            thread.start();
        }
    }
}

执行结果如下:
图片:
可以看到后面两个线程保存的线程名称和打印的线程名称不一致,各个线程没有隔离,发生了安全问题。那么如何解决呢?前面我们学习了很多关于线程安全的解决方法,这里我们使用ThreadLocal试试。代码段如下:

public class MyDemo01 {
   // 给每个线程绑定的局部变量
   private ThreadLocal<String> t1 = new ThreadLocal<>();

   private String content;

   public String getContent() {
       return t1.get();
   }

   public void setContent(String content) {
       t1.set(content);
   }

   public static void main(String[] args) {
       MyDemo01 demo = new MyDemo01();
       for (int i = 0; i < 5; i++) {
           Thread thread = new Thread(new Runnable() {
               @Override
               public void run() {
                   demo.setContent(Thread.currentThread().getName() + "的数据");
                   System.out.print("");
                   System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
               }
           }, "线程"+i);
           thread.start();
       }
   }
}

执行结果如下:
图片:
现在每个线程打印的都是自己的线程名称,线程完全隔离,线程安全。

与synchronized比较

对于上面的线程安全问题,如果不使用ThreadLcal类。使用synchronized关键字该怎么处理呢?代码段如下:

public class MyDemo02 {
    private String content;
    public String getContent() {
        return content;
    }
    public void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args) {
        MyDemo02 demo = new MyDemo02();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(() -> {
                synchronized (MyDemo02.class){
                    demo.setContent(Thread.currentThread().getName() + "的数据");
                    System.out.print("");
                    System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
                }

            }, "线程"+i);
            thread.start();
        }
    }
}

执行结果如下:
图片:
可以看到每个线程打印的都是自己的线程名称,线程也是安全的。

那么ThreadLocal和synchronized的区别在哪里呢?

synchronizedThreadLocal
原理同步机制采用’以时间换空间’的方式, 只提供了一份变量,让不同的线程排队访问ThreadLocal采用’以空间换时间’的方式, 为每一个线程都提供了一份变量的副本,从而实现同时访问而相不干扰
侧重点多个线程之间访问资源的同步多线程中让每个线程之间的数据相互隔离

总结:
在刚刚的案例中,虽然使用ThreadLocal和synchronized都能解决问题,但是使用ThreadLocal更为合适,因为这样可以使程序拥有更高的并发性。

与Thread配合使用原理

ThreadLocal内部结构

ThreadLocal类的内部结构图如下:
图片:
ThreadLocal类有:
2个内部类。ThreadLocalMap(还有内部类) 和 SuppliedThreadLocal(继承于ThreadLocal)。
3个核心方法。get()、set()、remove(),部分实现依靠ThreadLocalMap类。

这里看下重要的内部类ThreadLocalMap。内部结构如下图:
图片:
ThreadLocalMap类中重要内容有:
1一个内部类。Entry,该类继承于弱引用类WeakReference。
Entry类型的数组table。下标为ThreadLocal类对象(this)算出的哈希数值。
方法getEntry()、set()、remove()对数组table元素操作。被ThreadLocal类get()、set()、remove()方法调用。

Thread的重要字段

我们来进行源码追踪,找下这个重要字段。
先来看ThreadLocal#set()方法,如下图:
图片:

看ThreadLocal#get()方法,如下图:
图片:

再来看下这个t.threadLocals的声明,在Thread类里面,如下图:
图片:

这个字段就是Thread类的ThreadLocal.ThreadLocalMap类型的threadLocals字段。

图解使用原理

图片:
线程Thread里面维护一个ThreadLocal.ThreadLocalMap类型的字段threadLocals;
threadLocals字段指向的ThreadLocalMap类对象有一个Entry类型的数组;
Entry的外部类的外部类是ThreadLocal;
Entry类对象存储着this,即,ThreadLocal类对象为key;当前线程绑定的局部变量为value;

总结:
线程Thread携带着一个Entry类型的数组;
该数组的元素类型ThreadLocal内部类的内部类Entry;
该元素存贮着ThreadLocal类对象和当前线程绑定的局部变量。

核心源码分析

ThreadLocal类中:set()、get()、 remove()

public void set(T value) {
    Thread t = Thread.currentThread();
    // 获取当前线程携带的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 调用ThreadLocalMap的set方法
        map.set(this, value);
    else
        // 给当前线程的threadLocals字段赋值
        createMap(t, value);
}

// 获取当前线程携带的ThreadLocalMap对象
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// 给当前线程的threadLocals字段赋值
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}


public T get() {
    Thread t = Thread.currentThread();
    // 获取当前线程的ThreadLocalMap类型对象
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 根据当前this->ThreadLocal对象获取Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            // 获取Entry对象的value并返回
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 初始化并返回
    return setInitialValue();
}

// 初始化
private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

// 一个初始化值
protected T initialValue() {
    return null;
}

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        // 调用ThreadLocalMap的remove方法移除元素
        m.remove(this);
}

内部类ThreadLocalMap中:

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    // 根据传来key,也就是this对象,即,ThreadLocal类对象 得到 哈希值
    int i = key.threadLocalHashCode & (len-1);
    
    // 解决hash冲突的方法:线性勘探法
    // 遍历线程携带的数组,从哈希值i为下标之后遍历
    // 如果存在元素的key等于要保存的key,则更新该元素的value;如果某个位置元素为空,则把value保存到这个位置上
    // 一直向后“勘测”,下标i向后移动
    for (Entry e = tab[i]; e != null;e = tab[i = nextIndex(i, len)]) {
        // 获取到每个元素的key
        ThreadLocal<?> k = e.get();
        // 如果要保存的key和已有元素的key相同,则更新该元素的value
        if (k == key) {
            e.value = value;
            return;
        }
        // 如果某个元素的key为null,则把value保存在这个位置
        if (k == null) {
            // 用当前要保存的value、key构成的Entry对象替换旧的Entry对象
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // 保存当前key、value组成的Entry对象
    // 此时i已经移动到一个没有放元素的位置上(下标)
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

// 根据key获取元素
private Entry getEntry(ThreadLocal<?> key) {
   // 获取下标
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    // 从数组中取出的元素不为null且key与要查询的key相等,则返回该元素
    if (e != null && e.get() == key)
        return e;
    else
       // 线性勘探,向后寻找
        return getEntryAfterMiss(key, i, e);
}

// 移除元素
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    // 遍历数组获取到该元素
    for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            // 元素的key置为null
            e.clear();
            // 清除该元素
            expungeStaleEntry(i);
            return;
        }
    }
}

弱引用导致的内存泄漏吗

在使用ThreaLocal类绑给线程绑定局部变量,有时间可能发生内存泄漏,有人推测这个和ThreadLocal.ThreadLocalMap.Entry使用了弱引用有关,接下来我们一探究竟。

Entry结构

Entry的结构及继承关系,如下图:
图片:
Entry类继承了WeakReference类。
Entry类构造方法为:Entry(ThreadLocal<?> k, Object v)。
Entry的k为ThreadLocal类对象的确为弱引用。

什么是弱引用

参考文章https://blog.csdn.net/weixin_41968788/article/details/113122921的6.3.1章节。
java 的引用类型一般分为 4 种:强、软、弱,虚。

  • 强引用:普通的变量引用。例如,我们常在代码里面用User u1 = new User()就是。
  • 弱引用:将对象用 WeakReference 弱类型的对象包裹,弱引用跟没引用差不多,GC 直接回收掉,很少用。

分析

当Entry中的k使用了弱引用,使用时引用情况如下图(虚线为弱引用,实线为强引用)
图片:

  • a.假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。
  • b. 由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收,此时Entry中的key=null。
  • c. 但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry -> value ,value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏。

也就是说,ThreadLocalMap中Entry的key使用了弱引用, 也有可能内存泄漏。

当Entry中的k使用了强引用,使用时引用情况如下图
图片:

  • a. 假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。
  • b. 但是因为threadLocalMap的Entry强引用了threadLocal,造成threadLocal无法被回收。
  • c. 在没有手动删除这个Entry以及CurrentThread依然运行的前提下,始终有强引用链 threadRef->currentThread->threadLocalMap->entry,Entry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。

也就是说,ThreadLocalMap中的Entry的key使用了强引用, 是无法完全避免内存泄漏的。

内存泄漏根本原因 没有手动调用remove,删除这个Entry 且 线程一直在运行。
避免的方法 手动调用remove删除Entry。(线程不好控制其销毁,特别是线程池情况)

为什么使用弱引用

通过上面的分析,我们知道无论ThreadLocalMap中Entry的key使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。
使用了弱引用时,当 Entry中的key为null时,ThreadLocalMap中的set(),getEntry()会进行一些处理。

ThreadLocalMap#set()方法中代码片段如下:
图片:

ThreadLocalMap#getEntry()方法中调用到了getEntryAfterMiss(),接着又调用到了expungeStaleEntry(),代码片段如下:
图片:
这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值