Java并发编程-理论基础
Java并发编程-理论基础
1、什么是进程?
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
2、什么是线程?
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
线程是独立调度和分派的基本单位。线程可以为操作系统内核调度的内核线程,如Win32线程;由用户进程自行调度的用户线程,如Linux平台的POSIX Thread;或者由内核与用户进程,如Windows 7的线程,进行混合调度。
同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。
一个进程可以有很多线程,每条线程并行执行不同的任务。
在多核或多CPU,或支持Hyper-threading的CPU上使用多线程程序设计的好处是显而易见,即提高了程序的执行吞吐率。在单CPU单核的计算机上,使用多线程技术,也可以把进程中负责I/O处理、人机交互而常被阻塞的部分与密集计算的部分分开来执行,编写专门的workhorse线程执行密集计算,从而提高了程序的执行效率。
进程与线程的对比:
特性 | 进程 | 线程 |
定义 | 资源分配的基本单位 | CPU调度的基本单位 |
内存空间 | 独立内存空间 | 共享进程内存空间 |
通信 | 需要IPC机制(管道、消息队列等) | 可直接读写进程数据段 |
创建开销 | 大(需要分配独立资源) | 小(共享已有资源) |
稳定性 | 一个进程崩溃不影响其他进程 | 一个线程崩溃可能导致整个进程崩溃 |
并发性 | 进程间并发 | 线程间并发 |
3、为什么需要多线程?
从CPU、内存、I/O速度差异的角度分析
计算机系统中,CPU、内存、I/O设备的速度差异极大:
- CPU 计算速度极快(纳秒级)
- 内存 访问速度较慢(百纳秒级)
- I/O设备(磁盘、网络)更慢(毫秒级,比CPU慢百万倍)
为了合理利用CPU的高性能,计算机体系结构、操作系统和编译器分别采用了优化手段,但也引入了并发问题:
1. CPU缓存(Cache)——解决CPU与内存速度差异
- 问题:CPU计算速度远高于内存访问速度,直接读写内存会导致CPU大部分时间等待(内存墙问题)。
- 解决方案:引入多级缓存(L1/L2/L3 Cache),减少CPU访问内存的次数。
- 副作用(可见性问题):
-
- 多线程环境下,一个线程修改了缓存数据,另一个线程可能无法立即看到(缓存一致性问题)。
- 需要内存屏障(Memory Barrier)或volatile关键字保证可见性。
总结:缓存提高CPU利用率,但导致多线程间数据不可见。
2. 进程/线程分时复用——解决CPU与I/O速度差异
- 问题:I/O操作(磁盘、网络)极慢,单线程程序会让CPU大部分时间等待I/O。
- 解决方案:
-
- 进程:操作系统提供隔离的执行环境,但切换开销大。
- 线程:轻量级执行单元,共享进程资源,切换成本低。
- 多线程:当一个线程等待I/O时,CPU可以执行其他线程(提高利用率)。
- 副作用(原子性问题):
-
- 线程切换可能导致非原子操作被中断(如
i++
并非原子操作)。 - 需要锁(synchronized)或CAS(Compare-And-Swap)保证原子性。
- 线程切换可能导致非原子操作被中断(如
总结:多线程提高CPU利用率,但导致竞态条件(Race Condition)。
3. 编译器/CPU指令重排序——优化缓存利用率
- 问题:为了减少CPU等待数据的时间,编译器和CPU会优化指令执行顺序(如乱序执行)。
- 解决方案:
-
- 指令重排:调整无关指令的顺序,提高缓存命中率。
- 副作用(有序性问题):
-
- 多线程环境下,指令重排可能导致逻辑错误(如单例模式的双重检查锁失效)。
- 需要内存屏障(Memory Barrier)或
volatile
禁止重排序。
总结:指令重排提高性能,但导致多线程执行顺序不可预测。
4、线程不安全如何产生的?(Java中)
如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作的结果是不一致的。
银行账户取款问题
public class UnsafeBankAccount {private int balance; // 共享数据public UnsafeBankAccount(int initialBalance) {this.balance = initialBalance;}// 不安全的取款方法public void withdraw(int amount) {if (amount <= balance) {// 模拟一些处理时间try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}balance -= amount;System.out.println(Thread.currentThread().getName() + " 取款 " + amount + " 成功,余额: " + balance);} else {System.out.println(Thread.currentThread().getName() + " 取款 " + amount + " 失败,余额不足");}}public static void main(String[] args) {UnsafeBankAccount account = new UnsafeBankAccount(1000);// 创建多个线程同时取款for (int i = 0; i < 10; i++) {new Thread(() -> account.withdraw(800)).start();}}
}
运行结果:
问题分析
- 竞态条件(Race Condition)
-
- 多个线程同时检查余额
if (amount <= balance)
- 在检查后但实际扣款前,其他线程可能已经修改了余额
- 导致多个线程都认为可以取款,最终余额变为负数
- 多个线程同时检查余额
- 操作非原子性
balance -= amount
不是原子操作,它实际上是:
-
- java复制下载
int temp = balance;
temp = temp - amount;
balance = temp;
-
- 线程可能在中间步骤被中断
- 内存可见性问题
-
- 一个线程对balance的修改可能不会立即对其他线程可见
- 导致线程看到的是过期的balance值
线程不安全的核心原因
- 共享数据的并发访问:多个线程同时读写同一个变量
- 操作的非原子性:操作不是一次性完成的,可能被中断
- 缺乏同步机制:没有使用synchronized、Lock等同步手段
- 内存可见性问题:线程可能看不到其他线程的最新修改
5、并发三要素
在并发编程中,问题的根源可以归结为三个核心要素:可见性、原子性、有序性
可见性(visibility)
可见性问题指的是一个线程对共享变量的修改,另一个线程不能立即看到。这是由于现代计算机架构中CPU缓存与主内存之间的同步延迟导致的。
可见性问题源于现代计算机的多级存储架构:
- CPU缓存架构:每个CPU核心有自己的缓存(L1、L2),多个核心共享L3缓存
- 缓存一致性协议:如MESI协议,但存在延迟
- 编译器优化:可能将变量缓存在寄存器中而不是从内存读取
在Java内存模型(JMM)中,每个线程有自己的工作内存(可以理解为CPU缓存的抽象),导致线程对共享变量的修改可能不会立即同步到主内存,其他线程也就无法立即看到修改。
可见性完整案例
案例1:体现可见性问题的服务控制器(基础可见性问题)
package com.taiyuan.javademoone.threaddemo.demo001;import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** 修改后的ServiceController,强制体现可见性问题* 通过纯空循环和长时间运行来暴露可见性问题*/
public class ServiceController {// 无volatile修饰,保证会出现可见性问题private static boolean isRunning = true;//TODO 解决方案1、添加volatile修饰,保证可见性// private static volatile boolean isRunning = true;// TODO 解决方案2、AtomicBoolean,保证可见性// private static AtomicBoolean isRunning = new AtomicBoolean(true);// TODO 解决方案3、使用synchronized关键字,保证可见性// public static synchronized boolean isRunning() {// return isRunning;// }// public static synchronized void stopRunning() {// isRunning = false;// }// TODO 解决方案4、使用Lock,保证可见性private static final Lock lock = new ReentrantLock();public static boolean isRunning() {lock.lock();try {return isRunning;} finally {lock.unlock();}}public static void stopRunning() {lock.lock();try {isRunning = false;} finally {lock.unlock();}}// 添加一个计数器,用于验证循环执行次数private static long counter = 0;public static void main(String[] args) throws InterruptedException {// 工作线程 - 使用纯空循环Thread worker = new Thread(() -> {while (isRunning) {counter++; // 纯计数操作,无任何同步点}System.out.println("工作线程停止,循环次数: " + counter);});worker.start();// 主线程稍后停止服务Thread.sleep(3000);isRunning = false;// stopRunning();System.out.println("主线程将isRunning设置为false");// 等待工作线程结束(最多10秒)worker.join(10000);if (worker.isAlive()) {System.out.println("警告:工作线程未能正常停止!");System.out.println("最后计数: " + counter);}}
}
解决可见性问题:
方案1:使用volatile(推荐)
private static volatile boolean isRunning = true;
方案2:使用AtomicBoolean
private static AtomicBoolean isRunning = new AtomicBoolean(true);// 在循环中
while (isRunning.get()) { ... }// 设置停止
isRunning.set(false);
方案3:使用synchronized方法
private static boolean isRunning = true;public static synchronized boolean isRunning() {return isRunning;
}public static synchronized void stopRunning() {isRunning = false;
}// 使用方式
while (isRunning()) { ... }
stopRunning();
方案4:使用Lock
private static boolean isRunning = true;
private static final Lock lock = new ReentrantLock();public static boolean isRunning() {lock.lock();try {return isRunning;} finally {lock.unlock();}
}public static void stopRunning() {lock.lock();try {isRunning = false;} finally {lock.unlock();}
}
原子性(Atomicity)
原子性是指一个操作或一系列操作作为一个不可分割的整体执行,要么全部完成,要么完全不执行。在多线程环境中,非原子操作会导致竞态条件(Race Condition),产生不可预期的结果。
原子性三要素:
- 不可分割:操作过程不会被线程调度打断
- 完全成功或完全失败:没有中间状态
- 状态一致性:操作前后系统状态保持一致
原子性案例:
1、银行账户转账(经典竞态条件)
package com.taiyuan.javademoone.threaddemo.demo002;/*** BankAccount 类表示一个银行账户,用于演示多线程环境下的转账操作*/
public class BankAccount {// 定义账户余额private int balance;// 构造函数,初始化账户余额public BankAccount(int initialBalance) {this.balance = initialBalance;}/** transfer 方法用于从一个账户向另一个账户转账*/public void transfer(BankAccount dest, int amount) {// 判断账户余额是否足够if (this.balance >= amount) {// 模拟处理延迟(放大竞态窗口)try { Thread.sleep(10); } catch (InterruptedException e) {}// 扣款this.balance -= amount;// 收款dest.balance += amount;// 打印转账成功信息System.out.println(Thread.currentThread().getName() +" 转账成功: " + amount);}}/*** 获取账户余额*/public int getBalance() {return balance;}
}
2、测试类(暴露问题)
package com.taiyuan.javademoone.threaddemo.demo002;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;/*** 银行转账示例*/
public class BankTransferDemo {public static void main(String[] args) throws InterruptedException {// 创建两个账户,初始余额为1000BankAccount accountA = new BankAccount(1000);BankAccount accountB = new BankAccount(1000);// 输出初始余额System.out.println("初始余额:");System.out.println("账户A: " + accountA.getBalance());System.out.println("账户B: " + accountB.getBalance());// 创建转账线程池ExecutorService executor = Executors.newFixedThreadPool(10);// 模拟100次从A向B转账10元for (int i = 0; i < 100; i++) {executor.execute(() -> accountA.transfer(accountB, 10));}// 模拟100次从B向A转账10元for (int i = 0; i < 100; i++) {executor.execute(() -> accountB.transfer(accountA, 10));}// 关闭线程池executor.shutdown();// 等待线程池中的任务执行完毕executor.awaitTermination(1, TimeUnit.MINUTES);// 输出最终余额System.out.println("\n最终余额:");System.out.println("账户A: " + accountA.getBalance());System.out.println("账户B: " + accountB.getBalance());System.out.println("总额: " + (accountA.getBalance() + accountB.getBalance()));}
}
测试结果
结果分析:
出现总额不为2070的情况,说明发生了竞态条件导致金额不一致。
3、解决方案
1、synchronized方法同步
package com.taiyuan.javademoone.threaddemo.demo003;/*** 线程安全的银行账户类(使用synchronized解决原子性问题)*/
public class SynchronizedBankAccount {private int balance;public SynchronizedBankAccount(int initialBalance) {this.balance = initialBalance;}/*** 线程安全的转账方法** @param dest 目标账户* @param amount 转账金额* @return 转账成功返回true,否则返回false*/public boolean transfer(SynchronizedBankAccount dest, int amount) {// 按照账户对象的哈希值确定锁顺序,避免死锁SynchronizedBankAccount first = this.hashCode() < dest.hashCode() ? this : dest;SynchronizedBankAccount second = this.hashCode() < dest.hashCode() ? dest : this;// 先锁定账户1,再锁定账户2 (避免死锁)synchronized (first) {synchronized (second) {// 检查余额是否充足if (this.balance < amount) {System.out.println(Thread.currentThread().getName() +" 转账失败: 余额不足 (当前余额=" + this.balance + ")");return false;}// 模拟处理延迟try {Thread.sleep(10);} catch (InterruptedException e) {Thread.currentThread().interrupt();return false;}// 执行转账this.balance -= amount;dest.balance += amount;System.out.println(Thread.currentThread().getName() +" 转账成功: " + amount +" (转出账户余额=" + this.balance +", 目标账户余额=" + dest.balance + ")");return true;}}}/*** 获取账户余额(线程安全)*/public synchronized int getBalance() {return balance;}
}
测试:
package com.taiyuan.javademoone.threaddemo.demo003;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;/*** 银行账户转账测试类* 本类用于测试在多线程环境下,两个银行账户之间的转账操作是否能够正确执行* 通过创建两个初始余额相同的银行账户,使用固定数量的线程执行多次相互转账操作,* 最后检查两个账户的总余额是否与初始总余额相等,以此验证转账操作的线程安全性*/
public class BankTransferTest {// 初始余额常量private static final int INITIAL_BALANCE = 1000;// 每次转账金额常量private static final int TRANSFER_AMOUNT = 10;// 转账操作次数常量private static final int TRANSFER_COUNT = 100;// 线程池线程数量常量private static final int THREAD_COUNT = 10;public static void main(String[] args) throws InterruptedException {// 创建两个银行账户SynchronizedBankAccount accountA = new SynchronizedBankAccount(INITIAL_BALANCE);SynchronizedBankAccount accountB = new SynchronizedBankAccount(INITIAL_BALANCE);// 打印初始余额System.out.println("========== 初始余额 ==========");System.out.println("账户A余额: " + accountA.getBalance());System.out.println("账户B余额: " + accountB.getBalance());System.out.println("总额: " + (accountA.getBalance() + accountB.getBalance()));// 创建线程池ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);// 启动转账任务System.out.println("\n========== 开始转账 ==========");for (int i = 0; i < TRANSFER_COUNT; i++) {// A向B转账executor.execute(() -> accountA.transfer(accountB, TRANSFER_AMOUNT));// B向A转账executor.execute(() -> accountB.transfer(accountA, TRANSFER_AMOUNT));}// 关闭线程池并等待任务完成executor.shutdown();executor.awaitTermination(1, TimeUnit.MINUTES);// 打印最终余额System.out.println("\n========== 最终余额 ==========");System.out.println("账户A余额: " + accountA.getBalance());System.out.println("账户B余额: " + accountB.getBalance());// 验证总额不变int total = accountA.getBalance() + accountB.getBalance();int expectedTotal = 2 * INITIAL_BALANCE;System.out.println("总额: 实际=" + total + ", 预期=" + expectedTotal +(total == expectedTotal ? " (正确)" : " (错误!)"));}
}
运行结果:
2、ReentrantLock显式锁
package com.taiyuan.javademoone.threaddemo.demo004;import java.util.concurrent.locks.ReentrantLock;/*** BankAccount 类表示一个银行账户,使用ReentrantLock保证线程安全*/
public class BankLockAccount {// 账户余额private int balance;// 可重入锁private final ReentrantLock lock = new ReentrantLock();// 构造函数,初始化账户余额public BankLockAccount(int initialBalance) {this.balance = initialBalance;}/*** transfer 方法用于从一个账户向另一个账户转账* 使用ReentrantLock保证线程安全*/public void transfer(BankLockAccount dest, int amount) {// 确定锁获取顺序(例如通过hashCode比较)这种通过对象哈希码决定锁顺序的方法是一种常见的死锁避免策略,称为锁排序(Lock Ordering)。BankLockAccount first = this.hashCode() < dest.hashCode() ? this : dest;BankLockAccount second = this.hashCode() < dest.hashCode() ? dest : this;first.lock.lock();try {second.lock.lock();try {if (this.balance >= amount) {try { Thread.sleep(10); } catch (InterruptedException e) {}this.balance -= amount;dest.balance += amount;System.out.println(Thread.currentThread().getName() +" 转账成功: " + amount);}} finally {second.lock.unlock();}} finally {first.lock.unlock();}}/*** 获取账户余额*/public int getBalance() {lock.lock();try {return balance;} finally {lock.unlock();}}
}
测试:
package com.taiyuan.javademoone.threaddemo.demo004;import java.util.concurrent.locks.ReentrantLock;/*** BankAccount 类表示一个银行账户,使用ReentrantLock保证线程安全*/
public class BankLockAccount {// 账户余额private int balance;// 可重入锁private final ReentrantLock lock = new ReentrantLock();// 构造函数,初始化账户余额public BankLockAccount(int initialBalance) {this.balance = initialBalance;}/*** transfer 方法用于从一个账户向另一个账户转账* 使用ReentrantLock保证线程安全*/public void transfer(BankLockAccount dest, int amount) {// 确定锁获取顺序(例如通过hashCode比较)这种通过对象哈希码决定锁顺序的方法是一种常见的死锁避免策略,称为锁排序(Lock Ordering)。BankLockAccount first = this.hashCode() < dest.hashCode() ? this : dest;BankLockAccount second = this.hashCode() < dest.hashCode() ? dest : this;first.lock.lock();try {second.lock.lock();try {if (this.balance >= amount) {try { Thread.sleep(10); } catch (InterruptedException e) {}this.balance -= amount;dest.balance += amount;System.out.println(Thread.currentThread().getName() +" 转账成功: " + amount);}} finally {second.lock.unlock();}} finally {first.lock.unlock();}}/*** 获取账户余额*/public int getBalance() {lock.lock();try {return balance;} finally {lock.unlock();}}
}
3、使用AtomicReference(无锁方案)
package com.taiyuan.javademoone.threaddemo.demo0005;import java.util.concurrent.atomic.AtomicReference;/*** 使用AtomicReference实现的无锁银行账户*/
public class AtomicBankAccount {// 使用AtomicReference包装账户余额// AtomicReference<Integer>:提供对 Integer 类型值的原子操作,确保多线程环境下数据一致性。/*** AtomicReference 是 Java 中 java.util.concurrent.atomic 包的一部分,* 它提供了一种原子操作方式,用于更新引用类型的变量。* 与其他原子类(如 AtomicInteger 和 AtomicLong)类似,* AtomicReference 允许你在多线程环境下安全地对对象引用进行更新,* 而无需使用传统的同步机制(例如 synchronized 关键字)。*/private final AtomicReference<Integer> balanceRef;public AtomicBankAccount(int initialBalance) {this.balanceRef = new AtomicReference<>(initialBalance);}/*** 无锁转账实现** @param dest 目标账户* @param amount 转账金额* @return 转账成功返回true,否则返回false*/public boolean transfer(AtomicBankAccount dest, int amount) {// 参数检查if (amount <= 0) {return false;}/** CAS 是一种 硬件支持的原子指令,它在 Java 中通过 AtomicReference 和 Unsafe 类等机制实现。它的核心思想是:* 在更新一个值时,先检查该值是否与预期一致,如果一致则更新为新值,否则重试。*/// 使用CAS循环实现原子转账操作while (true) {// 获取当前转出账户余额的快照Integer current = balanceRef.get();// 检查当前余额是否足够进行转账if (current < amount) {// 余额不足时打印转账失败信息,并返回false表示转账失败System.out.println(Thread.currentThread().getName() +" 转账失败: 余额不足");return false;}// 模拟处理转账过程中的延迟,增加并发情况下冲突的可能性try {Thread.sleep((int) (Math.random() * 10));} catch (InterruptedException e) {// 当线程被中断时,设置中断标志并返回false表示转账失败Thread.currentThread().interrupt();return false;}// 使用CAS操作更新转出账户的余额if (balanceRef.compareAndSet(current, current - amount)) {// 更新目标账户余额,同样需要使用CAS操作以确保原子性while (true) {// 获取目标账户当前余额的快照Integer destCurrent = dest.balanceRef.get();// 使用CAS操作更新目标账户余额,成功则打印转账信息并返回true表示转账成功if (dest.balanceRef.compareAndSet(destCurrent, destCurrent + amount)) {System.out.println(Thread.currentThread().getName() +" 转账成功: " + amount +" (转出账户余额=" + (current - amount) +", 目标账户余额=" + (destCurrent + amount) + ")");return true;}}}// CAS操作失败则重试整个转账过程}}public int getBalance() {return balanceRef.get();}
}
测试:
package com.taiyuan.javademoone.threaddemo.demo0005;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;/*** AtomicBankTransferTest 类用于演示使用原子操作实现银行账户转账的线程安全性*/
public class AtomicBankTransferTest {// 定义初始余额常量private static final int INITIAL_BALANCE = 1000;// 定义每次转账金额常量private static final int TRANSFER_AMOUNT = 10;// 定义转账次数常量private static final int TRANSFER_COUNT = 100;// 定义线程池大小常量private static final int THREAD_COUNT = 10;/*** 主函数执行多个线程安全的转账操作* @param args 命令行参数* @throws InterruptedException 如果在等待线程池终止时被中断*/public static void main(String[] args) throws InterruptedException {// 初始化两个账户,账户A和账户B,每个账户初始余额为INITIAL_BALANCEAtomicBankAccount accountA = new AtomicBankAccount(INITIAL_BALANCE);AtomicBankAccount accountB = new AtomicBankAccount(INITIAL_BALANCE);// 打印初始余额System.out.println("初始余额:");System.out.println("账户A: " + accountA.getBalance());System.out.println("账户B: " + accountB.getBalance());// 创建固定大小的线程池ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);// 启动转账任务for (int i = 0; i < TRANSFER_COUNT; i++) {// 从账户A向账户B转账TRANSFER_AMOUNT金额executor.execute(() -> accountA.transfer(accountB, TRANSFER_AMOUNT));// 从账户B向账户A转账TRANSFER_AMOUNT金额executor.execute(() -> accountB.transfer(accountA, TRANSFER_AMOUNT));}// 关闭线程池,并等待所有任务完成executor.shutdown();// 等待所有任务完成executor.awaitTermination(1, TimeUnit.MINUTES);// 打印最终余额System.out.println("\n最终余额:");System.out.println("账户A: " + accountA.getBalance());System.out.println("账户B: " + accountB.getBalance());System.out.println("总额: " + (accountA.getBalance() + accountB.getBalance()));}
}
有序性(Ordering)
有序性(ordering):指程序执行的顺序必须符合预期,不能出现乱序的情况。在多线程环境下,由于编译器、处理器、缓存等因素的影响,程序执行的顺序可能会出现不一致的情况,导致程序出现错误。为了保证有序性,可以使用volatile关键字或者显式地使用锁来实现。同时,Java提供了happens-before规则,它可以保证在特定情况下,操作的顺序是按照预期的顺序执行的。
导致有序性问题的三大原因:
- 编译器优化重排序:在不改变单线程语义的前提下,编译器可能调整指令顺序
- 处理器指令级并行:现代CPU采用流水线、乱序执行等技术
- 内存系统重排序:由于多级缓存的存在,内存操作可能表现出乱序行为
Java提供了多种机制来保证有序性:
1. volatile关键字
volatile关键字可以:
- 禁止指令重排序(通过内存屏障实现)
- 保证变量的可见性
2. synchronized关键字
synchronized通过互斥锁保证:
- 同一时刻只有一个线程能访问同步代码块
- 在进入同步块前会清空工作内存,退出时会将变量刷新回主内存
- 禁止指令重排序
3. final关键字
正确初始化的final字段可以保证:
- 在构造函数完成初始化后对其他线程可见
- 禁止对final字段的写操作重排序到构造函数之外
4. happens-before原则
Java内存模型定义的happens-before关系保证了有序性,包括:
- 程序顺序规则
- 监视器锁规则
- volatile变量规则
- 线程启动规则
- 线程终止规则
- 线程中断规则
- 对象终结规则
- 传递性
5. 原子类
java.util.concurrent.atomic包下的原子类使用CAS操作和volatile语义保证有序性。
6、什么是happens-before原则?
Happens-Before原则是Java内存模型(JMM)的核心理论,它从语言层面定义了多线程环境中操作之间的可见性规则,为开发者提供了一种理解并发程序行为的框架。
Happens-Before关系本质上是一种可见性保证契约:
- 如果操作A happens-before 操作B,那么A的所有写操作对B都是可见的
- 这种关系可以跨线程传递
- JVM必须遵守这些规则,但可以在不违反规则的前提下进行优化
Happens-Before(先行发生)原则的定义:
- 程序次序规则(Program Order Rule):在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。
- 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。
- volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作。
- 线程启动规则(Thread Start Rule):Thread对象start()方法先行发生于此线程的每一个动作。
- 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法和Thread.isAlive()的返回值等手段检测线程是否已经终止执行。
- 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
- 对象终结规则(Finalizer Rule) :一个对象的初始化完成(构造函数结束)先行发生于它的finalize()方法的开始。
- 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。
7、volatile、synchronized 和 final详解
1. volatile关键字
特性:
- 可见性:保证变量的修改对所有线程立即可见
- 有序性:禁止指令重排序(通过内存屏障实现)
- 不保证原子性:复合操作仍需同步
适用场景:
- 单写多读的场景
- 作为状态标志位
- 双重检查锁定模式(DCL)
2. synchronized关键字
特性:
- 互斥性:同一时刻只有一个线程能进入同步块
- 可见性:同步块内的变量修改对其他线程可见
- 原子性:保证代码块内的操作不可分割
- 有序性:禁止指令重排序
使用方式:
- 同步实例方法
- 同步静态方法
- 同步代码块
3、final关键字
特性:
- 不可变性:基本类型值不可变,引用类型引用不可变
- 线程安全:正确构造的对象对所有线程可见
- 初始化安全:禁止final字段的写操作重排序到构造函数之外
使用场景:
- 常量定义
- 不可变对象
- 安全发布对象