当前位置: 首页>后端>正文

Java 锁相关问题

Java按照是否对资源加锁分为乐观锁和悲观锁,乐观锁和悲观锁并不是一种真实存在的锁,而是一种设计思想。

乐观锁

乐观锁是一种悲观思想,它总认为资源和数据不会被别人所修改,所以读取不会上锁,但是乐观锁在进行写入的操作的时候会判断当前数据是否被修改过。

乐观锁适合读多写少,并发写的可能性低的情况。

乐观锁实现方案一般有两种:版本号机制和CAS(Compare-and-Swap,即比较并替换)算法实现。

版本号机制采取在写时先读出当前版本号,然后加锁操作,比较跟上一次的版本号,如果一样则更新,如果失败则要重复读-比较-写的操作。

cas是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

悲观锁

悲观锁就是悲观思想,它认为数据很可能被其他人所修改,所以悲观锁在持有资源的数据的时候总会把资源或者数据锁住,这样其他线程想要请求这个资源的时候就会阻塞,知道等悲观锁把资源释放为止。

悲观锁适合写多,遇到并发写的可能性高的情况。

传统的关系型数据里面就用到了悲观锁的机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

Java中的Synchronized和ReentrantLock等独占锁(排他锁)也是一种悲观锁思想,因为Synchronized和ReentrantLock不管是否持有资源,它都会尝试去加锁。

自旋锁的提出背景

由于在多处理器环境中某些资源的有限性,有时需要互斥访问(mutual exclusion),这时候就需要引入锁的概念,只有获取了锁的线程才能够对资源进行访问,由于多线程的核心是CPU的时间分片,所以同一时刻只能有一个线程获取到锁。那么就面临一个问题,没有获取到锁的线程该怎么办?

通常有两种处理方式:

一种是没有获取到锁的线程就一直循环等待判断该资源是否已经释放锁,它不用将线程阻塞起来(NON-BLOCKING),这种叫做自旋锁。

还有一种就是把自己阻塞起来,等待重新调度请求,这种叫做互斥锁

自旋锁(spinlock)

当一个线程尝试去获取一把锁的时候,如果这个锁此时已经被别人占用,那么此线程就无法获取到这把锁,该线程会等待,间隔一段时间后会再次尝试获取。这种采用循环加锁->等待的机制被称为自旋锁。

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

缺点:线程自旋需消耗CPU,如果一直获取不到锁,则线程长时间占用CPU自旋,造成CPU浪费,并且需要设定一个自旋等待最大时间,在最大等待时间内仍未获取得锁,则停止自旋进入阻塞状态。

公平锁

公平锁指的是锁的分配机制是公平的,通常先对锁提出资源请求的线程会先被分配到锁,加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得。

非公平锁

JVM随机就近原则分配锁的机制称为非公平锁,非公平锁实际执行的效率要远超公平锁,所以一般常用非公平锁。

非公平锁性能优于公平锁是因为公平锁需要再多核的情况下维护等待队列

Java中的Synchronized是非公平锁,ReentrantLock默认的lock()方法采用的是非公平锁,可通过构造函数设置该锁是公平还是不公平。

可重入锁(递归锁)

指的是某个线程已经获得某个锁,可以再次获取该锁而不会出现死锁。再次获取锁的时候会判断当前线程是否已经加锁,如果加锁,那么锁的次数+1,释放时加了几次锁,就要释放几次。

Synchronized和Lock都是可重入锁,Synchronized加锁解锁自动完成,Lock加锁解锁手动完成。

独占锁

独占锁模式下,每次只能有一个线程能持有锁。

独占锁有:synchronized,ReentrantLock

共享锁

多个线程可以获取读锁,以共享方式持有。

共享锁有:ReentrantReadWriteLock(允许一个资源可以被多个读操作访问或者一个写操作访问,但两者不能同时进行)。

分段锁

分段锁是一种锁的设计,并不是具体的锁。

分段锁的设计目的是将锁的粒度进一步分化,当操作不需要更新整个数组的时候,就仅仅对数组中的一项进行加锁操作。

在 Java 语言中 CurrentHashMap 底层就用了分段锁,使用Segment,就可以进行并发使用了。

JDK1.6为了提升性能减少获得锁和释放锁所带来的消耗,引入了4中锁的状态:无锁状态、偏向锁、轻量级锁和重量级锁,它会随着多线程的竞争情况逐渐升级,但不能降级。

无锁

无锁状态就是上面讲的乐观锁。

偏向锁(Biased Locking)

是指它会偏向于第一个访问锁的进程,如果在运行过程中,只有一个线程访问加锁的资源,不存在多线程竞争的情况,那么线程是不需要重复获得锁的,这种情况下,就会给线程加一个偏向锁。

轻量级锁

当线程竞争变得比较激烈时,偏向锁就会升级为轻量级锁,轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式等待上一个线程释放锁。

重量级锁(Mutex Lock)

如果线程并发进一步加剧,线程的自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时,轻量级锁就会膨胀为重量级锁,重量级锁会使除了此时拥有锁的线程以外的线程都被阻塞。

升级到重量级锁其实就是互斥锁来了,一个线程拿到锁,其余线程都会处于阻塞等待状态。

在Java中,synchronized关键字内部实现原理就是锁状态的升级过程:无锁->偏向锁->轻量级锁->重量级锁。

synchronized和Lock

Java 锁相关问题,第1张
二者区别

锁优化思路

1.减少锁持有时间,只在有线程安全要求的程序上加锁。

2.减少颗粒度,将大对象(这个对象可能被很多线程访问)拆成小对象,增加并行度,降低锁竞争,这样偏向锁和轻量级锁成功率才会提高,案例concurrentHashMap。

3.锁分离,常见的锁分离就是读写锁ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能。队列从头部取出,从尾部方数据。

4.锁粗话,就是将多个同步块的数量减少,并将单个同步块的作用范围扩大,本质上就是将多次上锁、解锁的请求合并为一次同步请求。


https://www.xamrdz.com/backend/3nn1931791.html

相关文章: