十二、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();
    }
}

2、乐观锁

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

二、公平锁和非公平锁

1、概述

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

2、创建方式

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

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方法");
    }
}

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();
        }
    }
}

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、概述

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);
    }
}
运行结果:可以看到在写入的时候,写操作被其他线程打断了,就造成了还没有写入完成,其他线程又开始写,造成了数据不一致。
写线程 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

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();
        }
    }
}
运行结果:从运行结果可以看出,写入操作是一个一个线程进行执行的,并且中间不会被打断,而读操作的时候,是同时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

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();
    }
}
/**
 * @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();
    }
}

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
 */

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
 */
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值