美文网首页
Java 锁机制详解(三)Lock

Java 锁机制详解(三)Lock

作者: Parallel_Lines | 来源:发表于2020-06-17 20:00 被阅读0次

    简介

    Lock 以更强大灵活的方式,作为了 synchronized 锁的替代品。

    相比较 synchronizedLock 有如下优势:

    1. 可以尝试获取锁,线程不必一直等待;
    2. 可以判断锁状态;
    3. 支持公平锁。
    4. 可以通过读锁、写锁提升锁效率。
    5. ...

    功能

    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();
    }
    

    这些方法的用途如下。

    方法 用途
    lock() 获取锁,锁不可用则线程休眠。
    lockInterruptibly() 同 lock(),区别是可以响应中断。
    tryLock() 尝试获取锁,锁可用则获取后立即返回 true,不可用则立即返回 false。
    tryLock(long time, TimeUnit unit) 限定时间内尝试获取锁。有俩种情况返回 false:1.响应中断;2.超时。
    unlock() 释放锁。
    newCondition() 用于 Lock 加锁线程的等待和唤醒操作,后面会详细说明。

    2、ReentrantLock

    ReentrantLock 是排他锁,即同一时刻只允许一个线程访问。

    ReentrantLock 的方法有很多,详情可以参考官方文档,这里不一一介绍了。

    利用 ReentrantLock 可以实现如下简单加锁:

    Lock lock = new ReentrantLock();
    
    private void method() {
        lock.lock();
        try {
            // do sth
        } finally {
            lock.unlock();
        }
    }
    

    公平锁和非公平锁

    顾名思义,公平锁即线程执行按照先进先出的原则。

    ReentrantLock 构造函数中传入 true 以启用公平锁。

    3、Condition

    类似于 synchronized 配合 wait()/notify()/notifyAll() 实现等待唤醒,Lock 依赖 Condition 实现上述操作。

    相比 synchronizedLock 支持创建多个 Condition,从而可以根据需要按组将线程等待或唤醒(即可以唤醒指定线程),更加灵活。

    4、代码示例

    synchronized 一节中,我们利用 synchronized 关键字实现了俩线程交替执行、多线程顺序执行,所以这里将利用 Lock 实现这俩个功能。

    1、俩线程交替执行。

    直接上代码。

        private static class InTurnThread extends Thread {
    
            private Lock lock;
            private Condition condition;
    
            public InTurnThread(Lock lock, Condition condition) {
                this.lock = lock;
                this.condition = condition;
            }
    
            @Override
            public void run() {
                super.run();
                lock.lock();
                try {
                    // 交替运行
                    while (true) {
                        // todo sth 你的业务逻辑
                        condition.signalAll();
                        Thread.sleep(1000); //这里为了方便测试 暂停1s
                        condition.await();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }
    

    执行:

    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    
    Thread inTurnThreadA = new InTurnThread(lock, condition);
    Thread inTurnThreadB = new InTurnThread(lock, condition);
    inTurnThreadA.start();
    inTurnThreadB.start();
    

    2、多线程顺序执行

    照旧。

        private static class OrderThread extends Thread {
    
            private Lock lock;
            private Condition condition;
            private OrderThread next;
    
            public OrderThread(Lock lock, OrderThread next) {
                this.lock = lock;
                this.condition = lock.newCondition();
                this.next = next;
            }
    
            @Override
            public void run() {
                super.run();
                lock.lock();
                try {
                    while (true) {
                        // todo sth 你的业务逻辑
                        next.condition.signal();
                        Thread.sleep(1000); // 为了方便测试 暂停1s
                        condition.await();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
    
            public void setNext(OrderThread next) {
                this.next = next;
            }
        }
    

    乍一看没什么问题,但是上述代码是错误的。

    分析一下,假如现在有 A、B、C 三个线程,它的执行顺序可能是怎样的。

    1. A 启动,执行 next.condition.signal() 时,因为第二个线程还没有进入锁并 await(),所以无效。此时 B、C 都有机会先获得锁。
    2. 假如 C 先获得了锁,则执行 next.condition.signal() 时,因为 A 已经 await(),所以此时 A 被唤醒。此时 A、B 都有机会获得锁执行。
    3. 继续,假如 B 运气好,终于轮到它获得锁了,此时 B 执行 next.condition.signal() ,C 被唤醒。此时 A(上次没执行)、C 都有机会获得锁执行。

    到这儿相信都看出来了,总有俩个线程处于抢占锁状态,顺序不确定。究其原因,就是因为线程初次获取锁时,顺序随机,导致错误的唤醒了线程。

    Java 锁机制详解(一)synchronized 一节讲过顺序执行线程,它是如何避免问题的呢?

    回顾下代码:

    private class OrderInTurnThread extends Thread {
    
        private int order;
    
        public OrderInTurnThread(int order) {
            this.order = order;
        }
    
        @Override
        public void run() {
            super.run();
            synchronized (lock) {
                while (!isStop) {
                    // 符合条件 执行后 唤醒其它所有等待线程
                    if (flag % THREAD_COUNT == order) {
                        msg.in(order + "");
                        flag++;
                        lock.notifyAll();
                    }
                    // 不符合条件 或 符合条件执行完毕 进入等待 交给下个线程执行
                    try {
                        Thread.sleep(500);
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                lock.notifyAll();
            }
            msg.in(order + " end");
        }
    }
    

    可以看到,虽然 lock.notifyAll() 唤醒了全部线程,但是错误的线程即使抢占到了锁,也会立即休眠。

    所以 Lock 也可以参考类似实现,下面的例子以 ArrayList 添加次序为序,依次执行线程。

        private static class OrderInTurnThread extends Thread {
    
            private Lock lock;
            private Condition condition;
            private OrderManager manager;
    
            public OrderInTurnThread(Lock lock, OrderManager manager) {
                this.lock = lock;
                this.condition = lock.newCondition();
                this.manager = manager;
            }
    
            @Override
            public void run() {
                super.run();
                lock.lock();
                try {
                    while (true) {
                        // 判断是不是轮到当前线程执行
                        if (manager.isCurThreadOrder(this)) {
                            //todo sth 你的业务逻辑
                            manager.next().condition.signal();
                            Thread.sleep(1000); // 为了方便测试 暂停1s
                        }
                        condition.await();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }
        
         private static class OrderManager {
    
            private List<OrderInTurnThread> threads = new ArrayList<>();
            private int curIndex = 0;
            private OrderInTurnThread curThread;
    
            public void add(OrderInTurnThread thread) {
                threads.add(thread);
            }
    
            /**
             * 找到顺序添加的下一个OrderInTurnThread
             */
            public OrderInTurnThread next() {
                curThread = threads.get(++curIndex % threads.size());
                return curThread;
            }
    
            /**
             * 遍历开启线程
             */
            public void start() {
                if (threads.isEmpty()) {
                    return;
                }
                curThread = threads.get(curIndex);
                for (OrderInTurnThread thread : threads) {
                    thread.start();
                }
            }
    
            /**
             * 是否轮到当前线程执行
             */
            public boolean isCurThreadOrder(OrderInTurnThread thread) {
                if (curThread == thread) {
                    return true;
                }
                return false;
            }
        }
    

    执行:

    OrderManager manager = new OrderManager();
    OrderInTurnThread threadA = new OrderInTurnThread(lock, manager);
    threadA.setName("A");
    OrderInTurnThread threadB = new OrderInTurnThread(lock, manager);
    threadB.setName("B");
    OrderInTurnThread threadC = new OrderInTurnThread(lock, manager);
    threadC.setName("C");
    manager.add(threadA);
    manager.add(threadB);
    manager.add(threadC);
    manager.start();
    
    // 执行顺序为 A、B、C、A、B、C...
    

    现在来分析下修改后的代码,它的执行顺序可能是怎样的:

    1. A 先启动,执行 next.condition.signal() 时,因为第二个线程还没有进入锁并 await(),所以无效。此时 B、C 都有机会先获得锁,且新的目标线程为 B。
    2. 假如 C 先获得了锁,在判断是否是目标线程时未通过,所以直接休眠。此时只有 B 能获取锁。
    3. 至此顺序执行。

    因为限制了序列所对应的线程,所以即使错误的线程先行执行,也会直接休眠。而且相比较 synchronizedLock 在顺序正式建立起来后,只会唤醒下一个线程,比 notifyAll() 全部唤醒效率更高。

    上面只是个例子,代码不够安全健壮请自行忽略...

    5、ReentrantReadWriteLock

    上面说到 ReentrantLock 是排他锁,同一时刻只允许一个线程访问。

    但是存在这种情况:大多数场景下多线程都在读取数据,只有少数场景在写。如果多线程都在读数据时使用排他锁,对于读效率是很低且没有必要的。

    ReentrantReadWriteLock 则解决了这个问题。它的原则如下:

    1. 多个读锁不互斥;
    2. 读锁写锁互斥;
    3. 多个写锁互斥;

    即允许同时读,但只能有一方写。

    ReentrantReadWriteLock 读写锁的使用和 ReentrantLock 类似,比较简单,不赘述了。

    总结

    Lock 讲的很粗糙,主要是因为现在 synchronized 效率上与 Lock 并无二致,使用 Lock 多数在于一些特殊场景(需要响应中断、或者公平锁的场景),所以只简单阐述,没有深入细节。

    [TOC]

    相关文章

      网友评论

          本文标题:Java 锁机制详解(三)Lock

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