
JAVA并发
volatile
Java 内存模型 所有的变量都存储在主内存中。 每个线程也会有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程不能直接读写主内存中的变量。
然而,Java 内存模型会带来一个新的问题,那就是内存可见性问题,也就是当某个线程修改了主内存中共享变量的值之后,其他线程不能感知到此值被修改了,它会一直使用自己工作内存中的“旧值”,这样程序的执行结果就不符合我们的预期了,这就是内存可见性问题
所以,volatile的作用就是强制线程直接读取主内存的变量值。
创建线程的几种方式
java
public class MyThread extends Thread{//继承Thread类
public void run(){
//重写run方法
}
}
public class Main {
public static void main(String[] args){
new MyThread().start();//创建并启动线程
}
}
java
/*实现Runnable接口*/
private class UseRun implements Runnable{
@Override
public void run() {
System.out.println("I am implements Runnable");
}
public static void main(String[] args) {
UseRun useRun = new UseRun();
new Thread(useRun).start();
}
}
java
/*实现Callable接口,允许有返回值*/
private static class UseCall implements Callable<String>{
@Override
public String call() throws Exception {
System.out.println("I am implements Callable");
return "CallResult";
}
public static void main(String[] args) {
//第一步,先new一个类
UseCall useCall = new UseCall();
//第二步,用FutureTask类包装下,注意:FutureTask类继承了Runnable接口
FutureTask<String> futureTask = new FutureTask<>(useCall);
//第三步,启动
new Thread(futureTask).start();
//第四步,得到线程返回的结果,注意这里是阻塞式的。必须执行完线程才能拿到结果
System.out.println(futureTask.get());
}
}
AQS
AQS全称是AbstractQueuedSynchronizer,AQS是多线程同步器,它是J.U.C包中多个组件的底层实现,如Lock、CountDownLatch、Semaphore等都用到了AQS。
从本质上来说,AQS提供了两种锁机制,分别是排它锁和共享锁。
排它锁就是存在多线程竞争同一共享资源时,同一时刻只允许一个线程访问该共享资源,也就是多个线程中只能有一个线程获得锁资源,比如Lock中的ReentrantLock重入锁实现就是用到了AQS中的排它锁功能。
共享锁也称为读锁,就是在同一时刻允许多个线程同时获得锁资源,比如CountDownLatch和Semaphore都是用到了AQS中的共享锁功能。
原理:
用一个 volatile int 类型的 state 变量来表示同步状态,通过一个内置的 FIFO 双向队列来完成资源获取线程的排队工作。
state是锁的灵魂,判断是否获取锁成功,将所有暂时获取不到锁/资源的线程封装成 Node 节点,放入队列中排队等待。
比如:ReentrantLock: state 表示独占锁的持有计数(0=未锁定,>0=被锁定,>1=重入次数)。
CAS
CAS(Compare-And-Swap),即比较并交换,是一种无锁的、乐观的并发原子操作。它保证了一个线程在更新一个变量时,只有当变量的预期值和内存中的实际值相同时,才会将新值写入。
Java中主要通过 sun.misc.Unsafe
类提供的底层CAS方法(JVM内部使用)来实现。开发者最常接触的是 java.util.concurrent.atomic
包下的原子类,
例如:
- AtomicInteger
- AtomicLong
- AtomicReference
示例:AtomicInteger
的 incrementAndGet()
java
AtomicInteger count = new AtomicInteger(0);
public void safeIncrement() {
// 内部基于CAS实现,即使多线程调用也安全
count.incrementAndGet();
}
它的内部实现类似于一个自旋循环:
java
public final int incrementAndGet() {
for (;;) { // 自旋
int current = get(); // 获取当前值 A
int next = current + 1; // 计算新值 B
if (compareAndSet(current, next)) // 核心CAS操作
return next; // 成功则返回
} // 失败则循环重试
}
优点:
- 高性能:避免了重量级锁(如synchronized)带来的线程阻塞、上下文切换的开销,在竞争不激烈的情况下性能远超加锁。
- 无死锁:由于是乐观重试,不存在死锁问题。
缺点:
- ABA问题:一个值初始是A,中途被改为B,后又改回A。CAS检查时会误以为它没变,从而成功操作。解决方案是使用带版本号的原子引用类 AtomicStampedReference。
- 循环时间长开销大:在高竞争环境下,如果线程一直重试,会空耗CPU资源。
ABA 问题
解决 ABA 问题的一种方法是使用带版本号的 CAS,也称为双重 CAS(Double CAS)或者版本号 CAS。具体来说,每次进行 CAS 操作时,不仅需要比较要修改的内存地址的值与期望的值是否相等,还需要比较这个内存地址的版本号是否与期望的版本号相等。如果相等,才进行修改操作。这样,在修改后的值后面追加上一个版本号,即使变量的值从 A 变成了 B 再变成了 A,版本号也会发生变化,从而避免了误判。
以下是一个使用 AtomicStampedReference 来解决 ABA 问题的示例代码:
java
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABADemo {
private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<>(1, 0);
public static void main(String[] args) throws InterruptedException {
System.out.println("初始值:" + atomicStampedRef.getReference() + ",版本号:" + atomicStampedRef.getStamp());
// 线程 1 先执行一次 CAS 操作,期望值为 1,新值为 2,版本号为 0
Thread thread1 = new Thread(() -> {
int stamp = atomicStampedRef.getStamp();
atomicStampedRef.compareAndSet(1, 2, stamp, stamp + 1);
});
// 线程 2 先 sleep 1 秒,让线程 1 先执行一次 CAS 操作,然后再执行一次 CAS 操作,期望值为 2,新值为 1,版本号为 1
Thread thread2 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int stamp = atomicStampedRef.getStamp();
atomicStampedRef.compareAndSet(2, 1, stamp, stamp + 1);
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("最终值:" + atomicStampedRef.getReference() + ",版本号:" + atomicStampedRef.getStamp());
}
}
以上程序的执行结果为:
初始值:1,版本号:0
最终值:1,版本号:2