05 Java多线程之显式锁Lock

1 什么是显式锁

java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。
ReentrantLock (可重入锁)
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似轮询锁、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)

reentrant 锁意味着什么呢?简单来说,它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。这模仿了 synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁。
synchronized的缺陷
synchronized是java中的一个关键字,也就是说是Java语言内置的特性。那么为什么会出现Lock呢?

在上面一篇文章中,我们了解到如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;

2)线程执行发生异常,此时JVM会让线程自动释放锁。

那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。

因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。

再举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。

但是采用synchronized关键字来实现同步的话,就会导致一个问题:

如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。

因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。

另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。

总结一下,也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点:

1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;

2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

2 Lock接口提供的方法

image.png

lock()方法

  • 平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。

  • 由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
    tryLock()方法

  • 有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
    tryLock(long time, TimeUnit unit)方法

  • 和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。所以一般会在自旋使用tryLock()方法,获取到之后跳出循环。
    lockInterruptibly()方法

  • 当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。

  • 由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。

  • 注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为本身在前面的文章中讲过单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。

  • 因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。

  • 而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

4 Condition接口

4.1 Condition中的方法

Condition接口提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的 。

public class LockAndCondition {

    private Lock lock = new ReentrantLock();
    private Condition conditionOne = lock.newCondition();
    private Condition conditionTwo = lock.newCondition();
    
    
    private void conditionAwait() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+":获取锁并将要进入等待");
            conditionOne.await();
            System.out.println(Thread.currentThread().getName()+":被唤醒");
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    
    private void conditionSignal() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+":获取锁并将要唤醒等待线程");
            conditionOne.signal();
            System.out.println(Thread.currentThread().getName()+":唤醒结束");
        } finally {
            lock.unlock();
        }
    }
    
    public static void main(String[] args) {
        LockAndCondition lockAndCondition = new LockAndCondition();
        new Thread(new Runnable() {
            
            @Override
            public void run() {
                lockAndCondition.conditionAwait();
            }
        }).start();;
        new Thread(new Runnable() {
            
            @Override
            public void run() {
                lockAndCondition.conditionSignal();
            }
        }).start();
    }
}

执行结果

Thread-0:获取锁并将要进入等待
Thread-1:获取锁并将要唤醒等待线程
Thread-1:唤醒结束
Thread-0:被唤醒

同一个Condition的await()方法只能被自己的signal()/signalAll();方法唤醒,Condition的唤醒具有特定指向性,所以一般建议用signal()方法唤醒。而wait()对于的notify()/notifyAll(),因为notify()是随机唤醒,所以建议用notifyAll()。

4.2 Condition原理分析

ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition对象都包含着一个队列,该队列是Condition对象实现等待/通知功能的关键。下面将分析Condition的实现,主要包括:等待队列、等待和通知

等待队列
等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态
一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点(lastWaiter)。当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列,等待队列的基本结构如下图所示

image.png

因为Condition是同步器AbstractQueuedSynchronizer的内部类,他的数据结构和AQS的数据结构之间是有交互的,所以详细内容我会在AQS模块的博客中详解,此处只做简单解读。

3 实现类ReentrantLock(可重入锁)

什么时候选择用 ReentrantLock 代替 synchronized
既然如此,我们什么时候才应该使用 ReentrantLock 呢?答案非常简单 —— 在确实需要一些 synchronized 所没有的特性的时候,比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者轮询锁。 ReentrantLock 还具有可伸缩性的好处,应当在高度争用的情况下使用它,但是请记住,大多数 synchronized 块几乎从来没有出现过争用,所以可以把高度争用放在一边。我建议用 synchronized 开发,直到确实证明 synchronized 不合适,而不要仅仅是假设如果使用 ReentrantLock “性能会更好”。请记住,这些是供高级用户使用的高级工具。(而且,真正的高级用户喜欢选择能够找到的最简单工具,直到他们认为简单的工具不适用为止。)。一如既往,首先要把事情做好,然后再考虑是不是有必要做得更快。
可重入锁演示

public class TestReentrantLock {
    private Lock lock = new ReentrantLock();
    
    private void getReentLock() {
        int i = 0;
        int j = 0;
        try {
            for(;;) {
                if(lock.tryLock()) {
                    i++;
                    System.out.println(Thread.currentThread().getName()+"第"+i+"次获取当前锁");
                    if(i>=10) 
                        break;
                }else {
                    j++;
                    if(j>=10)
                        break;
                    System.out.println(Thread.currentThread().getName()+"第"+j+"次获取当前锁失败");
                }
                
            }
        } finally {
            System.out.println(Thread.currentThread().getName()+"释放锁");
            lock.unlock();
        }
        
    }
    public static void main(String[] args) {
        TestReentrantLock reentrantLock = new TestReentrantLock();
        new Thread(new Runnable() {
            
            @Override
            public void run() {
                reentrantLock.getReentLock();
            }
        }).start();
    }
}

运行结果

Thread-0第1次获取当前锁
Thread-0第2次获取当前锁
Thread-0第3次获取当前锁
Thread-0第4次获取当前锁
Thread-0第5次获取当前锁
Thread-0第6次获取当前锁
Thread-0第7次获取当前锁
Thread-0第8次获取当前锁
Thread-0第9次获取当前锁
Thread-0第10次获取当前锁
Thread-0释放锁

