锁的类型
可重入锁和不可重入锁
-
可重入锁:一个线程可以多次抢占同一个锁;
Synchronized
、ReentrantLock
都是可重入锁,用Synchronized
进行锁的可重入测试,在同一个线程中定义childMethod()
和childMethod2()
两个方法,在这两个方法中都使用Synchronized
修饰,代码可以正常运行;package com.xrl.juc.synchronized_demo;/*** synchronized 可重入实验** @version [v1.0]* @author: [xrl]* @create: [2024/06/28 14:43]**/ public class Child extends Parent {public synchronized void childMethod() {System.out.println("Child method");// 调用另一个加锁方法,嵌套调用childMethod2();// 在子类中调用父类的同步方法parentMethod();}public synchronized void childMethod2() {System.out.println("Child method2");// 在子类中调用父类的同步方法parentMethod();}public static void main(String[] args) {Child child = new Child();child.childMethod();} }class Parent {public synchronized void parentMethod() {System.out.println("Parent method");} }
-
不可重入锁:不可递归调用,递归调用就发生死锁,自旋锁一般情况下是不可重入的,但可在自旋加锁前加入判断可改为可重入的;
package com.xrl.juc.cas;import java.util.Objects; import java.util.concurrent.atomic.AtomicReference;/*** @version [v1.0]* @author: [xrl]* @create: [2024/06/28 14:59]**/ public class UnReentrantDemo {SimpleSpinLock lock = new SimpleSpinLock();public void set() {try {lock.lock();System.out.println("set 方法");//在同一个线程中先在set方法中获得锁 , 调用get方法 又在get方法中获得同一个锁,因为不可重入所以会进入死循环,不会输出get 方法get();} catch (Exception e) {e.printStackTrace();} finally {lock.unlock(); //释放锁}}public void get() {try {lock.lock();System.out.println("get 方法");} catch (Exception e) {e.printStackTrace();} finally {lock.unlock();}}public static void main(String[] args) {UnReentrantDemo lockTest = new UnReentrantDemo();lockTest.set();} } class SimpleSpinLock {private final AtomicReference<Thread> owner = new AtomicReference<>();private volatile boolean locked = false;private volatile int count = 0;public void lock() {Thread currentThread = Thread.currentThread();// 可重入判断 // if (owner.get() == currentThread) { // // 当前线程已经持有锁,增加计数器 // count++; // return; // }// 自旋尝试获取锁while (!locked || Objects.nonNull(owner.get())) {// 如果锁没有被占用或者被当前线程占用,尝试设置ownerif (!locked && owner.compareAndSet(null, currentThread)) {// 设置成功,获取锁locked = true;return;}}}public void unlock() {Thread currentThread = Thread.currentThread();// 可重入判断 // if (count > 0) { // count--; // return; // }if (owner.get() == currentThread) {// 释放锁locked = false;// 清除ownerowner.set(null);} else {throw new IllegalMonitorStateException("Calling thread has not locked this lock");}} }
公平锁和非公平锁
-
公平锁:先到先得,获得锁的顺序遵循先进先出FIFO原则,先对锁进行获取的请求,一定先满足 ;
-
ReentrantLock
可以是公平锁也可以是非公平锁,在创建时可以指定;public ReentrantLock(boolean fair);//sync = fair ? new FairSync() : new NonfairSync();
;两者之前的区别在于
FairSync
使用lock()
方法获取锁之前,会先去判断同步队列中是否存在元素(hasQueuedPredecessors()
方法),如果存在则将当前任务放入同步队列的队尾;而
NonfairSync
只判断当前锁是否有人占用,如果没有就直接获取;
-
-
非公平锁:不按加锁顺序执行,非公平锁的吞吐量大于公平锁,是主流操作系统线程调度的基本选择;
synchronized
是非公平锁:原因是synchronized
是重量级锁, 它的线程间的调度和状态变更由操作系统负责,在线程切换A线程到B线程的时候要进行上下文切换需要耗费一段时间,在这个时间间隔上锁是空闲的如果此时有另一个线程C在cpu
的另一个核心上执行,就不需要进行上下文切换,这时C就可以获得锁,这样利用了锁的空档期,提高了cpu
利用效率,但是可能会导致“线程饥饿 ”即某个线程一直没有获得到锁。
自旋锁
- 概念:当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,在循环中不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环;
- 简单实现:参考不可重入锁的例子;
悲观锁和乐观锁
- 悲观锁:悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改,悲观地认为,不加锁的并发操作一定会出问题;因此对于并发操作,悲观锁都会采取加锁的方式来保证线程安全,
synchronized
和ReentrantLock
都属于悲观锁; - 乐观锁:乐观锁认为在大多数情况下,多个用户对同一数据的修改操作是不会冲突的,因此不需要加锁限制,只是在修改数据时,通过版本比较判断数据是否被其他线程操作,如果一致则说明数据没被其他线程修改可以进行更新操作,不一致则说明数据被被其他线程操作过,不能进行更新操作,需要回滚事务或重新尝试;
- 实现:一般通过版本号和
CAS
实现,JAVA
中的原子操作类就是采用这一思想实现的,也可通过在mysql
表中添加Version
字段来实现乐观锁;
- 实现:一般通过版本号和
共享锁和独占锁
- 共享锁:指可以同时被多个线程获取的锁;
- 实现:
Java
中的ReentrantReadWriteLock
(读写锁)中的读锁、Mysql
读锁等;
- 实现:
- 独占锁:指任何时候都只有一个线程能执行资源操作;
- 实现:
Java
中的ReentrantReadWriteLock
(读写锁)中的写锁、Mysql
写锁、synchronized
、ReentrantLock
等;
- 实现:
可中断锁和不可中断锁
- 中断锁:指锁在执行时可被中断,也就是在执行时可以接收
interrupt
的通知,从而中断锁执行;- 实现:
ReentrantLock
是可中断锁;中断锁使用方法是lock.lockInterruptibly()
方法,它和lock.lock()
方法作用类似,只不过lockInterruptibly()
方法可以优先接收中断的请求;
- 实现:
- 不可中断锁:指锁在执行时不可被中断;
- 实现:
synchronized
是不可中断锁;
- 实现:
CAS(Compare And Swap)
-
概念:它核心就是比较并交换;CAS算法是一种无锁算法,乐观锁的一种实现;
-
原理:CAS有3个操作数,内存所存值V,旧的预期值A,要修改的新值B;当且仅当预期值A和内存值V相同时将内存值V修改为B并返回
true
,否则返回false
; -
操作系统的支持:它的最终实现是依赖于
CPU
原子性指令实现,也就是判断值是否相等、更新值等多个步骤是一个整体,执行必须连续,执行过程中也不可被中断; -
ABA问题:线程T1读取变量值为 A;线程T2将变量值更改为 B; 之后T2又将变量值更改为 A; 这时T1线程去更新,发现此时变量值与之前读到的值是相同的,就以为没有被其它线程更新过,但实际已经被变更过;--------------------,案例参考文章Unsafe类中CAS相关的代码案例
- 解决办法:加入一个时间戳或者递增变量等实现的版本号,来标识这个变量是否之前被更新过;
-
CAS
无锁自旋存在的问题:-
自旋时间长:如果自旋
CAS
长时间不成功,就会占用大量的CPU资源;解决办法:破坏掉
for
死循环,当超过一定时间或者一定次数时,退出循环; -
内存顺序冲突(
Memory Order Violation
):当持有锁的线程快要释放锁的时,一般情况下会执行一个store
命令,而等待获取锁的线程此时在不断自旋,会不断发出load
命令,而此处并没任何happen-before
规则限制,此时就可能出现持有锁线程store
命令尚未完成,而其他线程任然在不断执行lode
操作,这可能导致程序出现异常,为了避免出出现这种情况,处理器需要进行流水线清空并重排序;解决办法:在自旋等待时插入Pause指令,pause指令能让自旋失败时
cpu
稍微停顿一下再继续自旋,从而使load
命令执行频率低一些;- 流水线清空并重排序:当处理器检测到有依赖于未完成操作结果的后续指令时,它会选择清空流水线并重排序来保证内存操作顺序一致性的强制性;(当执行读取操作时,要确保所有先前的写入操作都已经完成并且对其他线程可见);
-
UNSAFE类:
-
概念:Java中的
Unsafe
类为我们提供了类似C++
直接访问、操作内存的能力,这也就意味Unsafe
可以操作不受JVM
管理的内存,也就代表这无法被GC
需要如C++
一样手动释放,稍有不慎就会出现内存泄漏; -
功能:主要提供了如下几大块能力,更详细的API能力可以参考官方文档;
-
Unsafe
对象的获取:Unsafe
功能虽然强大,但对于JAVA程序而言它的操作是不安全的,所以我们无法直接通过方法获取到Unsafe
,需要通过反射获取;package com.xrl.juc.unsafe_demo; import sun.misc.Unsafe; import java.lang.reflect.Field;public class UnsafeUtil {public static Unsafe get() {try {/***直接获取会抛出异常java.lang.SecurityException* 我们写的程序调用的是AppClassLoader加载器,Unsafe类Botstarp类价值器加载,* 在返回时回去判断是否是系统类加载器VM.isSystemDomainLoader(caller.getClassLoader())*/// Unsafe unsafe = Unsafe.getUnsafe();/*** 通过反射获取到Unsafe类中的 private static final Unsafe theUnsafe;这个属性* usafe在静态代码块中已经 theUnsafe = new Unsafe(); 创建了这个属性*/Field field = Unsafe.class.getDeclaredField("theUnsafe");//将属性设置为可取field.setAccessible(true);//获得属性值return (Unsafe) field.get(null);} catch (NoSuchFieldException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();}return null;} }
-
CAS
相关://参数说明: 要操作的对象 要操作属性的偏移量 预期值 要更新的值 public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5); public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
-
代码案例:通过一个案例来体会一下通过
Unsafe
实现CAS
修改属性值package com.xrl.juc.unsafe_demo;import sun.misc.Unsafe;public class MyUnsafe {//要设置的值public volatile int state = 0;public static void main(String[] args) throws NoSuchFieldException {MyUnsafe myUnsafe = new MyUnsafe();//获得Unsafe实例Unsafe unsafe = UnsafeUtil.get();//获得要修改属性的偏移量long stateOffset = unsafe.objectFieldOffset(MyUnsafe.class.getDeclaredField("state"));//通过偏移量来修改state的值 要操作的对象 要操作属性的偏移量 预期值 要更新的值boolean b = unsafe.compareAndSwapInt(myUnsafe, stateOffset, 0, 5);System.out.println("是否修改成功" + b);System.out.println("更新后的值" + myUnsafe.state);/*** 使用unsafe.compareAndSwapInt(myUnsafe, stateOffset, 0, 3)* 只有本身的值与预期值相同时才可修改成功**/boolean v = unsafe.compareAndSwapInt(myUnsafe, stateOffset, 5, 3);System.out.println("是否修改成功" + v);System.out.println("再次更新后的值" + myUnsafe.state);} }
-
UNSAFE
在JDK
版本中的变更:JDK9
计划移除:JEP 260- 提供
jdk.internal.misc.Unsafe
功能更加强大,使用jdk.internal.misc.Unsafe
需要添加启动参数--add-opens java.base/jdk.internal.misc=ALL-UNNAMED --illegal-access=warn
; - 发布
VarHandle
着手替代Unsafe
中部分功能;
- 提供
- 预计
JDK23
弃用内存访问方法:JDK 23 ;通过VarHandle API(JEP 193,JDK 9)
和Foreign Function & Memory API(JEP 454,JDK 22)
来替换;
原子操作类
-
概念:
JAVA
在JDK5
推出的JUC
并发包中提供了java.util.concurrent.atomic
原子包,在该包下提供了各种原子操作类,内部主要通过UNSAFE
和CAS
实现的原子操作; -
原子类的分类:具体的
API
使用可参考官方文档;-
基本数据类型的原子类:
AtomicInteger、AtomicBoolean、AtomicLong
; -
引用类型原子类:
AtomicReference、AtomicStampedReference、AtomicMarkableReference
; -
数组类型原子类:
AtomicIntegerArray、AtomicLongArray、AtomicLongArray
; -
属性类型原子类:
AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
; -
累加器:
LongAdder、DoubleAdder
; -
积累器:
LongAccumulator、DoubleAccumulator
;
-
-
使用限制:
- 操作的字段不能被
static、final
修饰; - 操作的字段需要被
volatile
修饰,从而保证数据的可见性; - 属性的访问权限范围必须包含当前的操作区域;
- 操作的字段不能被
-
自定义原子类:通过一个自定义原子类来体会一下原子类的实现原理,主要是依靠
UNSAFE
得到操作字段偏移量,从而获得到属性的旧值,之后通过自旋+CAS
来设置值,并通过volatile
保证操作字段的可见性;package com.xrl.juc.atomic_demo;import com.xrl.juc.unsafe_demo.UnsafeUtil; import sun.misc.Unsafe; /*** 自定义原子操作类* @author xrl* @create 2024-06-30 17:04*/ public class MyAtomicLong {private static Unsafe unsafe;private static long valueOffset;private volatile long value;static {unsafe = UnsafeUtil.get();try {// 通过属性名获取字段偏移量valueOffset = unsafe.objectFieldOffset(MyAtomicLong.class.getDeclaredField("value"));} catch (NoSuchFieldException e) {e.printStackTrace();}}public MyAtomicLong(long value) {this.value = value;}public final long incrementAndGet() {//自旋long v;do {v = unsafe.getLongVolatile(this, valueOffset);System.out.println(Thread.currentThread().getName() + "值为" + v);} while (!unsafe.compareAndSwapLong(this, valueOffset, v, v + 1L));return v;}public long get() {return unsafe.getLong(this, valueOffset);}public static void main(String[] args) {MyAtomicLong aLong = new MyAtomicLong(5);System.out.println(aLong.incrementAndGet());System.out.println(aLong.get());} }