美文网首页小白架构师之路
多线程篇三(Lock锁)

多线程篇三(Lock锁)

作者: Albert_Yu | 来源:发表于2018-06-23 01:53 被阅读47次

    前言
    之前的文章我已经提到过一个jdk中的独占锁synchronized
    synchronized是java的一个关键字,也就是java语言内置的特性。如果一个代码块被synchronized修饰,当一个线程获取到锁之后,其它需要获取该锁的线程便需要一直等待,等待获取锁的线程释放锁,获取锁的线程释放锁有三种情况:
    1、持有锁的线程执行完代码块,然后线程会释放对锁的占有
    2、线程执行抛出异常,此时JVM会让线程释放锁
    3、持有锁的线程调用了wait()方法,在等待的时候立即释放锁。

    正文
    一、有了synchronized(内置锁)为什么还要使用Lock呢?
    lock可以
    1、尝试非阻塞的获取锁
    2、获取锁的过程可以被中断
    3、可以超时获取锁
    二、java.util.concurrent.locks包下常用的类与接口


    lock.jpg

    根据上图,我们来逐一分析
    1、Lock接口
    Lock接口源码

    public interface Lock {
    
        void lock(); // 获取锁
    
        void lockInterruptibly() throws InterruptedException; // 可中断的获取锁
    
        boolean tryLock(); // 尝试获取锁
    
        boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //超时获取锁
    
        void unlock(); //释放锁
    
        Condition newCondition(); //获取一个新的Condition实例
    }
    

    (1)、lock()
    lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
    采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此,一般来说,使用Lock必须在try…catch…块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生,并且获取锁的操作应该方法try的外面,防止误释放锁。
    lock()方法使用模板

    /**
     * Lock使用标准
     *
     * @Author YUBIN
     */
    public class LockTemplete {
        public static void main(String[] args) {
            Lock lock = new ReentrantLock(); // 可重入锁
            lock.lock();// 获取锁 放在try外面 防止释放了其它线程的锁
            try {
                // do my work ...
            }finally {
                lock.unlock(); // 释放锁
            }
        }
    }
    

    (2)、tryLock()、tryLock(time,unit)
    tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true;如果获取失败(即锁已被其他线程获取),则返回false,也就是说,这个方法无论如何都会立即返回(在拿不到锁时不会一直在那等待)。
    tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false,同时可以响应中断。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
    tryLock()使用模板

    /**
     * tryLock()使用模板
     *
     * @Author YUBIN
     */
    public class TryLockTemplate {
        public static void main(String[] args) {
            Lock lock = new ReentrantLock();
            if (lock.tryLock()) {
                try {
                    // 获取到锁的处理逻辑
                } catch (Exception e) {
                    // 异常处理
                } finally {
                    lock.unlock();
                }
            } else {
                // 没有获取到锁的处理逻辑
            } 
        }
    }
    

    (3)、lockInterruptibly()
    lockInterruptibly()方法比较特殊,尝试获取锁,获取锁的过程是可以中断的。举个例子有两个线程threadA和threadB都调用了lockInterruptibly()在同一时刻只会有一个线程获取到锁,假如threadA获取到了锁那么在threadA获取到锁到释放锁的过程中threadB是一直处于等待状态的,如果在threadA中调用了threadB.interrupt()中断方法能够中断threadB线程的等待状态。
    由于lock.lockInterruptibly()的声明中抛出了中断异常,所以lock.lockInterruptibly()必须方法try/catch块中,或者在调用该方法的地方抛出异常,但推荐使用后者。
    lockInterruptibly()方法使用模板

        public void method(Lock lock) throws InterruptedException {
            lock.lockInterruptibly();
            try {
                //执行代码
            } catch (Exception e) {
                // 异常处理
            } finally {
                lock.unlock();
            }
        }
    

    2、ReentrantLock可重入锁
    ReentrantLock,即 可重入锁。ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。
    什么叫做可重入呢?最典型的一个例子就是递归的时候发生锁的重入。它是通过当前线程和一个整型变量来实现的。
    具体的使用方法会在下面的基于Lock和Condition实现一个阻塞队列来演示。

    3、ReadWritrLock
    ReadWriteLock也是一个接口,在它里面只定义了两个方法:

    public interface ReadWriteLock {
        Lock readLock();  // 获取读锁
    
        Lock writeLock(); // 获取写锁
    }
    

    一个用来获取读锁,一个用来获取写锁。也就是说,将对临界资源的读写操作分成两个锁来分配给线程,从而使得多个线程可以同时进行读操作,当有一个线程获取了写锁,其它不管是需要获取读锁还是写锁的线程都是处于阻塞的。下面的 ReentrantReadWriteLock 实现了 ReadWriteLock 接口。
    4、ReentrantReadWriteLock读写锁
    ReentrantReadWriteLock 里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和writeLock()用来获取读锁ReadLock和写锁WriteLock。下面通过几个例子来看一下读写锁在读多写少的场景下与synchronized独占锁的区别
    商品的实体类

    public class Goods {
        private final String id;
        private Long totalSaleNumber; // 总销售数
        private Long depotNumber; // 当前库存数
    
        public Goods(String id, Long totalSaleNumber, Long depotNumber) {
            this.id = id;
            this.totalSaleNumber = totalSaleNumber;
            this.depotNumber = depotNumber;
        }
    
        public Long getTotalSaleNumber() {
            return totalSaleNumber;
        }
    
        public Long getDepotNumber() {
            return depotNumber;
        }
    
        public void setGoodsNumber(Long changeNumber) {
            this.totalSaleNumber += changeNumber;
            this.depotNumber -= changeNumber;
        }
    }
    

    操作商品数量的接口

    public interface IGoodsNum {
        Goods getGoodsNumber();
    
        void setGoodsNumber(Long changeNumber);
    }
    

    使用synchronized操作商品数量

    public class NumSync implements IGoodsNum {
    
        private Goods goods;
    
        public NumSync(Goods goods) {
            this.goods = goods;
        }
    
        @Override
        public synchronized Goods getGoodsNumber() {
            try {
                // 此处休眠5ms来演示从数据库中读取商品数据
                Thread.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return goods;
        }
    
        @Override
        public synchronized void setGoodsNumber(Long changeNumber) {
            try {
                // 此处休眠50ms来演示修改商品的数量
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            goods.setGoodsNumber(changeNumber);
        }
    }
    

    使用ReentrantReadWriteLock操作商品数量

    public class RwNumSync implements IGoodsNum {
    
        private Goods goods;
    
        private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    
        private final Lock rLock = lock.readLock(); // 获取读锁
    
        private final Lock wLock = lock.writeLock(); // 获取写锁
    
        public RwNumSync(Goods goods) {
            this.goods = goods;
        }
    
        @Override
        public Goods getGoodsNumber() {
            rLock.lock();
            try {
                SleepUtils.second(1);
                return goods;
            }finally {
                rLock.unlock();
            }
        }
    
        @Override
        public void setGoodsNumber(Long changeNumber) {
            wLock.lock();
            try {
                SleepUtils.second(50);
                goods.setGoodsNumber(changeNumber);
            }finally {
                wLock.unlock();
            }
        }
    }
    

    测试类

    public class Test {
        private static final int threadRatio = 10; // 读线程与写线程的比例
        private static final int threadBaseCount = 3; // 写线程的数量
        private static CountDownLatch countDownLatch = new CountDownLatch(1);
    
        public static void main(String[] args) {
            Goods goods = new Goods("goods001", 100000L, 10000L);
            // 使用Synchronized操作商品数量
            IGoodsNum iGoodsNum = new NumSync(goods);
            // 使用ReentrantReadWriteLock操作商品的数量
            //IGoodsNum iGoodsNum = new RwNumSync(goods);
            for (int i = 0; i < threadBaseCount * threadRatio; i++) {
                Thread rThread = new Thread(new ReadThread(iGoodsNum));
                rThread.start();
            }
            for (int i = 0; i < threadBaseCount; i++) {
                Thread wThread = new Thread(new WriteThread(iGoodsNum));
                wThread.start();
            }
            countDownLatch.countDown();
        }
    
        // 读线程
        private static class ReadThread implements Runnable {
    
            private IGoodsNum iGoodsNum;
    
            public ReadThread(IGoodsNum iGoodsNum) {
                this.iGoodsNum = iGoodsNum;
            }
    
            @Override
            public void run() {
                try {
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                long start = System.currentTimeMillis();
                for (int i = 0; i < 100; i++) {
                    iGoodsNum.getGoodsNumber();
                }
                long duration = System.currentTimeMillis() - start;
                System.out.println(Thread.currentThread().getName() + "读取库存数据耗时:" + duration);
            }
        }
    
        // 写线程
        private static class WriteThread implements Runnable {
    
            private IGoodsNum iGoodsNum;
    
            public WriteThread(IGoodsNum iGoodsNum) {
                this.iGoodsNum = iGoodsNum;
            }
    
            @Override
            public void run() {
                try {
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                long start = System.currentTimeMillis();
                Random random = new Random();
                for (int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    iGoodsNum.setGoodsNumber(Long.valueOf(random.nextInt(10)));
                }
                long duration = System.currentTimeMillis() - start;
                System.out.println(Thread.currentThread().getName() + "写库存数据耗时:" + duration);
            }
        }
    }
    

    运行结果
    (1)、使用Synchronized操作商品数量

    Thread-22读取库存数据耗时:4428
    Thread-31写库存数据耗时:5662
    Thread-30写库存数据耗时:5882
    Thread-21读取库存数据耗时:8202
    Thread-32写库存数据耗时:8933
    Thread-0读取库存数据耗时:12876
    Thread-10读取库存数据耗时:13053
    Thread-18读取库存数据耗时:13528
    Thread-3读取库存数据耗时:13542
    Thread-7读取库存数据耗时:13888
    Thread-27读取库存数据耗时:14195
    Thread-14读取库存数据耗时:14389
    Thread-24读取库存数据耗时:14535
    Thread-2读取库存数据耗时:14605
    Thread-26读取库存数据耗时:14865
    Thread-11读取库存数据耗时:15038
    Thread-15读取库存数据耗时:15288
    Thread-23读取库存数据耗时:15638
    Thread-12读取库存数据耗时:15694
    Thread-19读取库存数据耗时:15848
    Thread-17读取库存数据耗时:15933
    Thread-1读取库存数据耗时:16272
    Thread-28读取库存数据耗时:16354
    Thread-13读取库存数据耗时:16403
    Thread-8读取库存数据耗时:16496
    Thread-16读取库存数据耗时:16514
    Thread-25读取库存数据耗时:16657
    Thread-5读取库存数据耗时:16727
    Thread-29读取库存数据耗时:16782
    Thread-20读取库存数据耗时:16809
    Thread-9读取库存数据耗时:16874
    Thread-6读取库存数据耗时:16965
    Thread-4读取库存数据耗时:17198
    

    (2)、使用ReentrantReadWriteLock操作商品数量

    Thread-7读取库存数据耗时:1129
    Thread-3读取库存数据耗时:1130
    Thread-11读取库存数据耗时:1135
    Thread-15读取库存数据耗时:1135
    Thread-19读取库存数据耗时:1125
    Thread-23读取库存数据耗时:1124
    Thread-27读取库存数据耗时:1124
    Thread-31写库存数据耗时:1781
    Thread-30写库存数据耗时:1826
    Thread-32写库存数据耗时:1886
    Thread-1读取库存数据耗时:1926
    Thread-17读取库存数据耗时:1913
    Thread-13读取库存数据耗时:1914
    Thread-5读取库存数据耗时:1915
    Thread-9读取库存数据耗时:1915
    Thread-21读取库存数据耗时:1910
    Thread-29读取库存数据耗时:1910
    Thread-25读取库存数据耗时:1911
    Thread-2读取库存数据耗时:1976
    Thread-18读取库存数据耗时:1962
    Thread-6读取库存数据耗时:1963
    Thread-10读取库存数据耗时:1964
    Thread-14读取库存数据耗时:1964
    Thread-22读取库存数据耗时:1963
    Thread-26读取库存数据耗时:1963
    Thread-8读取库存数据耗时:2013
    Thread-0读取库存数据耗时:2017
    Thread-12读取库存数据耗时:2018
    Thread-4读取库存数据耗时:2020
    Thread-16读取库存数据耗时:2010
    Thread-20读取库存数据耗时:2013
    Thread-24读取库存数据耗时:2013
    Thread-28读取库存数据耗时:2014
    

    我们可以看到,使用ReentrantReadWriteLock操作商品数量比使用Synchronized高效的多,因为同一时刻允许多个读线程同时获取到读锁,这样就大大提升了读操作的效率。不过要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程也会一直等待释放写锁。

    相关文章

      网友评论

      • 西山漫步:这个使用的场景是?能举个实例吗?
        Albert_Yu:@西山漫步 用synchronized的地方都可以用lock呀!lock的话功能更强大一些!对于读写锁顾名思义适合于读多写少的场景。你可以看我下一篇对ReentrantLock和ReentrantReadWriteLock的源码分析以及AQS的解析

      本文标题:多线程篇三(Lock锁)

      本文链接:https://www.haomeiwen.com/subject/yrifyftx.html