Java面试--Java多线程并发(三)长文全解析 Java锁

Java锁

1.乐观锁

乐观锁是认为读多写少,遇到并发的可能性低,所以去拿数据的时候都认为别人不会修改,所以不会上锁。

但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁的操作。比较和上一次的版本号,如果一样就更新,不一样就重复读--比较--写。

java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

2.悲观锁

悲观锁就是悲观思想,认为写多读少,遇到并发的可能性高,每次去拿数据的时候认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁,java种的悲观锁就是synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如ReetrantLock

3.自旋锁

如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,他们只需要等一等(自旋),等持有锁的线程释放锁后即可获得锁,这样避免用户线程和内核的切换消耗。

线程自旋是需要消耗cpu的,说白了让CPU在做无用功。如果一直获取不到,那么线程也不能一直占用cpu,所以需要设定一个自选等待最大时间

如果持有锁的线程执行时间超过自旋等待最大时间仍没有释放锁,就会导致其他争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

自旋锁的优缺点:

自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换/

但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu 做无用功,占着XX 不XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cpu 的线程又不能获取到cpu,造成cpu 的浪费。所以这种情况下我们要关闭自旋锁;

自旋锁时间阈值:

自旋锁的目的是为了占着CPU资源不释放,等到获取锁立即进行处理,但是如何去选择自旋的执行时间,如果自旋执行时间太长,会有大量的线程处于自旋状态而占用CPU,所以时间阈值很重要。

1.5是写死的,1.6引入适用性自旋锁。

1.7后去掉参数,由JVM控制。

4.Synchronized同步

synchronized它可以把任意一个非null的对象当作锁,它属于独占式的悲观锁,同时属于可重入锁。

Synchronized作用范围

1.作用于方法时,锁住的是对象的实例。

2.当作用于静态方法时,锁住的是Class实例,因为Class的相关数据存储在元数据,静态方法锁相当于类的一个全局锁。

3.sychronized作用于一个对象实例时,锁住的是所有以该对象为锁的代码块,它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

 

5.ReentrantLock

ReentrantLock继承接口Lock并实现了接口中定义的方法,他是一种可重入锁,除了能完成synchronied所能完成的所有工作外,还提供了诸如可响应中断锁,可轮询锁请求,定时锁等避免多线程死锁的方法。

Lock接口的主要方法

1.void lock() 执行此方法时,如果锁处于空闲状态,当前线程将获取到锁。

2. boolean trylock() 如果锁可用,则获取锁,并立即返回true,否则返回false。

3. void unlock() 执行此方法时,当前线程将释放持有的锁。

ReentrantLock与Synchronized

1. ReentrantLock通过方法lock与unlock来进行加锁与解锁操作,与synchronized会被JVM自动解锁机制不同,ReentrantLock加锁后需要手动进行解锁。 finally进行解锁。

2. ReentrantLock相比Synchronized的优势是可中断、公平锁、多个锁。这种情况下需要使用ReentrantLock

6.Semaphore信号量

Semaphore是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程获取许可信号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。Semaphore可以用来构建一些对象池,资源池之类的,比如数据库连接池。

Semaphore与ReentrantLock:

Semaphore基本能完成ReentrantLock的所有工作,使用方法也与之类似,通过acquire()与release()方法来获得和释放临界资源,经实测,Semaphore.acquire()方法默认为可响应中断锁,ReentrantLock.lockInterruptibly()作用效果一致,也就是说在等待临界资源的过程中可以被Thread.interrupt()方法中断。

,Semaphore 也实现了可轮询的锁请求与定时锁的功能

7.AtomicInteger

在多线程程序中,诸如++i或i++等运算不具有原子性,是不安全的线程操作之一。

Synchronized将该操作变成一个原子操作,而AtomicInteger的性能是ReentrantLock的好几倍。

8.可重入锁

本文里面讲的是广义上的可重入锁,而不是单指JAVA 下的ReentrantLock。可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。在JAVA 环境下 ReentrantLock 和synchronized 都是 可重入锁。

 

9.公平锁与非公平锁

公平锁:加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得。

非公平锁:加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待。

1.非公平锁性能比公平锁高5-10倍,公平锁需要在多核的情况下维护一个队列。

2.Java中的Synchronized是非公平锁,ReentrantLock的lock是非公平锁。

10.ReadWriteLock读写锁

为了提高性能, java提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制。

jvm自己控制的

11.共享锁和独占锁

ReentrantLock 就是以独占方式实现的互斥锁。独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,

共享锁则允许多个线程同时获取锁,并发访问 共享资源,如:ReadWriteLock。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。

12.重量级锁

Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock 来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized 效率低的原因。因此,这种依赖于操作系统Mutex Lock 所实现的锁我们称之为“重量级锁”。JDK 中对Synchronized 做的种种优化,其核心都是为了减少这种重量级锁的使用。JDK1.6 以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。

13.轻量级锁

锁升级

随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。

轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。

14.偏向锁

偏向锁的目的是某个线程获得锁之后,效除这个线程锁冲入(CAS)的开销,看起来让这个线程得到了偏护。

引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令。

轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁是在只有一个线程执行同步块时进一步提高性能。

15.分段锁

分段锁也并非是一种实际的锁,而是一种思想。ConcurrentHashMap

16.锁优化

减少锁持有时间
只用在有线程安全要求的程序上加锁
减小锁粒度
将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。最最典型的减小锁粒度的案例就是ConcurrentHashMap。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值