Java并发

Java并发

JAVA技术交流群:737698533

CAS

compare and swap 比较并交换,cas又叫做无锁,自旋锁,乐观锁,轻量级锁

例如下面的代码,如果想在多线程情况下保证结果的正确性,可以使用synchronized

public class A {

    private int i;

    public synchronized  void addI(){
        i++;
    }
    
    public int getI() {
        return i;
    }
}
public class ConcurrencyDemo {


    public static void main(String[] args) throws InterruptedException {

        long startTime=System.currentTimeMillis();

        A a = new A();

        Thread thread = new Thread(() -> {
            for (int i = 0; i < 1000000; i++) {
                a.addI();
            }
        });
        thread.start();

        for (int i = 0; i < 1000000; i++) {
            a.addI();
        }
        
        thread.join();
        
        System.out.println(a.getI());

        long endTime=System.currentTimeMillis();

        System.out.println("程序运行时间: "+(endTime-startTime)+"ms");
    }
}
结果 2000000
程序运行时间: 109ms

而使用AtomicInteger类来进行相同的操作

    private AtomicInteger atomicInteger=new AtomicInteger();

    public int getCAS() {
        return atomicInteger.get();
    }

    public  void addCAS(){
        atomicInteger.incrementAndGet();
    }
结果 2000000
程序运行时间: 67ms

缩短了不少时间,将循环数量调整到1亿次效果更加明显

使用synchronized 程序运行时间: 7512ms

使用atomicInteger类 程序运行时间: 2521ms

结果时间缩短了将近3倍,为什么atomicInteger类比synchronized关键字缩短这么长时间呢

当使用synchronized时,如果在非静态方法上锁住的是对象,因为上面只有一个对象,也就是一个锁,当这两个线程去获取锁时同一时间内只会有一个线程获取到了,而没有获取到的线程被添加到一个阻塞队列,只能等待着上一个线程释放锁,即线程阻塞,当释放完锁需要唤醒其他线程来获取锁,其中还有上下文切换,还要找到该线程上次运行位置,操作系统进行线程调度等等,消耗资源比较多

那atomic类工作原理是什么呢?

比较并交换

原理很简单,当它进行加一操作时,并不是直接进行加一然后赋值,首先获取到旧值,然后进行+1操作,最后比较内存中的值是否和取出来的旧值是否一样,如果一样则进行赋值,否则进行重试

总共就3步,获取数据,进行加1,比较原始数据和内存中的数据,如果相同赋值,否则重试

原子性

那么比较并交换是一个原子性操作吗,光听着名就感觉是两个操作,比较,交换,如果在比较后又有其他线程进行修改值呢

这个atomic方法点到最后是一个本地方法,无法看到内部实现了,

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

但是在JVM源码中它底层汇编的实现会在比较交换前添加一个lock指令

因为不懂汇编在网上搜了一下lock指令的作用:LOCK指令前缀会设置处理器的LOCK#信号(译注:这个信号会使总线锁定,阻止其他处理器接管总线访问内存),直到使用LOCK前缀的指令执行结束,这会使这条指令的执行变为原子操作。在多处理器环境下,设置LOCK#信号能保证某个处理器对共享内存的独占使用。 https://blog.csdn.net/imred/article/details/51994189

所以可以将比较并交换看做为一个原子性的操作,不用担心在比较后值进行了变化

ABA

还有一种情况,在获取值,增加1,比较交换三步中,当线程A在第一步获取完数据假设为3,正在进行下面的操作时,线程B将数据3改为了4,然后又改回了3,线程A继续执行加1,在进行比较交换时判断正确,原来是3当前代码中旧值也是3,然后进行了赋值

例如你买了瓶可乐,放桌上没来及喝有事出去了,这时小明刚从外面回来非常渴把你的可乐喝了,然后又去商店给你买了一瓶一样的放回去,你回来并没有发现有什么不同,可乐还是可乐,但是这瓶可乐已经不是你自己买的那一瓶了

这个也不算一个问题,因为结果正确,但又算一个问题,因为比较的数据已经和最开始获取的数据并不是同一个,如果想要解决这个问题添加一个版本号即可,在每次进行比较交换时同时判断版本号,上面的例子中如果使用了版本号,线程A最后判断旧值和版本号,例如版本号默认为1,B线程进行两次修改,版本号为3,A线程在比较并交换时同时判断版本号和旧版本号,如果不同则不进行交换

在java中有一个类可以解决这个问题,AtomicStampedReference这里就不再细讲了,有兴趣可以去看看

锁升级

一个对象的锁有4中状态:无锁,偏向锁,轻量级锁,重量级锁

一个对象的锁信息都会存储在对象头的运行时元数据(Mark Word)中,在MarkWord中不仅仅存储锁的信息,还有哈希值,GC年龄等等,具体可以看这篇文章

64位虚拟机对象头中的数据

偏向锁

偏向锁是啥?当线程A访问代码并获取锁时,会在对象头的markword中存储这某个线程的id,当下次这个线程进行操作时先判断和存储在对象头中的线程id是否相同,如果相同则直接进行操作,不需要进行加锁

如果不一致,例如线程B也访问代码块尝试获取锁时,首先判断记录在对象头中的线程A是否存活,如果没有存活则将锁状态设置为无锁,线程B竞争时将对象头中线程id设置为B线程的id,如果线程A存活,则查找这个线程的栈帧信息,如果还是需要持有这个锁对象则暂停线程A,撤销偏向锁,将锁升级为轻量级锁,如果不需要再持有线程A的锁则将锁设置为无状态,重新设置偏向锁

