Java锁机制
(一)线程安全问题
线程安全是多线程领域的问题,线程安全可以简单理解为一个方法或者一个实例可以在多线程环境中使用而不会出现问题。
在 Java 多线程编程当中,提供了多种实现 Java 线程安全的方式:
1、最简单的方式,使用 Synchronization
关键字
2、使用 java.util.concurrent.atomic
包中的原子类,例如 AtomicInteger
3、
使用 java.util.concurrent.locks
包中的锁
4、使用线程安全的集合 ConcurrentHashMap
5、
使用 volatile
关键字,保证变量可见性(直接从内存读,而不是从线程 cache
读)
(二)volatile 实现原理
(1)在 JVM 底层 volatile 是采用“内存屏障”来实现的
(2)缓存一致性协议(MESI协议)它确保每个缓存中使用的共享变量的副本是一致的。其核心思想如下:当某个 CPU 在写数据时,如果发现操作的变量是共享变量,则会通知其他 CPU 告知该变量的缓存行是无效的,因此其他 CPU 在读取该变量时,发现其无效会重新从主存中加载数据
(三)synchronize 实现原理
同步代码块是使用 monitorenter
和 monitorexit
指令实现的,同步方法(在这看不出来需要看 JVM 底层实现)依靠的是方法修饰符上的 ACC_SYNCHRONIZED
实现。
(四)synchronized 与 lock 的区别
1、synchronized 和 lock 的用法区别
synchronized(隐式锁):在需要同步的对象中加入此控制,synchronized
可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
lock(显示锁):需要显示指定起始位置和终止位置。一般使用 ReentrantLock
类做为锁,多个线程中必须要使用一个 ReentrantLock
类做为对象才能保证锁的生效。且在加锁和解锁处需要通过 lock()
和 unlock()
显示指出。所以一般会在 finally
块中写 unlock()
以防死锁。
2、synchronized 和 lock 性能区别 synchronized
是托管给 JVM 执行的,而 lock
是 Java 写的控制锁的代码。在 JDK 1.5 中,synchronize
是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用 Java 提供的 Lock
对象,性能更高一些。但是到了 JDK 1.6,发生了变化。synchronize
在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在 JDK 1.6 上 synchronize
的性能并不比 Lock
差。
3、synchronized 和 lock 机制区别
synchronized
原始采用的是 CPU 悲观锁机制,即线程获得的是独占锁。独占锁意味着其 他线程只能依靠阻塞来等待线程释放锁。
Lock
用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是 CAS 操作(Compare and Swap)。
(五)CAS 乐观锁
CAS 是项乐观锁技术,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”这其实和乐观锁的冲突检查 + 数据更新的原理是一样的。
(六)ABA 问题
CAS 会导致“ABA问题”。
CAS 算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化。
比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但是不代表这个过程就是没有问题的。
部分乐观锁的实现是通过版本号(version)的方式来解决 ABA 问题,乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行 +1 操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现 ABA 问题,因为版本号只会增加不会减少。
(七)乐观锁的业务场景及实现方式
乐观锁(Optimistic Lock):
1、每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。
2、比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。
转自:有梦想的咸鱼