Java 锁机制详解(三)Lock

2020-06-17  本文已影响0人  Parallel_Lines

简介

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]

上一篇下一篇

猜你喜欢

热点阅读