博主
258
258
258
258
专辑

第四节 java中的Lock锁

亮子 2021-10-05 23:15:13 6410 0 0 0

1、概述

Lock 是 java.util.concurrent.locks 包 下的接口,Lock 实现提供了比 synchronized 关键字 更广泛的锁操作,它能以更优雅的方式处理线程同步问题。Lock提供了比synchronized更多的功能。

java.util.concurrent.locks包下常用的类与接口(lock是jdk 1.5后新增的)

  • Lock和ReadWriteLock是两大锁的根接口,Lock代表实现类是ReentrantLock(可重入锁),ReadWriteLock(读写锁)的代表实现类是ReentrantReadWriteLock。

  • Lock 接口支持那些语义不同(重入、公平等)的锁规则,可以在非阻塞式结构的上下文(包括 hand-over-hand 和锁重排算法)中使用这些规则。

  • ReadWriteLock 接口以类似方式定义了一些读取者可以共享而写入者独占的锁。此包只提供了一个实现 ReentrantReadWriteLock。

  • Lock是可重入锁,可中断锁,可以实现公平锁和读写锁,读锁为排它锁,写锁为共享锁。ReentrantLock也是一种排他锁。

2、synchronized 与 Lock

1)、synchronized的缺陷

synchronized是java中的一个关键字,也就是说是Java语言内置的特性。那么为什么会出现Lock呢?

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

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

2)、synchronized 与 Lock 的区别

(1)synchronized是关键字,是JVM层面的,而Lock是一个接口,是JDK提供的API。

(2)当一个线程获取了synchronized锁,其他线程便只能一直等待直至占有锁的线程释放锁。当发生以下情况之一线程才会释放锁:
  a.占有锁的线程执行完了该代码,然后释放对锁的占有。
  b.占有锁线程执行发生异常,此时JVM会让线程自动释放锁。
  c.占有锁线程进入 WAITING 状态从而释放锁,例如在该线程中调用wait()方法等。

但是如果占有锁的线程由于要等待IO或者因为其他原因(比如调用sleep方法)而使线程阻塞了,但是又没有释放锁,那么线程就只能一直等待,那么这时我们可能需要一种可以不让线程无期限的等待下去的方法,比如只等待一定的时间(tryLock(long time, TimeUnit unit)或者能被人为中断lockInterrup0tibly(),这种情况我们需要Lock。

(3)当多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作也会发生冲突现象,但是读操作和读操作不会发生冲突现象,但是如果采用synchronized进行同步的话,就会导致当多个线程都只是进行读操作时也只有获取锁的线程才能进行读操作,其他线程只能等待锁释放后才能读,Lock则可以实现当多个线程都只是进行读操作时,线程之间不会发生冲突,例如:ReentrantReadWriteLock()。

(4)可以通过Lock得知线程有没有成功获取到锁 (例如:ReentrantLock) ,但这个是synchronized无法办到的。

(5)锁属性上的区别:synchronized是不可中断锁和非公平锁,ReentrantLock可以进行中断操作并别可以控制是否是公平锁。

(6)synchronized能锁住方法和代码块,而Lock只能锁住代码块。

(7)synchronized无法判断锁的状态,而Lock可以知道线程有没有拿到锁。

(8)在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时,此时Lock的性能要远远优于synchronized。

3)、synchronized 的局限性 与 Lock 的优点 

  如果一个代码块被synchronized关键字修饰,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待直至占有锁的线程释放锁。事实上,占有锁的线程释放锁一般会是以下三种情况之一:

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

  2:占有锁线程执行发生异常,此时JVM会让线程自动释放锁;

  3:占有锁线程进入 WAITING 状态从而释放锁,例如在该线程中调用wait()方法等。

  试考虑以下三种情况: 

Case 1 :

  在使用synchronized关键字的情形下,假如占有锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,那么其他线程就只能一直等待,别无他法。这会极大影响程序执行效率。因此,就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间 (解决方案:tryLock(long time, TimeUnit unit)) 或者 能够响应中断 (解决方案:lockInterruptibly())),这种情况可以通过 Lock 解决。

