Published on

Java并发-Lock

Authors
  • avatar
    Name
    Wenzhuo Zhao
    Twitter

本文介绍java包下的Lock接口,以及他们与synchronized的区别。

Locksynchronized的对比:

  • Lock是一个接口,而synchronized是Java中的关键字synchronized是内置的语言实现,是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须unLock()放到finally{}中;
  • synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
  • Lock可以让等待锁的线程响应中断,线程可以中断去干别的事务,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
  • 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
  • Lock可以提高多个线程进行读操作的效率。

举个例子: 当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。
如果采用synchronized关键字来实现同步的话,就会导致一个问题:

如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。
因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。
另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。

乐观锁和悲观锁

乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。在Java和数据库中都有此概念对应的实际应用。

先说概念。对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。

乐观锁则认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。

乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。

optimic_lock.png

  • 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
  • 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

公平锁和非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

可重入锁和非可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLocksynchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。下面用示例代码来进行分析:

public class Test{
    public synchronized void doSomething(){
        System.out.println("Do something");
        doOthers();
    }
    public synchronized void doOthers(){
        System.out.println("Do other things");
    }
}

在上面的代码中,类中的两个方法都是被内置锁synchronized修饰的,doSomething()方法中调用doOthers()方法。因为内置锁是可重入的,所以同一个线程在调用doOthers()时可以直接获得当前对象的锁,进入doOthers()进行操作。

如果是一个不可重入锁,那么当前线程在调用doOthers()之前需要将执行doSomething()时获取当前对象的锁释放掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁。

java.util.concurrent.locks

Lock

Lock是一个接口

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()方法是用来释放锁的。newCondition() 方法返回新建的Condition, 简单来说通过Condition 接口中的await(),signal(),signalAll()等方法可以实现线程间的等待通知机制(和使用synchronized结合Object上的waitnotify方法实现的效果一样,但是Condition 拥有更高级的用法),具体的下面介绍。

在Lock中声明了四个方法来获取锁,那么这四个方法有何区别呢?

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

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

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

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

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

由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用的:

Lock lock = ...;
lock.lock();
try{
    //处理任务
}catch(Exception ex){
     
}finally{
    lock.unlock();   //释放锁
}

ReentrantLock

ReentrantLock,意思是“可重入锁",意味着ReentrantLock是一个可重入锁,ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock既可以实现公平锁也可以实现非公平锁。

Lock lock=new ReentrantLock()//默认是非公平锁 
Lock lock=new ReentrantLock(true);//公平锁
Lock lock=new ReentrantLock(false);//非公平锁
  • getHoldCount() 查询当前线程保持此锁的次数,也就是执行此线程执行lock方法的次数

  • getQueueLength()返回正等待获取此锁的线程估计数,比如启动10个线程,1个线程获得锁,此时返回的是9

  • getWaitQueueLength(Condition condition)返回等待与此锁相关的给定条件的线程估计数。比如10个线程,用同一个condition对象,并且此时这10个线程都执行了condition对象的await方法,那么此时执行此方法返回10

  • hasWaiters(Condition condition)查询是否有线程等待与此锁有关的给定条件(condition),对于指定contidion对象,有多少线程执行了condition.await方法

  • hasQueuedThread(Thread thread)查询给定线程是否等待获取此锁

  • hasQueuedThreads()是否有线程等待此锁

  • isFair()该锁是否公平锁

  • isHeldByCurrentThread() 当前线程是否保持锁锁定,线程的执行lock方法的前后分别是false和true

  • isLock()此锁是否有任意线程占用

  • lockInterruptibly()当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。

  • tryLock()尝试获得锁,能获得锁就返回true,不能就立即返回false.

  • tryLock(long timeout TimeUnit unit)增加时间限制,如果超过该时间段还没获得锁,返回false

  • newCondition()方法: 返回一个新的Condition对象

lockInterruptibly使用示例

public class Test {
    private Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        Test test = new Test();
        MyThread thread1 = new MyThread(test);
        MyThread thread2 = new MyThread(test);
        thread1.setName("Thread1");
        thread2.setName("Thread2");
        thread1.start();
        thread2.start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.interrupt();
    }

    public void insert(Thread thread) throws InterruptedException {
        //注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出
        lock.lockInterruptibly();
        //lock.lock();
        try {
            System.out.println(thread.getName() + "得到了锁");
            long startTime = System.currentTimeMillis();
            for (; ; ) {
                if (System.currentTimeMillis() - startTime >= 5000L)
                    break;
                //插入数据
            }
        } finally {
            System.out.println(Thread.currentThread().getName() + "执行finally");
            lock.unlock();
            System.out.println(thread.getName() + "释放了锁");
        }
    }
}

class MyThread extends Thread {
    private Test test = null;

    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() + "被中断");
        }
    }
}

