有了锁,再也不用害怕多线程不安全了

本文详细介绍了Java中锁的分类,包括乐观锁、悲观锁、自旋锁、Synthetic与ReentrantLock,并探讨了锁的升级过程,如偏向锁、轻量级锁和重量级锁。同时,对比了ReentrantLock与Synthetic的区别,强调了锁优化的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  引入了线程之后,线程安全问题随之而来,所以就有了锁的概念,引入锁之后又有了锁的升级,锁的分类,下面就让我们详细了解锁;

一、锁的分类

1、乐观锁

乐观锁即表达一种乐观思想,认为读多写少,遇到并发写的情况可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作。java中的乐观锁基本都是通过CAS实现操作实现的,CAS是一种更新的原子操作(比较当前值跟传入值是否一样,一样则更新,否则失败)。

2、悲观锁

有了乐观锁就会有悲观锁,自然悲观锁表达的就是悲观思想,认为写多,遇到并发性的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试CAS乐观锁去获取到锁,获取不到,才会转换为悲观锁,如RetreenLock。

3、自旋锁

自旋锁的原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间切换进入阻塞挂起状态,他们只需要等一等(自旋),等持有锁的线程释放后立即获取锁,这样就避免用户线程和内核的切换的消耗。
线程自旋是需要消耗CPU的,说白了就是让CPU在做无用功,如果一直获取不到锁,那线程也不能一直占用CPU自旋做无用功,所以需要设定一个自旋等待的最大时间。如果持有锁的线程执行的时间超过自旋等待的最大时间仍没有释放锁,就会导致其他争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程就会停止自旋进入阻塞状态。
3.1自旋锁的优缺点
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁的时间非常短的代码块来说性能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换。

4、Synthronized同步锁

一种重量级锁,是java提供的原子性内置锁,这种使用者看不到的锁也称为监视器锁,使用synthronized之后,会在编译之后在同步代码块前后加上monitorenter和moniterexit字节码指令,它依赖操作系统底层互斥锁实现。主要的作用就是实现原子性操作和解决共享变量的内存可见性问题。是一个排他锁,当一个线程拿到锁之后,其他线程需要等待当前线程释放锁之后才能获取。线程在被阻塞或者被唤醒时会从用户态切换为内核态,这种转换对性能的消耗特别大。
4.1Synthronized作用范围
(1)作用于方法时,锁住的是对象的实例(this);
(2)当作用于静态方法时,锁住的Class实例,又因为Class的相关数据存储在永久代PermGen(jdk1.8则是metaspace),永久代是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
(3)synthronized作用于一个对象实例时,锁住的是所有以对象为锁的代码块。它有多个队列,当多个队列一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

5、ReentrantLock

ReentrantLock继承接口Lock并实现了接口中定义的方法,他是一种可重入锁,除了能完成synthronized所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。
5.1Lock接口的主要方法:
(1)void Lock():执行此方法时,如果锁处于空闲状态,当前线程将获取到锁。相反,如果锁已经被其他线程持有,将禁用当前线程,直到当前线程,直到当前线程获取到锁。
(2)boolean tryLock():如果锁可用,则获取锁,并立即返回true,否则返回false。该方法和lock()的区别在于,tryLock()只是“试图”获取锁,如果锁不可用,不会导致当前线程被禁用,当前线程仍然继续往下执行代码。而lock()方法则是一定要获取到锁,如果锁不可用,就一直等待,在未获得锁之前,当前线程并不继续向下执行。
(3)void unLock():执行此方法时,当前线程将释放持有的锁。锁只能由持有者释放,如果线程并不持有锁,却执行该方法,可能导致异常的发生。
(4)Condition newCondition():条件对象,获取等待等待通知组件。该组件和当前的锁绑定,当前线程只有获取到了锁,才能调用该组件的await()方法,而调用后,当前线程将释放锁。
(5)getHoldCount():查询当前线程保持次锁的次数,也就是执行此线程执行lock方法的次数。
5.2 非公平锁
JVM按随机、就近原则分配锁的机制则称为不公平锁,ReentrantLock在构造函数中提供了是否公平锁的初始化方式,默认为非公平锁。非公平锁实际执行的效率要远远超出公平锁,除非程序有特殊需要,否则最常用非公平锁的分配机制。
5.3 公平锁
公平锁指的是锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁,ReentrantLock在构造函数中提供了是否公平锁的初始化方式来定义公平锁。

6、ReentrantLock与Synthronized的区别

6.1ReentrantLock通过方法lock()与unlock()来进行加锁与解锁操作,与synthronized会被JVM自动解锁机制不同,ReentrantLock加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用ReentrantLock必须在family控制块中进行解锁操作。
6.2ReentrantLock相比synthronized的优势是可中断、公平锁、多个锁,在这种情况下需要使用ReentrantLock。

二、锁的升级

锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。
1、偏向锁
当一个线程访问同步代码块,锁记录里会存储偏向锁的线程ID,之后这个线程如果再进入同步代码块,它就不需要CAS来加锁或者解锁,如果后续没有其他线程来竞争锁,只有锁的线程就永远不需要进行同步,但如果有其他线程来竞争偏向锁,该线程就会释放锁。
2、轻量级锁:线程通过CAS方式获取锁,如果更新成功对象头就会标记为轻量级锁,如果失败,就会通过自旋来获取锁。
3、重量级锁
Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又
是依赖于底层的操作系统的 Mutex Lock 来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么
Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为
“重量级锁”。JDK 中对 Synchronized 做的种种优化,其核心都是为了减少这种重量级锁的使用。
JDK1.6 以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和
“偏向锁”。
关于锁就了解到这,继续期待后续锁的优化,在加锁的基础上,减小锁粒度,减少持有的时间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值