Case 2 :

  我们知道,当多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作也会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是如果采用synchronized关键字实现同步的话,就会导致一个问题,即当多个线程都只是进行读操作时,也只有一个线程在可以进行读操作,其他线程只能等待锁的释放而无法进行读操作。因此,需要一种机制来使得当多个线程都只是进行读操作时,线程之间不会发生冲突。同样地,Lock也可以解决这种情况 (解决方案:ReentrantReadWriteLock) 。

Case 3 :

  我们可以通过Lock得知线程有没有成功获取到锁 (解决方案:ReentrantLock) ,但这个是synchronized无法办到的。

上面提到的三种情形,我们都可以通过Lock来解决,但 synchronized 关键字却无能为力。事实上,Lock 是 java.util.concurrent.locks包 下的接口,Lock 实现提供了比 synchronized 关键字 更广泛的锁操作,它能以更优雅的方式处理线程同步问题。也就是说,Lock提供了比synchronized更多的功能。

3、Lock接口

Lock是一个接口,接口的实现类有 ReentrantLock和内部类ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock,该章节所描述的皆为 ReentrantLock

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

其中 lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的,unLock()方法是用来释放锁的 。

1)、 lock()

用来获取锁。如果锁已被其他线程获取,则进行等待。采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。

    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        lock.lock();
        try{
            System.out.println("获取锁成功!!");
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            System.out.println("释放锁成功");
            lock.unlock();
        }
    }

2)、 tryLock():

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

    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) {

        if(lock.tryLock()) {
            try{
                System.out.println("成功获取锁!!");
            }catch(Exception e){
                e.printStackTrace();
            }finally{
                lock.unlock();
            }
        }else {
            System.out.println("未获取锁,先干别的");
        }

    }

3)、 tryLock(long time, TimeUnit unit)

和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        try{
            if(lock.tryLock(5000, TimeUnit.MILLISECONDS)) {
                System.out.println("成功获取锁!!");
            }else {
                System.out.println("未获取锁,先干别的");
            }
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            lock.unlock();
        }
    }

4)、lockInterruptibly()

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

package ReentrantLockTest;
 
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
public class Test {
    private Lock lock = new ReentrantLock();
 
    public static void main(String[] args) {
        Test test = new Test();
        MyThread a = new MyThread(test);
        MyThread b = new MyThread(test);
        a.start();
        b.start();
 
        b.interrupt();
    }
 
    public void insert(Thread thread) throws InterruptedException {
        //注意:如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出
        lock.lockInterruptibly();
        try {
            System.out.println(thread.getName() + "得到了锁");
            Thread.sleep(3000);
        } finally {
            lock.unlock();
            System.out.println(thread.getName() + "释放了锁");
        }
    }
 
 
    static class MyThread extends Thread {
        private Test test;
 
        public MyThread(Test test) {
            this.test = test;
        }
 
        @Override
        public void run() {
            try {
                test.insert(Thread.currentThread());
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + "被中断");
            }
        }
    }
 
 
}

注意:当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,才可以响应中断。而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只能一直等待下去。

5)、Condition接口和newCondition()方法

synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类也可以借助于Condition接口与newCondition()方法。

synchronized关键字在使用notify/notifyAll()方法进行通知时,被通知的线程是由JVM选择的,使用ReentrantLock类结合Condition实例可以实现“选择性通知”。

synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在该一个实例上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程,这样会造成很大的效率问题,而Condition可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。从而可以有选择性的进行线程通知,在调度线程上更加灵活。

    //使当前线程在接到信号或被中断之前一直处于等待状态。
    void await();
 
    //使当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
    boolean await(long time, TimeUnit unit);
 
    //使当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
    long awaitNanos(long nanosTimeout);
 
    //使当前线程在接到信号之前一直处于等待状态。
    void awaitUninterruptibly();
 
    //使当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。
    boolean awaitUntil(Date deadline);
 
    //唤醒一个等待线程。
    void signal();
 
    //唤醒所有等待线程。
    void signalAll();

(1) Condition实现等待/通知机制

当调用 await() 语句后,线程将被阻塞,必须执行完signal()所在的try语句块之后才释放锁,condition.await()后的语句才能被执行。

package ReentrantLockTest;
 
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
public class Test {
 
    public static void main(String[] args) throws InterruptedException {
        MyService service = new MyService();
 
        MyThread a = new MyThread(service);
        a.start();
 
        Thread.sleep(3000);
 
        service.signal();
    }
 
    static public class MyService {
 
        private Lock lock = new ReentrantLock();
 
