一、为什么需要Lock
如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
2)线程执行发生异常,此时JVM会让线程自动释放锁。
那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能等待,试想一下,这多么影响程序执行效率。
因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。
二、synchronized的两大不足
第一大不足:由于我们没办法设置synchronized关键字在获取锁的时候等待时间,所以synchronized可能会导致线程为了加锁而无限期地处于阻塞状态。
第二大不足:使用synchronized关键字等同于使用了互斥锁,即其他线程都无法获得锁对象的访问权。这种策略对于读多写少的应用而言是很不利的,因为即使多个读者看似可以并发运行,但他们实际上还是串行的,并将最终导致并发性能的下降。
虽然synchronized已经作为一个关键字被固化在Java语言中了,但它只提供了一种相当保守的线程安全策略,且该策略开放给程序员的控制能力极弱。
三、synchronized与Lock的区别
类别 | synchronized | Lock |
存在层次 | 关键字,是内置的语言实现 | 是一个类 |
发生异常 | 会自动释放线程占有的锁,因此不会导致死锁现象发生 | 如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁 |
锁的释放 | 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 3、不需要主动释放锁 | 1、在finally中必须释放锁,不然容易造成线程死锁 2、需要主动释放锁 |
锁的获取 | 假设A线程获得锁,B线程等待。 如果A线程阻塞,B线程会一直等待 | 分情况而定,Lock有多个锁获取的方式,就是可以尝试获得锁,线程可以不用一直等待 |
锁状态 | 无法判断 | 可以判断 |
响应中断 | 不行 | 行 |
锁类型 | 可重入 不可中断 非公平 | 可重入 可判断 可公平(两者皆可) |
性能 | 少量同步 | 大量同步 可以提高多个线程进行读操作的效率 |
可重入锁 | 是 | 是 |
如何选择 | 如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized |
四、java.util.concurrent.locks包下常用的类
1、Lock是一个接口
public interface Lock {
/**
* 就是用来获取锁。如果锁已被其他线程获取,则进行等待;该方法是平常使用得最多的一个方法
*/
void lock();
/**
* 1、用该锁的获得方式,如果线程在获取锁的阶段进入了等待,那么可以中断此线程,即中断线程的等待状态,先去做别的事
* 2、也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,
* 那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
* 3、由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用
* lockInterruptibly()的方法外声明抛出InterruptedException。
* 4、当一个线程获取了锁之后,是不会被interrupt()方法中断的。
* 而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。
*/
void lockInterruptibly() throws InterruptedException;
/**
* 尝试锁定
* 注意返回类型是boolean,如果获取锁的时候锁被占用就返回false,否则返回true
* 也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
*/
boolean tryLock();
/**
* 尝试锁定
* 比起tryLock(),区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。
* 如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
* 释放锁
* 使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生
*/
void unlock();
}
2、ReentrantLock
意思是“可重入锁”,是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。
重入锁的中断响应功能就合理地避免了这样的情况。比如,一个正在等待获取锁的线程被“告知”无须继续等待下去,就可以停止工作了。
3、如何使用Lock
public class Test {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
/**
* 1、如果lock变量是局部变量,每个线程执行该方法时都会保存一个副本,那么理所当然每个线程执行到lock.lock()处获取的是不同的锁,所以就不会发生冲突。
* 2、如果lock是全局变量,则才会发生冲突
*/
private Lock lock = new ReentrantLock(); //注意这个地方
public static void main(String[] args) {
final Test test = new Test();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
}
public void insert(Thread thread) {
if(lock.tryLock()) {
try {
System.out.println(thread.getName()+"得到了锁");
for(int i=0;i<5;i++) {
arrayList.add(i);
}
} catch (Exception e) {
// TODO: handle exception
}finally {
System.out.println(thread.getName()+"释放了锁");
lock.unlock();
}
} else {
System.out.println(thread.getName()+"获取锁失败");
}
}
}
输出结果
Thread-0得到了锁
Thread-1获取锁失败
Thread-0释放了锁
2、公平锁
3、ReadWriteLock、ReentrantReadWriteLock
是Lock的另一种实现方式,我们已经知道了ReentrantLock是一个排他锁,同一时间只允许一个线程访问,而ReentrantReadWriteLock允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。相对于排他锁,提高了并发性。在实际应用中,大部分情况下对共享数据(如缓存)的访问都是读操作远多于写操作,这时ReentrantReadWriteLock能够提供比排他锁更好的并发性和吞吐量。
感谢:
https://blog.youkuaiyun.com/u012403290/article/details/64910926?locationNum=11&fps=1
https://www.cnblogs.com/handsomeye/p/5999362.html