乐观锁是一种并发控制的策略。在操作数据的时候,线程读取数据的时候不会进行加锁,先去查询原值,操作的时候比较原来的值,看一下是都被其他线程修改,如果没有修改则写回,否则就重新执行读取流程
悲观锁(底层是synchronized和ReentrantLock)就是考虑事情比较悲观,认为在访问共享资源的时候发生冲突的概率比较高,所以每次访问前线程都需要加锁
乐观锁底层是通过CAS机制实现的,CAS机制包含三个组件:内存地址V、预期值A和新值B
CAS的操作过程如下:
但是CAS并不完美,如果数据值一直发生改变,那么CAS会一直自旋,CPU会有巨大开销,而且CAS会有一个经典问题ABA问题
?我们捋顺一下这张流程图:
这个过程中任何线程都没有做错什么,但是值被改变了,线程一却没有办法发现,其实这样得情况出现对结果是没有任何影响的,但是我们要做到规范,所以如何防止ABA问题呢?
加标志位:搞一个自增的字段,操作一次就自增一次
例如:我们需要操作数据库,根据CAS的原则我们本来只需要查询原本值就可以,现在我们一同查出他们的标志位版本字段version
只查询原本值不能防止ABA
update table set value = newValue where value = #{oldValue}
加上标志位version
update table set value = newValue ,version = version + 1 where value = #{oldValue} and vision = #{vision}
// 判断原来的值和版本号是否匹配,中间有别的线程修改,值可能相等,但是版本号100%不一样
ReentrantLock是Java中的一个可重入锁,它的底层主要依赖于CAS和AQS(AbstractQueuedSynchronizer)队列
CAS:CAS是一种无锁算法,有三个操作数:内存值V,旧的预期值A,要修改的新值B。只有在A==V的时候,会将V值改成B,否则什么也不做。这个操作是一个原子操作,,被广泛应用于Java的底层实现
AQS队列:AQS是一个用于构建锁和同步容器的框架。AQS使用的是一个和FIFO队列,表示排队等待锁的线程
ReentrantLock的流程如下:
ReentrantLock先通过CAS尝试获取锁,如果此时锁已经被暂用,该线程加入到AQS队列中并wait
当前驱线程的锁被释放,在对手的线程就会被notify,然后继续CAS尝试获取锁
死锁指的是两个或者多个线程在执行过程中因为争夺资源而造成的一种相互等待的现象,若无外力干预,他们就将无法推进下去。下面是一段产生死锁的代码:
public class DeadlockDemo {
private static Object resource1 = new Object(); // 资源 1
private static Object resource2 = new Object(); // 资源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + " get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + " waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + " get resource2");
}
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + " get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + " waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + " get resource1");
}
}
}, "线程 2").start();
}
}
运行结果:
在这个例子中,线程一首先获取资源一的锁,然后进入休眠,这是为了让线程二有足够的时间锁住资源二,然后线程一和线程二都会尝试获取对方手中的资源,从而导致死锁
产生死锁的因素包括:
排查死锁的工具主要有几种:
使用JDK工具:JDK提供了一些工具,如jps和jstack,可以用来查看程序中的内存使用情况,线程堆栈信息等
使用可视化工具:JDK还提供了一些可视化工具,如jconsole和VisualVM,可以用来查看程序的内存使用情况、线程堆栈信息等
如:在JDK文件中bin文件夹中的jvisualvm.exe
解决死锁的方法主要是有以下几种:
在Java中,乐观锁和悲观锁的实现主要依赖于特定的类和机制:
乐观锁的实现类:
悲观锁的实现类:
除了乐观锁和悲伤锁之外,其他常用的锁策略还有以下几个:
Java中所有所有锁默认都是非公平锁,因为非公平锁效率高,但是很小几率非公平锁会产生线程饥饿问题(某个线程竞争了多次,依然没有获取到锁资源)
AtomicInteger是线程安全的,它是通过CAS算法和自旋锁来实现线程安全的
AtomicInteger属于乐观锁类型,乐观锁的概念是不加锁去完成某一项操作,如果冲突就重试,直到成功为止
AtomicInteger存在ABA问题,就是线程一获取到 AtomicInteger 的 value 为 A, 在准备做修改之前,线程二对 AtomicInteger 的 value 做了两次操作,一次是将值修改为 B,然后又将值修改为原来的 A. 此时线程一进行 CAS 操作,发现内存中的值依旧是 A, 更新成功
解决AtomicInteger的ABA问题,可以使用AtomicStampedReference,AtomicStampedReference内部维护了一个Pair对象,存储了value值和一个版本号,每次更新除了value还会更新版本号,这样就可以避免ABA问题
import java.util.concurrent.atomic.AtomicStampedReference;
public class AtomicStampedReferenceExample {
public static void main(String[] args) {
//创建一个初始值为100,初始版本号为0的AtomicStampedReference对象
AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<>(100, 0);
//获取当前值和版本号
int currentValue = atomicStampedRef.getReference();
int currentStamp = atomicStampedRef.getStamp();
System.out.println("当前版本号:" + currentStamp);
System.out.println("当前值:" + currentValue);
//尝试将值从100更新为200,版本号加1
int newStamp = currentStamp + 1;
boolean success = atomicStampedRef.compareAndSet(currentValue, 200, currentStamp, newStamp);
System.out.println("更新结果: " + success);
//获取更新后的值和版本号
int updatedValue = atomicStampedRef.getReference();
int updatedStamp = atomicStampedRef.getStamp();
System.out.println("更新后的值: " + updatedValue + ",更新后的版本号:" + updatedStamp);
}
}
Semaphore也被称为信号量,是一种同步工具,可以用来先致并发访问的数量,或者作为一种许可机制来管理资源的使用
Semaphore就好比停车场的保安,可以控制车位的使用资源。比如来了五辆车,只有两个车位,门卫可以先放两辆车进去,等有车为空出来,再让后面的车进入,实现代码如下:
import java.time.LocalTime;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.LinkedBlockingQueue;
public class SemaphoreDemo {
public static void main(String[] args) {
//同时只允许两个线程访问
Semaphore semaphore = new Semaphore(2);
ThreadPoolExecutor semaphoreThread = new ThreadPoolExecutor(10, 50, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
for (int i = 0; i < 5; i++) {
semaphoreThread.execute(() -> {
try {
//堵塞获取许可
semaphore.acquire();
System.out.println("Thread:" + Thread.currentThread().getName() + " " + LocalTime.now());
TimeUnit.SECONDS.sleep(2);
// 释放许可
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
semaphoreThread.shutdown();
}
}
运行结果:
Semaphore是基于AQS(AbstractQueueSynchronized)来实现的,它使用一个同步队列来管理等待许可的线程。Semaphore内部维护了一个计算器,记录着可用的许可数量。每次线程调用acquire()方法的时候,Semaphore会将计数器减一,如果计数器小于等于零,则线程会被加入到同步队列中阻塞等待。当线程调用release()方法释放许可时,Semaphore会将计数器加一,并唤醒一个等待的线程
CountDownLatch和CyclicBarrier都是Java中并发编程的同步辅助类,他们都用于多线程之间协调,但是它们的使用场景和工作方式有所不同:
为了让大家更能理解它们的区别,我用简洁的语言复述以下:
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(4);
for (int i = 1; i <= 4; i++) {
final int taskNum = i;
new Thread(() -> {
System.out.println("Task " + taskNum + " finished");
latch.countDown();
}).start();
}
latch.await();
System.out.println("All tasks finished, start task A");
}
}
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(4, () -> System.out.println("All tasks arrived at the barrier, start task A"));
for (int i = 1; i <= 4; i++) {
final int taskNum = i;
new Thread(() -> {
try {
System.out.println("Task " + taskNum + " arrived at the barrier");
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
总的来说,CountDownLatch
是一次性的,CyclicBarrier
是可以重复使用的。CountDownLatch
的计数器只能做减法,CyclicBarrier
的计数器可以重置为初始值。