Java的锁—重入锁(ReentrantLock)

重入锁简单理解就是对同一个线程而言,它可以重复的获取锁。例如这个线程可以连续获取两次锁,但是释放锁的次数也一定要是两次。下面是一个简单例子:

public class ReenterLock {

    private static ReentrantLock lock = new ReentrantLock();

    private static int i = 0;

    // 循环1000000次
    private static Runnable runnable = () -> IntStream.range(0, 1000000).forEach((j) -> {
        lock.lock();
        try {
            i++;
        } finally {
            lock.unlock();
        }
    });

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
        // 利用join,等thread1,thread2结束后,main线程才继续运行,并打印 i
        thread1.join();
        thread2.join();
        // 利用lock保护的 i,最终结果为 2000000,如果不加,则值肯定小于此数值
        System.out.println(i);
    }
}

从上面的代码可以看到,相比于synchronized,开发者必须手动指定锁的位置和什么时候释放锁,这样必然增加了灵活性。

线程中断响应

如果线程阻塞于synchronized,那么要么获取到锁,继续执行,要么一直等待。重入锁提供了另一种可能,就是中断线程。下面的例子是利用两个线程构建一个死锁,然后中断其中一个线程,使另一个线程获取锁的例子:

public class ReenterLockInterrupt {
    private static ReentrantLock lock = new ReentrantLock();

    private static Runnable runnable = () -> {
        try {
            // 利用 lockInterruptibly 申请锁,这是可以进中断申请的申请锁操作
            lock.lockInterruptibly();
            // 睡眠20秒,在睡眠结束之前,main方法里要中断thread2的获取锁操作
            Thread.sleep(20000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            String threadName = Thread.currentThread().getName();
            // 中断后抛出异常,最后要释放锁
            // 如果是线程1则释放锁,因为线程2就没拿到锁,所以不用释放
            if ("Thread-1".equals(threadName)) lock.unlock();
            System.out.println(threadName+" 停止");
        }
    };

    public static void main(String[] args) {
        Thread thread1 = new Thread(runnable, "thread-1");
        Thread thread2 = new Thread(runnable, "thread-2");
        thread1.start();

        // 让主线程停一下,让thread1获取锁后再启动thread2
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // 这里什么也不做
        }

        thread2.start();
        thread2.interrupt();
    }
}

thread-1拿到锁之后,线程即持有锁并等待20秒,然后thread-2启动,并没有拿到锁,这时候中断thread-2线程,线程2退出。

有限时间的等待锁

顾名思义,简单理解就是在指定的时间内如果拿不到锁,则不再等待锁。当持有锁的线程出问题导致长时间持有锁的时候,你不可能让其他线程永远等待其释放锁。下面是一个例子:

public class ReenterTryLock {
    private static ReentrantLock reenterLock = new ReentrantLock();

    private static Runnable runnable = () -> {
        try {
            // tryLock()方法会返回一个布尔值,获取锁成功则为true
            if (reenterLock.tryLock(3, TimeUnit.SECONDS)) {
                Thread.sleep(5000);
            } else {
                System.out.println(Thread.currentThread().getName() + "获取锁失败");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 最后,如果当前前程在持有锁,则释放锁
            if (reenterLock.isHeldByCurrentThread()) {
                System.out.println(Thread.currentThread().getName() + "释放锁了");
                reenterLock.unlock();
            }
        }
    };

    public static void main(String[] args) {
        Thread thread1 = new Thread(runnable, "thread-1");
        Thread thread2 = new Thread(runnable, "thread-2");

        thread1.start();
        thread2.start();
    }
}

这里使用tryLock()第一个获取锁的线程,会停止5秒。而获取锁的设置为3秒获取不到锁则放弃,所以第二个去尝试获取锁的线程是获取不到锁而被迫停止的。如果tryLock()方法不传入任何参数,那么获取锁的线程不会等待锁,则立即返回false。

公平锁与非公平锁

当一个线程释放锁时,其他等待的线程则有机会获取锁,如果是公平锁,则分先来后到的获取锁,如果是非公平锁则谁抢到锁算谁的,这就相当于排队买东西和不排队买东西是一个道理。Java的synchronized关键字就是非公平锁

那么重入锁ReentrantLock()是公平锁还是非公平锁?

重入锁ReentrantLock()是可以设置公平性的,可以参考其构造方法:

// 通过传入一个布尔值来设置公平锁,为true则是公平锁,false则为非公平锁
public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

构建一个公平锁需要维护一个有序队列,如果实际需求用不到公平锁则不需要使用公平锁。下面用一个例子来演示公平锁与非公平锁的区别:

public class ReenterTryLockFair {
    // 分别设置公平锁和非公平锁,分析打印结果
    private static ReentrantLock lock = new ReentrantLock(true);

    private static Runnable runnable = () -> {
        while (true) {
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName() + " 获取了锁");
            } finally {
                lock.unlock();
            }
        }
    };

    public static void main(String[] args) {
        Thread thread1 = new Thread(runnable, "thread---1");
        Thread thread2 = new Thread(runnable, "thread---2");
        Thread thread3 = new Thread(runnable, "thread---3");

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

当设置为true即公平锁的时候,可以看到打印非常规律,截取一段儿打印结果:

thread---1 获取了锁
thread---2 获取了锁
thread---3 获取了锁
thread---1 获取了锁
thread---2 获取了锁
thread---3 获取了锁
thread---1 获取了锁
thread---2 获取了锁
thread---3 获取了锁
thread---1 获取了锁
thread---2 获取了锁
thread---3 获取了锁
thread---1 获取了锁
thread---2 获取了锁
thread---3 获取了锁
thread---1 获取了锁
thread---2 获取了锁
thread---3 获取了锁

可以看到,都是thread--1,thread--2,thread--3,无限循环下去,如果设置的为非公平锁,打印结果就混乱没有规律了:

thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---1 获取了锁

Condition

同jdk中的等待/通知机制类似,只不过Condition是用在重入锁这里的。有了Condition,线程就可以在合适的时间等待,在合适的时间继续执行。

Condition接口包含以下方法:

// 让当前线程等待,并释放锁
void await() throws InterruptedException;
// 和await类似,但在等待过程中不会相应中断
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
// 唤醒等待中的线程
void signal();
// 唤醒等待中的所有线程
void signalAll();

下面是一个简单示例:

public class ReenterLockCondition {
    private static ReentrantLock lock = new ReentrantLock();

    private static Condition condition = lock.newCondition();

    private static Runnable runnable = () -> {
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + "进入等待。。");
            condition.await();
            System.out.println(Thread.currentThread().getName() + "继续执行");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    };

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(runnable, "thread--1");
        thread.start();

        Thread.sleep(2000);

        lock.lock();
        condition.signal();
        System.out.println("主线程发出信号");
        lock.unlock();
    }
}

thread--1启动,拿到锁,然后进入等待并且释放锁,2秒后,主线程拿到锁,然后发出信号并释放锁,最后,thread--1继续执行。下面是打印结果:

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

推荐阅读更多精彩内容