【并发编程】4.JUC中常用的锁

JUC即java.util.concurrent的简称,在这个包中增加了在并发编程中很常用的工具类,用于定义类似于线程的自定义子系统,包括线程池,异步 IO 和轻量级任务框架,还提供了设计用于多线程上下文中。通过她们能够很好地帮助我们在开发中提高一些程序的性能。

1.Lock与Condition

  • condition
    Lock与condition是Java中管程模型除了synchronized的另一套实现,不同是的支持多条件队列。Lock&Condition 实现的管程里只能使用前面的 await()、signal()、signalAll()。
    synchronized的管程模型

    Lock&Condition的管程模型

Condition的使用 需要获取到锁

public class BlockedQueue<T>{
    final Lock lock =
            new ReentrantLock();
    // 条件变量:队列不满
    final Condition notFull =
            lock.newCondition();
    // 条件变量:队列不空
    final Condition notEmpty =
            lock.newCondition();
    // 入队
    void enq(T x) {
        lock.lock();
        try {
            while (队列已满){
                // 等待队列不满
                notFull.await();
            }
            // 省略入队操作...
            // 入队后, 通知可出队
            notEmpty.signal();
        }finally {
            lock.unlock();
        }
    }
    // 出队
    void deq(){
        lock.lock();
        try {
            while (队列已空){
                // 等待队列不空
                notEmpty.await();
            }
            // 省略出队操作...
            // 出队后,通知可入队
            notFull.signal();
        }finally {
            lock.unlock();
        }
    }
}

上述代码中就是有两个条件变量,对应两个条件队列,分别进行入队和出队。 队列已满通知等待出队操作的线程,队列已空则通知等待入队操作的线程。
如果是使用synchronized关键字实现的话调用notify/notifyAll方法应该是通知唯一的等待队列里的所有线程,然后判断是执行入队或者是出队。

  • ReentrantLock
    上述代码中使用的锁就是 ReentrantLock 即为可重入锁,就是再已经获取锁的情况下可以再次获取到同一把锁。
class X {
    private final Lock rtl = new ReentrantLock();
    int value;
    public int get() {
        // 获取锁
        rtl.lock(); 
        try {
            return value;
        } finally {
            // 保证锁能释放
            rtl.unlock();
        }
    }
    public void addOne() {
        // 获取锁
        rtl.lock();
        try {
            value = 1 + get(); //get方法再次获取锁
        } finally {
            // 保证锁能释放
            rtl.unlock();
        }
    }
}
  • ReadWriteLock
    读写锁,适用于读多写少的场景,例如缓存
    ReadWriteLock 是接口
    ReentrantReadWriteLock 是实现类
  1. 允许多个线程同时读共享变量; (优于互斥锁的关键) 如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写,这是一种悲观的读锁。
  2. 只允许一个线程写共享变量;
  3. 如果一个写线程正在执行写操作,此时禁止读线程读共享变量 (区别于对读操作不加锁)
/**
* className: Cache
* create by: zhujun
* description: 使用读写锁实现的缓存工具类
* 读写锁 使用于读多写少的并发场景
* create time: 2019/7/23 11:21
*/
public class Cache<K,V> {
    //hashmap 存储数据
    final HashMap<K,V> hashMap = new HashMap<>();
    final ReentrantReadWriteLock reentrantReadWriteLock  = new ReentrantReadWriteLock();
    final Lock readLock = reentrantReadWriteLock.readLock();
    final Lock writeLock = reentrantReadWriteLock.writeLock();

    /**
     * 存入数据
     * @param key
     * @param value
     */
    void set(K key,V value){
        writeLock.lock();
        try{
            hashMap.put(key,value);
        }finally {
            writeLock.unlock();
        }
    }

    /**
     * 读取数据
     * @param key
     * @return
     */
    V get(K key){
      //读锁
      readLock.lock();
      try{
          return hashMap.get(key);
      }finally {
          readLock.unlock();
      }
    }
}

注意点:1.写锁支持条件变量,读锁不支持条件变量
2.支持锁的降级,不支持锁的升级
锁的升级

        read.lock();
        try {
            v = m.get(key);//验证值是否存在         
            //获取写锁从数据库中更新缓存                       //锁的升级
        } finally{
            read.unlock();
        }

锁的降级

      writeLock.lock();
      try{
              ....
          readLock.lock();//释放写锁之前 释放读锁
      }finally{
          writeLock.unlock();
          readLock.lock()
      }
  • StampedLock
    Java 1.8提供,性能优于ReadWriteLock 支持三种锁模式:写锁,乐观读锁,悲观读锁。
    因为 ReadWriteLock 是悲观读锁,读取的时候不允许写入,StampedLock为了提高性能提供了乐观读锁,读的过程中大概率不会有写入。

首先我们通过tryOptimisticRead()获取一个乐观读锁,并返回版本号。
接着进行读取,读取完成后,我们通过validate()去验证版本号,如果在读取过程中没有写入,版本号不变,验证成功,我们就可以放心地继续后续操作。
如果在读取过程中有写入,版本号会发生变化,验证将失败。在失败的时候,我们再通过获取悲观读锁再次读取。

/**
* className: Point
* create by: zhujun
* description:StampLock 的乐观读与悲观读锁
* create time: 2019/7/30 17:01
*/
public class Point {
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    final StampedLock s1 = new StampedLock();


    //计算带到原点的举例
    double getDistance() throws InterruptedException {
       long stamp = s1.tryOptimisticRead();//乐观读
        System.out.println("乐观读stamp:"+stamp);
        int curX = x;
        int cutY = y;
        System.out.println("读取成功:"+x+","+y);
        Thread.sleep(1000);//睡眠 方便测试时进行写操作
       if(!s1.validate(stamp)){//通过验证stamp
            //期间有写操作 升级为悲观读锁 等待写操作完成
           stamp =  s1.readLock();
            try{
                System.out.println("存在写操作,重新读取x,y坐标");
                System.out.println("此时stamp:"+stamp);
                curX = x;
                cutY = y;
            }finally {
                s1.unlockRead(stamp);
            }
       }
       return  Math.sqrt(curX*curX+cutY*cutY);
    }



    void reLocation(int x,int y){
        //写操作 写锁
        long stamp = s1.writeLock();
        System.out.println("写锁stamp:"+stamp);
        try{
            this.x = x;
            this.y = y;
            System.out.println("重定位成功:"+x+","+y);
        }finally {
            s1.unlockWrite(stamp);
        }
    }
}
public class PointTest {
    public static void main(String[] args) throws InterruptedException {
        Point p = new Point(1,2);
        //线程1 计算距离
        Thread th1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    double distance = p.getInstance();
                    System.out.println("距离原点的举例:"+distance);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        //线程2 重写坐标
        Thread th2 = new Thread(new Runnable() {
            @Override
            public void run() {
                p.reLocation(3,4);
            }
        });
        th1.start();
        th2.start();
    }
}

注意:1.StampedLock 不支持可重入
2.如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly()和写锁 writeLockInterruptibly()。

原文地址:https://www.cnblogs.com/shinyrou/p/13300983.html