Java 多线程

一、核心概念

  1. 进程与线程

    • 进程: 操作系统进行资源分配和调度的基本单位。一个进程包含多个线程,拥有独立的代码、数据和内存空间。
    • 线程: 进程中的一个执行路径,是CPU调度和执行的基本单位。同一进程的线程共享进程的堆和方法区资源,但每个线程都有自己的程序计数器、虚拟机栈和本地方法栈。
  2. 为什么需要多线程?

    • 提高 CPU 利用率: 当一个线程因 I/O操作阻塞时,CPU 可以切换到其他线程执行。
    • 并发处理: 同时处理多个任务,如 Web 服务器同时响应多个用户请求。
    • 更好的用户体验: 在图形界面中,使用多线程可以避免耗时操作导致界面“卡死”。
  3. 并发与并行

    • 并发: 在单核 CPU 上,通过时间片轮转,宏观上看起来是同时执行,微观上是交替执行。
    • 并行: 在多核 CPU 上,多个任务真正在同一时刻同时执行。

二、线程的创建与生命周期

  1. 创建线程的三种方式
  • 继承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();

优点:可以获取线程执行的返回值,并且可以抛出异常。

  1. 线程的生命周期(状态)
    线程的状态在Thread.State枚举中定义:
  • new (新建): 被创建但尚未启动。
  • Runnable(可运行): 可以在 JVM 中执行或等待操作系统资源。(如 CPU)。
  • Blocked(阻塞): 等待监视器锁(synchronized),试图进入一个同步块/方法。
  • Waiting(等待): 无限期等待,直到被其他线程显式唤醒。例如:Object.wait();Thread.join()
  • Time_waiting(超时等待): 在指定时间内等待。例如:Thread.sleep(millis)Object.wait(timeout)
  • Terminated(终止): 执行完毕。

三、核心问题与解决方案(并发编程的基石)

  1. 线程安全问题
  • 问题根源:当多个线程共享数据,并且同时对数据进行操作时,可能会导致数据不一致。
  • 临界区: 访问共享资源的代码块。
  1. 同步与锁
    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,按申请锁的顺序获取锁(性能有损耗)。
  1. 内存模型(JMM)与volatile关键字
  • JMM:规定了线程如何与主内存交互,以及线程的工作内存。
  • 问题:由于线程的工作内存缓存,可能导致可见性问题(一个线程修改了变量,另一个线程看不到)和指令重排序问题。
  • volatile的作用:
    • 保证可见性:volatile变量的写操作会立刻刷新到主内存,读操作会从主内存读取。
    • 禁止指令重排序: 通过内存屏障实现。
  • 注意:volatile 不保证原子性。例如 count++不是原子操作。

四、Java 并发工具包(java.util.concurrent - JUC)

这是 Java 处理高并发问题的利器。

  1. 原子类(java.util.concurrent.atomic)
    利用 CAS(Compare-And-Swap)操作保证单个变量的原子性。
AtomicInteger count = new AtomicInteger(0);
//原子性的 i++
count.incrementAndGet();
  1. 线程池(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接口及其实现(ArrayBlockingQueueLinkedBlockingQueue):用于“生产者-消费者”模型。
  1. 同步工具类
  • 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();
}

五、高级主题

  1. ThreadLocal
    • 为每个线程提供一个独立的变量副本,避免了共享问题。
    • 常用与存储用户会话信息、数据库连接等需要线程隔离的数据。
  2. 死锁、活锁、饥饿
    • 死锁:两个或以上的线程互相持有对方所需的资源,导致无限等待。
      • 必要条件:互斥、持有并等待、不可剥夺、循环等待。
      • 避免方法:按顺序获取锁、使用tryLock尝试获取锁。
  3. CAS 与 AQS
    • CAS:一种乐观锁技术,是Atomic和很多并发工具的基础。
    • AQSAbstractQueuedSynchronizer是,RenntrantLockCountDownLatch等同步器的核心框架。
  4. 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);
		}
	}
}

实际应用场景
  1. 双重检查锁定(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;
	}
}
  1. 生产者-消费者模式
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;
	}
}
最佳实践总结
  1. 首选 volatile:满足大多数内存可见性需求
  2. 负责同步使用 synchronized:需要原子性操作时使用
  3. 高级场景考虑显式屏障:性能极敏感或无锁算法时使用 VarHandle
  4. 避免过早优化:大多数情况下,JVM 自动插入的屏障已足够
  5. 理解 happens-before:这是理解 Java 并发的基础。

关键点:内存屏障不仅防止指令重排序,还确保内存可见性,让一个线程的修改对其他线程立即可见。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

菜鸟特工007

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值