【Java并发编程】显式锁:Lock接口

一、显式锁Lock的介绍

显式锁是自JDK1.5开始引入的排他锁。作为一种线程同步机制,其作用于内部锁相同。它提供了一些内部锁不具备的特性,但并不是内部锁的替代品。

显示锁(Explicit Lock)是java.util.concurrent.locks.Lock接口的实例。该接口对显式锁进行了抽象,其定义的方法如图所示:

方法 描述
void lock() 获得锁
void lockInterruptibly() 获取锁定,除非当前线程是 interrupted。
Condition newCondition() 返回一个新Condition绑定到该实例Lock实例。
boolean tryLock() 只有在调用时才可以获得锁。
boolean tryLock(long time, TimeUnit unit) 如果在给定的等待时间内是空闲的,并且当前的线程尚未得到 interrupted,则获取该锁。
void unlock() 释放锁
  • lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。在前面已经讲到,如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此,一般来说,使用Lock必须在try…catch…块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生

java.util.concurrent.ReentrantLock 是Lock接口的默认实现类

一个Lock接口实例就是一个显式锁对象,Lock接口定义的lock方法和unlock方法分别用于申请和释放相应Lock实例表示的锁。

显示锁的使用包括以下几个方面:

  • 创建Lock接口的实例——创建Lock接口默认的实现类ReentrantLock的实例作为显示锁使用
  • 在访问共享数据前申请相应的显式锁——直接调用Lock.lock()
  • 在临界区中访问共享数据——Lock.lock调用与Lock.unlock()调用之间的代码区域为临界区,一般为try 代码块为临界区
  • 共享数据访问结束后释放锁(finally 解锁)

【实例】

  • 使用两个线程进行i ++ , 加Lock锁后结果为20000,若不加锁,则出现意想不到的结果。
/**
 * @Author: LiangYiFeng
 * @Description  使用显式锁实现循环递增序列号生成器
 * @Date: Create in 2022/8/17 11:22
 * @Modified By:
 */
public class LockDemo implements Runnable{


    private static int i=0;
    private final Lock lock = new ReentrantLock();  // 创建一个Lock接口实例


    public void run() {
        lock.lock();        // 申请锁
        try{
            for (int j = 0; j < 10000; j++) {
                    i++;
            }
        } finally {
            lock.unlock();  // 无论出现异常,finally总能执行解锁
        }

    }

    public static void main(String[] args) throws InterruptedException {
        LockDemo lockDemo = new LockDemo();
        Thread thread1 = new Thread(lockDemo);
        Thread thread2 = new Thread(lockDemo);

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


        thread1.join();
        thread2.join();
        System.out.println(i);      
    }
}

输出结果:

20000



二、显式锁的调度

ReentrantLock即支持非公平锁也支持公平锁。ReentrantLock的一个构造器的签名如下:

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

该构造器可以看出:我们在创建显式锁实例的时候可以指定相应的锁是否为公平锁(fair参数值为true,表示是公平锁)

公平锁保障锁调度的公平性往往是增加了线程的暂停和唤醒的可能性,即增加了上下文切换为代价的。因此,公平锁适合于锁被持有的时间相对长 或者 线程申请锁的平均间隔时间相对长的情形。

总的来说,使用公平锁的开销比使用非公平锁的开销要大,因此显示锁默认使用的是非公平调度策略




三、显式锁与内部锁的比较

分类 内部锁(synchronized) 显式锁(Lock)
对象方面 基于代码块的锁,使用基础没有灵活性可言,要么使用,要么不使用 基于对象的锁,其使用可以充分发挥面向对象编程的灵活性。
代码方面 仅仅是一个关键字 是一个对象,可发挥面向对象编程的灵活性。
使用方面 简单易用,且不会导致锁泄露 容易被错用而导致锁泄露,必须注意将锁的释放操作放在finally块中
锁调度方面 仅支持非公平锁 即支持非公平锁,也支持公平锁
性能方面 jdk 1.6/1.7 对内部锁做了一些优化,在特定情况下可以减少锁的开销。优化包括:锁消除、锁粗化、偏向锁和适配性锁 jdk1.6后,显式锁和内部锁之间的可伸缩性差异越来越小了。

四、如何选择内部锁还是显式锁

  • 内部锁的优点是简单易用——默认情况下选用内部锁,仅在需要显式锁所提供的特性的时候才选用显式锁。

  • 显式锁的优点是功能强大——一般来说,新开发的代码中我们可以选用显式锁。

