JAVA线程
一、多线程基础
进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元。一个进程中可以包含多个线程,这些线程共享进程的资源。
并发是指处理器通过时间片轮转等方式,在宏观上同时处理多个任务,但微观上这些任务实际上是交替执行的。并行则是指多个处理器核心真正地同时执行多个任务。在多核CPU架构下,多线程不仅能提高程序响应速度(将耗时操作放入后台线程),还能充分利用多核计算能力。
WIN系统(以及所有现代操作系统)实现多线程确实采用了时间片轮转的调度机制。即使在单核CPU上,也可以通过快速切换执行不同的线程,给人“同时”进行的错觉。在多核CPU上,多个线程可以真正并行执行。
二、创建线程的方式
多线程的具体表现形式如下,你执行后就能够发现主线程和输出abc的线程是齐头并进的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class Mythread extends Thread{ public void run() { System.out.println("abc"); } }
public class Th{
public static void main(String[] args) { Mythread t = new Mythread(); t.start (); System.out.println("1"); System.out.println("2"); System.out.println("3"); System.out.println("4"); }
}
|
1. 继承Thread类
通过继承Thread
类并重写run
方法(该方法为线程的入口点)来创建线程。调用start()
方法(而非直接调用run
方法)来启动线程,start()
方法会启动一个新的执行线程并调用run
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class MyThread extends Thread { @Override public void run() { System.out.println("MyThread is running"); } }
public class Test { public static void main(String[] args) { MyThread t = new MyThread(); t.start(); System.out.println("Main thread"); } }
|
注意:直接调用run()
方法只会像普通方法一样在当前线程执行,而不会启动新线程。
2. 实现Runnable接口(推荐)
实现Runnable
接口的类并非线程类,而是任务类。需要将Runnable
实例作为参数传递给Thread
构造函数来创建线程。这种方式更灵活,避免了单继承的限制,也更符合面向对象的思想。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class MyRunnable implements Runnable { @Override public void run() { System.out.println("MyRunnable is running"); } }
public class Test { public static void main(String[] args) { MyRunnable task = new MyRunnable(); Thread t = new Thread(task); t.start(); System.out.println("Main thread"); } }
|
三、线程的状态与生命周期
Java线程在其生命周期中可能处于以下几种状态(定义在Thread.State
枚举中):
状态 |
说明 |
NEW (新建) |
线程被创建但尚未调用start() 方法 |
RUNNABLE (可运行) |
调用start() 后,线程正在JVM中执行或等待系统资源(如CPU) |
BLOCKED (阻塞) |
线程被阻塞,等待获取监视器锁(如进入synchronized 块) |
WAITING (等待) |
线程进入等待状态,需要其他线程显式唤醒(如Object.wait() ) |
TIMED_WAITING (超时等待) |
线程在指定的时间内等待(如Thread.sleep(time) ) |
TERMINATED (终止) |
线程已执行完毕(run() 方法完成) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| stateDiagram-v2 direction LR [*] --> NEW NEW --> RUNNABLE : start() RUNNABLE --> TERMINATED : run()方法执行完毕 RUNNABLE --> BLOCKED : 等待进入synchronized块/方法 BLOCKED --> RUNNABLE : 获取到锁 RUNNABLE --> WAITING : Object.wait()<br/>Thread.join()(无参) WAITING --> RUNNABLE : Object.notify()/notifyAll() RUNNABLE --> TIMED_WAITING : Thread.sleep(time)<br/>Object.wait(timeout)<br/>Thread.join(timeout) TIMED_WAITING --> RUNNABLE : 超时/被中断/被唤醒
|
四、线程同步
当多个线程访问共享资源时,可能会发生数据竞争(Data Race),导致数据不一致或其他不可预料的错误。Java提供了多种机制来保证线程安全。
1. synchronized 关键字
synchronized
是Java中最基本的同步机制,它可以同步代码块或方法。
1 2 3 4 5
| public void method() { synchronized (lockObject) { } }
|
同步实例方法:锁是当前实例对象(this
)。
1 2 3
| public synchronized void method() { }
|
同步静态方法:锁是当前类的Class对象(如MyClass.class
)。
1 2 3
| public static synchronized void method() { }
|
2. Lock 接口
从Java 5开始,提供了java.util.concurrent.locks.Lock
接口(如ReentrantLock
),它提供了比synchronized
更灵活的锁操作,如尝试非阻塞获取锁、可中断的获取锁以及超时获取锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock;
public class Counter { private final Lock lock = new ReentrantLock(); private int count = 0;
public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } } }
|
synchronized 与 Lock 的对比
特性 |
synchronized |
Lock |
实现级别 |
JVM层面,原生语法 |
Java API层面 |
锁的释放 |
自动释放 |
必须手动调用unlock() |
灵活性 |
相对简单,不灵活 |
非常灵活,支持多种获取锁的方式 |
性能 |
早期性能较差,后续版本有优化 |
在高竞争环境下性能可能更好 |
读写分离 |
不支持 |
支持(ReadWriteLock ) |
五、线程间通信
线程间通信主要依靠等待/通知机制,核心方法是Object
类的wait()
, notify()
, notifyAll()
。这些方法必须在synchronized
同步块或同步方法内调用,否则会抛出IllegalMonitorStateException
异常。
wait()
:使当前线程释放锁并进入等待(WAITING)状态,直到其他线程调用该对象的notify()
或notifyAll()
方法。
notify()
:随机唤醒一个在该对象上等待的线程。
notifyAll()
:唤醒所有在该对象上等待的线程。
一个典型的生产者-消费者模型示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| import java.util.LinkedList;
public class ProducerConsumerExample { private final LinkedList<Integer> buffer = new LinkedList<>(); private final int CAPACITY = 5; private final Object lock = new Object();
public void produce() throws InterruptedException { int value = 0; while (true) { synchronized (lock) { while (buffer.size() == CAPACITY) { lock.wait(); } buffer.add(value++); System.out.println("Produced: " + value); lock.notifyAll(); } Thread.sleep(1000); } }
public void consume() throws InterruptedException { while (true) { synchronized (lock) { while (buffer.isEmpty()) { lock.wait(); } int value = buffer.removeFirst(); System.out.println("Consumed: " + value); lock.notifyAll(); } Thread.sleep(1000); } } }
|
六、线程死锁
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉,这些线程都将无法继续执行下去
死锁产生的四个必要条件(缺一不可):
互斥条件:一个资源每次只能被一个线程使用。
请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:线程已获得的资源,在未使用完之前,不能被其他线程强行剥夺。
循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
避免死锁的常见方法:
避免嵌套锁:尽量只获取一个锁。如果必须多个,确保所有线程以相同的顺序获取锁。
使用定时锁:使用Lock
接口的tryLock(long time, TimeUnit unit)
方法,尝试获取锁超时则失败。
减少同步范围:减小临界区范围,尽快释放锁。
七、线程控制(补充)
join()
方法:等待目标线程执行完毕,当前线程才继续执行。可用于线程间同步。
1 2 3 4 5 6
| Thread t = new Thread(() -> { }); t.start(); t.join(); System.out.println("Thread t has finished.");
|
sleep()
方法:让当前正在执行的线程休眠(暂停执行)指定的时间,但不会释放锁。
yield()
方法:提示调度器当前线程愿意让出CPU,但调度器可以忽略这个提示。
interrupt()
方法:中断目标线程。如果目标线程因wait
, sleep
, join
而阻塞,会抛出InterruptedException
并清除中断状态;否则,只是设置一个中断标志位,需要线程自己检查(isInterrupted()
)并处理
注意:suspend()
, resume()
, stop()
等方法已被废弃,因为它们可能导致资源死锁和数据不一致,非常不安全。
八、线程池
频繁创建和销毁线程开销很大。线程池可以预先创建一定数量的线程,放入池中备用。有任务时,从池中取线程执行;任务完成后,线程返回池中复用。这减少了创建销毁的开销,可以控制并发数,更有效地管理线程。
Java通过java.util.concurrent.ExecutorService
接口及其实现类(如ThreadPoolExecutor
)来提供线程池功能。通常使用Executors
工具类来创建常见的线程池:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;
public class ThreadPoolExample { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(4);
for (int i = 0; i < 10; i++) { int taskId = i; executor.submit(() -> { System.out.println("Executing task " + taskId + " via " + Thread.currentThread().getName()); }); }
executor.shutdown(); } }
|