0
点赞
收藏
分享

微信扫一扫

Java中的锁​


1、锁的作用和分类

JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销 。

  1. 作用:java中的锁主要用于保障多线程并发情况下的数据一致性,线程必须先获取到锁才能进行操作
  2. 分类:
  3. 乐观锁和悲观锁
  4. 公平锁和非公平锁:获取资源的公平性
  5. 共享锁和排他锁:是否共享资源
  6. 偏向锁、轻量级锁和重量级锁:锁的状态角度描述
  7. 自旋锁:JVM中设计的以更快地使用
  8. 处理和同步主要是通过锁机制:
  9. 但是锁机制有两个层面:java代码层次上的同步锁 和 数据库层面的锁

2、乐观锁和悲观锁

  1. 乐观锁:采用乐观的思想处理数据,每次读取数据时都会认为别人不会修改该数据,所以不会上锁
  2. 但是:在更新时会判断在此期间别人有没有更新该数据,使用版本号机制,通常采用在写时先读出当前版本号。具体过程:比较当前版本号和上一次的版本号,如果一致则更新,否则重复进行读、比较、写操作(自旋?)。
  3. java中乐观锁基于CAS实现,CAS是一种单个原子操作,在对数据更新之前先比较当前值和传入的值是否一样,一样则更新,如果失败则表示发生冲突,那么就应该有相应的重试逻辑或放弃操作
  4. 实现方式:
  5. 使用版本号标识确定读到的是数据与提交的数据是否一致。提交后修改版本标识,不一致可以采取丢弃或再次尝试的策略
  6. 多个线程尝试使用CAS更新同一个变量时,只会有一个线程能更新变量的值(并不是说只有一个线程能拿到该值,而是只有一个能更新成功;其他线程也可以拿到该值,但是比较之后会更新失败,因为其他线程更新了该值,所以会有ABA问题),而其他线程都失败,失败的线程不会挂起,而是被告知失败,可以再次尝试
  7. 悲观锁:采用悲观思想处理数据,每次读取数据都会认为别人会修改数据,所以每次都会加锁,其他线程将被阻塞
  8. 悲观锁基于AQS实现,该框架的锁会尝试以CAS乐观锁获取锁,如果获取不到会转为悲观锁
  9. 使用
  10. 悲观锁适用于写操作多的场景,先加锁可以保证写操作时数据正确。
  11. 乐观锁适合读操作多的场景,不加锁可以使读操作的性能大幅提升。
  12. 问题:
  13. 乐观锁到底有没有加锁?
  14. java中的乐观锁和悲观锁和mysql中的乐观锁和悲观锁有区别吗?

3、CAS(Compare And Swap)

  1. CAS 是指 比较与交换;是一种无锁算法。在不适用锁(没有线程阻塞)的情况下实现线程之间的变量同步
  2. CAS算法 涉及到三个操作数:内存地址V、旧的预期值A、要修改的新值B
  3. 只有变量的预期值A和内存地址V当中的实际值相同 ,才会将内存地址V对应的值改为B
  4. 通俗来说就是:V是当前实际要被修改的值,A是当前线程获取到的原来的V的值,B是要更改之后的值
  5. 本质就是比较当前变量的值是否和原来的一致,不一致就不能更改
  6. 原理:
  7. 在内存地址V当中,存储着值为10的变量。
  8. 此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。
  9. 在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11
  10. 线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。
  11. 线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。
  12. 这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。线程1可以进行SWAP,把地址V的值替换为B,也就是12
  13. CAS是原子操作?涉及一次读和一次写操作,底层有指令保证读和写是原子的
  14. 但是不能保证有序性和可见性:需要lock指令
  15. CAS不涉及内核态和用户态的切换,用户态到内核态需要通过调用接口,CAS只是CPU的
  16. CAS的缺点
  17. CPU开销较大:在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,造成自旋,会给CPU带来很大的压力。
  18. 不能保证多个的原子性:CAS机制所保证的只是一个共享变量的原子性操作而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用独占锁了。
  19. ABA问题
  20. CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。
  21. 这个问题对银行来说是必须记录的
  22. ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。

JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。

  1. 前面我们说到java java.util.concurrent包下的原子类的自增操作是利用了CAS思想。我们查看AtomicInteger的自增函数incrementAndGet(),发现自增函数底层调用的是unsafe.getAndAddInt()。进一步查看unsafe.getAndAddInt() 的实现。
  2. 从下面中的源码中,我们可以看到getAndAddInt()循环获取给定对象O中的偏移量处的值v,然后判断内存值是否等于v, 如果相等则将内存值设置为v+delta,否则返回false,继续循环进行充实,知道设置成功才能推出循环,并且将旧值返回。整个“比较和更新”操作封装在compareAndSwapInt(),在JNI里借助于一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。

Java中的锁​_乐观锁


https://blog.csdn.net/qq_35462323/article/details/106931795

CAS缺点:

ABA 问题是乐观锁一个常见的问题

1 ABA 问题

如果一个变量"ABA"问题。

JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是 否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

2 循环时间长开销大

自旋

CPU 带来非常大的执行开销。 如果

效率会有一定的提升,pause 指令有两个作用,第一它可以延迟流水线执行指

令(de-pipeline),使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体

实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时

候因内存顺序冲突(memory order violation)而引起 CPU 流水线被清空