        public Condition condition = lock.newCondition();
 
        public void await() {
            lock.lock();
            try {
                System.out.println("准备调用condition.await()方法,将该线程阻塞");
                condition.await();
                System.out.println("已调用condition.await()方法,此时已被 signal() 方法唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
 
        public void signal() {
            lock.lock();
            try {
                System.out.println("准备调用condition.signal()方法");
                condition.signal();
                Thread.sleep(3000);
                System.out.println("已调用condition.signal()方法,去唤醒 await() 方法");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
 
 
    static public class MyThread extends Thread {
        private MyService service;
 
        public MyThread(MyService service) {
            this.service = service;
        }
 
        @Override
        public void run() {
            service.await();
        }
    }
 
}

输出:

准备调用condition.await()方法,将该程序阻塞
准备调用condition.signal()方法
已调用condition.signal()方法,去唤醒 await() 方法
已调用condition.await()方法,此时已被 signal() 方法唤醒

(2) 多个Condition实例实现等待/通知机制

一个Lock对象中可以创建多个Condition实例,调用某个实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。

package ReentrantLockTest;
 
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
public class LockTest {
 
    private Lock lock = new ReentrantLock();
 
    private Condition conditionA = lock.newCondition();
 
    private Condition conditionB = lock.newCondition();
 
    public void awaitA() {
        lock.lock();
        try {
            System.out.println("准备调用conditionA.await()方法,将该线程阻塞");
            conditionA.await();
            System.out.println(" awaitA 已被唤醒");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
 
    public void awaitB() {
        lock.lock();
        try {
            System.out.println("准备调用conditionB.await()方法,将该线程阻塞");
            conditionB.await();
            System.out.println(" awaitB 已被唤醒");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
 
    public void signalA() {
        lock.lock();
        try {
            System.out.println("准备唤醒 conditionA 下的所有线程");
            conditionA.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
 
    public void signalB() {
        lock.lock();
        try {
            System.out.println("准备唤醒 conditionB 下的所有线程");
            conditionB.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
 
}
package ReentrantLockTest;
 
public class Test {
 
    public static void main(String[] args) throws InterruptedException {
 
        LockTest lockTest = new LockTest();
 
        ThreadA a = new ThreadA(lockTest);
        a.setName("A");
        a.start();
 
        ThreadB b = new ThreadB(lockTest);
        b.setName("B");
        b.start();
 
        Thread.sleep(3000);
 
        lockTest.signalA();
 
    }
 
    static public class ThreadA extends Thread {
 
        private LockTest lockTest;
 
        public ThreadA(LockTest lockTest) {
            this.lockTest = lockTest;
        }
 
        @Override
        public void run() {
            lockTest.awaitA();
        }
    }
 
    static public class ThreadB extends Thread {
 
        private LockTest lockTest;
 
        public ThreadB(LockTest lockTest) {
            this.lockTest = lockTest;
        }
 
        @Override
        public void run() {
            lockTest.awaitB();
        }
    }
}

输出:

准备调用conditionA.await()方法,将该线程阻塞
准备调用conditionB.await()方法,将该线程阻塞
准备唤醒 conditionA 下的所有线程
 awaitA 已被唤醒

4、ReadWriteLock接口

public interface ReadWriteLock {
    
    // 读锁
    Lock readLock();
   
    // 写锁
    Lock writeLock();
}

ReentrantLock是一种排他锁,同一时刻只允许一个线程访问,ReadWriteLock 接口的实现类 ReentrantReadWriteLock 读写锁提供了两个方法:readLock()和writeLock()用来获取读锁和写锁,也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。

读写锁维护了两个锁,一个是读操作相关的锁也称为共享锁,一个是写操作相关的锁也称为排他锁。通过分离读锁和写锁,其并发性比一般排他锁有了很大提升。

多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥(只要出现写操作的过程就是互斥的)。在没有线程进行写操作时,进行读取操作的多个线程都可以获取读锁,而进行写入操作的线程只有在获取写锁后才能进行写操作。即多个线程可以同时进行读取操作,但是同一时刻只允许一个线程进行写入操作。

1)、读锁

package ReentrantLockTest;
 
import java.util.concurrent.locks.ReentrantReadWriteLock;
 
public class ReentrantReadWriteLockTest {
 
    private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
 
    public static void main(String[] args) {
        final ReentrantReadWriteLockTest test = new ReentrantReadWriteLockTest();
 
        new Thread() {
            public void run() {
                test.get(Thread.currentThread());
            }
        }.start();
 
        new Thread() {
            public void run() {
                test.get(Thread.currentThread());
            }
        }.start();
 
    }
 
    public void get(Thread thread){
        reentrantReadWriteLock.readLock().lock();
 
        try {
 
            for (int i=0;i<10;i++){
                System.out.println(thread.getName() + "正在进行读操作");
                Thread.sleep(1000);
            }
 
            System.out.println(thread.getName() + "读操作完毕");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            reentrantReadWriteLock.readLock().unlock();
        }
 
    }
 
 
}

输出:

Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-1读操作完毕
Thread-0读操作完毕

多个线程可以同时获得读锁

2)、读写互斥

package ReentrantLockTest;
 
import java.util.concurrent.locks.ReentrantReadWriteLock;
 
public class ReentrantReadWriteLockTest {
 
    public static void main(String[] args) {
        LockTest lockTest = new LockTest();
 
        ReadThread readThread = new ReadThread(lockTest);
        ReadThread readThread2 = new ReadThread(lockTest);
        WriteThread writeThread = new WriteThread(lockTest);
        ReadThread readThread3 = new ReadThread(lockTest);
        readThread.start();
        readThread2.start();
        writeThread.start();
 
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        readThread3.start();
    }
 
 
    static public class LockTest {
        private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
 
        public void read(Thread thread) {
            lock.readLock().lock();
            try {
                for (int i=0;i<5;i++){
                    System.out.println(thread.getName() + "正在进行读操作");
                    Thread.sleep(1000);
                }
                System.out.println(thread.getName() + "读操作完毕");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.readLock().unlock();
            }
        }
 
        public void write(Thread thread) {
            lock.writeLock().lock();
            try {
                for (int i=0;i<5;i++){
                    System.out.println(thread.getName() + "正在进行写操作");
                    Thread.sleep(1000);
                }
                System.out.println(thread.getName() + "写操作完毕");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.writeLock().unlock();
            }
        }
    }
 
    static public class ReadThread extends Thread {
        private LockTest lockTest;
 
        public ReadThread(LockTest lockTest) {
            this.lockTest = lockTest;
        }
 
        @Override
        public void run() {
            lockTest.read(Thread.currentThread());
        }
    }
 
 
    static public class WriteThread extends Thread {
        private LockTest lockTest;
 
        public WriteThread(LockTest lockTest) {
            this.lockTest = lockTest;
        }
 
        @Override
        public void run() {
            lockTest.write(Thread.currentThread());
        }
    }
}

输出:

Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-1读操作完毕
Thread-0读操作完毕
Thread-2正在进行写操作
Thread-2正在进行写操作
Thread-2正在进行写操作
Thread-2正在进行写操作
Thread-2正在进行写操作
Thread-2写操作完毕
Thread-3正在进行读操作
Thread-3正在进行读操作
Thread-3正在进行读操作
Thread-3正在进行读操作
Thread-3正在进行读操作
Thread-3读操作完毕

由此可以看出,读锁可以共享,写锁只有在所有读锁释放后才能执行,但是当写锁在阻塞和获取过程中,之后的读锁也会阻塞,需要等到写锁释放后才能获取。

5、锁的相关概念介绍

1)、可重入锁

    如果锁具备可重入性,则称作为 可重入锁 。像 synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了 锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。

class MyClass {
    public synchronized void method1() {
        method2();
    }

    public synchronized void method2() {

    }
}

  上述代码中的两个方法method1和method2都用synchronized修饰了。假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是,这就会造成死锁,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。而由于synchronized和Lock都具备可重入性,所以不会发生上述现象。

2)、可中断锁

  顾名思义,可中断锁就是可以响应中断的锁。在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
  如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。在前面演示tryLock(long time, TimeUnit unit)和lockInterruptibly()的用法时已经体现了Lock的可中断性。

3)、公平锁

  公平锁即 尽量 以请求锁的顺序来获取锁。比如,同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。而非公平锁则无法保证锁的获取是按照请求锁的顺序进行的,这样就可能导致某个或者一些线程永远获取不到锁。

  在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。而对于ReentrantLock 和 ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