Lock接口
锁是一种工具、控制对共享资源的访问
Lock和synchronized,是最常见的锁,都可以达到线程安全的目的,功能常见不同
Lock不是来代替synchronized的,而是当synchronzed不够用的时候,提供更高级的功能
最常用的类,就是ReentrantLock
通常是只允许一个访问,但是有并发访问的,ReadWriteLock中的ReadLock
1、效率低,
锁的释放情况少,只能执行完了释放,或者是异常,jvm来释放,比如想中途释放锁,或者超时,拿不到就不加锁了
2、不够灵活
加锁和释放时机太单一、不能分读写锁
3、不知道是否成功获取到了锁
对于我们开发来说,不知道到底获取到了锁没有
Lock、tryLock、tryLock(long time,TimeUnit unit)、lockInterruptibly()
不会有jvm帮忙在异常的时候释放锁,
因此,try finally里中,要释放
问题:不能被中断,一旦陷入死锁、lock就会陷入永久等待,所有就有了tryLock
/**
* @Author:Joseph
* Lock不会有jvm帮忙释放锁,需要finally里释放
*/
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带参数的,会等待,超时就放弃
这个方法,就是可以通过中断来丢掉锁,当在获取锁过程中,收到中断信号、或者在加上锁过程中收到中断信号,
都会进入catch逻辑,然后unlock锁
看过我JMM的伙伴肯定知道,lock和synchronized都是可以保证可加性的,都有近朱者赤的特性,
回顾下,因为加锁,线程只能先后执行,后拿到锁的线程,一定是可以看到前面的线程做的事情
悲观锁,是独占的,其他线程想要获取,必须等待,阻塞唤醒带来性能劣势,用户态、内核态切换等待
乐观锁不需要把线程挂起!
永久阻塞:持有锁的线程,陷入死锁等永久阻塞,那么其他等待的会永久阻塞
优先级反转:优先级低的线程,拿到锁执行很慢的话,优先级高的线程就会拿不到!!!出现反转的情况
悲观锁:任务这个资源,肯定会有人来抢,要锁住这个资源,比如synchronized lock
线程1拿到,线程2等待,
乐观锁:认为这个资源,不会有其他资源来干扰,对这个资源不会锁住,但是为了保证安全,要去检查有没有修改过,不一样的话,就会知道这个数据修改过了,一般通过CAS算法实现
乐观锁例子
原子类、并发容器
/**
* @Author:Joseph
* Lock不会有jvm帮忙释放锁,需要finally里释放
*/
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();
}
}
这是悲观锁,需要提前加锁处理
再看下,Atomic类,与synchronized a++原子性的保证
public class PessimismOptimismLock {
int a;
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();
}
public synchronized void testMethod(){
a++;
}
}
还有、git,就是通过版本号、乐观锁,提交的,悲观锁,一个人修改代码,其他都阻塞,直接!倒闭了哈哈哈
当然,乐观锁不是万金油,比如任务执行很长,那么等待的开销固定,就是阻塞,而乐观锁,会一直去尝试修改,自旋等操作,消耗cpu资源
并发写入多、执行时间长的操作,适合使用悲观锁,避免无用的自旋cpu开销,比如io操作,
并发写入少,大部分是读取场景,不加锁,使性能更高
前期肯定是悲观锁开销大,但是随着后面自旋,乐观锁会更大,所以!要注意使用场景
以ReentrantLock为例
用法演示:
**
* @Author:Joseph
* 演示多线程预定电影院座位
*/
public class CinemaBookSeat {
private static ReentrantLock lock = new ReentrantLock();
private static void bookSeat(){
lock.lock();
try {
try {
System.out.println(Thread.currentThread().getName()+"开始预定座位");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"完成预定座位");
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
new Thread(()->bookSeat()).start();
new Thread(()->bookSeat()).start();
new Thread(()->bookSeat()).start();
new Thread(()->bookSeat()).start();
}
}
就是同个线程,拿到这个锁,还没释放,想要再次执行逻辑,再拿到锁,就是可重入锁
ReentrantLock就是可重入锁
/**
* @Author:Joseph
* 验证可重入
*/
public class GetHoldCount {
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();
private static void accessResource(){
System.out.println("已经对资源进行了处理");
lock.lock();
try {
//要让他处理5次,业务需求
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();
}
}
ReentrantLock如何实现可重入锁的
依靠的AQS,这是可重入锁的逻辑,可能看的有些处理,我将在AQS的文章中,细致的讲,就能看懂了,这里先认识一下
再点下两个方法,调试可能会用到
为什么要非公平!!!
公平就是按照线程请求的顺序分配,非公平指的是,在一些时机,可以插队
什么适合可插队?
线程从阻塞到唤醒,是有时间的,通过非公平锁,可以让”清醒“的线程,也就是运行的线程,利用这个时间,
比如A B C A持有锁,B等待,准备去拿锁,A释放了,B正在唤醒,这个时间,清醒的C线程,运行完了,又不影响B的执行,!双赢局面
当ReentrantLock传入参数的时候true的时候,就是公平锁,false是非公平锁
注意》》》》LLL:::::<<<<<<>>>>>
tryLock是大恶霸,一旦有线程释放锁,那么正在执行tryLock的线程就能获取到,有优先权,即使在等待队列里
代码上的实现:
tryAcqure方法,尝试获取锁。
公平锁会判断队列里有没有元素,而非公平锁不会去看队列有没有元素,直接去获取锁
这个玩意儿,在数据库中,innoDB中,我在事物中有讲,mysql专栏里,可以看哈,
如果没读写锁,那么读的操作和写的操作,都会加锁,而读操作,只读的话,往往是安全的,这就诞生了读写锁
也就是共享锁 、 排他锁 S锁 X锁
排他锁也叫独占锁
关系,总结就是读读共享,其他都是互斥,你想想,读操作,并发是没问题的,但是其他,比如读写,写写,都要保证安全
java代码中,就是通过ReentrantReadWriteLock实现读写锁
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()+"得到了读锁,正在读取");
try {
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()+"得到了写锁,正在写入");
try {
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();
}
}
现在我们来升华一下思考
读写和写锁的怎么交互的?
插队:指的是从等待队列中选哪个来进来,写线程肯定要排队的,而读操作,是可以插队的,但是对其他线程不公平的
升降级问题:就是锁持有的时候,读锁能升级为写锁 吗 ,写锁能降级为读锁吗?
首先,RenntrantLock是可传参数的,true的话,不会有插队的问题,因为本身公平,按照策略先后顺序。
但是传入false的时候,读线程正在获取锁,比如1,2线程写线程,等待,3线程读线程,那么是可以插队的
但是:插队会有问题,
两种策略:让3插队,效率很高,但是,这个策略,哎,后面还想插队,容易出现饥饿,2这个写锁,等死
策略2:避免饥饿,renntrantlock非公平锁,不可以插队
如何避免: 就是不允许插队,哈哈哈,对不起没有更好的方法,但是通过上面的分析,如果队列头节点也是个读锁,不插写锁的队,但是可以去插读锁呀!这个是允许的。
也就是说,reentrantLock禁止读锁插队!
但是,策略2,写锁是可以随时插队
总结下吧:公平锁,不允许插队!
非公平锁:写锁可以随时插队
读锁仅仅在头节点是获取读锁的时候插队,也就是说,插队,但是不能插写锁的队,避免锁饥饿
可以插队列头部是读锁的队
源码看看::::
RenntrantLock下有这两个内部类,分别对应公平和非公平
公平锁只会看有没有人排队,有了,那就排毒,这两个方法,对应读阻塞、写阻塞,也就是,只有队列中,你前面有元素,那就等着
非公平锁,写入,直接返回fasle,不用管,直接插队
读锁,回去看队列是否是有写,有的话,返回true,不让插队,阻塞等着。’
这就是源码中,读者应该阻塞、写着应该阻塞的实现,通过这个完成插入逻辑的实现。
升降级,指的是,读锁升级为写锁,写锁降级为读锁。先说结论,读写锁允许锁的降级,不允许升级
为什么不允许读锁升级为写锁???
死锁,假设两个读锁都在读,都等对方释放锁,然后自己升级,就会出现写锁问题,所以ReentrantLockReadWriteLock不允许
适合读多写少的场景,提高效率!
阻塞唤醒需要操作系统切换cpu状态来完成,自旋操作的话,开销比阻塞唤醒小多的情况下,就适用于自旋锁了
适用于同步资源锁定时间很短,没必要去切换线程状态的情况
因为如果锁的占用时间,自旋的线程只会浪费cpu资源,随着时间增长,而增加
原子类中的各种操作,基本都是通过自旋锁+CAS操作实现的原子性,对资源保护,达到目的
手写自旋锁~
自旋锁有个要求,利用原子操作,把想要的值用cas换上去,通过cas去做自旋的状态判断
下面是一个例子
public class SpinLock {
private AtomicReference<Thread> sign = new AtomicReference<>();
public void lock(){
Thread current = Thread.currentThread();
while (!sign.compareAndSet(null,current)){
System.out.println("自旋获取失败,再次尝试");
}
}
public void unlock(){
Thread current = Thread.currentThread();
sign.compareAndSet(current,null);
}
public static void main(String[] args) throws InterruptedException {
SpinLock spinLock = new SpinLock();
Runnable runnable = ()->{
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 thread1 = new Thread(runnable, "线程1");
Thread thread2 = new Thread(runnable, "线程2");
thread1.start();
thread2.start();
}
}
自旋锁会有不断的循环,开销大,一般适用于多核服务器,并发布不是很高的情况下,比阻塞锁的效率高
记住,阻塞锁,并不是差,只是在特定情况下的适合,用自旋锁会更快而已,比如并发不是很高的情况下,以及临界区短,适用于自旋锁
临界区长:线程拿锁的时间长,很久再释放
不可中断锁:synchronized
可中断锁:Lock,比如tryLock,lockInterruptibly
就是可不可中断,在前面已经演示过了,执行期间可被中断
自旋锁就是临界区短的操作用的,刚才说了,通过自旋锁代替阻塞唤醒过程,减小开销,但是出个问题,时间长了,那开销可就大了,于是jvm搞了一个自适应,也就是说出现问题,会从乐观锁变为阻塞锁
一些场景下不必要加锁,不会有并发问题,jvm分析无需加锁
比如,代码在方法内部,不会有外人访问这个东西
锁粒度问题,但是粒度太小,密集的加了很多锁,不如加个范围大的锁,一个锁搞定,效率更高
1、缩小同步代码块
2、使用同步代码块,而不是锁住整个方法
3、减少请求锁的次数
比如,日志框架,10个线程都想打印日志,可以把多个线程汇聚到一起,执行操作,加一个锁
4、避免人为制造热点
比如,hashMap,size方法,会遍历操作,我们可以自己维护一个,自己计数,不认为用size
5、锁中不要再包含锁,容易出现死锁
6、选择适合类型的锁
比如读写锁,使用乐观锁(原子类是java中的乐观锁 )
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有