难者不会,会者不难
悲观锁,乐观锁,分段锁,重入锁,读写锁,CAS锁,排他锁(基本上是自带的功能),自旋锁,分布式锁(muji)
老生常谈的问题。
悲观锁每次在拿数据时,都会上锁,自带排他锁的功能。
举个具体的例子,有张Order表,里有id,orderId,state属性,另外张Money表中,里有id,orderId,money属性,现在有个业务执行三条sql语句:
select * from order where orderId=1 and state=0,如果select到内容继续往下执行:
update set state=1 where orderId=1,
update money set money=moeny+money where orderId=1
此时有两个jdbc连接,同时执行以上三条sql语句,会发生什么?产生重复读问题,都读到state=0,select出了数据,都继续往下执行了,导致money更新不对,那么要如何解决呢?
使用悲观锁,在sql中将第一句改为select * from order where orderId=1 and state=0 for update,在查的时候,十分悲观,整个事务只允许一个连接进行操作,拿不到锁的连接只能一直等着,等待拿到锁的连接提交完事务释放锁资源。
因为只能保证一个连接进行操作,效率十分低。项目中查询量比较大的情况下,不会使用悲观锁。
比较简单,表示比较乐观,它在别人在做修改的时候,不会上锁,不过加称为版本标识的判断(类似CAS无锁机制),主要特点是本质上没有锁,使用版本标识和影响行数(如version-版本号)进行控制,乐观锁有效的根本原因是sql执行的原子性。
回到具体例子,还是有两个连接执行那三句sql:
select * from order where orderId=1 and state=0,如果select到内容继续往下执行:
update set state=1 where orderId=1,
update money set money=moeny+money where orderId=1
使用乐观锁,Order表添加version字段,update的sql执行改为
update set state=1,set version=version+1 where orderId=1 and version=version
当一个连接执行select语句,获取version版本号,update语句中where附加version进行查找,修改完毕后将version+1,此时另外一个连接也select到了数据,不过version是旧的,再根据旧的version就使用不了update语句了,此时它的影响行数为0。这里的影响行数就是数据库返回给你的成功修改的行数值。
如果影响行数>0,执行第三句sql,第一个连接成功修改了第二句update语句,因此它的影响行数大于0,可以执行第三句sql。
如果查询量小,可以使用悲观锁,在请求量大时,多个请求来时,悲观锁只能让一个请求执行。
使用乐观锁使用版本控制操作,要使用乐观锁的话需要自己在表中添加一个版本标识字段,常规下(绝大多数)使用乐观锁。
死锁现象
具体见之前的死锁例子,当时已经有用到重入的概念了。
在Java中,ReentrantLock和synchronized都是可重入锁。
重入锁,又称为递归锁,指的是同一线程的外层函数获得锁资源之后,内层的递归函数仍然有该锁使用权。
get中调用set方法,函数进行嵌套,锁能够进行传递
import lombok.SneakyThrows;
class MyTThread implements Runnable {
public void get() {
System.out.println(Thread.currentThread().getId() + " get()");
set();
}
@SneakyThrows
public void set() {
Thread.sleep(100);
System.out.println(Thread.currentThread().getId() + " set()");
}
@Override
public void run() {
get();
}
}
public class Test2 {
public static void main(String[] args) {
MyTThread myTThread = new MyTThread();
new Thread(myTThread).start();
new Thread(myTThread).start();
new Thread(myTThread).start();
}
}
这段代码执行结果是
11 get()
13 get()
12 get()
13 set()
11 set()
12 set()
我们希望get(),set()交替执行,我们在两个方法都加上synchronized关键字修饰即可
public synchronized void get() {
System.out.println(Thread.currentThread().getId() + " get()");
set();
}
@SneakyThrows
public synchronized void set() {
Thread.sleep(100);
System.out.println(Thread.currentThread().getId() + " set()");
}
那么是为什么呢?
这就是因为synchronized是重入锁,线程①进行get()方法后占用this锁,调用set()时候,set方法仍然有this锁的使用权,而其他的线程②和③因为获取不到this锁被阻塞,只能等待线程①的set()方法执行完毕释放锁。
执行结果如下:
11 get()
11 set()
13 get()
13 set()
12 get()
12 set()
举一反三,使用同步代码块的时候,只要外层和内层函数使用的是同一个锁对象,那么就是可重入的;如果外层和内层函数使用的是不是同一个锁对象,那么就是非可重入的。
这个例子和上一个例子是一样的,不作过多解释
import lombok.SneakyThrows;
import java.util.concurrent.locks.ReentrantLock;
class MyTThread implements Runnable {
private ReentrantLock lock = new ReentrantLock();
public synchronized void get() {
lock.lock();
System.out.println(Thread.currentThread().getId() + " get()");
set();
lock.unlock();
}
@SneakyThrows
public synchronized void set() {
lock.lock();
Thread.sleep(100);
System.out.println(Thread.currentThread().getId() + " set()");
lock.unlock();
}
@Override
public void run() {
get();
}
}
public class Test2 {
public static void main(String[] args) {
MyTThread myTThread = new MyTThread();
new Thread(myTThread).start();
new Thread(myTThread).start();
new Thread(myTThread).start();
}
}
外层函数能将锁资源传递给内层函数,内层函数无需重新获取锁资源,效率提高。
内层函数释放锁资源不影响外层函数的锁资源
考虑这样一个场景,两个线程对一个共享文件进行读写操作,同时读是没有问题的,但是如果有一个线程想去写这个共享文件,那么就不应该有其他的线程对该资源进行读或写。
无论多少个线程如果都在读,其他的线程可以读或写,只要一个线程正在写,其他线程不可以读或写。
首先是不加锁的情况
import lombok.Data;
import lombok.SneakyThrows;
import java.util.HashMap;
import java.util.Map;
@Data
class Cache {
static private volatile Map<String, Object> map = new HashMap<>();
@SneakyThrows
public static Object write(String key, Object value){
System.out.println("正在开始写..." + ", key: " + key + ",value: " + value);
Thread.sleep(100);
Object o = map.put(key, value);
System.out.println("结束写..." + ", key: " + key + ",value: " + value);
return o;
}
@SneakyThrows
public static Object read(String key){
System.out.println("正在开始读..." + ", key: " + key);
Thread.sleep(100);
Object o = map.get(key);
System.out.println("结束读..." + ", key: " + key + ",value: " + o);
return o;
}
}
public class Test3 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
Cache.write(i + "", i + "");
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
Cache.read(i + "");
}
}
}).start();
}
}
运行结果可以看到,9正在写,还没有结束写,9就开始读了,读的结果是null,数据发生了异常
如何解决呢?使用ReentrantReadwriteLock-读写锁,使用起来还是十分容易的。
import lombok.Data;
import lombok.SneakyThrows;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@Data
class Cache {
static private volatile Map<String, Object> map = new HashMap<>();
static private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 读锁
static Lock r = readWriteLock.readLock();
// 写锁
static Lock w = readWriteLock.writeLock();
@SneakyThrows
public static Object write(String key, Object value) {
w.lock();
System.out.println("正在开始写..." + ", key: " + key + ",value: " + value);
Thread.sleep(100);
Object o = map.put(key, value);
System.out.println("结束写..." + ", key: " + key + ",value: " + value);
w.unlock();
return o;
}
@SneakyThrows
public static Object read(String key) {
r.lock();
System.out.println("正在开始读..." + ", key: " + key);
Thread.sleep(100);
Object o = map.get(key);
System.out.println("结束读..." + ", key: " + key + ",value: " + o);
r.unlock();
return o;
}
}
public class Test3 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
Cache.write(i + "", i + "");
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
Cache.read(i + "");
}
}
}).start();
}
}
Compare And Swap
CAS无锁机制和自旋锁是配套使用的,因为自旋锁的底层用的就是CAS无锁机制,CAS无锁机制效率非常高,CAS无锁机制其实和乐观锁是类似的概念,本身没有锁,而是用一个标识。
CAS体系中有三个参数,分别是V,E,N,V表示要更新的值,E表示期望值,N表示新值,线程执行先判断要更新的值V与期望值E,如果它们相同,说明没有任何线程更改,线程继续操作,将新值N覆盖V;如果V和E不同,说明其他线程更改过,当前线程不做任何操作,只把N覆盖V。
其实预期值E就是之前缓存的值,更新值V如果和预期值E不同的话,说明V被其他线程修改了,再进行操作共享数据将可能会发生冲突,所以不操作共享数据,只把新值N赋给V。
看AtomicInteger的源码,
public class Test1 {
public static void main(String[] args) {
new AtomicInteger().incrementAndGet();
}
}
ctrl+左键点进去,我发现我这里的源码和之前不一样了,应该被重构过,改到更底层去实现了,但是再怎么底层原理应该是不变的。
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
ctrl+左键进入unsafe.getAndAddInt,值得注意的是Unsafe中的方法是原子操作。
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));
return var5;
}
首先是var5 = this.getIntVolatile(var1, var2);表示通过对象和偏移量获取变量的值,Unsafe类可以让java操作内存地址,就是比如这里通过对象和偏移量直接从内存中获取对应变量的值,取得的值赋给var5,这里的var5其实是要更新的值V,由于用了Volatile修饰,因此V是线程之间可见的,就是说不同的线程看到的V都是相同的一个值。
compareAndSwapInt方法是Java的native方法,并不由Java语言实现。
public final native boolean compareAndSwapInt(Object o, long offset,int expected, int x);
方法的作用是,读取传入对象o在内存中偏移量为offset位置的值与期望值expected(上次缓存的V值)作比较。相等就把x(这里的x就是var5+var4,等同于V+1)值赋值给offset位置的值。方法返回true。不相等,就取消赋值,方法返回false,继续执行getIntVolatile刷新V值,继续和上次缓存了V值的E比较,直到在其它线程执行CAS操作之前,抢先退出循环操作,执行+1操作。
unsafe.getAndAddInt(this, valueOffset, 1) + 1;
自旋锁是采用让当前线程不停的在循环体内执行实现的,当循环的条件被其他线程改变时才能进入临界区。是不可重入的锁
下面例子展示自旋锁现象,效果是线程卡死。
当第一个线程调用这个不可重入的自旋锁去加锁(调用lock函数)是没有问题的,但当再次调用lock的时候,因为自旋锁已经持有引用已经不为空了,该线程对象会误认为是别人的线程持有自旋锁,释放不了锁资源,程序直接卡死。
自旋锁使用了CAS原子操作(compareAndSet方法),lock函数将所有者owner设置为当前线程,并且预测原来的值为空;unlock函数将所有者owner设置为空,并预测值为当前对象。
当有第二个线程调用lock方法时,由于owner值不为空(设置为了其他或者当前线程对象),导致循环一致被执行,直至第一个线程调用了unlock函数将owner设为空,第二个线程才能进入临界区。
由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都要执行,占用CPU时间
package ch6;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.sql.Statement;
import java.util.concurrent.atomic.AtomicReference;
class MySpinLock {
// 原子类 作用是对对象的引用,它可以保证你在修改对象引用时的线程安全性
private AtomicReference<Thread> sign = new AtomicReference<>();
public void lock(){
Thread current = Thread.currentThread();
while (!sign.compareAndSet(null, current)){
}
}
public void unlock(){
Thread current = Thread.currentThread();
sign.compareAndSet(current, null);
}
}
@Data
@AllArgsConstructor
public class Test2 implements Runnable{
static int sum;
private MySpinLock lock;
public static void main(String[] args) {
MySpinLock lock = new MySpinLock();
for (int i = 0; i < 10; i++) {
Test2 test2 = new Test2(lock);
Thread thread =new Thread(test2);
thread.start();
}
}
@Override
public void run() {
this.lock.lock();
this.lock.lock();
sum++;
this.lock.unlock();
this.lock.unlock();
}
}
//todo 先挖坑,挖坑填不填就不知道了