Lock与Synchronized

在前面章节已经介绍了Synchronized锁并发编程中的关键概念 。JAVA多线程同步的实现还可以通过Lock来实现,Lock与Synchronized的区别有:

  • Synchronized是JAVA关键字,底层是靠JVM调用对象的monitorenter与monitorexit指令实现的。而Lock不是关键字,是JDK1.5后新增的类,通过这个类实现多线程的对共享区域的同步访问。
  • Synchronized可以自动释放锁,就算出现了异常JVM也能确保锁能够被释放。Lock则必须手动释放锁。
  • Synchronized是不可中断锁。而Lock是可中断锁。可中断锁的意思是一线程如果尝试获取锁失败,可以自主选择不再等待(中断自己)或设置一个最大等待时间,或被别的线程中断。
  • 当多线程读写文件时,读操作和写操作会发生冲突,写操作和写操作也会发生冲突,但是读操作和读操作不会发生冲突现象。但是如果采用synchronized关键字实现同步的话,就会导致一个问题,即当多个线程都只是进行读操作时,也只有一个线程在可以进行读操作,其他线程只能等待锁的释放而无法进行读操作。因此,需要一种机制来使得当多个线程都只是进行并行读操作时,线程之间不会发生互斥。Lock可以通过设定读锁还是死锁来解决这种情况,提升多线程访问共享区域的性能。
  • 可以通过Lock得知线程有没有成功获取到锁 ,但Synchronized无法办到。

Lock接口的方法

Lock接口定义了使用锁的基本方法:

// 获得锁,锁被占用则等待  
void lock()   

// 尝试获取锁,若获取失败则阻塞,但是可以响应(别的线程的)中断
void lockInterruptibly()   

// 返回绑定到此Lock实例的新Condition 实例  
Condition newCondition()   

// 调用时尝试获取锁,锁为空闲状态则获取成功,否则中断  
boolean tryLock()   

// 在给定的等待时间尝试获取锁空闲,若超时仍未成功获取则中断
boolean tryLock(long time, TimeUnit unit)   

// 释放锁  
void unlock()

上述方法中,lock()、tryLock()、tryLock(long time, TimeUnit unit) 和 lockInterruptibly()都是用来获取锁的。unLock()方法是用来释放锁的。newCondition()方法主要用于进程间通信,此处暂跳过。下面介绍几种获得锁方式的不同,通过下面的案例能够了解Synchronized与Lock、中断锁/不可中断锁等的区别。

  1. Lock()
    如果锁已被其他线程获取,则进行等待。在前面已经讲到,如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此,一般来说,使用Lock必须在try…catch…块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用的:
Lock lock = ...;
lock.lock();
try{
    //处理任务
}catch(Exception ex){

}finally{
    lock.unlock();   //释放锁
}
  1. tryLock() & tryLock(long time, TimeUnit unit)
    tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true;如果获取失败(即锁已被其他线程获取),则返回false,也就是说,这个方法无论如何都会立即返回(拿不到锁时不会等待)。
    tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false,同时可以响应中断。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
    使用trylock()方法如下所示:
Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }catch(Exception ex){

     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //如果不能获取锁,则直接做其他事情
}
  1. lockInterruptibly()
    调用lockInterruptibly()的线程A会尝试获取锁,如果获取失败则进入等待。和lock()不同的是,(别的线程)可以对等待阶段的线程调用进程A的A.interrupt()方法中断线程A的等待过程,被中断(或者说唤醒)的线程A会抛出InterruptedException异常,因此使用lockInterruptibly()的时候要注意处理中断异常。需要注意的是,如果成功获取了锁的线程,是不会被调用它的interrupt()方法中断的。总结:lockInterruptibly()是一个可以响应中断的获取锁的方法。如果获取了锁则不会响应中断,而在等待(阻塞)期间的线程可以响应中断。使用模板如下:
public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}

Lock的实现类ReentrantLock

ReentrantLock实现了Lock接口,能使用上文所描述的功能,并有更多的功能。

ReentrantLock构造方法

可以通过ReentrantLock构造方法构造出公平锁和非公平锁。

  • 公平锁:尽量以请求锁的顺序来获取锁。比如,同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程会获得锁,这种就是公平锁。
  • 非公平锁:不保证锁的获取是按照请求锁的顺序进行的,这样做可以获得更大的吞吐量,因为从获取锁的阶段来分析,当某一线程要获取锁时,非公平锁可以直接尝试获取锁,而不是判断当前队列中是否有线程在等待。一定情况下可以避免线程频繁的上下文切换,这样的结果是活跃的线程最可能获得锁,而在队列中的锁还要进行唤醒才能继续尝试获取锁。但缺点是可能导致某个或者一些线程永远获取不到锁(饿死)。

