Skip to content

多线程

约 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();
    }
}
这个代码无法结束运行,流程大概如下: * main函数开始执行 * thread1线程开始执行,获取lock1的锁,再试图获取lock2的锁,但是lock2的锁已经被thread获取了,所以thread就开始等待 * thread2线程随后开始执行,先获取lock2的锁,再试图获取lock1的锁,同理thread2会等待thread1释放lock1

因而程序中出现了死锁,死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。

ReentrantLock

ReentrantLockjava.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 是一个接口,提供了线程池的管理功能,常用的实现类有 ThreadPoolExecutorScheduledThreadPoolExecutor

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();
    }
}

线程间通信

waitnotify

这个是针对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对象来实现waitnotify的功能。用法差不多,提供了await()signal()signalAll():方法。