目录
3、可重入锁和非可重入锁,以 ReentrantLock 为例(重点)
一、Lock 简介、地位、作用
1、锁是一种工具,用于控制对共享资源的访问;
2、Lock 和 synchronized,这两个是最常见的锁,他们都可以达到线程安全的目的,但是在使用上和功能上又有较大的不同;
3、Locak 并不是用来代替 synchronized 的,而是当使用 synchronized 不合适或不足以满足要求的时候,来提供高级功能的;
4、Lock 接口最常见的实现类是 ReentrantLock;
5、通常情况下,Lock只允许一个线程来访问这个共享资源。不过有的时候,一些特殊的实现可允许并发访问,比如 ReadWriteLock 里面的 ReadLock;
6、为什么 synchronized 不够用?
- 效率低:锁的释放情况少、试图获得锁时不能设定超时、不能中断一个正在试图获得锁的线程(当进入 synchronized 中的线程,如果发生异常,JVM 会让其释放锁);
- 不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的;
- 无法知道是否成功获取到锁。
二、Lock 方法
1、lock()
- 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()方法:
- 作用是测试此线程是否被中断 ,不清除中断状态。
三、锁
1、锁的分类如图所示:
2.乐观锁 悲观锁
2.1 乐观锁是是什么
认为自己在处理操作时不会有其他线程打扰1,所以不会锁住被操作的对象
在更新时,去对比在我修改的数据期间的数据有没有被其他人修改过,如果没被改变过,就说明真的只有我们自己在操作,就去正常修改数据。
如果数据和一开始拿到的不一样,说明其他人修改了数据,不能用刚才更新过的数据,放弃、报错重试等策略。
乐观锁一般是利用CAS算法实现。
乐观锁的典型就是原子类,并发容器。
2.2悲观锁
- 如果我不锁住这个资源,别人就会来争抢,就会造成数据结果错误,所以每次悲观锁为了确保结果的正确性,会在每次获取并修改数据时,把数据锁住,让别人无法访问该数据,这样就可以确保数据内容万无一失;
- Java 中悲观锁的实现就是 synchronized 和 Lock 相关类。
2.3两种锁的使用场景
1.悲观锁:适合并发写入多的情况,用于临界区吃锁时间比较长的情况,悲观锁可以避免大量无用自旋等消耗。
典型情况:
- 临界区有IO操作。
- 临界区代码复杂,循环量大
- 临界区竞争激烈
2.乐观锁,适合并发写入少,大部分是读取的场景,不加锁的能让读取性能大幅度提高。
3、可重入锁和非可重入锁,以 ReentrantLock 为例(重点)
1)可重入锁
同一个线程没有释放锁的情况下,重复获取自己所持有的锁。
2)非可重入锁
1、非重入锁是直接尝试获取锁,如果已经持有锁,不能获再次取锁;
2、释放锁时也是直接将 status 置为0。
3)可重入锁和非可重入锁区别:
1、可重入锁在使用的时候一般是一个类当中有AB两个方法,而A和B都是有统一的一把锁,当实施A方法的时候就可以获得锁,但在A办法的所还没有全部释放的时候也可以直接使用B方法,而在这个时候也是可以获得这个锁的。
2、不可重入锁也是指的是A和B两个方法,A和B可以获得统一的一把锁,而在A方法还没有释放的时候是没有办法使用B方法的,也就是说必须要等A释放之后才可以使用B方法。
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、共享锁和排它锁
1、什么是共享锁和排它锁
1)共享锁
-
- 共享锁,又称为读锁,获得共享锁之后,可以查看但无法修改和删除数据,其他线程此时也可以获取到共享锁,也可以查看但无法修改和删除数据。
2)排它锁
-
- 排它锁,又称独占锁、独享锁。
3)共享锁和排它锁的典型是读写锁 ReentrantReadWriteLock,其中读锁是共享锁,写锁是独享锁。
2、读写锁的作用
1)在没有读写锁之前,我们假设使用 ReentrantLock,那么虽然我们保证了线程安全,但是也浪费了一定的资源:多个读操作同时进行,并没有线程安全问题;
2)在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,提高了程序的执行效率。
3、读写锁的规则
1)多个线程只申请读锁,都可以申请到;
2)如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁;
3)如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁;
4)一句话总结:要么是一个或多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现(读读共享、其他都互斥(写写互斥、读写互斥、写读互斥))。
6.锁状态
在synchronized锁最初的实现方式是“阻塞或唤醒一个线程需要操作系统切换CPU状态来完成,这种状态需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行时间还长”,这种方式就是synchronized实现同步最初的方式,这也是当初开发者诟病的地方,这也是在JDK6以前synchronized效率低下的原因,JDK6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。
所以目前锁状态一种有四种,从级别由低到高依次是:无锁、偏向锁,轻量级锁,重量级锁,锁状态只能升级,不能降级。
6.1无锁
无锁是指没有对资源进行锁定,所有线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。无锁的特点是修改操作会在循环内进行,线程会不断尝试修改共享资源,如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果多个线程修改同一个值,必定会有一个线程修改成功,而其他修改失败的线程就会不断重试,直到修改成功。
6.2偏向锁
偏向锁是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程竞争时,那么该线程在后序访问时便会自动获得锁,从而降低获得锁带来的消耗,提高性能。
6.3轻量级锁
加锁机制尽可能不使用 mutex, 而是尽量在用户态代码完成. 实在搞不定了, 再使用 mutex.
- 少量的内核态用户态切换.
- 不太容易引发线程调度.
6.4重量级锁
加锁机制重度依赖了 OS 提供了 mutex
- 大量的内核态用户态切换
- 很容易引发线程的调度