• 十二、Java中的各种锁


    一、乐观锁与悲观锁

    1、悲观锁

    1、认为在使用数据时一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改(一上来就加锁)。
    2、synchronized关键字和Lock的实现类都是悲观锁。
    3、适合写操作多的场景,先加锁可以保证写操作时数据正确。
    public synchronized void m1() {
        //加锁后的业务逻辑
    }
    
    //保证多个线程使用的是同一个lock对象的前提下
    ReentrantLock lock = new ReentrantLock();
    public void m2() {
        lock.lock();
        try {
            //操作同步资源
        }finally {
            lock.unlock();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    2、乐观锁

    1、认为在使用数据时不会有别的线程来修改数据,所以不会加锁,只是在更新数据的时候去判断有没有别的线程修改过这个数据:
    • 如果数据没有被其他线程修改,当前线程将自己的修改的数据成功写入。
    • 如果数据已经被其他线程修改,则根据不同的实现方法实现不同的操作。
    2、乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的还有一种是通过版本号机制,修改数据时判断拿到的版本号和数据库中的版本号是否一致,不一致则不操作;如果版本号一致,则修改数据,并将版本号加1
    3、适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升
    //保证多个线程使用的是同一个AtomicInteger
    private AtomicInteger atomicInteger = new AtomicInteger();
    atomicInteger.incrementAndGet();
    
    • 1
    • 2
    • 3

    二、公平锁和非公平锁

    1、概述

    1、公平锁:是指多个线程按照申请锁的顺序来获取锁,先来后到,先来先服务就是公平的,也就是队列。
    2、非公平锁:是指在多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取到锁,在高并发的情况下,有可能造成优先级反转或者饥饿现象(也就是某个线程一直得不到锁即为饥饿)。对于synchronized而言,也是一种非公平锁

    2、创建方式

    1、并发包ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或者非公平锁,默认是非公平锁,因为非公平锁的优点在于吞吐量比公平锁大。
    public class LockTest {
        public static void main(String[] args) {
            /**
             * 创建一个可重入锁,true表示公平锁,false表示非公平锁。默认非公平锁(空参)
             */
            Lock lock = new ReentrantLock(true);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    3、两者的区别

    1、公平锁就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列中的第一个,就占用锁,否者就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。
    2、非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。对于synchronized而言,也是一种非公平锁

    4、默认非公平锁的解释

    1、恢复挂起的线程到真正获取到锁是有时间差的,从CPU角度上看,这个时间差存在很明显的,所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲时间。
    2、使用多线程很重要的考量点是线程切换的开销,当使用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销
    3、使用说明:如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;否则那就用公平锁,大家公平使用。

    三、可重入锁递归锁ReentrantLock

    1、概述

    1、可重入锁(也叫递归锁)指的是同一线程外层方法获得锁之后,内层递归方法仍然能获取该锁的代码,在同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。也就是说,线程可以进入任何一个它已经拥有的锁所同步的代码块(前提是锁对象必须是同一个对象)
    2、Java中ReentrantLock、synchronized就是典型的可重入锁
    3、作用:避免死锁

    2、验证synchronized可重入

    1、在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
    /**
     * @Date: 2022/5/23
     * 验证synchronized可重入:同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁
     * 结果:
     * t1调用method1方法	t1在外层方法获取锁的时候
     * t1调用method2方法	t1在进入内层方法会自动获取锁
     * t2调用method1方法
     * t2调用method2方法
     */
    public class SyncLockTest {
        public static void main(String[] args) {
            SyncLockTest test = new SyncLockTest();
            //两个线程操作
            new Thread(() ->{
                test.method1();
            }, "t1").start();
    
            new Thread(() ->{
                test.method1();
            }, "t2").start();
        }
    
        public synchronized void method1() {
            System.out.println(Thread.currentThread().getName() + "调用method1方法");
            //在同步方法中,调用另一个同步方法
            method2();
        }
    
        public synchronized void method2() {
            System.out.println(Thread.currentThread().getName() + "调用method2方法");
        }
    }
    
    • 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

    3、synchronized实现重入锁原理

    1、每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针
    2、当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
    3、在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁
    4、当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为0代表锁已被释放。

    4、验证ReentrantLock可重入

    注意:加锁几次就要解锁几次,否则导致加锁解锁次数不一致,第二个线程无法获取到锁,导致一直等待的情况
    /**
     * @Date: 2022/5/23
     * 验证ReentrantLock可重入:同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁
     * 结果:与加synchronized关键字的方法是一致的,都是在外层的方法获取锁之后,线程能够进入内层
     * t1调用method1方法
     * t1调用method2方法
     * t2调用method1方法
     * t2调用method2方法
     */
    public class ReenLockTest {
        Lock lock = new ReentrantLock();
    
        public static void main(String[] args) {
            ReenLockTest test = new ReenLockTest();
            //两个线程操作
            new Thread(() ->{
                //进入method1方法时,就加了锁,再在方法中调用另一个加了锁的方法
                test.method1();
            }, "t1").start();
    
            new Thread(() ->{
                //进入method1方法时,就加了锁,再在方法中调用另一个加了锁的方法
                test.method1();
            }, "t2").start();
        }
    
        public void method1() {
            //加锁
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "调用method1方法");
                //调用另一个加了锁的方法
                method2();
            } finally {
                //释放锁
                lock.unlock();
            }
        }
    
        public void method2() {
            //加锁
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "调用method2方法");
            } finally {
                //释放锁
                lock.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
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    5、死锁及排查

    死锁说明及排查方法:

    四、自旋锁SpinLock

    1、概述

    1、自旋锁(spinlock)是指尝试获取锁的时候线程不会立即阻塞,而是采用循环的方式去尝试获取锁
    • 好处:减少线程上下文切换的消耗。循环比较获取直到成功为止,没有类似于wait的阻塞。
    • 缺点:当不断自旋的线程越来越多的时候,会因为执行while循环不断的消耗CPU资源。
    2、CAS底层使用的就是自旋,自旋就是多次尝试,多次方法,不会阻塞的状态的就是自旋

    在这里插入图片描述

    2、实现自旋锁

    /**
     * @Date: 2022/5/24
     * 实现自旋锁,通过CAS操作完成自旋锁,A线程先持有锁5秒钟,
     * B线程发现当前有线程持有锁,自旋等待,直到A释放锁。
     * 运行结果:
     *      线程A进入lock
     *      线程B进入lock
     *      线程B正在自旋
     *      线程B正在自旋
     *      线程B正在自旋
     *      线程A退出,设置为null
     *      线程B正在自旋
     *      线程B退出,设置为null
     */
    public class SpinLockTest {
        //原子引用线程
        AtomicReference<Thread> atomicReference = new AtomicReference<>();
    
        /**
         * 加锁
         */
        public void lock() {
            //获取当前线程
            Thread thread = Thread.currentThread();
            System.out.println(thread.getName() + "进入lock");
            //开始自旋,期望值是null,更新值是当前线程,如果是null,则更新为当前线程,否则自旋
            while (!atomicReference.compareAndSet(null, thread)) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(thread.getName() + "正在自旋");
            }
        }
    
        /**
         * 解锁
         */
        public void unLock() {
            //获取当前线程
            Thread thread = Thread.currentThread();
            //用完之后,把atomicReference变成null
            atomicReference.compareAndSet(thread, null);
            System.out.println(thread.getName() + "退出,设置为null");
        }
    
        public static void main(String[] args) {
            SpinLockTest test = new SpinLockTest();
            new Thread(() -> {
                //占有锁
                test.lock();
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //释放锁
                test.unLock();
            }, "线程A").start();
    
            //主线程暂停1秒,使得线程A先执行
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            new Thread(() -> {
                //占有锁
                test.lock();
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 开始释放锁
                test.unLock();
            }, "线程B").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

    五、独占锁(写)/共享锁(读)/互斥锁

    1、概述

    1、独占锁:指该锁一次只能被一个线程所持有,对ReentrantLock和Synchronized而言都是独占锁。
    2、共享锁:指该锁可以被多个线程所持有。
    3、对于ReentrantReadWriteLock其读锁是共享锁,写锁是独占锁。写入的时候只能一个线程写,读的时候可以多个线程同时读,但是不能同时存在读写线程。

    2、为什么会有读锁和写锁

    1、使用ReentrantLock创建锁的时候,是独占锁,也就是说一次只能一个线程访问,但是有一个读写分离场景,读的时候想同时进行,因此原来独占锁的并发性就没这么好了,因为读锁并不会造成数据不一致的问题,因此可以多个人共享读。
    2、多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行,但是如果一个线程想去写共享资源,就不应该再有其它线程可以对该资源进行读或写。
    • 读-读:能共存
    • 读-写:不能共存
    • 写-写:不能共存
    3、读操作没有完成之前,写锁是无法获取的

    3、读写锁问题分析

    1、实现一个读写缓存的操作,假设开始没有加锁的时候,会出现什么情况
    /**
     * @Date: 2022/5/24
     * 读写锁问题分析
     */
    public class ReadWriteLockTest {
        public static void main(String[] args) {
            CacheMap cache = new CacheMap();
            //5个线程写
            for (int i = 1; i <= 5; i++) {
                //lambda表达式内部必须是final
                final int threadName = i;
                new Thread(() -> {
                    cache.put(threadName + "", UUID.randomUUID().toString().substring(0, 6));
                }, "写线程 " + threadName).start();
            }
            try {
                TimeUnit.MILLISECONDS.sleep(700);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //5个线程读
            for (int i = 1; i <= 5; i++) {
                final int threadName = i;
                new Thread(() -> {
                    cache.get(threadName + "");
                }, "读线程 " + threadName).start();
            }
        }
    }
    
    class CacheMap {
        private volatile Map<String, Object> map = new HashMap<>();
    
        /**
         * 写操作
         * 满足:原子 + 独占,整个过程必须是一个完整的统一体,中间不允许被打断,被分割
         * @param k
         * @param v
         */
        public void put(String k, Object v) {
            Thread thread = Thread.currentThread();
            System.out.println(thread.getName() + " 正在写入 " + k);
            //模拟网络拥堵,延迟0.3秒
            try {
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            map.put(k, v);
            System.out.println(thread.getName() + " 写入成功 ");
        }
    
        /**
         * 读操作
         * @param k
         */
        public void get(String k) {
            Thread thread = Thread.currentThread();
            System.out.println(thread.getName() + " 正在读取 " + k);
            try {
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Object v = map.get(k);
            System.out.println(thread.getName() + " 读取成功 " + v);
        }
    }
    
    • 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
    运行结果:可以看到在写入的时候,写操作被其他线程打断了,就造成了还没有写入完成,其他线程又开始写,造成了数据不一致。
    写线程 5 正在写入 5
    写线程 1 正在写入 1
    写线程 3 正在写入 3
    写线程 4 正在写入 4
    写线程 2 正在写入 2
    读线程 1 正在读取 1
    读线程 2 正在读取 2
    读线程 3 正在读取 3
    读线程 4 正在读取 4
    读线程 5 正在读取 5
    写线程 5 写入成功 
    写线程 1 写入成功 
    写线程 2 写入成功 
    写线程 4 写入成功 
    写线程 3 写入成功 
    读线程 4 读取成功 null
    读线程 1 读取成功 80e053
    读线程 3 读取成功 null
    读线程 2 读取成功 null
    读线程 5 读取成功 null
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    4、读写锁问题解决

    1、上面的代码是没有加锁的,这样就会造成线程在进行写入操作的时候,被其它线程频繁打断,从而不具备原子性,这个时候,就需要用到读写锁来解决了。
    /**
     * @Date: 2022/5/24
     * 读写锁问题解决
     */
    public class ReadWriteLockTest {
        public static void main(String[] args) {
            CacheMap cache = new CacheMap();
            //5个线程写
            for (int i = 1; i <= 5; i++) {
                //lambda表达式内部必须是final
                final int threadName = i;
                new Thread(() -> {
                    cache.put(threadName + "", UUID.randomUUID().toString().substring(0, 6));
                }, "写线程 " + threadName).start();
            }
            try {
                TimeUnit.MILLISECONDS.sleep(900);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //5个线程读
            for (int i = 1; i <= 5; i++) {
                final int threadName = i;
                new Thread(() -> {
                    cache.get(threadName + "");
                }, "读线程 " + threadName).start();
            }
        }
    }
    
    class CacheMap {
        /**
         * volatile:保证内存可见性
         */
        private volatile Map<String, Object> map = new HashMap<>();
    
        /**
         * 创建一个读写锁,它是一个读写融为一体的锁,在使用的时候,需要转换
         */
        private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    
        /**
         * 写操作
         * 满足:原子 + 独占,整个过程必须是一个完整的统一体,中间不允许被打断,被分割
         * @param k
         * @param v
         */
        public void put(String k, Object v) {
            //创建写锁
            readWriteLock.writeLock().lock();
            try {
                Thread thread = Thread.currentThread();
                System.out.println(thread.getName() + " 正在写入 " + k);
                //模拟网络拥堵,延迟0.3秒
                TimeUnit.MILLISECONDS.sleep(300);
                map.put(k, v);
                System.out.println(thread.getName() + " 写入成功 ");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                //写锁释放
                readWriteLock.writeLock().unlock();
            }
        }
    
        /**
         * 读操作
         * @param k
         */
        public void get(String k) {
            //创建读锁
            readWriteLock.readLock().lock();
            try {
                Thread thread = Thread.currentThread();
                System.out.println(thread.getName() + " 正在读取 " + k);
                TimeUnit.MILLISECONDS.sleep(300);
                Object v = map.get(k);
                System.out.println(thread.getName() + " 读取成功 " + v);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                //释放读锁
                readWriteLock.readLock().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
    • 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
    运行结果:从运行结果可以看出,写入操作是一个一个线程进行执行的,并且中间不会被打断,而读操作的时候,是同时5个线程进入,然后并发读取。
    写线程 3 正在写入 3
    写线程 3 写入成功 
    写线程 5 正在写入 5
    写线程 5 写入成功 
    写线程 1 正在写入 1
    写线程 1 写入成功 
    写线程 2 正在写入 2
    写线程 2 写入成功 
    写线程 4 正在写入 4
    写线程 4 写入成功 
    读线程 3 正在读取 3
    读线程 1 正在读取 1
    读线程 2 正在读取 2
    读线程 5 正在读取 5
    读线程 4 正在读取 4
    读线程 2 读取成功 389838
    读线程 3 读取成功 94f449
    读线程 1 读取成功 b48dec
    读线程 5 读取成功 f822f0
    读线程 4 读取成功 11c5a1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    5、锁降级

    1、ReentrantWriteReadLock有锁降级的特性,将写锁降级为读锁,锁的严苛程度变强叫做升级,反之叫做降级
    2、锁降级:遵循先获取写锁,再获取读锁,再释放写锁的次序,写锁能够降级成为读锁
    3、写锁降级:
    • 如果同一个线程持有了写锁,在没有释放写锁的前提下,它还可以继续获得读锁(可重入特性),这个就是写锁的降级,降级成为了读锁。
    • 按照先获取写锁,再获取读锁,再释放写锁的次序。如果释放了写锁,那么就完全转为了读锁。
    • 注意:读锁是无法升级到写锁的;读锁没有释放,写锁无法获得
    /**
     * @Date: 2022/9/12
     * 锁降级:遵循先获取写锁,再获取读锁,再释放写锁的次序,写锁能够降级成为读锁
     */
    public class LockDownGradeTest1 {
        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();
            readLock.unlock();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    /**
     * @Author: ye.yanbin
     * 读锁是无法升级到写锁的;读锁没有释放,写锁无法获得,程序阻塞
     */
    public class LockDownGradeTest1 {
        public static void main(String[] args) {
            ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
            // 读锁
            ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
            // 写锁
            ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
    
            // 读锁无法升级为写锁,且读锁没有释放,无法获取写锁,程序阻塞
            readLock.lock();
            System.out.println("读取...");
    
            writeLock.lock();
            System.out.println("写入...");
    
            writeLock.unlock();
            readLock.unlock();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    6、总结

    1、写锁和读锁是互斥的(是指线程间的互斥,但当前线程仍然可以获取写锁又获取读锁,但是获取到了读锁就不能继续获取写锁),这是因为读写锁要保持写操作的可见性。如果允许读锁在被获取的情况下对写锁的获取,那么正在运行的其他读线程无法感知到当前写线程的操作。
    2、如果有线程正在读,那么写线程需要等待读线程释放锁之后,才能获取写锁。

    六、邮戳锁StampedLock

    1、概述

    1、它是Java8在java.util.concurrent.locks新增的一个API,也是对JDK1.5中的读写锁ReentrantReadWriteLock的优化。
    2、StampedLock它是由锁饥饿问题引出来的。
    3、一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问。在读锁上分为悲观锁和乐观锁。

    2、锁饥饿问题

    1、ReentrantReadWriteLock实现了读写分离,但是如果读取执行情况很多,写入很少的情况下,使用ReentrantReadWriteLock可能会使写入线程遭遇饥饿(Starvation)问题,也就是写入线程迟迟无法竞争到锁定而一直处于等待状态
    2、使用公平策略可以一定程度缓解锁饥饿问题new ReentrantReadWriteLock(true),但是公平策略是以牺牲系统吞吐量为代价的。
    3、ReentrantReadWriteLock的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。但是,StampedLock采取乐观获取锁后,其他线程尝试获取写锁时不会被阻塞,这其实是对读锁的优化,所以,在获取乐观读锁后,还需要对结果进行校验。

    3、StampedLock的特点

    1、所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为0表示获取失败,其余都表示成功
    2、所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致
    3、StampedLock是不可重入的,如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁

    4、StampedLock三种访问模式

    1、读模式(Reading):功能和ReentrantReadWriteLock的读锁类似
    2、写模式(Writing):功能和ReentrantReadWriteLock的写锁类似
    3、乐观读模式(Optimistic reading):无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式。

    5、StampedLock的读写模式

    读锁没有释放,写锁无法获得,程序阻塞
    /**
     * @Date: 2022/9/12
     * 读写模式:读锁没有释放,写锁无法获得,程序阻塞
     */
    public class StampedLockTest {
        static StampedLock stampedLock = new StampedLock();
    
        static int number = 1;
    
        public static void main(String[] args) throws InterruptedException {
            StampedLockTest lockTest = new StampedLockTest();
            new Thread(() -> {
                lockTest.read();
            }, "读线程").start();
    
            Thread.sleep(1000);
    
            new Thread(() -> {
                lockTest.write();
            }, "写线程").start();
        }
    
        /**
         * 写操作
         */
        public static void write() {
            System.out.println(Thread.currentThread().getName() + " 准备写入...");
            // 获取标记
            long stamp = stampedLock.writeLock();
            try {
                number = number + 9;
            } finally {
                stampedLock.unlockWrite(stamp);
            }
            System.out.println(Thread.currentThread().getName() + " 写入完成,值为:" + number);
        }
    
        /**
         * 悲观读,读取操作没有完成,写锁无法获得
         */
        public static void read() {
            System.out.println(Thread.currentThread().getName() + " 正在读取中...");
            // 获取标记
            long stamp = stampedLock.readLock();
            try {
                for (int i = 0; i < 4; i++) {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " 正在读取中...");
                }
                int result = number;
                System.out.println(Thread.currentThread().getName() + " 读取的值为:" + result);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 释放
                stampedLock.unlockRead(stamp);
            }
        }
    }
    /**
     * 运行结果:可以看到写线程会一直等待读操作完成后,才能写入完成
     * 读线程 正在读取中...
     * 读线程 正在读取中...
     * 写线程 准备写入...
     * 读线程 正在读取中...
     * 读线程 正在读取中...
     * 读线程 正在读取中...
     * 读线程 读取的值为:1
     * 写线程 写入完成,值为:10
     */
    
    • 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

    6、StampedLock的乐观读模式

    无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式
    /**
     * @Date: 2022/9/12
     * 读写模式:读锁没有释放,写锁无法获得,程序阻塞
     */
    public class StampedLockTest1 {
        static StampedLock stampedLock = new StampedLock();
    
        static int number = 1;
    
        public static void main(String[] args) throws InterruptedException {
            StampedLockTest1 lockTest = new StampedLockTest1();
            new Thread(() -> {
                lockTest.tryOptimisticRead();
            }, "读线程").start();
    
            // 暂停2秒钟
            Thread.sleep(2000);// 会输出:悲观读后的值为:10
    
            // 暂停6秒钟
            // Thread.sleep(6000);// 会输出:读线程 读取成功,值为:1
    
            new Thread(() -> {
                lockTest.write();
            }, "写线程").start();
        }
    
        /**
         * 写操作
         */
        public static void write() {
            System.out.println(Thread.currentThread().getName() + " 准备写入...");
            // 获取标记
            long stamp = stampedLock.writeLock();
            try {
                number = number + 9;
            } finally {
                stampedLock.unlockWrite(stamp);
            }
            System.out.println(Thread.currentThread().getName() + " 写入完成,值为:" + number);
        }
    
        /**
         * 乐观读,认为读取时没人修改,假如被修改再实现升级为悲观读模式
         */
        public static void tryOptimisticRead() {
            // 获得一个乐观读锁
            long stamp = stampedLock.tryOptimisticRead();
            // 获取原始值,如果没有写锁获取,那么读取的值是不会改变的
            int result = number;
            // stampedLock.validate(stamp):检查发出乐观读锁后同时是否有其他写锁发生,true:有写锁介入
            System.out.println("读取之前锁标记是否被修改(true:无修改,false:被修改)" + stampedLock.validate(stamp));
            for (int i = 0; i < 4; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " 正在读取中... 锁标记:" + stampedLock.validate(stamp));
            }
            // 判断是否被修改过
            if (!stampedLock.validate(stamp)) {
                System.out.println("有写锁介入...锁标记被修改...");
                try {
                    // 悲观读
                    stamp = stampedLock.readLock();
                    result = number;
                    System.out.println("从乐观读 升级为 悲观读");
                    System.out.println("悲观读后的值为:" + result);
                } finally {
                    stampedLock.unlockRead(stamp);
                }
            }
            System.out.println(Thread.currentThread().getName() + " 读取成功,值为:" + result);
        }
    }
    /**
     * 暂停2s时的运行结果:可以看到写线程获取到锁,读线程中锁标记值被修改过,从乐观读 升级为 悲观读
     * 读取之前锁标记(true:无修改,false:被修改)true
     * 读线程 正在读取中... 锁标记:true
     * 读线程 正在读取中... 锁标记:true
     * 写线程 准备写入...
     * 写线程 写入完成,值为:10
     * 读线程 正在读取中... 锁标记:false
     * 读线程 正在读取中... 锁标记:false
     * 有写锁介入...锁标记被修改...
     * 从乐观读 升级为 悲观读
     * 悲观读后的值为:10
     * 读线程 读取成功,值为:10
     *
     *
     * 暂停6s时的运行结果:可以看到读线程中锁标记值没有被修改过,仍然是乐观读
     * 读取之前锁标记(true:无修改,false:被修改)true
     * 读线程 正在读取中... 锁标记:true
     * 读线程 正在读取中... 锁标记:true
     * 读线程 正在读取中... 锁标记:true
     * 读线程 正在读取中... 锁标记:true
     * 读线程 读取成功,值为:1
     * 写线程 准备写入...
     * 写线程 写入完成,值为:10
     */
    
    • 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
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
  • 相关阅读:
    Pytorch的入门操作(三)
    Cocos Creator3.8 项目实战(七)Listview 控件的实现和使用
    Notepad++ 通过HexEditor插件查看.hprof文件、heap dump文件的堆转储数据
    kubernetes集群编排——k8s存储(volumes,持久卷,statefulset控制器)
    【2012】408联考数据结构真题整理
    Spring Boot项目误将Integer类型写成int来进行传参
    企业用户如何快速搭建自己官方网站或者是电商网站?
    Linux学习-30-chgrp、chown等修改文件和目录的所有者和所属组命令
    【Java】泛型 之 extends通配符
    什么是深克隆,浅克隆?(案例详解)
  • 原文地址:https://blog.csdn.net/qq_42200163/article/details/126944840