目录
1、乐观锁&悲观锁
1.1 乐观锁的定义
乐观锁认为一般情况下不会出现冲突,所以只会在更新数据的时候才对冲突进行数据检测,如果没有发生冲突则直接修改,如果发生冲突则不做任何修改,然后把结果返回给用户,让用户自行处理。
1.2 乐观锁的实现(CAS机制)
CAS(COMPARE AND SWAP):对比替换
V:内存中的值
A:预期的旧值
B:要替换的新值
CAS实现机制:比较V和A是否相等,相等则将V更改为B,否则提示修改失败。核心:先比较再替换
CAS实现借助Unsafe类,Unsafe类调用操作系统的Atomic::cmpxchg(原子性汇编指令)
我们一般不用Unsafe类,因为其中的方法可以直接对内存进行操作,是不安全的。
我们推荐使用Atomicxxx来实现CAS机制保证线程安全。如下:
import java.util.concurrent.atomic.AtomicInteger;
/**
* CAS 使用
*/
public class CASDemo1 {
private static int number = 0;
private static AtomicInteger atomicInteger = new AtomicInteger(0);
private final static int MAX_COUNT = 100000;
public static void main(String[] args) throws InterruptedException {
// ++
Thread t1 = new Thread(() -> {
for (int i = 0; i < MAX_COUNT; i++) {
atomicInteger.getAndIncrement(); // i++
// number++;
}
});
t1.start();
// --
Thread t2 = new Thread(() -> {
for (int i = 0; i < MAX_COUNT; i++) {
atomicInteger.getAndDecrement(); // i--
// number--;
}
});
t2.start();
t1.join();
t2.join();
System.out.println("最终结果:" + atomicInteger.get());
}
}
1.3 乐观锁存在的问题:ABA问题
ABA问题代码演示:
import java.util.concurrent.atomic.AtomicInteger;
/**
* CAS ABA 演示
*/
public class ABADemo1 {
private static AtomicInteger money = new AtomicInteger(100);
public static void main(String[] args) throws InterruptedException {
//第一次点击转账(-50)
Thread t1 = new Thread(()->{
int old_money = money.get();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
money.compareAndSet(old_money,old_money+50);
});
t1.start();
//第二次点击转账(-50)[不小心点击的,因为第一次点击没有反应,所以又点了一次]
Thread t2 = new Thread(()->{
int old_money = money.get();
money.compareAndSet(old_money,old_money+50);
});
t2.start();
//给账户+50元
Thread t3 = new Thread(()->{
//执行花费1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int old_money = money.get();
money.compareAndSet(old_money,old_money+50);
});
t1.join();
t2.join();
t3.join();
System.out.println("最终账户余额:"+money.get());
}
}
ABA解决方案:
引入版本号,每次操作之后让版本号+1,执行下一步操作前,判断值和版本号,如果二者都为true才进行操作。
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* ABA 问题演示
*/
public class ABADemo2 {
private static AtomicStampedReference<Integer> money =
new AtomicStampedReference<>(100, 0);
public static void main(String[] args) throws InterruptedException {
// 第 1 次点击转账按钮(-50)
Thread t1 = new Thread(() -> {
int old_money = money.getReference(); // 先得到余额
int version = money.getStamp(); // 得到版本号
// 执行花费 2s
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
money.compareAndSet(old_money, old_money - 50, version, version + 1);
});
t1.start();
// 第 2 次点击转账按钮(-50)【不小心点击的,因为第一次点击之后没反应,所以不小心又点了一次】
Thread t2 = new Thread(() -> {
int old_money = money.getReference(); // 先得到余额
int version = money.getStamp(); // 得到版本号
money.compareAndSet(old_money, old_money - 50,
version, version + 1);
});
t2.start();
// 给账户 +50 元
Thread t3 = new Thread(() -> {
// 执行花费 1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int old_money = money.getReference();
int version = money.getStamp();
money.compareAndSet(old_money, old_money + 50,
version, version + 1);
});
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println("最终账号余额:" + money.getReference());
}
}
1.4 悲观锁
定义:总是假设最坏的情况,每次去拿数据的时候就认为别人会修改,所以每次进行访问都要进行上锁,这样被人拿数据时就会阻塞直到它拿到锁。
应用:synchronized、lock 都是悲观锁
2、公平锁&非公平锁
非公平锁:抢占式执行,有一些先来的任务还在排队,刚好释放锁的时候新来了一个任务,此时并不会通知任务队列来执行任务,而是执行新来的任务。
公平锁:所有任务来了之后先排队,线程空闲之后去任务队列按顺序执行最早任务。(排队做核酸)
3、读写锁
读写锁 (Readers-Writer Lock)顾名思义就是把一把锁分为两个部分:读锁和写锁,读锁允许多个线程同时获得,因为读本身就是线程安全的,而写锁是互斥锁,不允许多个线程同时获得,并且读写也是互斥的。ALL IN ALL :读读不互斥、读写互斥、写写互斥。
JAVA标准库提供了ReentrantReadWriteLock类,实现了读写锁
读写锁适合于“频繁读,不频繁写”的场景
import java.time.LocalDateTime;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 演示读写锁的使用
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
//创建读写锁
final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//创建读锁
final ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
//创建写锁
final ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
//创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5,5,0,
TimeUnit.SECONDS,new LinkedBlockingQueue<>());
//线程池执行任务1【读操作】
threadPool.submit(()->{
//加锁操作
readLock.lock();
try {
System.out.println("执行读锁1:"+ LocalDateTime.now());
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
readLock.unlock();
}
});
//线程池执行任务2【读操作】
threadPool.submit(()->{
//加锁操作
readLock.lock();
try {
System.out.println("执行读锁2:"+ LocalDateTime.now());
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
readLock.unlock();
}
});
//线程池执行任务3【写操作】
threadPool.submit(()->{
//加锁操作
writeLock.lock();
try {
System.out.println("执行写锁1:"+ LocalDateTime.now());
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
writeLock.unlock();
}
});
//线程池执行任务4【写操作】
threadPool.submit(()->{
//加锁操作
writeLock.lock();
try {
System.out.println("执行写锁2:"+ LocalDateTime.now());
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
writeLock.unlock();
}
});
}
}
由此可看出读操作是同时进行的,而写操作则不能同时操作
4、独占锁&共享锁
独占锁:任何时候只有一个线程能执行资源操作:synchronized、lock
共享锁:可以同时被多个线程读取,但只能被一个线程修改。比如java中的读写锁。读锁可以同时被多个线程读取,而写锁则只能被一个线程修改。
5、可重入锁&自旋锁
可重入锁指的是该线程获取了该锁之后,可以无限次的进入该锁锁住的代码。
自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。