多线程
约 1390 个字 138 行代码 预计阅读时间 6 分钟
Java的多线程之所以要单独出来,主要是因为这个机制特别重要,web编程需要用到
进程与线程
在计算机中,我们把一个任务称作一个进程,某些进程中还需要执行多个子任务,叫做线程,所以一个进程可以包含一个或多个线程,但至少有一个线程。此外,还有一组概念叫并行与并发,并行是多个任务同时执行,依赖多核CPU,并发是多个任务交替执行,单核CPU也能实现。多进程常用于并行,适合CPU密集型任务,多线程常用于并发,适合I/O密集型任务。
Java语言内置了多线程支持:一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()
方法,在main()
方法内部,我们又可以启动多个线程。此外,JVM还有负责垃圾回收的其他工作线程等。
创建新线程
在 Java 中,有两种主要的方式来创建线程:
继承 Thread 类
创建一个类并继承 Thread 类,重写 run() 方法,定义线程执行的任务,创建该类的实例并调用 start() 方法启动线程。
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread is running");
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 启动线程
}
}
实现 Runnable 接口
创建一个类并实现 Runnable 接口,实现 run() 方法,定义线程执行的任务,创建 Thread 对象并将 Runnable 实例作为参数传递,调用 start() 方法启动线程。
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Thread is running");
}
}
public class Main {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start(); // 启动线程
}
}
线程调度由操作系统决定,程序本身无法决定调度顺序,因为不同线程是同时执行的,不过使用Thread.sleep()
可以把当前线程暂停一段时间
线程同步
当多个线程同时访问共享资源时,可能会导致数据不一致的问题。为了避免这种情况,Java 提供了同步机制
synchronized
关键字
可以用于方法或代码块,确保同一时间只有一个线程可以执行该代码,也就是不允许同时读写
class Counter {
private int count = 0;
// 实际上锁住的对象是this
//这种写法等价于
/*
public void increment(){
synchronized(this){
count++;
}
}
*/
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
死锁
Java的线程锁是可重入的锁,JVM允许同一个线程重复获取同一个锁。由于Java的线程锁是可重入锁,所以,获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录+1,每退出synchronized块,记录-1,减到0的时候,才会真正释放锁。不过,一个线程可以获取一个锁后,再继续获取另一个锁,在获取多个锁的时候,不同线程获取多个不同对象的锁可能导致死锁。
死锁(Deadlock)是多线程编程中的一种常见问题,指的是两个或多个线程在执行过程中,因为争夺资源而造成的一种互相等待的现象,导致这些线程都无法继续执行下去。死锁通常发生在多个线程需要同时持有多个锁的情况下。
public class DeadlockExample {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1: Holding lock 1...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1: Waiting for lock 2...");
synchronized (lock2) {
System.out.println("Thread 1: Holding lock 1 and lock 2...");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2: Holding lock 2...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2: Waiting for lock 1...");
synchronized (lock1) {
System.out.println("Thread 2: Holding lock 2 and lock 1...");
}
}
});
thread1.start();
thread2.start();
}
}
因而程序中出现了死锁,死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。
ReentrantLock
ReentrantLock
是 java.util.concurrent.locks
包中的一个类,提供了比 synchronized
更灵活的锁机制,和synchronized
不同的是,ReentrantLock
可以尝试获取锁。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Counter {
private int count = 0;
private Lock lock = new ReentrantLock();
//这里最多等待 1s,超过1s tryLock返回false
public void increment() {
if(lock.tryLock(1, TimeUnit.SECONDS)){
try {
count++;
} finally {
lock.unlock();
}
}
}
public int getCount() {
return count;
}
}
ReadWriteLock
其实读是可以同时进行的,使用ReadWriteLock可以实现只允许一个线程写入(其他线程既不能写入也不能读取),没有写入时,多个线程允许同时读。
在Java中,赋值操作本身是原子的,这意味着对于基本数据类型(如int、boolean等)和引用类型的赋值,JVM会确保这些操作在单个步骤内完成,不会被其他线程打断。因此,在单次赋值操作中,不需要额外的同步机制。
如果一个类被设计为允许多线程正确访问,我们就说这个类就是“线程安全”的(thread-safe),一个类没有特殊说明,默认不是thread-safe。
线程池
为了更高效地管理线程,Java 提供了线程池机制。线程池可以复用线程,减少线程创建和销毁的开销。
ExecutorService
ExecutorService
是一个接口,提供了线程池的管理功能,常用的实现类有 ThreadPoolExecutor
和 ScheduledThreadPoolExecutor
。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
Runnable task = new MyRunnable();
executor.execute(task);
}
executor.shutdown();
}
}
线程间通信
wait
与notify
这个是针对synchronized
来使用的,wait()
方法使当前线程进入等待状态并释放锁,直到其他线程调用该对象的 notify()
或 notifyAll()
方法,或者指定的超时时间到达。
notify()
方法唤醒在该对象上等待的单个线程。如果有多个线程在等待,具体唤醒哪一个线程是不确定的,取决于操作系统的调度。
notifyAll()
方法唤醒在该对象上等待的所有线程。所有被唤醒的线程会竞争锁,最终只有一个线程能够获取锁并继续执行。
class SharedResource {
private boolean isReady = false;
public synchronized void waitForReady() throws InterruptedException {
while (!isReady) {
wait(); // 等待直到资源准备好
}
}
public synchronized void setReady() {
isReady = true;
notifyAll(); // 唤醒所有等待的线程
}
}
condition
对于ReentrantLock
,可以使用使用Condition
对象来实现wait
和notify
的功能。用法差不多,提供了await()
,signal()
和signalAll():
方法。