下面是ReentrantLock的构造方法,无参构造默认使用非公平锁,有参构通过传入boolean值指定是否为公平锁。

/**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

使用ReentrantLock案例如下所示:

import java.util.concurrent.locks.Lock;  
import java.util.concurrent.locks.ReentrantLock;
public class LockThread {
    Lock lock = new ReentrantLock(); 
    public void lock(String name) {  
        // 获取锁  
        lock.lock();  
        try {  
            System.out.println(name + " get the lock");  
            // 访问此锁保护的资源  
        } finally {  
            // 释放锁  
            lock.unlock();  
            System.out.println(name + " release the lock");  
        }  
    }  

    public static void main(String[] args) {
        LockThread lt = new LockThread();  
        new Thread(() -> lt.lock("A")).start();  
        new Thread(() -> lt.lock("B")).start();  
    }
}

// 结果
A get the lock
A release the lock
B get the lock
B release the lock

在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态。

ReadWriteLock锁

ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持,而写入锁是独占的。ReadWriteLock接口获得读锁和写锁的相关方法:

//返回用于读取操作的锁  
Lock readLock()   
//返回用于写入操作的锁  
Lock writeLock() 

ReentrantReadWriteLock是ReadWriteLock接口的实现类。下面展示如何通过ReentrantReadWriteLock来为共享区域加读锁与写锁:

import java.util.Random;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class Queue {
    //共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。
    private Object data = null;

    ReadWriteLock lock = new ReentrantReadWriteLock();

    // 读数据
    public void get() {
        // 加读锁
        lock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " be ready to read data!");
            Thread.sleep((long) (Math.random() * 1000));
            System.out.println(Thread.currentThread().getName() + " have read data :" + data);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放读锁
            lock.readLock().unlock();
        }
    }

    // 写数据
    public void put(Object data) {
        // 加写锁
        lock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " be ready to write data!");
            Thread.sleep((long) (Math.random() * 1000));
            this.data = data;
            System.out.println(Thread.currentThread().getName() + " have write data: " + data);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放写锁
            lock.writeLock().unlock();
        }

    }
}

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        final Queue queue = new Queue();
        //一共启动6个线程,3个读线程,3个写线程
        for (int i = 0; i < 3; i++) {
            //启动1个读线程
            new Thread() {
                public void run() {
                    while (true) {
                        queue.get();
                    }
                }

            }.start();
            //启动1个写线程
            new Thread() {
                public void run() {
                    while (true) {
                        queue.put(new Random().nextInt(10000));
                    }
                }
            }.start();
        }
    }
}

总结:调用ReentrantReadWriteLock对象.readLock()/.writeLock()方法可以获得读/写锁,然后调用读/写锁.lock()/.unlock()方法进行加锁与释放锁。

ReentrantLock/ReentrantReadWriteLock底层实现

在对Sychronized的认识中我们知道Sychronized的底层实现是依赖于JVM对对象monitorenter与monitorexit指令的调用实现的。而Lock作为Java的一个同步类,ReentrantLock与ReentrantReadWriteLock两者底层都是依赖AQS实现的。

AbstractQueuedSynchronizer(AQS),抽象队列同步器,定义了一套多线程访问共享资源的同步器框架。简单来说就是定义了实现同步的基本数据结构。
图片说明
可以从图上看到,定义了两个核心。一个volatile int state的变量,作为共享资源,或者说锁被占有的标识。(关于volatile的详情可见上篇文章 )还有一个FIFO线程等待队列(多线程争用资源被阻塞时进入此队列)。
AQS定义使用共享资源的方式:Exclusive(独占,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。不同的自定义同步器争用共享资源的方式也不同,如ReentrantLock和read锁就不一样。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在底层实现好了。自定义同步器使用AQS实现同步时主要实现以下几种方法:

  • isHeldExclusively():根据用户定义的方式决定该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取共享资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放共享资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取共享资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放共享资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

ReentrantLock使用AQS实现同步方式如下:state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
同时我们也可以根据AQS提供的接口实现ReentrantReadWriteLock。读锁和写锁对共享资源的使用,正是对应AQS中的共享方式或独占方式使用state变量。

总结基于AQS实现的同步:自定义同步器要么是独占方法,要么是共享方式,他们也一般只需实现tryAcquire+tryRelease、tryAcquireShared+tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

参考

https://www.cnblogs.com/myseries/p/10784076.html
https://blog.csdn.net/u013851082/article/details/70140223
https://baijiahao.baidu.com/s?id=1664090632564134140&wfr=spider&for=pc
https://www.cnblogs.com/waterystone/p/4920797.html
http://www.cnblogs.com/waterystone/p/4920797.html