(CPU pipeline flush),从而提高 CPU 的执行效率。

3 只能保证一个共享变量的原子操作

CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是

可以把多个变量放在一个对象里来进行

4、AQS(AbstractQueueSynchronizer)

  1. 抽象的队列同步器;AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch...。

Java中的锁​_乐观锁_02


  1. AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
  2. AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。

https://blog.csdn.net/striveb/article/details/86761900

https://www.cnblogs.com/waterystone/p/4920797.html

5、公平锁与非公平锁

  1. 公平锁:指在分配锁前检查是否有线程在排队等待获取该锁,优先将锁分配给排队时间最长的线程
  2. 非公平锁:指在分配锁时不考虑线程排队情况,直接尝试获取锁,获取不到锁就再排到队尾等待
  3. 效率:公平锁需要在多核情况下维护一个锁线程等待队列,基于该队列进行锁分配,因此效率比非公平锁低很多
  4. 举例:synchronized和ReentrantLock默认的lock方法是非公平锁

6、共享锁和排他锁

  1. 共享锁:允许多个线程同时获取该锁,并发访问共享资源
  2. ReentrantReadLock的读锁是共享锁的实现
  3. 因为并发读线程并不会影响数据的一致性,因此共享锁采用了乐观的加锁策略,允许多个执行读操作的线程同时访问共享资源
  4. 排他锁:也叫互斥锁,每次只允许有一个线程独占该锁,
  5. ReentrantLock就是排他锁
  6. 排他锁是悲观锁的加锁策略,同一时刻只允许一个线程读取锁资源,限制了该操作的并发性

7、自旋锁

  1. 自旋锁:认为如果持有锁的线程能在很短的时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞、挂起状态,而是在synchronized的边界做忙循环,这就是自旋;如果做了多次循环发现还没获得锁再阻塞。这样避免了用户线程在内核态的切换上导致锁时间消耗
  2. 优点:减少了CPU的上下文切换,对于占用锁时间非常短或锁竞争不激烈的代码块来说性能很高
  3. 缺点:在持有锁的线程长时间占用锁或竞争过于激烈时,线程会长时间自旋浪费CPU资源,有复杂锁依赖的情况不适合使用自旋锁

自适应性自旋锁:

8、读写锁

  1. Lock接口 提供的琐是普通锁,为了提高性能,java提供了读写锁,读写锁分为读锁和写锁
  2. 读锁:系统要求共享数据可以同时支持很多线程并发读,但不能支持很多线程并发写,读锁就能大大提高效率
  3. 写锁:系统要求共享数据在同一时刻只能有一个线程在写,且写的过程不能读,则需要使用写锁
  4. 互斥:读锁之间不互斥,读锁与写锁、写锁之间都互斥
  5. 提高juc的locks包中ReadWriteLock的实现类ReentrantReadWriteLock的readLock()和writeLock()来分别获取读锁和写锁

9、锁的状态

  1. 锁的状态有:无锁、偏向锁、轻量级锁、重量级锁
  2. 重量级锁:是基于操作系统互斥实现的,会导致进程在用户和内核态之间来回切换,开销大
  3. synchronized 内部基于监视器实现,监视器基于底层操作系统实现,因此属于重量级锁,运行效率不高。
  4. JDK1.6 后为了减少 获取锁和释放锁带来的性能消耗,引入了轻量级锁和偏向锁
  5. 轻量级锁:是相对于重量级锁来说,核心设计是在没有多线程竞争的前提下,减少重量级锁的使用来提高性能
  6. 适用于线程交替执行同步代码块的情况;
  7. 如果同一时刻有多线程访问同一个锁,会导致轻量级锁膨胀为重量级锁
  8. 偏向锁用于在某个线程获取某个锁后,消除这个线程锁重入的开销,看起来似乎是这个线程得到了锁的偏袒。
  9. 偏向锁的主要目的是在同一个线程多次获取某个锁的情况下尽量减少轻量级锁的执行路径:
  10. 因为轻量级锁需要多次
  11. 而偏向锁只需要切换
  12. 出现多线程竞争锁时(轻量级锁会膨胀为重量级锁),JVM会自动撤销偏向锁
  13. 偏向级锁是进一步提高轻量级锁性能的
  14. 锁的升级
  15. 随着锁竞争越来越激烈,锁可能会从偏向锁升级到轻量级锁再到重量级锁
  16. 但是java中只会单向升级不会降级

10、如何进行锁优化?

  1. 减少锁持有的时间:只在有线程安全要求的程序上加锁来尽量减少同步代码块对锁的持有时间
  2. 减小锁粒度:将单个耗时较多的锁操作拆分为多个耗时较少的锁操作来增加锁的并行度,减少同一个锁上的竞争。
  3. 在减少锁的竞争后,偏向锁。轻量级锁的使用率才会提高:
  4. 如:ConcurrentHashMap中的分段锁
  5. 读分离:指根据不同的应用场景将锁的功能进行分离以应对不同的变化
  6. 最常见的锁分离思想:读写锁,既保证安全又提高性能
  7. 锁粗化:指为保障性能,会要求尽可能将锁的操作细化以减少线程持有时间,但如果锁分的太细反而会影响性能;这种情况建议将关联性强的锁集中处理
  8. 锁消除:注意代码规范,消除不必要锁
举报

相关推荐

0 条评论