无锁->ReentrantLock->ReentrantReadWriteLock->StampedLock讲解

一. 概述

锁的演变过程

无锁->独占锁->读写锁->邮戳锁

1. 无锁:多线程抢占,数据混乱,无序

2. ReentrantLock、synchronized有序,数据一致性,每次只能来一个线程,不管什么操作

缺点:读操作效率低

3. ReentrantReadWriteLock读写互斥,读读可以共享,提升大面积的共享性能,同时多人读。

缺点:

        1. 锁饥饿问题,写线程获得不到锁

        2. 读的过程中,如果没有释放,写线程不可以获得锁;必须读完后,才能有机会写

4. StampedLock读的过程中,也允许写

二. ReentrantLock

        独占锁,读写都是一致性,每次only one一个,程序正确,但是每次只有一个读,读效率不高

代码示例

         读写分离,写写分离,读读分离,每次一个线程执行完后,才能执行下一个线程。

class MyResource01{
    Map<String,String> map = new HashMap<>();
    //独占锁,读写都是一致性,每次only one一个,程序正确,但是每次只有一个读,读效率不高
    Lock lock = new ReentrantLock();

    public void write(String key,String value){
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+" 正在写");
            map.put(key, value);
            TimeUnit.MILLISECONDS.sleep(500);
            System.out.println(Thread.currentThread().getName()+" 完成写");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void read(String key){
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+" 正在读");
            String res = map.get(key);
            System.out.println(Thread.currentThread().getName()+" 完成读,res: "+res);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

public class RwLockDemo01 {
    public static void main(String[] args) {
        MyResource01 myResource = new MyResource01();

        for (int i = 0; i < 10; i++) {
            String k = i+"";
            new Thread(()->{
                myResource.write(k,k);
            },String.valueOf(i)).start();
        }
        for (int i = 0; i < 10; i++) {
            String k = i+"";
            new Thread(()->{
                myResource.read(k);
            },String.valueOf(i)).start();
        }
    }
}

三. ReentrantReadWriteLock

1. 读写锁的定义 

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

2. 读写锁的特点

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

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

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

3. 代码示例

        读写分离,写写分离,读读共存。如下面所示,线程2读的过程中,允许3,5,8共读

class MyResource02 {
    Map<String, String> map = new HashMap<>();
    //读写锁,允许并发读
    ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void write(String key, String value) {
        rwLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 正在写");
            map.put(key, value);
            TimeUnit.MILLISECONDS.sleep(500);
            System.out.println(Thread.currentThread().getName() + " 完成写");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    public void read(String key) {
        rwLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 正在读");
            String res = map.get(key);
            System.out.println(Thread.currentThread().getName() + " 完成读,res: " + res);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rwLock.readLock().unlock();
        }
    }
}

public class RwLockDemo02 {
    public static void main(String[] args) {

        MyResource02 myResource = new MyResource02();
        for (int i = 0; i < 10; i++) {
            String k = i + "";
            new Thread(() -> {
                myResource.write(k, k);
            }, String.valueOf(i)).start();
        }
        for (int i = 0; i < 10; i++) {
            String k = i + "";
            new Thread(() -> {
                myResource.read(k);
            }, String.valueOf(i)).start();
        }

    }
}

 4. 锁降级(写锁->读锁)

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

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

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

         锁降级的目的:锁降级是为了让当前线程感知到数据的变化,目的是保证数据可见性(写后立即可以读,把写锁降为读锁,保证其他写进程无法进来写,没有读完,写不进去)

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

 代码示例

        先获取写锁,再获取读锁,此时写锁降为了读锁

public class LockDownGradingDemo {
    public static void main(String[] args) {
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

        //有且只有一个线程main,来验证锁降级策略要求
        writeLock.lock();
        System.out.println("-------write");
        readLock.lock();
        System.out.println("-------read");
        writeLock.unlock();
        readLock.unlock();
    }
    
}

 

          反过来,读后写,如果有线程在读,那么写线程是无法获取写锁的,是悲观锁的策略。在ReentrantReadWriteLock中,当读锁被使用时,如果有线程尝试获取写锁,该写线程会被阻塞。所以,需要释放所有读锁,才可获取写锁。

public class LockDownGradingDemo {
    public static void main(String[] args) {
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();


        //反过来,读后写
        //如果有线程在读,那么写线程是无法获取写锁的,是悲观锁的策略
        //在ReentrantReadWriteLock中,当读锁被使用时,如果有线程尝试获取写锁,该写线程会被阻塞。所以,需要释放所有读锁,才可获取写锁,
        readLock.lock();
        System.out.println("-------read");
        writeLock.lock();
        System.out.println("-------write");
        readLock.unlock();
        writeLock.unlock();
    }
    
}

5. 写锁和读锁互斥

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

