线程锁及锁的升降级
目录:
1、Lock 简介、地位、作用
2、Lock 方法
3、锁
4、共享锁和排它锁
5、锁的升降级
6、自旋锁和阻塞锁
7、可中断锁
8、如何使用锁
第三章 线程锁
1、Lock 简介、地位、作用
1、锁是一种工具,用于控制对共享资源的访问;
2、Lock 和 synchronized,这两个是最常见的锁,他们都可以达到线程安全的目的,但是在使用上和功能上又有较大的不同;
3、Locak 并不是用来代替 synchronized 的,而是当使用 synchronized 不合适或不足以满足要求的时候,来提供高级功能的;
4、Lock 接口最常见的实现类是 ReentrantLock;
5、通常情况下,Lock只允许一个线程来访问这个共享资源。不过有的时候,一些特殊的实现可允许并发访问,比如 ReadWriteLock 里面的 ReadLock;
6、为什么 synchronized 不够用?
- 效率低:锁的释放情况少、试图获得锁时不能设定超时、不能中断一个正在试图获得锁的线程(当进入 synchronized 中的线程,如果发生异常,JVM 会让其释放锁);
- 不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的;
- 无法知道是否成功获取到锁。
2、Lock 方法
1、locak()
- lock() 就是最普通的获取锁。如果锁以被其他线程获取,则进行等待;
- Lock 不会像 synchronized 一样在异常时自动释放锁;
- 因此最佳实践是,在 finally 中释放锁,以保证发生异常时锁一定被释放;
2、tryLock()
- tryLock() 用来尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,并返回 true,否则返回 false;
- 该方法会立即返回,不会像 lock() 一样,拿不到锁就阻塞在那里;
- tryLock(long time , TimeUnit unit):超时时间内如果还没有获取到锁,返回 false,如果能获取到锁,就获取锁,并返回 true。
3、lockInterruptibly()
- lockInterruptibly() 相当于 tryLock(long time , TimeUnit unit) 把超时时间设置为无限。在等待锁的过程中,线程可以被中断。
4、unlock():解锁
5、interrupt()方法:
- 其作用是中断此线程,它可以中断使用 lockInterruptibly() 获取锁,并且正在运行的线程,也可以中断使用 lockInterruptibly() 等待获取锁的线程。但是 interrupt()不能中断使用 lock() 等待获取锁的线程,也不能中断使用 lock() 获取锁,并正在运行的线程,除非,这个线程运行到了 Thread.sleep(),他会抛出异常,而执行 finally 中的 unlock() 释放锁。
6、interrupted()方法:
- 作用是测试当前线程是否被中断(检查中断标志),返回一个boolean并清除中断状态,第二次再调用时中断状态已经被清除,将返回一个false。
7、isInterrupted()方法:
- 作用是测试此线程是否被中断 ,不清除中断状态。
3、锁
1、锁的分类如图所示:
2、乐观锁和悲观锁
乐观锁可以称为非互斥同步锁,悲观锁可以称为互斥同步锁。
1)乐观锁是什么?
- 认为自己在处理操作的时候不会有其他线程来干扰,所以并不会锁住被操作对象;
- 在更新的时候,去对比在我修改的期间数据有没有被其他人改变过:如果没被改变过,就说明真的是只有我自己在操作,那我就正常去修改数据;
- 如果数据和我一开始拿到的不一样了,说明其他人在这段时间内改过数据,那我就不能继续刚才的更新数据过程了,我会选择放弃、报错、重试等策略;
- 乐观锁的实现一般都是利用 CAS 算法来实现的;
- 乐观锁的典型例子就是原子类、并发容器等。
2)悲观锁是什么?
- 如果我不锁住这个资源,别人就会来争抢,就会造成数据结果错误,所以每次悲观锁为了确保结果的正确性,会在每次获取并修改数据时,把数据锁住,让别人无法访问该数据,这样就可以确保数据内容万无一失;
- Java 中悲观锁的实现就是 synchronized 和 Lock 相关类。
3)两种锁的使用场景
- 悲观锁:适合并发写入多的情况,用用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗。典型情况:
- 临界区有 IO 操作;
- 临界区代码复杂或者循环量大;
- 临界区竞争非常激烈。
- 乐观锁:适合并发写入少,大部分是读取的场景,不加锁的能让读取性能大幅提高。
3、可重入锁和非可重入锁,以 ReentrantLock 为例(重点)
1)可重入锁
1、同一个线程没有释放锁的情况下,重复获取自己所持有的锁。
- 获取锁时先判断,如果当前线程就是已经占有锁的线程,则 status 值 +1,并返回 true。
- 释放锁时也是先判断当前线程是否已占有锁的线程,然后在判断 status。如果 status 等于 0,才真正的释放锁。
2)非可重入锁
1、非重入锁是直接尝试获取锁,如果已经持有锁,不能获再次取锁;
2、释放锁时也是直接将 status 置为0。
3)可重入锁和非可重入锁区别:
1、可重入锁在使用的时候一般是一个类当中有AB两个方法,而A和B都是有统一的一把锁,当实施A方法的时候就可以获得锁,但在A办法的所还没有全部释放的时候也可以直接使用B方法,而在这个时候也是可以获得这个锁的。
2、不可重入锁也是指的是A和B两个方法,A和B可以获得统一的一把锁,而在A方法还没有释放的时候是没有办法使用B方法的,也就是说必须要等A释放之后才可以使用B方法。
6)可重入锁演示:
一:
public class LockDemo3 { private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { System.out.println(lock.getHoldCount());//打印获取了几次锁 lock.lock(); System.out.println(lock.getHoldCount()); lock.lock(); System.out.println(lock.getHoldCount()); lock.lock(); System.out.println(lock.getHoldCount()); lock.unlock(); System.out.println(lock.getHoldCount()); lock.unlock(); System.out.println(lock.getHoldCount()); lock.unlock(); System.out.println(lock.getHoldCount()); } }
二:
public class RecursionDemo { private static ReentrantLock lock = new ReentrantLock(); public static void accessResource() { try{ lock.lock(); System.out.println(Thread.currentThread().getName()+":进入access并获取了锁"); agin(); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } public static void agin() { try{ lock.lock(); System.out.println(Thread.currentThread().getName()+":进入agin并获取了锁"); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } public static void main(String[] args) { accessResource(); } }
5)ReentrantLock 的其他方法介绍
1、isHeldByCurrentThread 可以看出锁是否被当前线程持有;
2、getQueueLength 可以返回当前正在等待这把锁的队列有多长,一般这两个方法是开发和调试时候使用,上线后用到的不多。
4、公平锁和非公平锁
1)公平的情况(以 ReentrantLock 为例)
1、创建 ReentrantLock 时传入 true 参数即可;
2、在线程1执行 unlock() 释放锁之后,由于此时线程2的等待时间最久,所以线程2先得到执行,然后是线程3、线程4。
2)不公平情况(以 ReentrantLock 为例)
1、创建 ReentrantLock 时传入 false 参数或者不传即可;
2、如果在线程1释放锁的时候,线程5恰好去执行 lock(),就是插队了;
3、线程5可以插队,直接拿到这把锁,也是 ReentrantLock 默认的公平策略,也就是“不公平”
5、演示公平锁
/** * 公平锁 */ public class FairLock { //公平 private static Lock lock = new ReentrantLock(true); private static Thread[] threads = new Thread[5]; public static void fairLockTest(){ try{ lock.lock(); System.out.println(Thread.currentThread().getName()+":获取到锁,正在执行业务"); Random random = new Random(); int i = random.nextInt(8); Thread.sleep(i * 1000); System.out.println(Thread.currentThread().getName()+":执行业务完毕,耗时:"+i); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 5; i++) { threads[i] = new Thread(() -> { fairLockTest(); }); } for (int i = 0; i < 5; i++) { threads[i].start(); } } }
运行结果:
6、演示非公平锁
/** * 非公平锁 */ public class FairLock { //非公平 private static Lock lock = new ReentrantLock(); private static Thread[] threads = new Thread[5]; public static void fairLockTest(){ try{ lock.lock(); System.out.println(Thread.currentThread().getName()+":获取到锁,正在执行业务"); Random random = new Random(); int i = random.nextInt(8); Thread.sleep(i * 1000); System.out.println(Thread.currentThread().getName()+":执行业务完毕,耗时:"+i); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 5; i++) { threads[i] = new Thread(() -> { fairLockTest(); }); } for (int i = 0; i < 5; i++) { threads[i].start(); } } }
运行结果:
7、特例
1)针对 tryLock(),他是很猛的,他不遵守设定的公平的规则;
2)例如,当有线程执行 tryLock()的时候,一旦有线程释放了锁,那么这个正在 tryLock() 的线程就能获取到锁,即使在他之前已经有其他现在在等待队列里了。
8、对比公平和非公平的优缺点
优势 | 劣势 | |
公平锁 | 各线程公平平等,每个线程在等待一段时间后,总有执行的机会 | 更慢,吞吐量更小 |
不公平锁 | 更快,吞吐量更大 | 有可能产生线程饥饿,也就是某些线程长时间内,始终得不到执行 |
4、共享锁和排它锁
1、什么是共享锁和排它锁
1)共享锁
- 共享锁,又称为读锁,获得共享锁之后,可以查看但无法修改和删除数据,其他线程此时也可以获取到共享锁,也可以查看但无法修改和删除数据。
2)排它锁
- 排它锁,又称独占锁、独享锁。
3)共享锁和排它锁的典型是读写锁 ReentrantReadWriteLock,其中读锁是共享锁,写锁是独享锁。
2、读写锁的作用
1)在没有读写锁之前,我们假设使用 ReentrantLock,那么虽然我们保证了线程安全,但是也浪费了一定的资源:多个读操作同时进行,并没有线程安全问题;
2)在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,提高了程序的执行效率。
3、读写锁的规则
1)多个线程只申请读锁,都可以申请到;
2)如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁;
3)如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁;
4)一句话总结:要么是一个或多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现(要么多读,要么一写)。
4、ReentrantReadWriteLock(boolean b) 具体用法
ReentrantReadWriteLock(boolean b)可以设置参数为公平或非公平锁,默认为非公平锁。
简单演示:
public class CinemaReadWrite { private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private static ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock(); private static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock(); private static void read(){ readLock.lock(); try{ System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取"); Thread.sleep(2000); }catch (Exception e){ e.printStackTrace(); }finally { System.out.println("释放读锁"); readLock.unlock(); } } private static void write(){ writeLock.lock(); try{ System.out.println(Thread.currentThread().getName() + "得到了写锁,正在读取"); Thread.sleep(2000); }catch (Exception e){ e.printStackTrace(); }finally { System.out.println("释放写锁"); writeLock.unlock(); } } public static void main(String[] args) { new Thread(() ->{ read(); },"线程一").start(); new Thread(() ->{ read(); },"线程二").start(); new Thread(() ->{ write(); },"线程三").start(); new Thread(() ->{ write(); },"线程四").start(); new Thread(() ->{ read(); },"线程五").start(); } }
运行结果:
5、读写锁插队策略
1)公平锁:不允许插队;
2)非公平锁:
1、获取写锁的线程可以随时插队。
2、如果等待队列中有线程要获取写锁,则其他想要获取读锁的线程不能插要获取写锁线程的队。举个例子:假设等待队列中,一个写锁前面有三个读锁,后面又十个读锁,前面的三个读锁可以互相插队,后面的十个读锁也能互相插队,但是不能插到写锁前面。
3、原因:因为读锁是可以共享的,一旦获取到了读锁,写锁就不可被获取,如果读锁可以一直插队的话,写锁将一直阻塞。
示例:
public class NonfairBargeDemo { private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(false); //创建读锁 private static ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock(); //创建写锁 private static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock(); private static void read(){ System.out.println(Thread.currentThread().getName() + "开始尝试获取读锁"); readLock.lock(); try{ System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取"); Thread.sleep(500); }catch (Exception e){ e.printStackTrace(); }finally { System.out.println(Thread.currentThread().getName() + "释放读锁"); readLock.unlock(); } } private static void write(){ System.out.println(Thread.currentThread().getName() + "开始尝试获取写锁"); writeLock.lock(); try{ System.out.println(Thread.currentThread().getName() + "得到写锁"); Thread.sleep(700); }catch (Exception e){ e.printStackTrace(); }finally { System.out.println(Thread.currentThread().getName() + "释放写锁"); writeLock.unlock(); } } public static void main(String[] args) { new Thread(() -> write(),"Thread1").start(); new Thread(() -> read(),"Thread2").start(); new Thread(() -> read(),"Thread3").start(); new Thread(() -> write(),"Thread4").start(); new Thread(() -> read(),"Thread5").start(); new Thread(() -> { Thread thread[] = new Thread[10]; for (int i = 0; i < 10; i++) { thread[i] = new Thread(() -> read(),"子线程创建的Thread"+i); } for (int i = 0; i < 10; i++) { thread[i].start(); } }).start(); } }
运行结果:
6、共享锁和排它锁总结
1)ReentrantReadWriteLock 实现了 ReadWriteLock 接口,最主要的有两个方法:readLock() 和 writeLock() 用来获取读锁和写锁;
2)锁申请和释放策略:
- 多个线程只申请读锁,都可以申请到;
- 如果一个线程已经占用了读锁,则其他线程如果要申请写锁,则申请写锁的线程会一直等待释放写锁。如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
3)如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁;
4)插队策略:为了防止饥饿,读锁不能插写锁的队;
5)升降级策略:读锁不能升级,写锁能降级;
6)适用场合:相比与 ReentrantLock 适用于一般场合,ReentrantReadWriteLock 适用于读多写少的情况,合理使用,可以进一步提高并发效率。
5、锁的升降级
1、锁的升降级介绍:
1)升级(以读锁升级到写锁为例)
- 首先声明,读锁是不支持升级到写锁的。
- 当一个线程拥有一个读锁,此时该线程想要获取写锁来做一些事情,但是又不想释放手上的读锁,否则下次再获取读锁时就要排队。
- 如何实现锁的升级?
- 获取读锁成功后,在释放读锁前,获取写锁,然后在做相关逻辑,最后在释放锁。
- 为什么读锁不支持升级到写锁?
- 容易造成死锁。假设有A、B两个线程,他们都拿着读锁,然后他们都想升级为写锁,但是,因为写锁不能与读锁并存,必须要先释放完读锁后才能获取写锁,所以导致A等待B释放读锁,然后A在升级为写锁,B的情况也是一样,等待A释放读锁,然后升级为写锁,由此造成了死锁;
- 如果强行升级为写锁,程序会一直阻塞在获取写锁上。
示例:
public class CinemaReadWrite { private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private static ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock(); private static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock(); private static void read(){ readLock.lock(); try{ System.out.println(Thread.currentThread().getName() + "得到了读锁"); Thread.sleep(800); System.out.println(Thread.currentThread().getName()+"尝试升级为写锁"); writeLock.lock(); System.out.println(Thread.currentThread().getName()+"升级成功"); }catch (Exception e){ e.printStackTrace(); }finally { System.out.println("释放读锁"); readLock.unlock(); } } private static void write(){ writeLock.lock(); try{ System.out.println(Thread.currentThread().getName() + "得到了写锁"); Thread.sleep(800); System.out.println(Thread.currentThread().getName() + "尝试降级为读锁"); readLock.lock(); System.out.println(Thread.currentThread().getName() + "降级成功"); }catch (Exception e){ e.printStackTrace(); }finally { System.out.println("释放写锁"); writeLock.unlock(); } } public static void main(String[] args) { //升级 //new Thread(() -> read(),"Thread01").start(); //降级 new Thread(() -> write(),"Thread02").start(); } }
降级结果:
升级结果:
结论:读锁升级为写锁,程序会一直阻塞获取写锁(所以读锁不支持升级),写锁可以降级为读锁。
6、自旋锁和阻塞锁
1、什么是自旋锁?
1)为了让当前线程 “稍等一下” ,我们需要让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。
2、什么是阻塞锁?
1)阻塞锁和自旋锁相反,阻塞锁如果遇到没拿到锁的情况,会直接把线程阻塞,直到被唤醒。
3、自旋锁的缺点
1)如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源;
2)在自旋的过程中,一直消耗 cpu,所以虽然自旋锁的起始开销低于悲观锁,但是随着自旋时间的增长,开销也是线性增长的。
4、自旋锁的适用场景
1)自旋锁一般用于多核的服务器,在并发度不是特别高的情况下,比阻塞锁的效率高;
2)另外,自旋锁适用于临界区比较短小的情况,否则如果临界区很大(现场一旦拿到锁,很久以后才会释放)。那也是不合适。
5、原理和源码分析
1)在 java1.5 以上的并发框架 atmoic 包下的类基本都是自旋锁的实现;
2)AtomicInteger 的实现:自旋锁的实现原理是 CAS,AtomicInteger 中调用 unsafe 进行自增操作的源码中的 do-while 循环就是一个自选操作,如果修改过程中遇到其他线程竞争导致没修改成功,就在 while 里死循环,直至修改成功。
6、模拟自旋锁
示例:
public class SpinLock { private AtomicReference<Thread> sign = new AtomicReference<>(); public void lock() { Thread current = Thread.currentThread(); while(!sign.compareAndSet(null,current)){ } } 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(1000); }catch (Exception e){ e.printStackTrace(); }finally { spinLock.unlock(); System.out.println(Thread.currentThread().getName() + "释放了自旋锁"); } } }; Thread thread1 = new Thread(runnable); Thread thread2 = new Thread(runnable); thread1.start(); thread2.start(); } }
7、可中断锁
1、在 java中,synchronized 就不是可中断锁,而 Lock 是可中断锁,因为 tryLock(time) 和 lockInterruptibly() 都能响应中断(上面有详细介绍).
8、如何使用锁
1、缩小同步代码块;
2、尽量不要锁住方法;
3、减少请求锁的次数;
4、锁中尽量不要再包含锁;
5、选择合适的锁类型或合适的工具类。