运行结果:

Thread1得到了锁
Thread2被中断
Thread1执行finally
Thread1释放了锁

Thread2根本没有执行,等待的时候就中断等待了,并且抛出了InterruptedException 异常.

Condition

Object类的wait和notify/notify与synchronized关键字配合完成线程间的等待/通知机制,而Condition与Lock配合使用也是可以完成等待/通知机制的,前者是java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。但是这两者在使用方式以及功能特性上还是有区别的。Object的监视器方法与Condition接口对比如下:

condition.png

public interface Condition {
    void await() throws InterruptedException;
    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();
}

解释:

  • await() 当前线程进入等待状态直到被通知(signal)或中断。

  • awaitUninterruptibly() 当前线程进入等待状态直到被通知,该方法不响应中断。

  • awaitNanos(long nanosTimeout) 当前线程进入等待状态直到被通知、中断或者超时,返回值表示剩余超时时间。

  • await(long time, TimeUnit unit) 当前线程进入等待状态直到被通知、中断或者到某个时间。如果没有到指定时间就被通知,方法返回true,否则,表示到了指定时间,返回false。

  • awaitUntil(Date deadline) 当前线程进入等待状态直到被通知、中断或者到某个时间。如果没有到指定时间就被通知,方法返回true,否则,表示到了指定时间,返回false。

  • signal() 唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition相关联的锁。

  • signalAll() 唤醒所有等待在Condition上的线程,能够从等待方法返回的线程必须获得与Condition相关联的锁。

例子:生产者消费者模型,作用与一个buffer上

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class BoundedBuffer {
    private final String[] buffer;
    private final int capacity;

    private int front;
    private int rear;
    private int count;

    private final Lock lock = new ReentrantLock();

    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    public BoundedBuffer(int capacity) {
        super();
        this.capacity = capacity;
        buffer = new String[capacity];
    }

    public void deposit(String data) throws InterruptedException {
        lock.lock();
        try {
            while (count == capacity) {
                notFull.await();
            }
            buffer[rear] = data;
            rear = (rear + 1) % capacity;
            count++;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public String fetch() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await();
            }
            String result = buffer[front];
            front = (front + 1) % capacity;
            count--;
            notFull.signal();
            return result;
        } finally {
            lock.unlock();
        }
    }
}
  1. 两个方法先获得锁,以保证互斥
  2. 使用两个condition:empty或full
  3. 将await放在while循环中,原因是:

对于从wait中被notify的进程来说,它在被notify之后还需要重新检查是否符合执行条件,如果不符合,就必须再次被wait,如果符合才能往下执行。所以:wait方法应该使用循环模式来调用。《Effective Java》

ReadWriteLock

ReadWriteLock也是一个接口,在它里面只定义了两个方法:

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。

读写锁的阻塞情况如下图:

table_readwritelock.png

由于读写锁本身的实现就远比独占锁复杂,因此,读写锁比较适用于以下情形:

  1. 高频次的读操作,相对较低频次的写操作;

  2. 读操作所用时间不会太短。(否则读写锁本身的复杂实现所带来的开销会成为主要消耗成本)

下面的ReentrantReadWriteLock实现了ReadWriteLock接口。

ReentrantReadWriteLock

ReentrantReadWriteLock的意思是"可重入的读写锁", 那么ReentrantReadWriteLock是

  1. 一个可重入锁
  2. 既可以实现公平锁也可以实现非公平锁
  3. 支持使用Condition
  4. 支持锁降级

注解:

所谓锁降级,就是:先获取写锁,然后获取读锁,最后释放写锁,这样写锁就降级成了读锁。但是,读锁不能升级到写锁。简言之,就是:写锁可以降级成读锁,读锁不能升级成写锁。但是写锁并不会直接降成读锁, 不会随着读锁的释放而释放, 因此需要显式地释放写锁.

ReentrantReadWriteLock类最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁。

public class ReentrantReadWriteLockDemo {

    private final Map<String, Data> map = new TreeMap<>();

    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    private final Lock readLock = lock.readLock();

    private final Lock writeLock = lock.writeLock();

    public Data get(String key) {
        readLock.lock();
        
        try {
            return map.get(key);
        } finally {
            readLock.unlock();
        }
    }

    public Set<String> getAllKeys() {
        readLock.lock();
        try {
            return map.keySet();
        } finally {
            readLock.unlock();
        }
    }

    public Data put(String key, Data value) {
        writeLock.lock();
        try {
            return map.put(key, value);
        } finally {
            readLock.unlock();
        }
    }

    class Data {

    }
}

版权声明
版权所属:高亚轩的BLOG
本文地址:https://www.gaoyaxuan.net/blog/369.html
版权声明:原创文章,转载时必须以链接形式注明原始出处及本声明。