Lock接口
锁是一种工具,用于控制对共享资源的访问。
Lock和synchronized,是两种最常见的锁,它们都可以达到线程安全的目的,但在使用上和功能上有较大的不同。
Lock不是 synchronized的代替品,而是当使用sychronized不合适或不足以满足要求的时候,才使用来提供较为高级的功能的。
Lock接口最常见的实现类是ReentrantLock
通常情况下,Lock只允许一个线程来访问被锁住的共享资源。不过有些时候,一些特殊的实现也可以允许并发访问,比如ReadWriteLock里面的ReadLock。
为什么需要Lock?
为什么synchronized不够用?
- 效率低:锁的释放情况少,试图获取锁时不能设定超时,不能中断一个正在试图获得锁的线程。
- 不够灵活:加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的。
- 无法知道synchronized是否成功获取到锁
Lock的主要接口
lock()
ReentrantLock中的lock实现
代码演示
/**
* @Classname MustUnlock
* @Description Lock不会像synchronized一样,异常的时候自动释放锁,所以最佳时间是,finally中释放锁,保证异常的时候锁一定被释放
* @Date 2021/2/17 13:28
* @Created by YoungLiu
*/
public class MustUnlock {
private static Lock lock = new ReentrantLock();
public static void main(String[] args){
lock.lock();
try{
//获取本锁保护的资源
System.out.println(Thread.currentThread().getName()+"开始执行任务");
}finally {
lock.unlock();
}
}
}
tryLock()
还有另外一个重载;这个重载会等一段时间,如果超时就放弃获取锁的行为。
代码演示1:tryLock避免死锁
/**
* @Classname TryLockDeadlock
* @Description 用TryLock避免死锁
* @Date 2021/2/17 13:39
* @Created by YoungLiu
*/
public class TryLockDeadlock implements Runnable {
int flag = 1;
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
public static void main(String[] args) {
TryLockDeadlock r1 = new TryLockDeadlock();
TryLockDeadlock r2 = new TryLockDeadlock();
r1.flag=1;
r2.flag=2;
new Thread(r1).start();
new Thread(r2).start();
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (flag == 1) {
try {
if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println("线程1获取到了锁1");
Thread.sleep(1000);
if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println("线程1获取到了锁2");
System.out.println("线程1成功获取到了两把锁");
break;
} finally {
lock2.unlock();
System.out.println("线程1释放了锁2");
}
}else{
System.out.println("线程1获取锁2失败,已重试");
}
} finally {
lock1.unlock();
System.out.println("线程1释放了锁1");
Thread.sleep(1000);
}
} else {
System.out.println("线程1获取锁1失败,已重试");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (flag == 2) {
try {
if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println("线程2获取到了锁1");
Thread.sleep(500);
if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println("线程2获取到了锁2");
System.out.println("线程2成功获取到了两把锁");
break;
} finally {
System.out.println("线程2释放了锁2");
lock2.unlock();
}
}else{
System.out.println("线程2获取锁2失败,已重试");
}
} finally {
lock1.unlock();
System.out.println("线程2释放了锁1");
Thread.sleep(500);
}
} else {
System.out.println("线程2获取锁1失败,已重试");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
lockInterruptibly()
代码演示
/**
* @Classname LockInterruptibly
* @Description TODO
* @Date 2021/2/17 13:54
* @Created by YoungLiu
*/
public class LockInterruptibly implements Runnable {
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
LockInterruptibly lockInterruptibly = new LockInterruptibly();
Thread t0 = new Thread(lockInterruptibly);
Thread t1 = new Thread(lockInterruptibly);
t0.start();
t1.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.interrupt();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 尝试获取锁");
try {
//此方法若获取不到锁,会一直等待;若等待期间被中断,触发会触发InterruptedException
lock.lockInterruptibly();
try{
System.out.println(Thread.currentThread().getName()+" 获取到了锁");
//在睡眠期间被中断,会触发InterruptedException
Thread.sleep(5000);
}catch (InterruptedException e){
System.out.println("睡眠期间被中断");
}finally{
lock.unlock();
System.out.println(Thread.currentThread().getName()+" 释放了锁");
}
} catch (InterruptedException e) {
System.out.println("等锁期间被中断");
}
}
}
lock的可见性保证
锁的分类
乐观锁和悲观锁
悲观锁
乐观锁
可重入锁和非可重入锁 以ReentrantLock为例
https://segmentfault.com/a/1190000022408592 参考此博文
可重入性质
可重入锁也叫递归锁,指的是同一线程外层函数获得锁之后,内层递归函数仍然有获取该锁的代码,但不受影响。
代码演示
public class Recursion {
private static ReentrantLock lock = new ReentrantLock();
private static void accessResource(){
lock.lock();
try{
System.out.println("已经对资源进行了处理");
if(lock.getHoldCount()<5){
System.out.println(lock.getHoldCount());
accessResource();
System.out.println(lock.getHoldCount());
}
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
accessResource();
}
}
源码分析
公平锁和非公平锁
公平指的是按照线程请求的顺序,来分配锁;
非公平指的是,不完全按照请求的顺序,在一定的情况下,可以插队。
notice:非公平也同样不提倡“插队”行为,这里的非公平,指的是“在合适的时机”插队,而不是盲目插队。
代码演示
/**
* @Classname FairLock
* @Description show fair and not fair lock.
* @Date 2021/2/17 15:18
* @Created by YoungLiu
*/
public class FairLock {
public static void main(String[] args) throws InterruptedException {
PrintQueue printQueue = new PrintQueue();
Thread [] threads =new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i]= new Thread(new Job(printQueue));
}
for (int i = 0; i < 10; i++) {
threads[i].start();
Thread.sleep(100);
}
}
}
class Job implements Runnable{
PrintQueue printQueue ;
Job(PrintQueue printQueue){this.printQueue=printQueue;}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"开始打印");
printQueue.printJob(new Object());
System.out.println(Thread.currentThread().getName()+"打印完毕");
}
}
class PrintQueue{
//参数代表 fair or not fair
// private Lock queueLock = new ReentrantLock(true);
private Lock queueLock = new ReentrantLock(false);
public void printJob(Object document){
queueLock.lock();
try{
Long duration = (long)(new Random().nextInt(10)+1);
System.out.println(Thread.currentThread().getName()+" 正在打印,需要"+duration);
Thread.sleep(duration*1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
queueLock.lock();
try{
Long duration = (long)(new Random().nextInt(10)+1);
System.out.println(Thread.currentThread().getName()+" 正在打印,需要"+duration);
Thread.sleep(duration*1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
}
}
优缺点
源码分析
共享锁和排他锁 以Reentrant为例
排他锁,又称为独占锁,独享锁;
共享锁,又称为读锁,获得共享锁之后,可以查看但是无法修改和删除数据,其他线程此时也可以获取到共享锁,也可以查看,但无法修改和删除。
共享锁和排他锁的典型是读写锁ReentrantReadWriteLock,其中ReadLock是共享锁,WriteLock是排他锁。
读写锁的作用
读写锁的规则
代码演示
程序的输出,显然能够同时读,但不能够同时写。
public class CinemaReadWrite {
private static ReentrantReadWriteLock reentrantReadWriteLock =
new ReentrantReadWriteLock();
private static ReentrantReadWriteLock.ReadLock readLock=
reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock =
reentrantReadWriteLock.writeLock();
private static void read(){
readLock.lock();
try{
System.out.println(Thread.currentThread().getName()+" 得到了读锁,正在读取");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName()+" 释放了读锁");
readLock.unlock();
}
}
private static void write(){
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"得到了写锁,正在写入");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName()+"释放了写锁");
writeLock.unlock();
}
}
public static void main(String[] args) {
new Thread(()->read(),"Thread1").start();
new Thread(()->read(),"Thread2").start();
new Thread(()->write(),"Thread3").start();
new Thread(()->write(),"Thread4").start();
}
}
ReentrantReadWriteLock的实现
ReentrantReadWriteLock 在非公平的情况下,不允许读锁插队,以此来避免饥饿发生;
在升降级方面,允许升级,不允许降级。
插队策略
公平策略下的源码:可以看到都要进入队列缓冲。
非公平的情况下:可以看到注释,写锁总是可以插队的。
在非公平策略下,读锁在某种情况下可以插队:看此方法的名字:apparentlyFirstQueuedIsExclusive(); 队列头是否是排它锁(此处为写锁);如果为排他锁则不可以插队,返回true;若队头为共享锁(此处为读锁),则可以插队,返回false。
锁的升级和降级
在某些操作中可能一开始获取的是写锁,但完成某一些操作后,便不再需要写入,如果继续持有写锁会造成资源的浪费,所以要降级为读锁。
降级:在持有WriteLock时,再次申请ReadLock,就将此线程持有的锁降级为readlock。
升级:在持有readlock时,再次申请writelock,将此线程持有的锁升级为writelock,但这是不被支持的。
在此处只支持锁的降低,不支持升级。
代码演示
public class Upgrading {
private static ReentrantReadWriteLock reentrantReadWriteLock =
new ReentrantReadWriteLock();
private static ReentrantReadWriteLock.ReadLock readLock=
reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock =
reentrantReadWriteLock.writeLock();
private static void readUpgrading(){
readLock.lock();
try{
System.out.println(Thread.currentThread().getName()+" 得到了读锁,正在读取");
Thread.sleep(1000);
System.out.println("升级会带来阻塞");
writeLock.lock();
System.out.println(Thread.currentThread().getName()+"获取到了些锁,升级成功");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName()+" 释放了读锁");
readLock.unlock();
}
}
private static void writeDowngrading(){
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"得到了写锁,正在写入");
Thread.sleep(1000);
System.out.println("开始降级");
readLock.lock();
System.out.println("在不释放写锁的情况下,直接获取读锁,成功降级");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
System.out.println(Thread.currentThread().getName()+"释放了写锁");
writeLock.unlock();
}
}
public static void main(String[] args) {
// System.out.println("演示降级");
// new Thread(()->writeDowngrading(),"Thread1").start();
// System.out.println("-------------------------");
System.out.println("演示升级是不行的");
new Thread(()->readUpgrading(),"Thread2").start();
}
}
共享锁和排他锁的总结
自旋锁和阻塞锁
自旋锁的缺点
原理和源码分析
CAS:Compare and Swap,即比较再交换,一种实现并发算法时常用到的技术。
有兴趣可以阅读wiki:https://zh.wikipedia.org/wiki/%E6%AF%94%E8%BE%83%E5%B9%B6%E4%BA%A4%E6%8D%A2
代码演示
public class SpinLock {
private AtomicReference<Thread> sign= new AtomicReference<>();
public void lock(){
//得到当前线程的引用
Thread current = Thread.currentThread();
while(!sign.compareAndSet(null,current)){
System.out.println(Thread.currentThread().getName()+"本次尝试获取自旋锁失败");
}
}
public void unlock(){
Thread current = Thread.currentThread();
sign.compareAndSet(current,null);
}
public static void main(String[] args) {
SpinLock spinLock = new SpinLock();
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始尝试获取自旋锁");
spinLock.lock();
System.out.println(Thread.currentThread().getName() + "获取到了自旋锁");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
spinLock.unlock();
System.out.println(Thread.currentThread().getName()+"释放了自旋锁");
}
}
};
Thread t0 = new Thread(runnable);
Thread t1 = new Thread(runnable);
t0.start();
t1.start();
}
}
自旋锁的适用场景
JVM对锁的优化
JVM会对锁进行优化,可自行搜索相关阅读材料i。
如何较为正确的使用lock