一. 概述

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

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

  2.Lock 接口支持那些语义不同(重入、公平等)的锁规则,可以在非阻塞式结构的上下文(包括 hand-over-hand 和锁重排算法)中使用这些规则。
   
   3.ReadWriteLock 接口以类似方式定义了一些
读取者可以共享而写入者独占的锁。此包只提供了一个实现 ReentrantReadWriteLock

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

二.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。

三.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();

5.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() 方法唤醒

5.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 已被唤醒

四.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读操作完毕

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

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