为什么添加偏向锁?在应用程序中大部分时间并不存在锁的竞争,如果还是使用重量级锁进行一系列操作浪费了许多无用的资源,但是如果不加锁在一些出现线程竞争的时候,就无法保证数据的准确性

偏向锁是默认是在jvm启动后4秒开启的,如果不想有延迟可以在启动参数中添加:XX:BiasedLockingStartUpDelay=0

如果不需要偏向锁可以添加-XX:-UseBiasedLocking = false来设置

什么时候升级:线程A只要进行一次访问后,在对象头markWord中存储了线程A的id,只要下次访问的线程ID和上次存储的不一致符合上面的条件则升级为偏向锁

轻量级锁

使用CAS进行轻量级的获取锁,如果没有获取到根据条件升级为重量级锁

过程:

  1. 在当前虚拟机栈帧中创建一份锁记录(LockRecord)的空间,DisplacedMarkWord
  2. 首先将对象头中的markWord复制一份到当前栈帧的锁记录中
  3. 然后使用CAS将对象头的内容改为指向线程存储锁记录的地址
  4. 如果在线程A复制对象头后,对象头中的markWord还没有更换之前,线程B也准备获取锁,复制对象头到线程B的锁记录中,在线程B使用CAS进行替换对象头时发现,线程A已经将对象头中数据改变了,则线程B的CAS失败,尝试10次CAS来获取锁,如果没有获取到则升级为重量级锁

https://edu.51cto.com/study/11144 可以看这篇博客

什么时候升级:当线程A获取到了对象锁,线程B也进行了访问尝试通过CAS获取,自旋10次后还是没有等到线程A释放锁则升级为重量级锁,如果在10次内获取到了则还是轻量级锁

因为在JDK1.6之前synchronized是一个重量级锁,比较消耗资源,在JDK1.6之后对synchronized进行了优化,当使用synchronized修饰并不会默默认初始一个重量级锁,而是先使用偏向锁->轻量级锁->重量级锁

锁升级的条件

导入包jol-core OpenJDK提供了JOL包,可以在运行时查看对象,类的细节

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>

下面的代码演示锁升级

public class LockUpDemo {
    public static void main(String[] args) throws InterruptedException {
        A a1 = new A();
        System.out.println(ClassLayout.parseInstance(a1).toPrintable()); ///====第一个输出
        //等待JVM开启偏向锁
        Thread.sleep(5000);
        A a = new A();
        System.out.println(ClassLayout.parseInstance(a).toPrintable()); ///====第二个输出

        synchronized (a) {
            System.out.println(ClassLayout.parseInstance(a).toPrintable()); ///====第三个输出
        }

        new Thread(() -> {
            synchronized (a) {
                System.out.println(ClassLayout.parseInstance(a).toPrintable()); ///====第四个输出
                try {
                    Thread.sleep(1500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

		//等待一会防止两个线程同时抢夺锁导致直接升级为重量级锁
        Thread.sleep(500);

        new Thread(() -> {
            synchronized (a) {
                System.out.println(ClassLayout.parseInstance(a).toPrintable());  //====第五个输出
            }
        }).start();
    }
}

每次输出前三行都是对象头中的信息,在第一行中存储markWord的线程信息

==>0  4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)==<--这一行
4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)

第一个输出

com.jame.concurrency.cas.A object internals:							====主要看这段====
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE            ↓
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4    int A.i                                       0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

先看第一个输出,在第一行数据中,在小箭头指向位置为00000001,存储这偏向锁(1bit)和锁信息(2bit),也就是最后的001

对比这上面的图,0为偏向锁标识,也就是没有启用偏向锁,前面说过在JVM启动后偏向锁会延迟一会再启动,所以这里为0 而后面的01对应着锁的标识,也就是偏向锁

第二个输出

com.jame.concurrency.cas.A object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4    int A.i                                       0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

前面代码等待了5秒,能看到101已经启动偏向锁了

第三个输出

com.jame.concurrency.cas.A object internals:                                ===注意这些数据和上次输出发生了改变===
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE                    ↓       ↓        ↓
      0     4        (object header)                           05 e8 8b 02 (00000101 11101000 10001011 00000010) (42723333)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4    int A.i                                       0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

因为启动了偏向锁,而这次输出是在synchronized中进行的,也就是进行了获取锁的操作,对比上次输出能发现在锁信息后面多了一些其他数据,而这些数据中就包含了当前线程的id

第四个输出

com.jame.concurrency.cas.A object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           78 f0 f5 20 (01111000 11110000 11110101 00100000) (552988792)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4    int A.i                                       0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

又创建了一个线程进行获取锁的操作,能看到这里已经升级为轻量级锁了000

第五个输出

com.jame.concurrency.cas.A object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           aa f3 2b 1d (10101010 11110011 00101011 00011101) (489419690)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4    int A.i                                       0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

创建一个新线程获取锁,后三位010现在已经升级为重量级锁了,也就是原来理解的synchronized,原因就是上一个获取锁睡眠了1500ms,而sleep不会释放掉锁,所以获取不到,产生争抢锁,升级为了重量级锁

注意:锁的升级是不可逆的,即一旦从偏向锁升级为轻量级锁,轻量级升级为重量级锁,则不能再降级,但是偏向锁可以再设置回无锁的状态

posted @ 2021-07-12 18:32  Jame!  阅读(394)  评论(0编辑  收藏  举报