Java 锁机制详解(三)Lock

简介

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]

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343