• ReentrantLock、ReentrantReadWriteLock、StampedLock


    无锁→独占锁→读写锁→邮戳锁

    1.ReentrantReadWriteLock

    读写锁定义
    一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程。

    意义和特点

    读写锁ReentrantReadWriteLock并不是真正意义上的读写分离,它只允许读读共存,而读写和写写依然是互斥的,
    大多实际场景是“读/读”线程间并不存在互斥关系,只有"读/写"线程或"写/写"线程间的操作需要互斥的。因此引入ReentrantReadWriteLock

    一个ReentrantReadWriteLock同时只能存在一个写锁但是可以存在多个读锁,但不能同时存在写锁和读锁。
    也即 一个资源可以被多个读操作访问或一个写操作访问,但两者不能同时进行。

    只有在读多写少情境之下,读写锁才具有较高的性能体现。

    1.特点

    可重入

    读写分离

    演示ReentrantReadWriteLock

    无锁无序→加锁→读写锁演变

    import java.util.HashMap;
    import java.util.Map;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    class MyResource
    {
        Map<String,String> map = new HashMap<>();
        //=====ReentrantLock 等价于 =====synchronized
        Lock lock = new ReentrantLock();
        //=====ReentrantReadWriteLock 一体两面,读写互斥,读读共享
        ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    
        public void write(String key,String value)
        {
            rwLock.writeLock().lock();
            try
            {
                System.out.println(Thread.currentThread().getName()+"\t"+"---正在写入");
                map.put(key,value);
                //暂停毫秒
                try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
                System.out.println(Thread.currentThread().getName()+"\t"+"---完成写入");
            }finally {
                rwLock.writeLock().unlock();
            }
        }
        public void read(String key)
        {
            rwLock.readLock().lock();
            try
            {
                System.out.println(Thread.currentThread().getName()+"\t"+"---正在读取");
                String result = map.get(key);
                //后续开启注释修改为2000,演示一体两面,读写互斥,读读共享,读没有完成时候写锁无法获得
                //try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }
                System.out.println(Thread.currentThread().getName()+"\t"+"---完成读取result:"+result);
            }finally {
                rwLock.readLock().unlock();
            }
        }
    }
    
    
    public class ReentrantReadWriteLockDemo
    {
        public static void main(String[] args)
        {
            MyResource myResource = new MyResource();
    
            for (int i = 1; i <=10; i++) {
                int finalI = i;
                new Thread(() -> {
                    myResource.write(finalI +"", finalI +"");
                },String.valueOf(i)).start();
            }
    
            for (int i = 1; i <=10; i++) {
                int finalI = i;
                new Thread(() -> {
                    myResource.read(finalI +"");
                },String.valueOf(i)).start();
            }
    
            //暂停几秒钟线程
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
    
            //读全部over才可以继续写
            for (int i = 1; i <=3; i++) {
                int finalI = i;
                new Thread(() -> {
                    myResource.write(finalI +"", finalI +"");
                },"newWriteThread==="+String.valueOf(i)).start();
            }
        }
    }
    
    • 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
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78

    从写锁→读锁,ReentrantReadWriteLock可以降级

    可以降级

    锁的严苛程度变强叫做升级,反之叫做降级

    在这里插入图片描述

    锁降级:将写入锁降级为读锁(类似Linux文件读写权限理解,就像写权限要高于读权限一样)

    读写锁降级演示

    锁降级:遵循获取写锁→再获取读锁→再释放写锁的次序,写锁能够降级成为读锁。
    如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。

    在这里插入图片描述

    重入还允许通过获取写入锁定,然后读取锁然后释放写锁从写锁到读取锁,
    但是,从读锁定升级到写锁是不可能的。

    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    /**
     * 锁降级:遵循获取写锁→再获取读锁→再释放写锁的次序,写锁能够降级成为读锁。
     *
     * 如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。
     */
    public class LockDownGradingDemo
    {
        public static void main(String[] args)
        {
            ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    
            ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
            ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
    
    
            writeLock.lock();
            System.out.println("-------正在写入");
    
    
            readLock.lock();
            System.out.println("-------正在读取");
    
            writeLock.unlock();
    
        }
    }
    
    • 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
    • 27
    • 28

    如果有线程在读,那么写线程是无法获取写锁的,是悲观锁的策略

    不可锁升级

    线程获取读锁是不能直接升级为写入锁的。

    在这里插入图片描述

    在ReentrantReadWriteLock中,当读锁被使用时,如果有线程尝试获取写锁,该写线程会被阻塞。
    所以,需要释放所有读锁,才可获取写锁,

    在这里插入图片描述

    写锁和读锁是互斥的

    写锁和读锁是互斥的(这里的互斥是指线程间的互斥
    当前线程可以获取到写锁又获取到读锁,但是获取到了读锁不能继续获取写锁),这是因为读写锁要保持写操作的可见性
    因为,如果允许读锁在被获取的情况下对写锁的获取,那么正在运行的其他读线程无法感知到当前写线程的操作。

    因此,
    分析读写锁ReentrantReadWriteLock,会发现它有个潜在的问题:
    读锁全完,写锁有望;写锁独占,读写全堵
    如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,见前面Case《code演示LockDownGradingDemo》
    即ReadWriteLock读的过程中不允许写只有等待线程都释放了读锁,当前线程才能获取 写锁
    也就是写入必须等待,这是一种悲观的读锁,o(╥﹏╥)o,人家还在读着那,你先别去写,省的数据乱

    分析StampedLock,会发现它改进之处在于:
    读的过程中也允许获取写锁介入(相当牛B,读和写两个操作也让你“共享”(注意引号)),这样会导致我们读的数据就可能不一致!
    所以,需要额外的方法来判断读的过程中是否有写入,这是一种乐观的读锁,O(∩_∩)O哈哈~
    显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。

    读写锁之读写规矩,再说降级

    锁降级
    ReentrantWriteReadLock支持锁降级,遵循按照获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁,不支持锁升级。

    在这里插入图片描述

    1 代码中声明了一个volatile类型的cacheValid变量,保证其可见性。

    2 首先获取读锁,如果cache不可用,则释放读锁,获取写锁,在更改数据之前,再检查一次cacheValid的值,然后修改数据,将cacheValid置为true,然后在释放写锁前获取读锁;此时,cache中数据可用,处理cache中数据,最后释放读锁。这个过程就是一个完整的锁降级的过程,目的是保证数据可见性。

    如果违背锁降级的步骤
    如果当前的线程C在修改完cache中的数据后,没有获取读锁而是直接释放了写锁,那么假设此时另一个线程D获取了写锁并修改了数据,那么C线程无法感知到数据已被修改,则数据出现错误。

    如果遵循锁降级的步骤
    线程C在释放写锁之前获取读锁,那么线程D在获取写锁时将被阻塞,直到线程C完成数据处理过程,释放读锁。这样可以保证返回的数据是这次更新的数据,该机制是专门为了缓存设计的。

    2.邮戳锁StampedLock

    无锁→独占锁→读写锁→邮戳锁

    1.简介

    StampedLock是JDK1.8中新增的一个读写锁,也是对JDK1.5中的读写锁ReentrantReadWriteLock的优化。

    邮戳锁也叫票据锁。


    stamp(戳记,long类型):代表了锁的状态。当stamp返回零时,表示线程获取锁失败。并且,当释放锁或者转换锁的时候,都要传入最初获取的stamp值。


    它是由锁饥饿问题引出的。

    锁饥饿问题:

    ReentrantReadWriteLock实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了,
    假如当前1000个线程,999个读,1个写,有可能999个读取线程长时间抢到了锁,那1个写线程就悲剧了
    因为当前有可能会一直存在读锁,而无法获得写锁,根本没机会写,o(╥﹏╥)o

    2.如何缓解锁饥饿问题?

    使用“公平”策略可以一定程度上缓解这个问题,例如:new ReentrantReadWriteLock(true);

    但是“公平”策略是以牺牲系统吞吐量为代价的

    3.StampedLock类的乐观读锁

    ReentrantReadWriteLock
    允许多个线程同时读,但是只允许一个线程写,在线程获取到写锁的时候,其他写操作和读操作都会处于阻塞状态,
    读锁和写锁也是互斥的,所以在读的时候是不允许写的,读写锁比传统的synchronized速度要快很多,
    原因就是在于 ReentrantReadWriteLock支持读并发

    StampedLock横空出世
    ReentrantReadWriteLock的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。
    但是,StampedLock采取乐观获取锁后,其他线程尝试获取写锁时 不会被阻塞,这其实是对读锁的优化,
    所以,在获取乐观读锁后,还需要对结果进行校验。

    4.StampedLock的特点

    所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为零表示获取失败,其余都表示成功;

    所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;

    StampedLock是不可重入的,危险(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)

    StampedLock有三种访问模式

    ①Reading(读模式):功能和ReentrantReadWriteLock的读锁类似

    ②Writing(写模式):功能和ReentrantReadWriteLock的写锁类似

    ③Optimistic reading(乐观读模式):无锁机制,类似于数据库中的乐观锁,
    支持读写并发,很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式

    乐观读模式演示

    读的过程中也允许获取写锁介入

    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.StampedLock;
    
    public class StampedLockDemo
    {
        static int number = 37;
        static StampedLock stampedLock = new StampedLock();
    
        public void write()
        {
            long stamp = stampedLock.writeLock();
            System.out.println(Thread.currentThread().getName()+"\t"+"=====写线程准备修改");
            try
            {
                number = number + 13;
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                stampedLock.unlockWrite(stamp);
            }
            System.out.println(Thread.currentThread().getName()+"\t"+"=====写线程结束修改");
        }
    
        //悲观读
        public void read()
        {
            long stamp = stampedLock.readLock();
            System.out.println(Thread.currentThread().getName()+"\t come in readlock block,4 seconds continue...");
            //暂停几秒钟线程
            for (int i = 0; i <4 ; i++) {
                try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
                System.out.println(Thread.currentThread().getName()+"\t 正在读取中......");
            }
            try
            {
                int result = number;
                System.out.println(Thread.currentThread().getName()+"\t"+" 获得成员变量值result:" + result);
                System.out.println("写线程没有修改值,因为 stampedLock.readLock()读的时候,不可以写,读写互斥");
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                stampedLock.unlockRead(stamp);
            }
        }
    
        //乐观读
        public void tryOptimisticRead()
        {
            long stamp = stampedLock.tryOptimisticRead();
            int result = number;
            //间隔4秒钟,我们很乐观的认为没有其他线程修改过number值,实际靠判断。
            System.out.println("4秒前stampedLock.validate值(true无修改,false有修改)"+"\t"+stampedLock.validate(stamp));
            for (int i = 1; i <=4 ; i++) {
                try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
                System.out.println(Thread.currentThread().getName()+"\t 正在读取中......"+i+
                        "秒后stampedLock.validate值(true无修改,false有修改)"+"\t"
                        +stampedLock.validate(stamp));
            }
            if(!stampedLock.validate(stamp)) {
                System.out.println("有人动过--------存在写操作!");
                stamp = stampedLock.readLock();
                try {
                    System.out.println("从乐观读 升级为 悲观读");
                    result = number;
                    System.out.println("重新悲观读锁通过获取到的成员变量值result:" + result);
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    stampedLock.unlockRead(stamp);
                }
            }
            System.out.println(Thread.currentThread().getName()+"\t finally value: "+result);
        }
    
        public static void main(String[] args)
        {
            StampedLockDemo resource = new StampedLockDemo();
    
            new Thread(() -> {
                resource.read();
                //resource.tryOptimisticRead();
            },"readThread").start();
    
            // 2秒钟时乐观读失败,6秒钟乐观读取成功resource.tryOptimisticRead();,修改切换演示
            //try { TimeUnit.SECONDS.sleep(6); } catch (InterruptedException e) { e.printStackTrace(); }
    
            new Thread(() -> {
                resource.write();
            },"writeThread").start();
        }
    }
    
    
    • 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
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92

    StampedLock的缺点

    1)StampedLock 不支持重入,没有Re开头

    2)StampedLock 的悲观读锁和写锁都不支持条件变量(Condition),这个也需要注意。

    3)使用 StampedLock一定不要调用中断操作,即不要调用interrupt() 方法。如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly()和写锁writeLockInterruptibly()

    3.JUC总结

    1)CompletableFuture

    2)“锁”

    1. 悲观锁
    2. 乐观锁
    3. 自旋锁
    4. 可重入锁(递归锁)
    5. 写锁(独占锁)/读锁(共享锁)
    6. 公平锁/非公平锁
    7. 死锁
    8. 偏向锁
    9. 轻量锁
    10. 重量锁
    11. 邮戳(票据)锁
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    3)JMM内存模型

    4)synchronized及升级优化

      ```java
      作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁;
      作用于代码块,对括号里配置的对象加锁。
      作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁;
      ```
    
    • 1
    • 2
    • 3
    • 4
    • 5

    无锁→偏向锁→轻量锁→重量锁

    Java对象内存布局和对象头

    在这里插入图片描述

    5)CAS

    ​ 原理:比较并交换

    //unsafe.cpp
    UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
      UnsafeWrapper("Unsafe_CompareAndSwapInt");
      oop p = JNIHandles::resolve(obj);
      jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
      return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
    UNSAFE_END
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    JDK提供的CAS机制,在汇编层级,会禁止变量两侧的指令优化,然后使用cmpxchg指令比较并更新变量值

    ABA问题

    问题:
    线程X准备将变量的值从A改为B,然而这期间线程Y将变量的值从A改为C,然后再改为A;最后线程X检测变量值是A,并置换为B。
     
    但实际上,A已经不再是原来的A了解决方法,是把变量定为唯一类型。值可以加上版本号,或者时间戳。
     
    解决:
    如加上版本号,线程Y的修改变为A1->B2->A3,此时线程X再更新则可以判断出A1不等于A3
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    6)volatile

    特性:可见性,禁重排

    如何实现的:内存屏障

    7)LockSupport

    LockSupport是基于Unsafe类,由JDK提供的线程操作工具类,主要作用就是挂起线程,唤醒线程。
    LockSupport.park
    LockSupport.unpark

    LockSupport.park和Object.wait区别
    线程在Object.wait之后必须等到Object.notify才能唤醒
    LockSupport可以先unpark线程,等线程执行LockSupport.park是不会挂起的,可以继续执行

    8)AbstractQueuedSynchronizer

    volatile+cas机制实现的锁模板,保证了代码的同步性和可见性,而AQS封装了线程阻塞等待挂起,解锁唤醒其他线程的逻辑。AQS子类只需根据状态变量,判断是否可获取锁,是否释放锁,使用LockSupport挂起、唤醒线程即可

    //AbstractQueuedSynchronizer.java
    public class AbstractQueuedSynchronizer{
        //线程节点
        static final class Node {
            volatile Node prev;
            volatile Node next;
            volatile Thread thread;
            ...
        }    
        //head 等待队列头尾节点
        private transient volatile Node head;
        private transient volatile Node tail;
        private volatile int state;      // The synchronization state. 同步状态
        ...
        //提供CAS操作,状态具体的修改由子类实现
        protected final boolean compareAndSetState(int expect, int update) {
            return STATE.compareAndSet(this, expect, update);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    出队入队Node

    在这里插入图片描述

    AQS内部维护一个同步队列,元素就是包装了线程的Node。

    同步队列中首节点是获取到锁的节点,它在释放锁的时会唤醒后继节点,后继节点获取到锁的时候,会把自己设为首节点。
    线程会先尝试获取锁,失败则封装成Node,CAS加入同步队列的尾部。在加入同步队列的尾部时,会判断前驱节点是否是head结点,并尝试加锁(可能前驱节点刚好释放锁),否则线程进入阻塞等待。

    9)ThreadLocal

    当使用ThreadLocal声明变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
    每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本

    10)原子增强类Atomic

  • 相关阅读:
    [运维|数据库] PostgreSQL数据库对MySQL的 READS SQL DATA 修饰符处理
    基于Delft3D模型水体流动、污染物对流扩散、质点运移、溢油漂移及地表水环境报告编制教程
    Java Class.forName()具有什么功能呢?
    【JavaEE】多线程案例-线程池
    容器云平台的六个最佳实践
    操作系统——cpu、内存、缓存介绍
    位运算 离散化 区间和算法
    Activity中何时能拿到组件的宽高
    矩阵的c++实现(2)
    (Applied Intelligence-2022)TransGait: 基于多模态的步态识别与集合Transformer
  • 原文地址:https://blog.csdn.net/weixin_43847283/article/details/125470856