4-9-java锁

文章目录
  1. 1. java锁
    1. 1.1. 锁的分类
      1. 1.1.1. 1.乐观锁和悲观锁
      2. 1.1.2. 2.独享锁和共享锁
      3. 1.1.3. 3.互斥锁和读写锁
      4. 1.1.4. 4.可重入锁和不可重入
      5. 1.1.5. 5. 公平锁和非公平锁
      6. 1.1.6. 6.锁的设计:分段锁,自旋锁
      7. 1.1.7. 7.锁的状态:无锁->偏向锁->轻量级锁->重量级锁
    2. 1.2. 1.乐观锁(读多写少,对比版本,cas, AQS)
      1. 1.2.1. CAS 是一种更新的原子操作(java乐观锁通过cas操作实现)
      2. 1.2.2. AQS(AbstractQueuedSynchronizer)为抽象队列同步器
    3. 1.3. 2.悲观锁(写多,先上锁,synchronized)
    4. 1.4. 3.自旋锁(适合占锁短时间)
    5. 1.5. 4.synchronized同步锁(重量级)
      1. 1.5.1. 作用范围
      2. 1.5.2. 原理(标记monitor对象)
      3. 1.5.3. Jdk6后优化了(在对象头标记,不需要操作系统)
      4. 1.5.4. 锁膨胀
    6. 1.6. 5. reentrantlock(可终端,公平,多个锁)
      1. 1.6.1. 可设公平锁
      2. 1.6.2. synchronized与reentrantlock
      3. 1.6.3. Condition类和Object类锁方法区别
      4. 1.6.4. tryLock和lock和lockInterruptibly的区别
    7. 1.7. 6.semaphore信号量
      1. 1.7.1. 和reentrantlock
    8. 1.8. 7.atomic*原子类
    9. 1.9. 8.可重入锁
    10. 1.10. 9.公平锁与非公平锁
    11. 1.11. 10.readwritelock读写锁
    12. 1.12. 11.共享锁和独占锁
      1. 1.12.1. 独占锁就是每次都只有一个线程运行,例如ReentrantLock,synchronized
      2. 1.12.2. 共享锁就是同时可以多个线程运行,如Semaphore、CountDownLatch、ReentrantReadWriteLock
    13. 1.13. 12.重量级锁(monitor本质是操作系统 Mutex Lock实现的锁)
    14. 1.14. 13.轻量级锁(交替执行)
      1. 1.14.1. 原理
      2. 1.14.2. 交替执行(否则锁膨胀)
    15. 1.15. 14.偏向锁(只有一个线程执行同步块)
    16. 1.16. 15.分段锁(concurrentHashMap)
    17. 1.17. 16.锁优化

java锁

锁的分类

1.乐观锁和悲观锁

2.独享锁和共享锁

3.互斥锁和读写锁

4.可重入锁和不可重入

5. 公平锁和非公平锁

6.锁的设计:分段锁,自旋锁

7.锁的状态:无锁->偏向锁->轻量级锁->重量级锁

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。

轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

1.乐观锁(读多写少,对比版本,cas, AQS)

即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。

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

CAS 是一种更新的原子操作(java乐观锁通过cas操作实现)

AQS(AbstractQueuedSynchronizer)为抽象队列同步器

在AQS中的锁类型有两种:分别是Exclusive(独占锁)和Share(共享锁)。

  1. 独占锁就是每次都只有一个线程运行,例如ReentrantLock。

  2. 共享锁就是同时可以多个线程运行,如Semaphore、CountDownLatch、ReentrantReadWriteLock。

AQS管理一个关于状态信息的单一整数,该整数可以表现任何状态。比如, Semaphore 用它来表现剩余的许可数,ReentrantLock 用它来表现拥有它的线程已经请求了多少次锁;FutureTask 用它来表现任务的状态(尚未开始、运行、完成和取消)

对于state的修改操作提供了setState和compareAndSetState,那么为什么要提供这两个对state的修改呢?

  1. 因为compareAndSetState方法通常使用在获取到锁之前,当前线程不是锁持有者,对于state的修改可能存在线程安全问题,所以需要保证对state修改的原子性操作。(CAS)

  2. 而setState方法通常用于当前正持有锁的线程对state共享变量进行修改,因为不存在竞争,是线程安全的,所以没必要使用CAS操作。

2.悲观锁(写多,先上锁,synchronized)

悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。

java中的悲观锁就是Synchronized。

3.自旋锁(适合占锁短时间)

原理:当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

优点:

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

缺点:

  1. 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。

  2. 上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。

自旋锁的开启

