- Published on
Java并发-Lock
- Authors
- Name
- Wenzhuo Zhao
本文介绍java包下的Lock接口,以及他们与synchronized的区别。
Lock
和synchronized
的对比:
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自旋实现的。
- 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
- 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
公平锁和非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
可重入锁和非可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock
和synchronized
都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。下面用示例代码来进行分析:
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上的wait
和notify
方法实现的效果一样,但是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个线程获得锁,此时返回的是9getWaitQueueLength(Condition condition)
返回等待与此锁相关的给定条件的线程估计数。比如10个线程,用同一个condition对象,并且此时这10个线程都执行了condition对象的await方法,那么此时执行此方法返回10hasWaiters(Condition condition)
查询是否有线程等待与此锁有关的给定条件(condition),对于指定contidion对象,有多少线程执行了condition.await方法hasQueuedThread(Thread thread)
查询给定线程是否等待获取此锁hasQueuedThreads()
是否有线程等待此锁isFair()
该锁是否公平锁isHeldByCurrentThread()
当前线程是否保持锁锁定,线程的执行lock方法的前后分别是false和trueisLock()
此锁是否有任意线程占用lockInterruptibly()
当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。tryLock()
尝试获得锁,能获得锁就返回true,不能就立即返回false.tryLock(long timeout TimeUnit unit)
增加时间限制,如果超过该时间段还没获得锁,返回falsenewCondition()
方法: 返回一个新的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接口对比如下:
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();
}
}
}
- 两个方法先获得锁,以保证互斥
- 使用两个condition:empty或full
- 将await放在while循环中,原因是:
对于从wait中被notify的进程来说,它在被notify之后还需要重新检查是否符合执行条件,如果不符合,就必须再次被wait,如果符合才能往下执行。所以:wait方法应该使用循环模式来调用。《Effective Java》
ReadWriteLock
ReadWriteLock也是一个接口,在它里面只定义了两个方法:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。
读写锁的阻塞情况如下图:
由于读写锁本身的实现就远比独占锁复杂,因此,读写锁比较适用于以下情形:
高频次的读操作,相对较低频次的写操作;
读操作所用时间不会太短。(否则读写锁本身的复杂实现所带来的开销会成为主要消耗成本)
下面的ReentrantReadWriteLock实现了ReadWriteLock接口。
ReentrantReadWriteLock
ReentrantReadWriteLock的意思是"可重入的读写锁", 那么ReentrantReadWriteLock是
- 一个可重入锁
- 既可以实现公平锁也可以实现非公平锁
- 支持使用Condition
- 支持锁降级
注解:
所谓锁降级,就是:先获取写锁,然后获取读锁,最后释放写锁,这样写锁就降级成了读锁。但是,读锁不能升级到写锁。简言之,就是:写锁可以降级成读锁,读锁不能升级成写锁。但是写锁并不会直接降成读锁, 不会随着读锁的释放而释放, 因此需要显式地释放写锁.
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
版权声明:原创文章,转载时必须以链接形式注明原始出处及本声明。