zoukankan      html  css  js  c++  java
  • Java多线程系列--“JUC锁”02之 互斥锁ReentrantLock

    本章对ReentrantLock包进行基本介绍,这一章主要对ReentrantLock进行概括性的介绍,内容包括:
    ReentrantLock介绍
    ReentrantLock函数列表
    ReentrantLock示例
    在后面的两章,会分别介绍ReentrantLock的两个子类(公平锁和非公平锁)的实现原理。
    转载请注明出处:http://www.cnblogs.com/skywang12345/p/3496101.html

    ReentrantLock介绍

    ReentrantLock是一个可重入的互斥锁,又被称为“独占锁”。

    顾名思义,ReentrantLock锁在同一个时间点只能被一个线程锁持有;而可重入的意思是,ReentrantLock锁,可以被单个线程多次获取。
    ReentrantLock分为“公平锁”和“非公平锁”。它们的区别体现在获取锁的机制上是否公平。“锁”是为了保护竞争资源,防止多个线程同时操作线程而出错,ReentrantLock在同一个时间点只能被一个线程获取(当某线程获取到“锁”时,其它线程就必须等待);ReentraantLock是通过一个FIFO的等待队列来管理获取该锁所有线程的。在“公平锁”的机制下,线程依次排队获取锁;而“非公平锁”在锁是可获取状态时,不管自己是不是在队列的开头都会获取锁。

    ReentrantLock函数列表

    // 创建一个 ReentrantLock ,默认是“非公平锁”。
    ReentrantLock()
    // 创建策略是fair的 ReentrantLock。fair为true表示是公平锁,fair为false表示是非公平锁。
    ReentrantLock(boolean fair)
    
    // 查询当前线程保持此锁的次数。
    int getHoldCount()
    // 返回目前拥有此锁的线程,如果此锁不被任何线程拥有,则返回 null。
    protected Thread getOwner()
    // 返回一个 collection,它包含可能正等待获取此锁的线程。
    protected Collection<Thread> getQueuedThreads()
    // 返回正等待获取此锁的线程估计数。
    int getQueueLength()
    // 返回一个 collection,它包含可能正在等待与此锁相关给定条件的那些线程。
    protected Collection<Thread> getWaitingThreads(Condition condition)
    // 返回等待与此锁相关的给定条件的线程估计数。
    int getWaitQueueLength(Condition condition)
    // 查询给定线程是否正在等待获取此锁。
    boolean hasQueuedThread(Thread thread)
    // 查询是否有些线程正在等待获取此锁。
    boolean hasQueuedThreads()
    // 查询是否有些线程正在等待与此锁有关的给定条件。
    boolean hasWaiters(Condition condition)
    // 如果是“公平锁”返回true,否则返回false。
    boolean isFair()
    // 查询当前线程是否保持此锁。
    boolean isHeldByCurrentThread()
    // 查询此锁是否由任意线程保持。
    boolean isLocked()
    // 获取锁。
    void lock()
    // 如果当前线程未被中断,则获取锁。
    void lockInterruptibly()
    // 返回用来与此 Lock 实例一起使用的 Condition 实例。
    Condition newCondition()
    // 仅在调用时锁未被另一个线程保持的情况下,才获取该锁。
    boolean tryLock()
    // 如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。
    boolean tryLock(long timeout, TimeUnit unit)
    // 试图释放此锁。
    void unlock()

    ReentrantLock示例

    通过对比“示例1”和“示例2”,我们能够清晰的认识lock和unlock的作用

    示例1

    package lock;
    
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    // 仓库
    class Depot {
        private int size; // 仓库的实际数量
        private Lock lock; // 独占锁
    
        public Depot() {
            this.size = 0;
            this.lock = new ReentrantLock();
        }
    
        public void produce(int val) {
            lock.lock();
            try {
                size += val;
                System.out.printf("%s produce(%d) --> size=%d
    ", Thread.currentThread().getName(), val, size);
            } finally {
                lock.unlock();
            }
        }
    
        public void consume(int val) {
            lock.lock();
            try {
                size -= val;
                System.out.printf("%s consume(%d) <-- size=%d
    ", Thread.currentThread().getName(), val, size);
            } finally {
                lock.unlock();
            }
        }
    };
    
    // 生产者
    class Producer {
        private Depot depot;
    
        public Producer(Depot depot) {
            this.depot = depot;
        }
    
        // 消费产品:新建一个线程向仓库中生产产品。
        public void produce(final int val) {
            new Thread() {
                public void run() {
                    depot.produce(val);
                }
            }.start();
        }
    }
    
    // 消费者
    class Customer {
        private Depot depot;
    
        public Customer(Depot depot) {
            this.depot = depot;
        }
    
        // 消费产品:新建一个线程从仓库中消费产品。
        public void consume(final int val) {
            new Thread() {
                public void run() {
                    depot.consume(val);
                }
            }.start();
        }
    }
    
    public class LockTest1 {
        public static void main(String[] args) {
            Depot mDepot = new Depot();
            Producer mPro = new Producer(mDepot);
            Customer mCus = new Customer(mDepot);
    
            mPro.produce(60);
            mPro.produce(120);
            mCus.consume(90);
            mCus.consume(150);
            mPro.produce(110);
        }
    }

    运行结果

    Thread-0 produce(60) --> size=60
    Thread-1 produce(120) --> size=180
    Thread-3 consume(150) <-- size=30
    Thread-2 consume(90) <-- size=-60
    Thread-4 produce(110) --> size=50

    结果分析
    (01) Depot 是个仓库。通过produce()能往仓库中生产货物,通过consume()能消费仓库中的货物。通过独占锁lock实现对仓库的互斥访问:在操作(生产/消费)仓库中货品前,会先通过lock()锁住仓库,操作完之后再通过unlock()解锁。
    (02) Producer是生产者类。调用Producer中的produce()函数可以新建一个线程往仓库中生产产品。
    (03) Customer是消费者类。调用Customer中的consume()函数可以新建一个线程消费仓库中的产品。
    (04) 在主线程main中,我们会新建1个生产者mPro,同时新建1个消费者mCus。它们分别向仓库中生产/消费产品。
    根据main中的生产/消费数量,仓库最终剩余的产品应该是50。运行结果是符合我们预期的!

    这个模型存在两个问题:
    (01) 现实中,仓库的容量不可能为负数。但是,此模型中的仓库容量可以为负数,这与现实相矛盾!
    (02) 现实中,仓库的容量是有限制的。但是,此模型中的容量确实没有限制的!
    这两个问题,我们稍微会讲到如何解决。现在,先看个简单的示例2;通过对比“示例1”和“示例2”,我们能更清晰的认识lock(),unlock()的用途。

    示例2

    package lock.demo2;
    
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    // 仓库
    class Depot {
        private int size; // 仓库的实际数量
        private Lock lock; // 独占锁
    
        public Depot() {
            this.size = 0;
            this.lock = new ReentrantLock();
        }
    
        public void produce(int val) {
            // lock.lock();
            // try {
            size += val;
            System.out.printf("%s produce(%d) --> size=%d
    ", Thread.currentThread().getName(), val, size);
            // } catch (InterruptedException e) {
            // } finally {
            // lock.unlock();
            // }
        }
    
        public void consume(int val) {
            // lock.lock();
            // try {
            size -= val;
            System.out.printf("%s consume(%d) <-- size=%d
    ", Thread.currentThread().getName(), val, size);
            // } finally {
            // lock.unlock();
            // }
        }
    };
    
    // 生产者
    class Producer {
        private Depot depot;
    
        public Producer(Depot depot) {
            this.depot = depot;
        }
    
        // 消费产品:新建一个线程向仓库中生产产品。
        public void produce(final int val) {
            new Thread() {
                public void run() {
                    depot.produce(val);
                }
            }.start();
        }
    }
    
    // 消费者
    class Customer {
        private Depot depot;
    
        public Customer(Depot depot) {
            this.depot = depot;
        }
    
        // 消费产品:新建一个线程从仓库中消费产品。
        public void consume(final int val) {
            new Thread() {
                public void run() {
                    depot.consume(val);
                }
            }.start();
        }
    }
    
    public class LockTest2 {
        public static void main(String[] args) {
            Depot mDepot = new Depot();
            Producer mPro = new Producer(mDepot);
            Customer mCus = new Customer(mDepot);
    
            mPro.produce(60);
            mPro.produce(120);
            mCus.consume(90);
            mCus.consume(150);
            mPro.produce(110);
        }
    }

    (某一次)运行结果

    Thread-0 produce(60) --> size=-60
    Thread-4 produce(110) --> size=50
    Thread-2 consume(90) <-- size=-60
    Thread-1 produce(120) --> size=-60
    Thread-3 consume(150) <-- size=-60

    结果说明
    “示例2”在“示例1”的基础上去掉了lock锁。在“示例2”中,仓库中最终剩余的产品是-60,而不是我们期望的50。原因是我们没有实现对仓库的互斥访问。

    示例3

    在“示例3”中,我们通过Condition去解决“示例1”中的两个问题:“仓库的容量不可能为负数”以及“仓库的容量是有限制的”。
    解决该问题是通过Condition。Condition是需要和Lock联合使用的:通过Condition中的await()方法,能让线程阻塞[类似于wait()];通过Condition的signal()方法,能让唤醒线程[类似于notify()]。

    /*
     * Project Name:Test  
     * File Name:LockTest3.java  
     * Package Name:lock.demo3  
     * Date:2016年12月30日上午9:18:20  
     * Copyright (c) 2016, S.F. Express Inc. All rights reserved.  
     */
    package lock.demo3;
    
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    import java.util.concurrent.locks.Condition;
    
    // LockTest3.java
    // 仓库
    class Depot {
        private int capacity; // 仓库的容量
        private int size; // 仓库的实际数量
        private Lock lock; // 独占锁
        private Condition fullCondtion; // 生产条件
        private Condition emptyCondtion; // 消费条件
    
        public Depot(int capacity) {
            this.capacity = capacity;
            this.size = 0;
            this.lock = new ReentrantLock();
            this.fullCondtion = lock.newCondition();
            this.emptyCondtion = lock.newCondition();
        }
    
        public void produce(int val) {
            lock.lock();
            try {
                // left 表示“想要生产的数量”(有可能生产量太多,需多次生产)
                int left = val;
                while (left > 0) {
                    // 库存已满时,等待“消费者”消费产品。
                    while (size >= capacity)
                        fullCondtion.await();
                    // 获取“实际生产的数量”(即库存中新增的数量)
                    // 如果“库存”+“想要生产的数量”>“总的容量”,则“实际增量”=“总的容量”-“当前容量”。(此时填满仓库)
                    // 否则“实际增量”=“想要生产的数量”
                    int inc = (size + left) > capacity ? (capacity - size) : left;
                    size += inc;
                    left -= inc;
                    System.out.printf("%s produce(%3d) --> left=%3d, inc=%3d, size=%3d
    ", Thread.currentThread().getName(),
                            val, left, inc, size);
                    // 通知“消费者”可以消费了。
                    emptyCondtion.signal();
                }
            } catch (InterruptedException e) {
            } finally {
                lock.unlock();
            }
        }
    
        public void consume(int val) {
            lock.lock();
            try {
                // left 表示“客户要消费数量”(有可能消费量太大,库存不够,需多此消费)
                int left = val;
                while (left > 0) {
                    // 库存为0时,等待“生产者”生产产品。
                    while (size <= 0)
                        emptyCondtion.await();
                    // 获取“实际消费的数量”(即库存中实际减少的数量)
                    // 如果“库存”<“客户要消费的数量”,则“实际消费量”=“库存”;
                    // 否则,“实际消费量”=“客户要消费的数量”。
                    int dec = (size < left) ? size : left;
                    size -= dec;
                    left -= dec;
                    System.out.printf("%s consume(%3d) <-- left=%3d, dec=%3d, size=%3d
    ", Thread.currentThread().getName(),
                            val, left, dec, size);
                    fullCondtion.signal();
                }
            } catch (InterruptedException e) {
            } finally {
                lock.unlock();
            }
        }
    
        public String toString() {
            return "capacity:" + capacity + ", actual size:" + size;
        }
    };
    
    // 生产者
    class Producer {
        private Depot depot;
    
        public Producer(Depot depot) {
            this.depot = depot;
        }
    
        // 消费产品:新建一个线程向仓库中生产产品。
        public void produce(final int val) {
            new Thread() {
                public void run() {
                    depot.produce(val);
                }
            }.start();
        }
    }
    
    // 消费者
    class Customer {
        private Depot depot;
    
        public Customer(Depot depot) {
            this.depot = depot;
        }
    
        // 消费产品:新建一个线程从仓库中消费产品。
        public void consume(final int val) {
            new Thread() {
                public void run() {
                    depot.consume(val);
                }
            }.start();
        }
    }
    
    public class LockTest3 {
        public static void main(String[] args) {
            Depot mDepot = new Depot(100);
            Producer mPro = new Producer(mDepot);
            Customer mCus = new Customer(mDepot);
    
            mPro.produce(60);
            mPro.produce(120);
            mCus.consume(90);
            mCus.consume(150);
            mPro.produce(110);
        }
    }

    (某一次)运行结果

    复制代码
    Thread-0 produce( 60) --> left=  0, inc= 60, size= 60
    Thread-1 produce(120) --> left= 80, inc= 40, size=100
    Thread-2 consume( 90) <-- left=  0, dec= 90, size= 10
    Thread-3 consume(150) <-- left=140, dec= 10, size=  0
    Thread-4 produce(110) --> left= 10, inc=100, size=100
    Thread-3 consume(150) <-- left= 40, dec=100, size=  0
    Thread-4 produce(110) --> left=  0, inc= 10, size= 10
    Thread-3 consume(150) <-- left= 30, dec= 10, size=  0
    Thread-1 produce(120) --> left=  0, inc= 80, size= 80
    Thread-3 consume(150) <-- left=  0, dec= 30, size= 50
    复制代码

    代码中的已经包含了很详细的注释,这里就不再说明了。

    使用场景

    从使用场景的角度出发来介绍对ReentrantLock的使用,相对来说容易理解一些。

    场景1:如果发现该操作已经在执行中则不再执行(有状态执行)

    a、用在定时任务时,如果任务执行时间可能超过下次计划执行时间,确保该有状态任务只有一个正在执行,忽略重复触发。
    b、用在界面交互时点击执行较长时间请求操作时,防止多次点击导致后台重复执行(忽略重复触发)。

    以上两种情况多用于进行非重要任务防止重复执行,(如:清除无用临时文件,检查某些资源的可用性,数据备份操作等)

        private ReentrantLock lock = new ReentrantLock();//参数默认false,不公平锁
        
        public void test1() {
             if (lock.tryLock()) {  //如果已经被lock,则立即返回false不会等待,达到忽略操作的效果 
                 try {
                     System.out.println("test1 获得锁");
                     System.out.println("test1 工作");
                 } finally {
                     lock.unlock();
                 }
             } else {
                 System.out.println("test1 未获得锁");
             }
        }

    场景2:如果发现该操作已经在执行,等待一个一个执行(同步执行,类似synchronized)

    这种比较常见大家也都在用,主要是防止资源使用冲突,保证同一时间内只有一个操作可以使用该资源。
    但与synchronized的明显区别是性能优势(伴随jvm的优化这个差距在减小)。同时Lock有更灵活的锁定方式,公平锁与不公平锁,而synchronized永远是公平的。

    这种情况主要用于对资源的争抢(如:文件操作,同步消息发送,有状态的操作等)

    ReentrantLock默认情况下为不公平锁

        private ReentrantLock lock = new ReentrantLock(true); //公平锁
        
        public void test2() {
            try {
                lock.lock(); //如果被其它资源锁定,会在此等待锁释放,达到暂停的效果
                //操作
                System.out.println("test2 获得锁");
                    System.out.println("test2 工作");
            } finally {
                lock.unlock();
            }
        }

    不公平锁与公平锁的区别:

    公平情况下,操作会排一个队按顺序执行,来保证执行顺序。(会消耗更多的时间来排队)
    不公平情况下,是无序状态允许插队,jvm会自动计算如何处理更快速来调度插队。(如果不关心顺序,这个速度会更快)

    场景3:如果发现该操作已经在执行,则尝试等待一段时间,等待超时则不执行(尝试等待执行)

    这种其实属于场景2的改进,等待获得锁的操作有一个时间的限制,如果超时则放弃执行。
    用来防止由于资源处理不当长时间占用导致死锁情况(大家都在等待资源,导致线程队列溢出)。

        private ReentrantLock lock = new ReentrantLock(true); //公平锁
        
        public void test3() {
            try {
                if (lock.tryLock(5, TimeUnit.SECONDS)) {  //如果已经被lock,尝试等待5s,看是否可以获得锁,如果5s后仍然无法获得锁则返回false继续执行
                    try {
                        //操作
                        System.out.println("test3 获得锁");
                            System.out.println("test3 工作");
                    } finally {
                        lock.unlock();
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace(); //当前线程被中断时(interrupt),会抛InterruptedException                 
            }
            
        }

    场景4:如果发现该操作已经在执行,等待执行。这时可中断正在进行的操作立刻释放锁继续下一操作。

    synchronized与Lock在默认情况下是不会响应中断(interrupt)操作,会继续执行完。lockInterruptibly()提供了可中断锁来解决此问题。(场景2的另一种改进,没有超时,只能等待中断或执行完毕)

    这种情况主要用于取消某些操作对资源的占用。如:(取消正在同步运行的操作,来防止不正常操作长时间占用造成的阻塞)

    ReentrantLock的lock机制有2种,忽略中断锁和响应中断锁与synchronized的区别,这给我们带来了很大的灵活性。比如:如果A、B 两个线程去竞争锁,A线程得到了锁,B线程等待,但是A线程这个时候实在有太多事情要处理,就是一直不返回,B线程可能就会等不及了,想中断自己,不再等待这个锁了,转而处理其他事情。这个时候ReentrantLock就提供了两种方式处理:

    第一,B线程中断自己(或者别的线程中断它),但是ReentrantLock 不去响应,继续让B线程等待,你再怎么中断,我全当耳边风(synchronized原语就是如此);

    第二,B线程中断自己(或者别的线程中断它),ReentrantLock 处理了这个中断,并且不再等待这个锁的到来,继续向下执行B线程的其它工作。

    package lock.demo4;
    
    import java.util.concurrent.locks.ReentrantLock;
    
    public class Test {
        //是用ReentrantLock,还是用synchronized
        public static boolean useSynchronized = false;
    
        public static void main(String[] args) {
            IBuffer buff = null;
            if (useSynchronized) {
                buff = new Buffer();
            } else {
                buff = new BufferInterruptibly();
            }
            final Writer writer = new Writer(buff);
            final Reader reader = new Reader(buff);
            writer.start();
            reader.start();
            new Thread(new Runnable() {
                public void run() {
                    long start = System.currentTimeMillis();
                    for (;;) {
                        // 等5秒钟去中断读
                        if (System.currentTimeMillis() - start > 5000) {
                            System.out.println("[主线程]调用reader.interrupt()发出“的尝试中断消息”,让Reader线程不再等了");
                            reader.interrupt();
                            break;
                        }
    
                    }
    
                }
            }).start();
        }
    }
    
    interface IBuffer {
        public void write();
    
        public void read() throws InterruptedException;
    }
    
    class Buffer implements IBuffer {
        private Object lock;
    
        public Buffer() {
            lock = this;
        }
    
        public void write() {
            synchronized (lock) {
                long startTime = System.currentTimeMillis();
                System.out.println("[Writer线程]开始往这个buff写入数据…");
                for (;;)// 模拟要处理很长时间
                {
                    if (System.currentTimeMillis() - startTime > Integer.MAX_VALUE)
                        break;
                }
                System.out.println("终于写完了");
            }
        }
    
        public void read() {
            synchronized (lock) {
                System.out.println("从这个buff读数据");
            }
        }
    }
    
    class BufferInterruptibly implements IBuffer {
    
        private ReentrantLock lock = new ReentrantLock();
    
        public void write() {
            lock.lock();
            try {
                long startTime = System.currentTimeMillis();
                System.out.println("[Writer线程]开始往这个buff写入数据…");
                for (;;)// 模拟要处理很长时间
                {
                    if (System.currentTimeMillis() - startTime > Integer.MAX_VALUE)
                        break;
                }
                System.out.println("终于写完了");
            } finally {
                lock.unlock();
            }
        }
    
        public void read() throws InterruptedException {
            lock.lockInterruptibly();// 注意这里,可以响应中断
            try {
                System.out.println("从这个buff读数据");
            } finally {
                lock.unlock();
            }
        }
    
    }
    
    class Writer extends Thread {
    
        private IBuffer buff;
    
        public Writer(IBuffer buff) {
            this.buff = buff;
        }
    
        @Override
        public void run() {
            buff.write();
        }
    
    }
    
    class Reader extends Thread {
    
        private IBuffer buff;
    
        public Reader(IBuffer buff) {
            this.buff = buff;
        }
    
        @Override
        public void run() {
    
            try {
                buff.read();
            } catch (InterruptedException e) {
                System.out.println("[Reader线程]我不读了");
            }
    
            System.out.println("[Reader线程]读结束");
    
        }
    }

    结果:

    [Writer线程]开始往这个buff写入数据…
    [主线程]调用reader.interrupt()发出“的尝试中断消息”,让Reader线程不再等了
    [Reader线程]我不读了
    [Reader线程]读结束

    可重入概念

    若一个程序或子程序可以“安全的被并行执行(Parallel computing)”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,可以再次进入并执行它(并行执行时,个别的执行结果,都符合设计时的预期)。可重入概念是在单线程操作系统的时代提出的。

  • 相关阅读:
    Codeforces 22E(图论)
    Codeforces Educational Round 23
    bzoj1444 有趣的游戏(AC自动机+概率dp)
    AtCoder Grand Contest 012 D Colorful Balls
    计蒜客15430 XOR Queries(Trie处理位运算问题)
    AtCoder Grand Contest 012 B Splatter Painting(记忆化搜索)
    Codeforces 799E(贪心)
    Codeforces Round #414
    Codeforces Educational Round 21
    LOJ10078
  • 原文地址:https://www.cnblogs.com/duanxz/p/6063673.html
Copyright © 2011-2022 走看看