五、改进型锁:读写锁

锁的排他性:使多个线程无法以线程安全的方式在同一时刻对共享变量进行读取(只是读取而不更新),这不利于提供系统的并发性。

对于同步在同一锁之上的线程而言:
       读线程——对共享变量仅读取而没有更新的线程
       写线程——对共享变量更新(包括先读取后更新)的线程

读写锁(Read/Write Lock) 是一种改进型的排他锁,也被称为共享/排他锁(Shared/ Exclusive)锁。

  • 读取锁可以允许多个线程可以同时读取(只读)共享变量,但是一次只允许一个线程对共享变量进行更新(包括读取后更新)
  • 任何线程读取共享变量的时候,其他线程都无法更新这些变量
  • 一个线程更新共享变量的时候,任何线程都无法访问该变量

读写锁的功能是通过其——读锁(Read Lock)和写锁(Write Lock)实现的。

  • 读锁可以同时被多个线程所持有的,即读锁是共享的
  • 写锁是排他的,即一个线程持有写锁的时候其他线程无法获得相应锁的写锁或读锁,保障了写线程对共享变量的访问(包括更新)是独占的。
获得条件 排他性 作用
读锁 相应的写锁未被任何线程持有 对读线程是共享的,对写线程是排他的 允许多个读线程可以同时读取共享变量,并保障读线程读取共享变量期间没有其他任何线程能够更新这些共享变量
写锁 写锁未被其他任何线程持有,并且相应的读锁未被其他任何线程持有 对写线程和读线程都是排他的 使得写线程能够以独占的方式访问共享变量

1)ReadWriteLock 接口 是什么

首先明确一下,不是说 ReentrantLock 不好,只是 ReentrantLock 某些时候有局限。如果使用 ReentrantLock,可能本身是为了防止线程 A 在写数据、线程 B 在读数据造成的数据不一致,但这样,如果线程 C 在读数据、线程 D 也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。因为这个,才诞生了读写锁 ReadWriteLock。

ReadWriteLock 是一个读写锁接口,读写锁是用来提升并发程序性能的锁分离技术,ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

而读写锁有以下三个重要的特性:

(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。

(2)重进入:读锁和写锁都支持线程重进入。

(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。

2)ReadWriteLock的 API

java.util.concurrent.locks.ReadWriteLock 接口 是对读写锁的抽象,其默认实现类是 java.util.concurrent.locks.ReentrantReadWriteLock 。ReadWriteLock 接口定义了两个方法

方法 描述
Lock readLock() 返回用于阅读的锁。
Lock writeLock() 返回用于写入的锁。

实例:

public class ReadWriteLockDemo {

    private static int sequence = 1;

    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();


    // 读线程执行方法
    public void reader(){
        readLock.lock(); //申请读锁
        try {
            //读取共享变量
            System.out.println(sequence);
        } finally {
            readLock.unlock();
        }
    }

    // 读线程执行方法
    public void writeLock(){
        writeLock.lock(); //申请读锁
        try {
            //读取共享变量
            sequence = 2;
            System.out.println(sequence);
        } finally {
            writeLock.unlock();
        }
    }

}

读写锁适用于:

  • 只读操作比写操作更频繁得多
  • 读线程 持有锁的时间比较长

ReentrantReadWriteLock 所实现的读写锁是个可重入锁。ReentrantReadWriteLock支持锁的降级(Downgrade), 即一个线程持有读写锁的写锁的情况下可以继续获得相应的读锁。

六、 ReentrantLock(重入锁)实现原理与公平锁非公平锁区别

什么是可重入锁(ReentrantLock)

ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。

在java关键字synchronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入。与此同时,ReentrantLock还支持公平锁和非公平锁两种方式。那么,要想完完全全的弄懂ReentrantLock的话,主要也就是ReentrantLock同步语义的学习:1.

  1. 可重入性的实现原理;
  2. 公平锁和非公平锁。

可重入性的实现原理

要想支持重入性,就要解决两个问题:

  1. 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;
  2. 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。

ReentrantLock支持两种锁:公平锁和非公平锁。何谓公平性,是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO。

七、 Lock 和synchronized 有什么区别?

  • 首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类;
  • synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
  • synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁; 而
    lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
  • 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

你可能感兴趣的:(java,jvm,开发语言)