引文
聊到锁 其实在业务当中用的很多,包括一些底层的代码都有出现
锁的逻辑可以分为很多种
轻量锁
重量锁
自旋锁
重入锁
公平锁
非公平锁
排他锁
共享锁
包括juc包里面的各种锁
信号量
闭锁
读写锁
jvm
提供的 synchronized
关键字锁
偏向锁
轻量锁
自旋锁
重量锁
mysql
的行锁
间隙锁
乐观锁
悲观锁
以及cpu
提供的 cas
对比交换锁
有些锁我们是用不到的,但是需要在代码里面使用先要了解
轻量锁
聊起轻量锁,其实也就是cpu层面的锁,在java sun
包里面有一个本地nactive
方法,也就是通过比较内存地址值来判断偏移量。如果说地址值期望值是相对等的那么就可以进行更新,如果是多核cpu
其实在cas
里面进行了加lock
行为防止并发,那么由cpu
的原子性来保证并发变成队列的一致处理。
问题是这里面可能会涉及到一个aba问题不过通过后面兼容一个对比值也就是乐观锁就可以进行处理。
详情我们看代码
以AtomicInteger
为例
// 拿到本地方法
private static final Unsafe unsafe = Unsafe.getUnsafe();
static {
try {
// 获取位移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
public final boolean compareAndSet(int expect, int update) {
// 比较i换,如果expect 与偏移量对应的期望值相等进行更新 由cpu 进行保证
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
测试一下
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(1);
System.out.println("主要方法启动");
CompletableFuture<Void> runAsync = CompletableFuture.runAsync(() -> {
System.out.println("进入线程1方法");
boolean b = atomicInteger.compareAndSet(1, 2);
if (b) {
atomicInteger.set(2);
}
});
CompletableFuture<Void> runAsync1 = CompletableFuture.runAsync(() -> {
System.out.println("进入线程2方法");
boolean b = atomicInteger.compareAndSet(1, 3);
if (b) {
atomicInteger.set(3);
}
});
CompletableFuture<Void> runAsync2 = CompletableFuture.runAsync(() -> {
System.out.println("进入线程3方法");
boolean b = atomicInteger.compareAndSet(1, 4);
if (b) {
atomicInteger.set(4);
}
});
CompletableFuture<Void> runAsync3 = CompletableFuture.runAsync(() -> {
System.out.println("进入线程4方法");
boolean b = atomicInteger.compareAndSet(1, 5);
if (b) {
atomicInteger.set(5);
}
});
CompletableFuture.allOf(runAsync,runAsync1,runAsync2,runAsync3);
System.out.println("atomicInteger = " + atomicInteger.get());
}
拿到数据为2 也就是说其他的set
全部时效因为比对数据有问题就无法变更 再多线程抢夺保持同步
重量锁
什么是重量锁:可以理解为,直接上锁,只有拿到锁之后才可以使用没有拿到就等待,等待别人释放锁之后,再被唤醒阻塞的线程不使用自旋锁,一直等。
本来其实可以通过关键字 synchronized
进行的,但是synchronized 1.7
之后进行了升级,我用的是1.8
所以只能用过lock
锁进行处理,示例用的是重入锁 ReentrantLock
大致可以理一下
static Integer integer = 1;
public static void main(String[] args) throws ExecutionException, InterruptedException {
Lock lock = new ReentrantLock() ;
System.out.println("主要方法启动");
CompletableFuture<Void> runAsync = CompletableFuture.runAsync(() -> {
System.out.println("进入线程1方法");
lock.lock();
System.out.println("线程1 拿到锁");
integer=2;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1 赋值释放锁");
lock.unlock();
});
CompletableFuture<Void> runAsync1 = CompletableFuture.runAsync(() -> {
System.out.println("进入线程2方法");
lock.lock();
System.out.println("线程2 拿到锁");
integer=3;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程2 赋值释放锁");
lock.unlock();
});
CompletableFuture<Void> runAsync2 = CompletableFuture.runAsync(() -> {
System.out.println("进入线程3方法");
lock.lock();
System.out.println("线程3 拿到锁");
integer=4;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程3 赋值释放锁");
lock.unlock();
});
CompletableFuture<Void> runAsync3 = CompletableFuture.runAsync(() -> {
System.out.println("进入线程4方法");
lock.lock();
System.out.println("线程4 拿到锁");
integer=5;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程4 赋值释放锁");
lock.unlock();
});
runAsync.get();
runAsync1.get();
runAsync2.get();
runAsync3.get();
System.out.println("主线程结束");
}
结论是
同步处理
其实聊到ReentrantLock
可以往上面聊聊,可以看出的是 ReentrantLock
的接口是lock
public class ReentrantLock implements Lock,
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
默认是非公平锁
,所以衍生一下 非公平锁
和公平锁
的关联
所以引出公平
和非公平锁
公平锁 非公平锁
区别:
公平锁就是很公平
,争抢锁的几率一样,每个线程会先看等待队列是否为空,若为空,直接获取锁,若不为空,自动排队等候获取锁;
非公平锁就是所有的线程都会优先去尝试争抢锁,不会按顺序等待,若抢不到锁,再用类似公平锁的方式获取锁。
示例
static Integer integer = 1;
static Lock lock = new ReentrantLock();
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("主要方法启动");
for (int i = 0; i <50 ; i++) {
newThread(i);
}
Thread.sleep(600000);
System.out.println("主线程结束");
}
static void newThread(Integer i) {
CompletableFuture.runAsync(() -> {
System.out.println("进入线程"+i+"方法");
lock.lock();
System.out.println("线程"+i+" 拿到锁");
try {
Thread.sleep(1200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程"+i+" 赋值释放锁");
lock.unlock();
});
}
当锁释放的那一刻会唤醒大部分线程,这些线程一起蜂拥而上,进行抢锁
那么公平锁相反就不示例代码了
重入锁
聊完上面,其实ReentrantLock
是通过锁aqs模板实现的自己实现,他实现了一个重入锁级制。
可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。由于aqs 是有一个状态的,所以他对于这个状态的定义就是重入了多次
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("主要方法启动");
CompletableFuture.runAsync(() -> {
try {
lock.lock();
System.out.println("第1次获取锁,这个锁是:" + lock);
int index = 1;
while (true) {
try {
lock.lock();
System.out.println("第" + (++index) + "次获取锁,这个锁是:" + lock);
try {
Thread.sleep(new Random().nextInt(200));
} catch (InterruptedException e) {
e.printStackTrace();
}
if (index == 10) {
break;
}
} finally {
lock.unlock();
}
}
} finally {
lock.unlock();
}
});
Thread.sleep(5000);
System.out.println("主线程结束");
}
}
聊一下原理 需要聊一下aqs
放到后面压轴一起说把
自旋锁
当一个线程在获取锁的时候,如果这个锁已经被其它线程获取了,那么该线程就循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环,对于关键字升级也就是synchronized
是自旋膨胀十次还是拿不到锁,就加重量锁,锁上 可以看下示例代码 代码比较简单直接看就好了,
/**
* Atomically adds the given value to the current value.
*
* @param delta the value to add
* @return the previous value
*/
public final int getAndAdd(int delta) {
// 调用方法
return unsafe.getAndAddInt(this, valueOffset, delta);
}
// 被调用方法
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
// 获取值
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
// 赋新值失败 继续 true
return var5; // 返回值
}
System.out.println("主要方法启动");
int andAdd = atomicInteger.getAndAdd(2);
System.out.println("返回参数 = " + andAdd);
System.out.println("得到新值 = " + atomicInteger.get());
// Thread.sleep(5000);
System.out.println("主线程结束");
}
可以看到当多个线程在对一个值赋值的时候 如果拿不到结果,就会一直尝试直到获取锁,这种情况就是自旋
排他锁 共享锁 读写锁
共享锁 语义就是: 被多个线程共享的锁,也可以理解为读锁
排他锁 语义就是一个人的锁,可以理解为独占锁,那么就可以一个人处理查写,可以理解为写锁
读写锁:读一部分,写一部分,写入的文档的时候独占,读的时候支持多个线程一起读
代码示例:
static Integer integer = 1;
static ReentrantLock lock = new ReentrantLock();
static AtomicInteger atomicInteger = new AtomicInteger(1);
private static volatile Map<String, Object> map = new HashMap<>();
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("主要方法启动");
for (int i = 1; i <= 10; i++) {
final int tempInt = i;
CompletableFuture.runAsync(() -> put(tempInt + "", tempInt + ""));
}
for (int i = 1; i <= 10; i++) {
final int tempInt = i;
CompletableFuture.runAsync(() -> get(tempInt + ""));
}
Thread.sleep(100000);
System.out.println("主线程结束");
}
public static void put(String key, Object value) {
System.out.println(Thread.currentThread().getName() + "正在写入:" + key);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入完成:" + key);
}
public static void get(String key) {
System.out.println(Thread.currentThread().getName() + "正在读取");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object value = map.get(key);
System.out.println(Thread.currentThread().getName() + "读取完成:" + value);
}
执行结果
由于多个线程在读写的时候,无法保证线程的有序,所以会出现读写出现问题的情况,为了保证这种问题,由读写锁进行处理
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
调用读锁进行加锁,调用写锁进行加锁
看一下读锁
从方法注释既可以看出,如果写锁没有被持有拿不到写锁,则获取读锁,如果写锁被另外线程持有,那么当前线程不可调度,等待拿到读锁。写锁是独占的。
关于 mysql 间隙锁 乐观锁 悲观锁 行锁 表锁
乐观锁:对于mysql
来说就是读锁,然后再锁主,可以理解为,源数据需要有一个唯一值,比如说是版本号,每次写入加一,再更新的时候也要判断 这就是乐观的模式
业务场景示例
static Integer integer = 1;
static ReentrantLock lock = new ReentrantLock();
static AtomicInteger atomicInteger = new AtomicInteger(1);
private static volatile Map<String, Object> map = new HashMap<>();
static ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("主要方法启动");
CompletableFuture.runAsync(() -> {
if (atomicInteger.get() == 1) {
integer = 2;
atomicInteger.set(2);
}
});
CompletableFuture.runAsync(() -> {
if (atomicInteger.get() == 1) {
integer = 3;
atomicInteger.set(2);
}
});
CompletableFuture.runAsync(() -> {
if (atomicInteger.get() == 1) {
integer = 4;
atomicInteger.set(2);
}
});
Thread.sleep(10000);
System.out.println("主线程结束");
}
悲观锁:可以理解为独占锁类型, 在写一条数据的时候进行独占,不允许别人修改,只有自己修改完成释放了锁,那么别人才能进行修改 for update
行锁:是指mysql
支持对于某一条数据进行修改的时候,如果指定了主键,那么这一行数据就被设为独占,不允许别人修改
表锁:是指如果mysql
开启了表锁,这个需要手动开启的,开启之后指定非主键字段会对表进行独占
间隙锁是指,如果修改某个范围内的数据,比如是1 - 100 的数据全部把名称改为张三,你数据库里面主键是非生成的 刚好你插入到了50 这个时候有一条插入数据主键是51,插入到索引1-100之间的数据那么就会锁住这个区间,
或者查询的时候加了独占 for update;
,别人插入的时候也是会有间隙锁,这样做的主要目的是为了防止幻读
关于synchronized
聊synchronized 其实在前面我已经说了一些,这个要分场景 jdk1.7之前这些他就是一个重量锁,1.7之后他进行了一次升级,其实知道了上面的锁就好说,他的升级过程是
无锁
偏向锁:偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令,也就是一个线程在哪里重入
轻量锁:如果一次CAS 指令获取不到,就进行重复获取如果还是获取不到就升级自旋10次
重量锁:还是拿不到就变成重量级
关于锁模板AQS
sync 就是锁模板的实现 包括闭锁,信号量 重入锁
而lock 是一个接口,关于加锁和解锁的一些方法重写, 可以看出核心的还是sync
根据不同的锁类型确定模板的重写。
抛出上诉 看一下aqs
可以看出模板抽象有很多自己实现的方法,和一些共有方法,以及说明 维护了一个头尾相连的队列,说明了队列为CLH
队列,维护了熟悉的头尾指针
后面的在源码里面就可以看到,就不说了,看注释即可, 我说一下我的大致理解,AQS 是锁的一个模板,也就是实现锁的核心,关于lock
接口只是增加一些重写方法,关于AQS
是有自己的一套实现逻辑的,那么对于共有方法进行写入,对于需要别人去实现的方法自己进行重写,以ReentrantLock
为例对于头尾相连的队列前面有一个status
的状态,这个就是重入的数量,如果说为0那么说明以及全部出来,否者+1 表示还持有锁。
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
/**
* Performs {@link Lock#lock}. The main reason for subclassing
* is to allow fast path for nonfair version.
*/
abstract void lock();
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
// Methods relayed from outer class
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
final boolean isLocked() {
return getState() != 0;
}
/**
* Reconstitutes the instance from a stream (that is, deserializes it).
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}
基于自己实现的aqs
和 lock
重写来实现自定义锁