一、核心概念
-
进程与线程
- 进程: 操作系统进行资源分配和调度的基本单位。一个进程包含多个线程,拥有独立的代码、数据和内存空间。
- 线程: 进程中的一个执行路径,是CPU调度和执行的基本单位。同一进程的线程共享进程的堆和方法区资源,但每个线程都有自己的程序计数器、虚拟机栈和本地方法栈。
-
为什么需要多线程?
- 提高 CPU 利用率: 当一个线程因 I/O操作阻塞时,CPU 可以切换到其他线程执行。
- 并发处理: 同时处理多个任务,如 Web 服务器同时响应多个用户请求。
- 更好的用户体验: 在图形界面中,使用多线程可以避免耗时操作导致界面“卡死”。
-
并发与并行
- 并发: 在单核 CPU 上,通过时间片轮转,宏观上看起来是同时执行,微观上是交替执行。
- 并行: 在多核 CPU 上,多个任务真正在同一时刻同时执行。
二、线程的创建与生命周期
- 创建线程的三种方式
- 继承
Thread类
class MyThread extends Thread{
@override
public void run(){
System.out.println("线程运行了: " + Thread.currentThread().getName());
}
}
new MyThread().start();
- 实现
Runnable接口(推荐)
class MyRunnable implements Runnable{
@override
public void run(){
System.out.println("线程运行了: " + Thread.currentThread().getName());
}
}
Thread thread = new Thread(new MyRunnable());
thread.start();
// 或者使用如下方式
new Thread(() -> System.out.println("Lambda线程")).start();
优点:避免了单继承的局限性,更适合多个线程共享一个资源的情景。
- 实现
Callable接口
class MyCallable implements Callable<String>{
@override
public String call() throws Exception{
//执行任务,并返回结果
Thread.sleep(1000);
return "任务执行完毕";
}
}
//使用
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(new MyCallable());
//获取返回值(阻塞方法)
String result = future.get();
System.out.println(result);
executor.shutdown();
优点:可以获取线程执行的返回值,并且可以抛出异常。
- 线程的生命周期(状态)
线程的状态在Thread.State枚举中定义:
- new (新建): 被创建但尚未启动。
- Runnable(可运行): 可以在 JVM 中执行或等待操作系统资源。(如 CPU)。
- Blocked(阻塞): 等待监视器锁(synchronized),试图进入一个同步块/方法。
- Waiting(等待): 无限期等待,直到被其他线程显式唤醒。例如:
Object.wait();,Thread.join()。 - Time_waiting(超时等待): 在指定时间内等待。例如:
Thread.sleep(millis),Object.wait(timeout)。 - Terminated(终止): 执行完毕。
三、核心问题与解决方案(并发编程的基石)
- 线程安全问题
- 问题根源:当多个线程共享数据,并且同时对数据进行写操作时,可能会导致数据不一致。
- 临界区: 访问共享资源的代码块。
- 同步与锁
a)synchronized关键字
- 同步代码块:
private final Object lock = new Object();
pubilc void method(){
synchronized(lock){
//临界区代码
}
}
- 同步实例方法: 锁是 当前实例对象(
this)。
public synchronized void method(){
//临界区代码
}
- 同步静态方法: 锁是当前类的 Class 对象(
MyCLass.class)。
public static synchronized void staticMethod(){
//临界区代码
}
b) java.util.concurrent.locks.Lock 接口
ReentrantLock是其主要实现类,比synchronized更灵活。
private final ReentrantLock lock = new ReentrantLock();
public void method(){
lock.lock();
try{
//临界区代码
}finally{
//释放锁
lock.unlock();
}
}
ReentrantLock优势
- 可尝试加锁:
tryLock() - 可中断枷锁:
lockInterruptibly() - 公平锁: 构造函数传入
true,按申请锁的顺序获取锁(性能有损耗)。
- 内存模型(JMM)与
volatile关键字
- JMM:规定了线程如何与主内存交互,以及线程的工作内存。
- 问题:由于线程的工作内存缓存,可能导致可见性问题(一个线程修改了变量,另一个线程看不到)和指令重排序问题。
volatile的作用:- 保证可见性: 对
volatile变量的写操作会立刻刷新到主内存,读操作会从主内存读取。 - 禁止指令重排序: 通过内存屏障实现。
- 保证可见性: 对
- 注意:
volatile不保证原子性。例如count++不是原子操作。
四、Java 并发工具包(java.util.concurrent - JUC)
这是 Java 处理高并发问题的利器。
- 原子类(java.util.concurrent.atomic)
利用 CAS(Compare-And-Swap)操作保证单个变量的原子性。
AtomicInteger count = new AtomicInteger(0);
//原子性的 i++
count.incrementAndGet();
- 线程池(ExecutorService)
为什么使用线程池?
- 降低资源损耗(重复利用已创建的线程)。
- 提高响应速度(任务到达时无需等待线程创建)
- 提高线程的可管理性
使用 Executors 工厂类创建:
//1.固定线程数的线程池
ExecutorService fixedPool = Executors.newFixedThreadPool(10);
//2.单线程线程池
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
//3.可缓存线程池(线程数可根据需要自动扩建)
ExecutorService cachedPool = Executors.newCachedThreadPool();
//4.定时任务线程池
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(5);
//提交任务
future = executor.submit(runnableOrCallable);
//关闭线程池
executor.shutdown();
推荐使用ThreadPoolExecutor构造函数手动创建,以便更精确的控制参数(核心线程数、最大线程数、队列等)
3. 并发集合类
ConcurrentHashMap:高并发下的线程安全 HashMap。CopyOnWriteArrayList:读多写少场景下的线程安全 List,写时复制。ConcurrentLinkedQueue:高效的并发队列。BlockingQueue接口及其实现(ArrayBlockingQueue,LinkedBlockingQueue):用于“生产者-消费者”模型。
- 同步工具类
CountDownLatch:一个或多个线程等待其他线程完成操作。
CountDownLatch latch = new CountDownLatch(3);
// do work···
//在主线程中,等待直到计数器为 0
latch.await();
CyclicBarrier:一组线程互相等待,到达一个公共的屏障点。
CyclicBarrier barrier = new CyclicBarrier(3,() -> System.out.println("所有线程到达屏障"));
// do work
//等待其他线程
barrier.await();
Semaphore:控制同时访问特定资源的线程数量(信号量)。
Semaphore semaphore = new Semaphore(5);//许可证数量
semaphore.acquire();//获取许可
try{
//使用资源
}finally{
//释放许可
semaphore.release();
}
五、高级主题
ThreadLocal- 为每个线程提供一个独立的变量副本,避免了共享问题。
- 常用与存储用户会话信息、数据库连接等需要线程隔离的数据。
- 死锁、活锁、饥饿
- 死锁:两个或以上的线程互相持有对方所需的资源,导致无限等待。
- 必要条件:互斥、持有并等待、不可剥夺、循环等待。
- 避免方法:按顺序获取锁、使用
tryLock尝试获取锁。
- 死锁:两个或以上的线程互相持有对方所需的资源,导致无限等待。
- CAS 与 AQS
- CAS:一种乐观锁技术,是
Atomic和很多并发工具的基础。 - AQS:
AbstractQueuedSynchronizer是,RenntrantLock,CountDownLatch等同步器的核心框架。
- CAS:一种乐观锁技术,是
- Fork/Join 框架
- JDK7 引入,用于并发执行任务,将大任务拆分为小任务(分治法),适用于 CPU 密集型任务。
六、最佳实践与总结
- 优先使用高层并发工具: 如线程池、并发集合,而不是自己手动管理
wait/notify。 - 尽量使用不可变对象: 状态不可变的对象永远是线程安全的。
- 缩小同步范围: 只对必要的代码块加锁,提高并发性。
- 谨慎使用锁: 避免死锁,注意锁的性能开销。
- 处理好中断: 线程应对中断请求做出相应。
- 使用
ExecutorService而不是new Thread()。
七、如何使用内存屏障,禁止指令重排序
什么是指令重排序?
编译器、JVM 和 CPU 为了提高性能,可能会对指令排序进行重新排序,但会保证在单线程环境下的最终结果一致性。
//可能被重排序的示例
int a = 1;
int b = 2;
//可能先执行 b=2,再执行 a=1
内存屏障类型
内存屏障阻止特定类型的重排序:
- LoadLoad 屏障:确保屏障前的 Load 操作在屏障后的 Load 操作之前完成
- StoreStore 屏障:确保屏障前的 Store 操作在屏障后的 Store 操作之前完成
- LoadStore 屏障:确保屏障前的 Load 操作在屏障后的 Store 操作之前完成
- StoreLoad屏障:全能屏障,确保屏障前的 Store 操作在屏障后的 Load 操作之前完成
Java中的内存屏障使用方式
1. 使用volatile关键字(最常用)
public class MemoryBarrierExample{
private volatile boolean flag = false;
private int data = 0;
//写线程
public void write(){
//普通写
data = 42;
//StoreStore 屏障 - 确保 data=42 对其他线程在 flag=true 之前可见
//volatile写
flag = true;
//StoreLoad 屏障 - 确保 flag=true 立即对所有线程可见
}
//读线程
public void read(){
// volatile读
if(flag){
// LoadLoad屏障 + LoadStore 屏障
//普通读,此时一定能看到 data=42
System.out.println(data);
}
}
}
volatile 的内存屏障语义:
- volatile写:前面插入 StoreStore 屏障,后面插入 StoreLoad 屏障
- volatile 读:后面插入 LoadLoad 屏障和 LoadStore 屏障
2. 使用synchronized关键字
synchronized块会自动创建内存屏障:
public class SynchronizedBarrier{
private int x = 0;
private int y = 0;
private final Object lock = new Object();
public void writer(){
synchronized(lock){
x =1;
y =2;
// 在 synchronized 块内,JVM 保证不会重排序
// 退出 synchronized 时会有 StoreLoad 屏障
}
}
public void reader(){
synchronized(lock){
//进入synchronized时会有 LoadLoad 屏障
int r1 = x;
int r2 = y;
System.out.println("x="+r1+",y="+r2);
}
}
}
3. 使用 Unsafe 类(高级用法)
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class ExplicitMemoryBarrier{
private static final Unsafe UNSAFE;
private int data = 0;
private volatile boolean ready = false;
static {
try{
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
UNSAFE =(Unsafe)field.get(null);
}catch(){
throw new RuntimeException(e);
}
}
public void write(){
data = 100;
//显式插入 StoreStore 屏障
UNSAFE.storeStoreFence();
ready = true;
//显式插入 StoreLoad 屏障
UNSAFE.storeFence(); //或者 UNSAFE.fullFence()
}
public void read(){
if(ready){
//显式插入 LoadLoad 屏障
UNSAFE.loadloadFence();
//保证看到 data = 100
System.out.println("Data:"+data);
}
}
}
4. 使用 VarHandle 类(Java9+推荐)
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
public class VarHandleBarrier{
private int data = 0;
private volatile boolean flag = false;
private static final VarHandle DATA_HANDLE;
static{
try{
DATA_HANDLE = MethodHandles.lookup().findVarHandle(VarHandleBarrier.class,"data",int.class);
}catch(Exception e){
throw new RuntimeException(e);
}
}
public void write(){
// 使用 release 语义写入 - 插入 StoreStore 屏障
DATA_HANDLE.setRelease(this,42);
flag = true;
}
public void read(){
if(flag){
// 使用acquire语义读取 - 插入 LoadLoad 屏障
int value = (int) DATA_HANDLE.getAcquire(this);
System.out.println("Value:"+value);
}
}
}
实际应用场景
- 双重检查锁定(DCL)模式
public class Singleton{
// volatile防止重排序
private static volatile Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
// 第一次检查
if(instance == null){
synchronized(Singleton.class){
// 第二次检查
if(instance == null){
//如果没有 volatile,可能发生重排序
instance = new Singleton();
// 对象的创建可能被重排序为:
// 1.分配内存
// 2.将引用赋值给 instance(此时 instance != null)
// 3.初始化对象
// volatile 防止 2 和 3 重排序
}
}
}
return instance;
}
}
- 生产者-消费者模式
public class ProducerConsumer{
private final Object [] items;
private int putPtr,takePtr,count;
private volatile boolean isRunning = true;
public ProducerConsumer(int capacity){
items = new Object[capacity];
}
public void put(Object x) throws InterruptedException{
synchronized(this){
while(count == items.length){
wait();
}
items[putPtr] = x;
if(++putPtr == items.length) putPtr = 0;
++count;
notifyAll();
}
}
public Object take() throws InterruptedException{
synchronized(this){
while(count ==0 && isRunning){
wait();
}
if(count==0) return null;
Object x = items[taskPtr];
if(++takePtr == items.length) taskPtr =0;
--count;
notifyAll();
return x;
}
}
public void stop(){
// volatile写,插入内存屏障,确保修改对所有线程立即可见
isRunning = false;
synchronized(this){
//唤醒所有等待线程
notifyAll();
}
}
}
内存屏障的底层原理
JMM(Java 内存模型)中的 happens-before 关系
public class HappensBeforeExample{
private int x = 0;
private volatile boolean v = false;
public void writer(){
x = 42; //普通写
v = true; //volatile 写
// 根据 happens-before 规则:
// x = 42 happens-before v =true
}
public void reader(){
if(v){
// volatile读
// 根据 happens-before 规则:
// v = true happens-before 这个读操作
// 因此这里一定能看到 x = 42
System.out.println(x);
}
}
}
内存屏障的实际效果
public class BarrierEffect{
private int a,b;
private volatile int volatileVar;
public void test(){
//可能会被重排序
a = 1;
b = 2;
//插入内存屏障,防止前后指令重排序
volatileVar = 3;
//不会被重排序到 volatile 写之前
int c = a + b;
}
}
最佳实践总结
- 首选 volatile:满足大多数内存可见性需求
- 负责同步使用 synchronized:需要原子性操作时使用
- 高级场景考虑显式屏障:性能极敏感或无锁算法时使用 VarHandle
- 避免过早优化:大多数情况下,JVM 自动插入的屏障已足够
- 理解 happens-before:这是理解 Java 并发的基础。
关键点:内存屏障不仅防止指令重排序,还确保内存可见性,让一个线程的修改对其他线程立即可见。

3240

被折叠的 条评论
为什么被折叠?