由此可见一个线程可多次获取当前锁
注意
可重入锁当前线程每获取一次锁,相应的锁计时器就会加一,同样的在释放锁的时候锁计时器会减一,所以获取锁的次数要和释放锁次数对应起来,否则释放次数少会造成该锁未释放,其他的线程无法获取当前锁,释放次数多会导致报错

4 ReentrantReadWriteLock(读写锁)

  • ReentrantReadWriteLock字面意思可以看出是一个读写锁。
  • ReentrantReadWriteLock有两种锁,一种是读锁,称为共享锁,一个种是写锁为互斥锁。

读写锁的特性:

  • 可重入:允许读锁可重入,写锁可重入,但是写锁可以获得读锁,读锁不能获得写锁。 注意:在锁重入中,读锁不能获取写锁
  • 锁降级:允许写锁降低为读锁,就是在锁重入中,写锁重入到了读锁中.称为锁降级.
  • 中断锁的获取:在读锁和写锁的获取过程中支持中断
  • 支持Condition:写锁提供Condition实现
  • 监控:提供确定锁是否被持有等辅助方法
    读锁共享
    为了展示读写锁的读锁是共享锁特性,我写了两段代码来比较
    第一段,用读写锁
/**
 * 
 * @author dongyue
    *   读写锁案例
 */
public class TestReentrantReadWriteLock {
    private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private int i = 0;
    private void countOne() {
        rwLock.readLock().lock();
        System.out.println(Thread.currentThread().getName()+":获取锁");
        try {
            i++;
            Thread.sleep(1000);
            System.out.println(i);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
        rwLock.readLock().unlock();
        System.out.println(Thread.currentThread().getName()+":释放锁");
        }
    }
    
    private void countTwo() {
        rwLock.readLock().lock();
        System.out.println(Thread.currentThread().getName()+":获取锁");
        try {
            i++;
            Thread.sleep(1000);
            System.out.println(i);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {

        rwLock.readLock().unlock();
        System.out.println(Thread.currentThread().getName()+":释放锁");
        }
    }
    public static void main(String[] args) {
        TestReentrantReadWriteLock readWriteLock = new TestReentrantReadWriteLock();
        new Thread(new Runnable() {
            @Override
            public void run() {
                readWriteLock.countOne();
            }
        }).start();;
        new Thread(new Runnable() {
            @Override
            public void run() {
                readWriteLock.countTwo();
            }
        }).start();
    }
}

运行结果

Thread-0:获取锁
Thread-1:获取锁
2
Thread-1:释放锁
2
Thread-0:释放锁

可以看出针对同一个读锁,是线程共享的,1线程没有释放该锁时,2线程就可以拿到该锁,两个线程可以都对i操作,最终i=2

第二段,用重入锁

/**
 * 
 * @author dongyue
    *   读写锁案例
 */
public class TestReentrantReadWriteLock {
    private Lock lock = new ReentrantLock();
    private int i = 0;
    private void countOne() {
        lock.lock();
                System.out.println(Thread.currentThread().getName()+":获取锁");
        try {
            i++;
            Thread.sleep(1000);
            System.out.println(i);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
        lock.unlock();
                System.out.println(Thread.currentThread().getName()+":释放锁");
        }
    }
    
    private void countTwo() {
        lock.lock();
                System.out.println(Thread.currentThread().getName()+":获取锁");
        try {
            i++;
            Thread.sleep(1000);
            System.out.println(i);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
        lock.unlock();
                System.out.println(Thread.currentThread().getName()+":释放锁");
        }
    }
    public static void main(String[] args) {
        TestReentrantReadWriteLock readWriteLock = new TestReentrantReadWriteLock();
        new Thread(new Runnable() {
            @Override
            public void run() {
                readWriteLock.countOne();
            }
        }).start();;
        new Thread(new Runnable() {
            @Override
            public void run() {
                readWriteLock.countTwo();
            }
        }).start();
    }
}

执行结果

Thread-0:获取锁
1
Thread-0:释放锁
Thread-1:获取锁
2
Thread-1:释放锁

可以看出,该锁不是共享锁,而是独占锁,只有当前一个线程将锁释放后,另一个线程才可以拿到。
读写锁互斥

/**
 * 
 * @author dongyue
    *   读写锁案例
 */
public class TestReentrantReadWriteLock {
    private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
//  private Lock lock = new ReentrantLock();
    private int i = 0;
    private void countOne() {
        rwLock.readLock().lock();
//      lock.lock();
        System.out.println(Thread.currentThread().getName()+":获取读锁");
        try {
            i++;
            Thread.sleep(1000);
            System.out.println(i);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {

        rwLock.readLock().unlock();
//      lock.unlock();
        System.out.println(Thread.currentThread().getName()+":释放读锁");
        }
    }
    
    
    private void countThr() {
        rwLock.writeLock().lock();
//      lock.lock();
        System.out.println(Thread.currentThread().getName()+":获取写锁");
        try {
            i++;
            Thread.sleep(1000);
            System.out.println(i);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {

        rwLock.writeLock().unlock();
//      lock.unlock();
        System.out.println(Thread.currentThread().getName()+":释放写锁");
        }
    }
    public static void main(String[] args) {
        TestReentrantReadWriteLock readWriteLock = new TestReentrantReadWriteLock();
        new Thread(new Runnable() {
            @Override
            public void run() {
                readWriteLock.countOne();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                readWriteLock.countThr();
            }
        }).start();
    }
}

运行结果

Thread-0:获取读锁
1
Thread-0:释放读锁
Thread-1:获取写锁
2
Thread-1:释放写锁

可见读写锁是互斥锁

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