马桶🚽Java 上厕所就能看完的小知识! 欢迎关注、点赞 持续更新!
此次准备面试,正好复习一下关于锁机制。
悲观锁
总是假设最坏的情况,获取数据时必回遭到修改数据导致的不同步。
悲观锁主要是当一个线程获取锁以后就会阻塞其他想要获取锁的进程。
这种阻塞-唤醒的过程需要使线程在用户态和内核态之间切换,消耗大量资源。
synchronized
这是由JVM 底层实现的锁机制。其特点是可以自动释放资源,后续的jdk中对Synchronized进行了大量优化。
如锁粗化、锁消除、自旋锁、偏向锁、自适应自旋锁。
synchronized关键字最主要的三种使用方式:
修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
修饰静态方法: :也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
synchronized 关键字的底层原理
- 同步语句块的原理
通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息
首先 当执行monitorenter时,线程会获取锁。即获取monitor(monitor存在于对象头中)的持有权。
当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。
- 修饰方法原理
修饰方法主要通过ACC_SYNCHRONIZED 进行标记同步方法。JVM获取到指定标记来执行同步操作。
synchronized和ReentrantLock 的区别
- 他们都是可重入锁
什么是可重入锁?
可重入锁就是当一个线程已获取了当前的对象锁,他还想进入带有当前对象锁的同步方法。他是可以进入的。如果不可重入就会造成死锁。自己等待自己
在此获取时,锁计数器会+1还是当计数器降为0时才能释放锁。
- synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
syn 的优化都是jvm去实现的,并没有直接让我们看到。而 ReentrantLock是基于API实现的,他的锁获取释放是由lock() unlock() 决定的。我们可以查看到
- ReentrantLock 比 synchronized 增加了一些高级功能
ReentrantLock是可以实现公平锁,而Syn不是。
公平锁就是允许先阻塞的(阻塞时间长的)线程先获取锁。(ReentrantLock(boolean fair)
)通过构造方法设置。
乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁。(想的太美,线程量太大就得总自旋🔒)
说白了 乐观锁就是通过值比较关系。来保证每次只有一个线程对变量进行修改。
java.util.concurrent.atomic
包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
想线程安全,又觉得自己写的东西性能很差,就可以启用atomic包下的类。
优势:因为线程不会进行阻塞。所以减少了阻塞唤醒的资源消耗。
缺点:ABA问题 (通过版本号来解决问题)。 如果需要自旋更新值处理。有可能多个线程请求需要多次自旋。开销多。
CAS只对单个共享变量有效,如果需要使用多个共享变量值。可以将他们传入AtomicReference
来保证引用对象之间的原子性。即把多个需要共享的值 保存成一个共享变量。
CAS算法
每次操作之前需要读取主内存中的值。查看和本线程值副本是否一致。如果一致则进行赋值处理。
如果不一致则通过自旋。自旋操作: 刷新本线程值。还要确定本线程值和主内存中的值是否一致 ,一致再进行赋值操作·。以此重复知道获取出结果为止。
CAS算法的ABA问题
假设 我们有三组线程。主内存线程中 a = 1。第一次 1线程获取并进行了值修改。3线程发现值不同就会进行自旋,刷新其线程变量副本。
而如果 1线程将值修改为2,2 线程将值修改为1,则3线程看到的值没变。所以不知道有之间2线程的操作。
比如说:挪用公款在放回去没人知道。钱虽然没变,但是这个过程一样危险重重。
版本号机制
我个人觉得:版本号机制是对CAS算法出现ABA问题的一种处理。 而并不应该是一种乐观锁策略。
因为他同样需要进行比较,同样查询不一致 拒绝操作或者自旋
解决ABA问题就是 就是添加版本号。每次操作都在版本号上+1,当获取的版本号与线程的变量版本号相同时才能进行操作。防止丢失上面说的2线程进行的操作。