        如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即ReadWriteLock读的过程中不允许写,只有等待线程都释放了读锁,当前线程才能获取写锁,也就是写入必须等待,这是一种悲观的读锁。

6. Oracle公司ReentrantWriteReadLock源码总结
       

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


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


如果违背锁降级的步骤 

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

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

四. StampedLock

1. 有没有比读写锁更快的锁?

        StampedLock

2. 解决锁饥饿问题:

        1.使用“公平"策略可以一定程度上缓解这个问题,new ReentrantReadWriteLock(true);但是“公平”策略是以牺牲系统吞吐量为代价的

        2.StampedLock

3. StampedLock的定义

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

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

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

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

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

4. StampedLock的改进

        StampedLock改进之处在于读的过程中也允许获取写锁介入,这样会导致我们读的数据就可能不一致!所以,需要额外的方法来判断读的过程中是否有写入,这是一种乐观的读锁 ,显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。

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

5. StampedLock的三种访问模式

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

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

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

6. 代码示例

         定义写方法,悲观读和乐观读,比较并分析悲观读、乐观读成功,乐观读失败的情况。

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()+" -----写线程准备");
        try {
            number = number + 13;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            stampedLock.unlockWrite(stamp);
        }
        System.out.println(Thread.currentThread().getName()+" -----写线程完毕");
    }
    //悲观读
    public void read(){
        long stamp = stampedLock.readLock();
        System.out.println(Thread.currentThread().getName()+" 读线程进入,读取4秒");
        for (int i = 0; i < 4; i++) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"-------正在读取");
        }
        try {
            int res = number;
            System.out.println(Thread.currentThread().getName()+" 获取成员变量值res:"+res);
            System.out.println("写线程没有修改值,因stampedLock.readLock()读的时候,不可以写,读写互斥");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            stampedLock.unlockRead(stamp);
        }

    }
    //乐观读
    public void optimisticRead(){
        long stamp = stampedLock.tryOptimisticRead();
        //先把数据获取一次
        int res = number;
        //间隔4秒,很乐观的认为没有其他线程修改过number值,但实际情况靠判断。
        System.out.println("4秒前,stampedLock.validate值(ture无修改,false有修改)"+"\t"+stampedLock.validate(stamp));
        for (int i = 0; i < 4; i++) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"-------正在读取,"+i+"秒后stampedLock.validate值(ture无修改,false有修改)"+"\t"+stampedLock.validate(stamp));
        }
        if(!stampedLock.validate(stamp)){
            System.out.println("有人动过——存在写操作");
            //从乐观读切换到普通的悲观读
            stamp = stampedLock.readLock();
            try {
                System.out.println("从乐观读升级为悲观读,并重新获取数据");
                //重新获取数据
                res = number;
                System.out.println("悲观读锁通过获取到的成员变量值res:"+res);
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                stampedLock.unlockRead(stamp);
            }
        }
        System.out.println(Thread.currentThread().getName()+" 最终获取res:"+res);
    }

  }

         悲观读,与ReentrantReadWriteLock一致,读完才能写

  public static void main(String[] args) {
        StampedLockDemo resource = new StampedLockDemo();

        //1.悲观读,与ReentrantReadWriteLock一致,读完才能写
        new Thread(()->{
            resource.read();
        },"read").start();
        new Thread(()->{
            resource.write();
        },"write").start();

    }

        乐观读,成功,其他线程尝试获取写锁时不会被阻塞,对读锁优化

  public static void main(String[] args) {
        StampedLockDemo resource = new StampedLockDemo();


        //2. 乐观读,成功
        new Thread(()->{
            resource.optimisticRead();
        },"optimisticRead").start();
        //6秒后,tryOptimisticRead()成功
        try {
            TimeUnit.SECONDS.sleep(6);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            resource.write();
        },"write").start();


    }

         乐观读,失败,转为悲观读,重新读一次。读的过程中也允许获取写锁介入

 public static void main(String[] args) {
        StampedLockDemo resource = new StampedLockDemo();

        //乐观读,失败,转为悲观读,重新读一次
        new Thread(()->{
            resource.optimisticRead();
        },"optimisticRead").start();
        //2秒后,写线程介入
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            resource.write();
        },"write").start();

    }

7. StampedLock的缺点

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

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

        3. 使用StampedLock一定不要调用中断操作,即不要调用interrupt()方法

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值