JDK1.6 中-XX:+UseSpinning 开启;

-XX:PreBlockSpin=10 为自旋次数;

JDK1.7 后,去掉此参数,由 jvm 控制;

4.synchronized同步锁(重量级)

作用范围

  1. 作用于普通方法,锁住的是对象的实例this。
  2. 作用于静态方法,锁住class实例,永久代是全局共享,会锁所有调用该方法的线程。
  3. 作用于对象实例,锁住以该对象为锁的代码块。

状态:

image-20210705114114100

entrylist是候选竞争线程,ondeck是ready thread,获取到锁就是owner thread,wait了就去waitset,直到Notify就回去entrylist。

contentionlist,entrylist, waitset都处于则色状态。

原理(标记monitor对象)

每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的。

Jdk6后优化了(在对象头标记,不需要操作系统)

Java1.6,synchronized 进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后推出的 Java1.7 与 1.8 中,均对该关键字的实现机理做了优化。引入了偏向锁和轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。

锁膨胀

锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀;

5. reentrantlock(可终端,公平,多个锁)

除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。

可设公平锁

synchronized与reentrantlock

  1. ReentrantLock 通过方法 lock()与 unlock()来进行加锁与解锁操作,与 synchronized 会 被 JVM 自动解锁机制不同,ReentrantLock 加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用 ReentrantLock 必须在 finally 控制块中进行解锁操作。

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

Condition类和Object类锁方法区别

  1. Condition 类的 awiat 方法和 Object 类的 wait 方法等效

  2. Condition 类的 signal 方法和 Object 类的 notify 方法等效

  3. Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效

  4. ReentrantLock 类可以唤醒指定条件的线程,而 object 的唤醒是随机的

tryLock和lock和lockInterruptibly的区别

  1. tryLock 能获得锁就返回 true,不能就立即返回 false,tryLock(long timeout,TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁,返回 false

  2. lock 能获得锁就返回 true,不能的话一直等待获得锁

  3. lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程,lock 不会抛出异常,而 lockInterruptibly 会抛出异常。

6.semaphore信号量

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

和reentrantlock

作用差不多,方法的实现也差不多。就是相当于semaphore多了几个许可。

7.atomic*原子类

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

通常我们会使用 synchronized 将该操作变成一个原子操作。

通常AtomicInteger的性能是 ReentantLock 的好几倍。

8.可重入锁

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

9.公平锁与非公平锁

公平锁(Fair

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

非公平锁(Nonfair

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

10.readwritelock读写锁

为了提高性能,Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的,你只要上好相应的锁即可。

11.共享锁和独占锁

独占锁就是每次都只有一个线程运行,例如ReentrantLock,synchronized

共享锁就是同时可以多个线程运行,如Semaphore、CountDownLatch、ReentrantReadWriteLock

12.重量级锁(monitor本质是操作系统 Mutex Lock实现的锁)

Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的 Mutex Lock 来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高。

JDK 中对 Synchronized 做的种种优化,其核心都是为了减少这种重量级锁的使用。

JDK1.6 以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。

13.轻量级锁(交替执行)

锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。

“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。

原理

轻量级锁在对象内存布局中 MarkWord 锁标志位为 00。

  1. 在线程执行同步代码块之前,JVM会现在当前线程的栈桢中创建用于存储锁记录的空间

  2. 将锁对象头中的 markWord 信息复制到锁记录中,这个官方称为 Displaced Mard Word。然后线程尝试使用 CAS 将对象头中的 MarkWord 替换为指向锁记录的指针。如果替换成功,则进入步骤3,失败则进入步骤4。

  3. CAS 替换成功说明当前线程已获得该锁,此时在栈桢中锁标志位信息也更新为轻量级锁状态:00。

  4. 如果CAS 替换失败则说明当前时间锁对象已被某个线程占有,那么此时当前线程只有通过自旋的方式去获取锁。如果在自旋一定次数后仍为获得锁,那么轻量级锁将会升级成重量级锁。

    升级成重量级锁带来的变化就是对象头中锁标志位将变为 10(重量级锁),MarkWord 中存储的也就是指向互斥量(重量级锁)的指针。(注意!!!此时,锁对象头的 MarkWord 已经发生了改变)。

交替执行(否则锁膨胀)

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

14.偏向锁(只有一个线程执行同步块)

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

15.分段锁(concurrentHashMap)

16.锁优化

  1. 减少锁持有时间

  2. 减小锁粒度

  3. 锁分离(读写锁。LinkedBlockingQueue 从头部取出,从尾部放数据。)

  4. 锁粗化

    通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化 。

  5. 锁消除