二、Lock 锁—— 2.3 读写锁

不加锁

如果不加锁,任何得到的读写操作是无序的,甚至可能读出来 null 数据。

class Cache {
    Map<String, String> map = new HashMap<>();

    public void put(String key, String value) {
        System.out.println(Thread.currentThread().getName() + " 开始写");
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + " 写数据: " + value);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    public String get(String key) {
        System.out.println(Thread.currentThread().getName() + " 开始读");
        String value = map.get(key);
        System.out.println(Thread.currentThread().getName() + " 读数据: " + value);
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return value;
    }
}

public class RWDemo {

    public static void main(String[] args) {
        Cache cache = new Cache();

        for (int i = 0; i < 5; i++) {
            String num = String.valueOf(i);
            new Thread(() -> {
                cache.put(num, num);
            }).start();
        }

        for (int i = 0; i < 5; i++) {
            String num = String.valueOf(i);
            new Thread(() -> {
                cache.get(num);
            }).start();
        }
    }
}

普通 lock 锁

使用普通加锁lock,读操作和写操作都是串行执行,而且写操作全部完成,才会进行读。这是因为 main方法中,先定义了写线程。

写串行保证数据安全,读操作怎么可以串行呢?

class Cache {

    private static final ReentrantLock lock = new ReentrantLock();

    Map<String, String> map = new HashMap<>();

    public void put(String key, String value) {
        lock.lock();

        try {
            System.out.println(Thread.currentThread().getName() + " 开始写");
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + " 写数据: " + value);
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }

    }

    public void get(String key) {
        lock.lock();

        try {
            System.out.println(Thread.currentThread().getName() + " 开始读");
            String value = map.get(key);
            System.out.println(Thread.currentThread().getName() + " 读数据: " + value);
            Thread.sleep(200);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
    }
}

public class RWDemo {

    public static void main(String[] args) {
        Cache cache = new Cache();

        for (int i = 0; i < 5; i++) {
            String num = String.valueOf(i);
            new Thread(() -> {
                cache.put(num, num);
            }).start();
        }

        for (int i = 0; i < 5; i++) {
            String num = String.valueOf(i);
            new Thread(() -> {
                cache.get(num);
            }).start();
        }
    }
}

读写锁

使用读写锁,写锁串行,但是读并发,瞬间完成了读效果非常好,如下图。

结论:读写锁保证了写操作的原子性,并且可以进行并发读。

class Cache {

    private static final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    Map<String, String> map = new HashMap<>();

    public void put(String key, String value) {
        readWriteLock.writeLock().lock();

        try {
            System.out.println(Thread.currentThread().getName() + " 开始写");
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + " 写数据: " + value);
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            readWriteLock.writeLock().unlock();
        }

    }

    public void get(String key) {
        readWriteLock.readLock().lock();

        try {
            System.out.println(Thread.currentThread().getName() + " 开始读");
            String value = map.get(key);
            System.out.println(Thread.currentThread().getName() + " 读数据: " + value);
            Thread.sleep(200);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            readWriteLock.readLock().unlock();
        }
    }
}

public class RWDemo {

    public static void main(String[] args) {
        Cache cache = new Cache();

        for (int i = 0; i < 5; i++) {
            String num = String.valueOf(i);
            new Thread(() -> {
                cache.put(num, num);
            }).start();
        }

        for (int i = 0; i < 5; i++) {
            String num = String.valueOf(i);
            new Thread(() -> {
                cache.get(num);
            }).start();
        }
    }
}

锁饥饿

锁饥饿(Lock Starvation)是指在多线程编程中的一种情况,其中某些线程可能无法获得所需的锁,而一直等待下去,从而无法继续执行,即线程被"饿死"在等待锁的过程中。这可能会导致应用程序的性能问题和不稳定性。

读写锁用不好就会出现锁饥饿。锁饥饿,写操作执行完才进行读,那么读操作就一直在等待,全部等待。

那可不可以,读写交叉并行呢?不是说 写开始 -> 读开始 -> 写结束 -> 读结束这样穿插。

是读写操作独立,但是可以交叉执行两个操作,如图。

只需要修改执行线程的这个步骤,for 循环中允许读线程,也允许写线程。

for (int i = 0; i < 5; i++) {
    String num = String.valueOf(i);
    new Thread(() -> {
        cache.put(num, num);
    }).start();
    //        }
    //
    //        for (int i = 0; i < 5; i++) {
    //            String num = String.valueOf(i);
    new Thread(() -> {
        cache.get(num);
    }).start();
}

锁降级

  • 什么是锁降级呢?
加写锁
写入数据 a = 1
释放写锁

...

加读锁
读取数据 a = 2
释放读锁

问题在于该在释放写锁、获取读锁中间能够有其他的写线程插入,从而影响数据的一致性。影响了我们想要读取到的数据a = 1,如果避免呢?锁降级。

即,先加读锁再释放写锁,就是锁降级。

加写锁
写入数据 a = 1

加读锁
释放写锁

... 这地方无论怎么操作,不能影响 a = 1

读取数据 a = 1
释放读锁

锁降级是一种多线程编程中的一种锁策略,它指的是在持有某个锁的同时,降低该锁的粒度,也就是大粒度锁 -> 小粒度锁。这通常是为了减少锁的竞争,提高程序的并发性能。

锁降级的典型场景是在持有某个写锁时,释放写锁并获取读锁。这样做的好处是在执行读操作期间,其他线程可以同时执行读操作,提高了并发性能。

看到结果就是,读线程读取的数据和写线程写入的数据是完全一样的。

因为我们没法模拟其他线程在中间修改data的一个情形,但是这样一定是对的!

public class RWDemo {

    private static final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    private static Integer data = 0;

    public static int update(Integer newData) {

        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 开始写");
            data = newData;
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + " 写数据: " + data);

            // 写操作结束释放锁之前,锁降级开始
            readWriteLock.readLock().lock();

        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            readWriteLock.writeLock().unlock();
        }

        try {
            return get();
        } finally {
            readWriteLock.readLock().unlock();
        }
    }

    public static int get() {
        readWriteLock.readLock().lock();

        try {
            System.out.println(Thread.currentThread().getName() + " 开始读");
            Thread.sleep(200);
            System.out.println(Thread.currentThread().getName() + " 读数据: " + data);

        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            readWriteLock.readLock().unlock();
        }
        return data;
    }

    public static void main(String[] args) {

        new Thread(() -> {
            update(999);
        },"写线程-1").start();

        new Thread(() -> {
            update(666);
        },"写线程-2").start();

        new Thread(() -> {
            get();
        },"读线程-1").start();

        new Thread(() -> {
            get();
        },"读线程-2").start();
    }
}

小结

  1. 支持公平/非公平策略(可以去看源码,支持参数true/false
  2. 支持可重入

可以理解为就是粒度的转换呗。

是否可重入

再去读锁

再去写锁

获取了读锁

获取了写锁

        3. 支持锁降级,不支持锁升级

粒度大 -> 粒度小 √

粒度小 -> 粒度大 ×

        4. 读写锁如果使用不当,很容易产生“饥饿”问题:

在读线程非常多,写线程很少的情况下,很容易导致写线程“饥饿”,虽然使用“公平”策略可以一定程度上缓解这个问题,但是“公平”策略是以牺牲系统吞吐量为代价的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值