• Java多线程——synchronized,volatile,CAS,ReentrantLock


    Java多线程

    Synchronized和Volatile

    1. volatile

      volatile是JVM提供的轻量级同步机制,是线程不安全的.volatile保证了可见性和有序性.

      volatile的作用

      1. 保证内存可见性

        假设变量a已经从主内存中存储到了工作内存中,那么在内存中对变量a的值进行修改,工作内存中的变量

        a不会立刻更新.而volatile修饰的变量可以保证工作内存中的值是最新的(和主内存中的变量值一致)

        volatile保证内存可见性的原理:JVM会对添加了volatile关键字修饰的变量添加一条lock前缀指令.lock前缀指令在多核处理器中会做2件事情:将当前工作内存中的变量的值写回到主内存中;写回操作会引起其他处理器中缓存了该数据的地址无效.

        因此当某个变量被volatile修饰时,该当前CPU中的最新的变量写回到主内存中,同时为了保证多个处理器的缓存一致,就会实现缓存一致性协议,每个处理器会通过检查主内存中的数据来判断自己缓存的数据是否已经过期,如果发现两个数据的地址不一致,则认为当前缓存的数据的地址失效,之后该处理器在处理该数据时由于该数据已经无效所以会从主内存中读取该数据的最新值.

        static int a = 1;
        public static void main(String[] args){
            Thread t1 = new Thread(){
                @Override
                public void run(){
                    int i = 1;
                    while(a == 1){
                        i++;
                    }
                }
            }
            Thread t2 = new Thread(){
                @Override
                public void run(){
                    a = 2;
                }
            }
            t1.start();
            Thread.sleep(1000);
            t2.start();
        }
        
        // volatile适用于变量在一个线程中只读取,在另一个线程中修改的场景
        // 上述程序当不添加volatile关键字,程序会无限执行下去,原因是因为t2线程对a变量作的修改t1线程看不到,t1线程每次读取的仍是自己工作内存中存储的a的旧值1.
        // 当添加了volatile关键字后,t2线程对a变量作的修改会同步到主内存上,同时会让t1线程工作内存内存放a变量的地址失效,从而再下一次读取的时候从主内存中读取到a的最新值
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
        • 19
        • 20
        • 21
        • 22
        • 23
        • 24
        • 25
      2. 禁止指令重排序

        volatile实现禁止指令重排序是依靠JMM在编译器和处理器层面限制指令重排序,而JMM的内存屏障策略是保守策略

        LoadLoadBarriers:该屏障之前的语句中数据的读取优先于该屏障之后所有语句中数据的读取

        StoreStoreBarriers:该屏障之前的语句中数据的存储优先于该屏障之后所有语句中数据的存储

        LoadStoreBarriers:该屏障之前的语句中数据的读取优先于该屏障之后所有语句中数据的存储

        StoreLoadBarriers:该屏障之前的语句中数据的存储优先于该屏障之后所有语句中数据的读取

      volatile的使用情况

      1. 当存在两个及以上的线程访问同一个变量时使用volatile,当要访问的变量已在synchronized代码块中或已经是常量时无须用volatile修饰
      2. volatile修饰的变量禁止了编译器中对代码的优化,所以执行效率低,必要时再使用
    2. synchronized

      synchronized可以保证线程的安全,添加synchronized的代码块保证在同一时刻只能被一个线程执行,一旦一个线程执行持有synchronized的代码块后,其他线程想要处理该代码块就需要等待已经持有的线程释放锁,否则处于阻塞状态.除此之外,synchronized还可以保证持有该锁的线程对代码块的修改可以被其他线程共享,也就是synchronized可以完全代替volatile来保证可见性

      synchronized的原理

      在JVM中,每一个对象都有一个对象头,而对象头的Mark Word中记录着该对象持有的锁类型

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2w9GpIgC-1662704920483)(C:\Users\qiu\AppData\Roaming\Typora\typora-user-images\1662449802155.png)]

      synchronized的对象锁是一把重量级锁时,该锁指向一个monitor对象(监视器锁),每一个对象都与一个monitor关联,当一个monitor被某个线程占有时,它便处于锁定状态.monitor有ObjectMonitor实现.当多个线程同时请求某个对象监视器时,对象监视器会将多个线程进行区分:

      1. Contension List:所有请求该对象监视器的线程首先都会进入到Contension竞争队列中

      2. Entry List:Contension List中有机会成为候选人的线程进入Entry队列中

      3. Wait Set:调用wait方法而阻塞的线程会被放入到Wait Set中

      4. OnDeck:抢占到锁的线程被称为OnDeck,同一时刻OnDeck只能有一个

      5. Owner:获得锁的线程

      6. !Owner:释放锁的线程

        [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aAAlgd3K-1662704920484)(C:\Users\qiu\AppData\Roaming\Typora\typora-user-images\1662450979818.png)]

        同步代码块:多个线程同时访问一个代码块,这些线程首先会进入EntryList中,抢占到锁对象的monitor后通过OnDeck进入Owner区域,同时将monitor对象的count值+1(由0到1),执行代码块中的内容,若进入Owner区域的线程调用了wait方法则将count的值变为0,同时释放持有的monitor同时进入到WaitSet中

        同步方法:多个线程同时访问一个实例方法,线程会通过检查ACC_SYNCHRONIZED标志是否被设置,如果被设置就持有该方法的monitor,成功获取monitor后执行该方法,执行结束后释放monitor.

        注意:synchronized是可重入的,抢占到synchronized的线程会将维护的count变量值从0变为1,因为synchronized是可重入的,因此当当前线程继续占用该锁时,count变量会从1变为2,之后释放的时候也是从2—1—0

      synchronized加锁过程的优化

      为了提升加锁的效率,JVM将synchronized锁对象分成了无锁,偏向锁,自旋锁,重量级锁四种.

      当没有线程占用锁对象时处于无锁,当有一个线程占用该锁时变成偏向锁,偏向搜不是真的加锁,而是添加一种锁标记,如果之后没有发生锁竞争就无须开销锁资源,如果有锁竞争就添加自旋锁,自旋锁是一种轻量级锁,在锁冲突不激烈的情况下,在抢占不到锁时不会处于阻塞状态而会一致尝试抢占锁,当锁竞争激烈时,自旋锁不能马上获取到锁,就会膨胀为重量级锁,调用OS的mutex将没有抢占到锁的线程加入到阻塞队列中,重量级锁的具体执行过程如上述介绍

    CAS

    CAS:Compare and Swap,比较并交换.CAS的原理是存在三个值内存中的值A,旧的预期值B和要修改成的值C,当A和B相等就将内存值A改为C,否则什么都不做

    在Java中的AtomicInteger类中就运用CAS实现了线程安全的加法操作.

    CAS的缺点就是无法解决ABA问题.

    ABA问题就是将原本值为A的变量的值改为B,之后再把这个变量的值重新改为A.

    CAS的Compare比较的是内存中的值和旧的预期的值,比较的值相等意味着内存中的值没有被改变过,而ABA问题显然改变了内存中的值.为了区分值相等是因为ABA还是没有发生过改变,可以通过控制变量的版本(版本自增)来保证CAS的正确性

    ReentrantLock

    ReentrantLock是基于AQS实现的,AQS是基于CAS的一种队列.

    在ReentrantLock有一个抽象类Sync实现了AbstractQueueSynchronizer(AQS)

    ReentrantLock的构造方法中的参数决定了ReentrantLock是公平锁(true)还是非公平锁(false/默认),公平锁就是先到达的线程优先抢占到锁,非公平锁就是所有线程随机抢占到锁

    ReentrantLock最常用的方法就是lock和unlock

    lock

    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    首先当当前锁空闲时,state值为0,第一个抢占到该锁的线程会执行if语句,利用CAS将当前线程的值设置为1,同时将AbstractQueueSynchronizer的Thread设置为当前线程

    之后当当前锁被占用时,state值为1,之后竞争该锁的线程会进入else语句的acquire(1)

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    首先会继续尝试占用锁(tryAcquire),tryAcquire会执行Sync.nonfairTryAcquire方法

    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    首先会继续尝试占用锁,之后如果是当前线程嵌套锁就给state的值+1(ReentrantLock是可重入锁),否则返回false

    然后继续调用addWaiter方法:创建一个当前线程的Node然后加入到AQS队列中

    然后调用acquireQueued方法将线程阻塞

    至此lock方法实现了某个线程独占锁,其他线程加入到AQS队列中阻塞

    unlock

    public void unlock() {
        sync.release(1);
    }
    
    • 1
    • 2
    • 3

    调用sync.release方法

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    首先尝试释放锁(tryRelease)

    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    只有一个线程完全对一个ReentrantLock全部解锁后才能真正释放(即一个lock对应一个unlock),然后将占有当前锁的线程设置为空.然后走unparkSuccessor(h)方法

    private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
    
        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    将AQS队列中的第一个节点取出然后调用unpark方法令该线程占用锁,同时从AQS队列中删除该Node节点.AQS队列结构的调整得益于acquireQueued方法

    final boolean acquireQueued(final Node node, int arg) {
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } catch (RuntimeException ex) {
            cancelAcquire(node);
            throw ex;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    注意这里的for循环是个无条件循环,且只有队列的第一个节点抢占到锁时才返回,因此当队列中某个线程抢占到锁时会第一时间执行第一个if语句然后调整AQS队列的结构

    ReentrantLock的底层实现

    首先ReentrantLock是一把可重入锁,它是基于Java当中的一个类,是基于 AQS同步器实现的,而AQS是一个运用了大量CAS的队列.在ReentrantLock类中,有3个常用的方法lock,unlock和trylock,相比如synchronized而言,ReentrantLock的trylock更加灵活,它的含义是在抢占锁失败后不会马上进入阻塞队列,而是在规定时间内不断的尝试抢占锁,相当于是一个自旋锁.

    而lock方法它会首先利用CAS操作去尝试将内存中的state的值从0改为1,如果成功说明第一个线程抢占到该锁,然后把AQS的线程设置为当前线程,如果CAS操作失败,就尝试将线程加入到AQS队列中,在加入的过程中会不断尝试锁资源是否被释放,如果没有释放,则会判断AQS队列是否为空,如果为空就创建队列将线程包装成Node加入到队列,否则直接加入到队列

    而unlock方法则是调用AQS的release方法,release方法首先判断state的值是否为0,如果不为0说明存在可重入锁,释放失败,单纯的将state-1,如果state的值为0,则将队列中第一个节点的线程取出并占用锁资源,然后调整队列的结构.

    ReentrantLock和synchronized的区别

    1. synchronized是一个关键字,是JVM内部实现的;ReentrantLock是标准库的一个类,是基于Java实现的
    2. synchronized使用时不需要手动释放锁,ReentrantLock需用手动调用unlock释放锁
    3. synchronized只有抢占锁和阻塞两种状态,而ReentrantLock在这两者基础上还有一个更灵活的tryLock去在一定时间内尝试获取锁
    4. synchronized是非公平锁,ReentrantLock默认是非公平锁,但通过传入true参数可以设置成公平锁
  • 相关阅读:
    WEB自动化-04-Cypress 测试用例编写和组织
    专肽生物:蛋白激酶C底物 Protein Kinase C Substrate
    重写Object类的方法
    Android学习笔记 79. 菜单和选择器
    NodeJS 5分钟 连接 Redis 读写操作
    如何高效自学(黑客技术)方法——网络安全
    小程序如何自定义navber,使用了navber需要注意的点
    达梦数据库DM8的命令行工具DIsql介绍与使用
    开启Clash和系统代理后Chrome无法打开网页但Edge正常
    OrangePIPC2---镜像制作
  • 原文地址:https://blog.csdn.net/weixin_52477733/article/details/126782364