Administrator
发布于 2025-11-17 / 10 阅读
0
0

Java 多线程

线程的生命周期

  • 就绪状态:就绪状态的线程又叫做可运行状态,表示当前线程具有抢夺CPU时间片的权力(CPU时间片就是执行权)。
  • 运行状态:run方法的开始执行标志着这个线程进入运行状态,当之前占有的CPU时间片用完之后,会重新回到就绪状态继续抢夺CPU时间片,当再次抢到CPU时间之后,会重新进入run方法接着上一次的代码继续往下执行。
  • 阻塞状态:当一个线程遇到阻塞事件,例如接收用户键盘输入,或者sleep方法等,此时线程会进入阻塞状态,阻塞状态的线程会放弃之前占有的CPU时间片。之前的时间片没了需要再次回到就绪状态抢夺CPU时间片。
  • 锁池:在这里找共享对象的对象锁线程进入锁池找共享对象的对象锁的时候,会释放之前占有CPU时间片,有可能找到了,有可能没找到,没找到则在锁池中等待,如果找到了会进入就绪状态继续抢夺CPU时间片。(这个进入锁池,可以理解为一种阻塞状态)

线程的控制

// 使当前线程让步,重新回到争夺CPU执行权的队列中
// 这个API只是一个标志,具体还得看底层怎么实现
// 实际上好像也没有合适的使用场景。
static void yield();

// 使当前正在执行的线程停留指定的毫秒数
static void sleep(long ms);

// 等被调用的这个线程执行结束后再执行当前线程
// 如果自己调用自己的join()方法会造成死锁
void join();

// 中断线程执行。实际上并不会立刻中断线程的执行,只是将中断标志设置为true
// 线程可以通过 Thread.currentThread().isInterrupted()来检查这个标志自己决定退出还
// 是继续执行。类似的API是stop(),可以立即中断线程,但是可能会导致不可预知的错误和资源未释
// 放等问题。已经不推荐这样使用。
void interrupt();

线程间通信

wait & notify

wait()方法必须在拿到锁对象之后,在同步代码块中调用,否则会出现异常。在调用wait()之后,线程会停止执行进入等待状态同时释放对象锁。
notify和notifyAll是通知当前对象锁调用wait()状态下的线程。这些线程唤醒后会再次竞争这个对象锁。

通过管道实现通信

一个线程发送数据到输出管道,另一个线程从输入管道中读取数据。PipeInputStream , PipeOutStream, PipeReader , PipedWriter。

import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;

public class Test {
    public static void main(String[] args) throws IOException {
        /*定义管道字节流*/
        PipedInputStream  inputStream = new PipedInputStream();
        PipedOutputStream outputStream = new PipedOutputStream();

        inputStream.connect(outputStream);

        /*创建两个线程向管道流中读写数据*/
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    writeData(outputStream);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
      
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    readData(inputStream);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();

    }
    /**
     * 定义方法向管道流中写入数据
     */
    public static void writeData(PipedOutputStream out) throws IOException {
        /*把0-100 之间的数写入管道中*/
        for (int i = 0; i < 100; i++) {
            String data = "-" + i;
            out.write(data.getBytes()); //把字节数组写入到输出管道流中
        }
        out.close();
    }

    /**
     * 定义方法从管道中读取数据
     */
    public static void readData(PipedInputStream input) throws IOException {
        /*从管道中读取0-100*/
        byte[] bytes = new byte[1024];
      //返回读到的字节数,如果没有读到任何数据返回-1
        while(input.read(bytes) != -1){
            //把bytes数组中从0开始到len个字节转换为字符串打印出来
            System.out.println(new String(bytes,0,len));
        }
        input.close();
    }
}

线程安全

当多个线程对同一个对象的实例变量,做写(修改)的操作时,可能会受到其他线程的干扰,发生线程安全的问题。

  • 原子性:线程对一个数据的操作对于其他线程来说应该是不可分割的,要么没做,要么做完了。
  • 可见性:一个线程对共享数据完成修改之后,其他线程可能无法立刻得到这个更新后的结果。
  • 有序性:涉及到底层的指令的重排序问题,指令可能乱序执行。

以下代码实例可以详细说明指令重排的问题

public class Singleton {

    private static Singleton instance = null;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    // new Singleton()过程可以分为3步
                    // 1、为Singleton对象分配内存空间
                    // 2、初始化Singleton对象
                    // 3、将instance指向Singleton的内存空间
                    // 指令重排之后可能的顺序是 1 => 3 => 2
                    // 在这种情况下A线程还没有完成初始化Singleton对象,B线程就可能直接使用instance,会出现问题
                    // 可以用volatile修饰保证原子性,防止指令重排
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

线程访问同步方法

场景执行顺序备注
两个线程同时访问一个对象的同步方法串行执行
两个线程访问的是两个对象的同步方法并行执行因为两个线程持有的是各自的对象锁,互补影响。
两个线程访问的是synchronized的static方法串行执行持有一个类锁
同时访问同步方法和非同步方法并行执行无论是同一对象还是不同对象,普通方法都不会受到影响
访问同一对象的不同的普通同步方法串行执行持有相同的锁对象
同时访问静态的synchronized方法和非静态的synchronized方法并行执行因为一个是持有的class类锁,一个是持有的是this对象锁,不同的锁,互补干扰。

synchronized无论是正常结束还是抛出异常后,都会释放锁,而lock必须手动释放锁才可以。

死锁的问题

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

死锁产生的4个条件

  • 互斥使用
  • 不可抢占
  • 请求和保持: 指等待资源时不释放已经获得的资源。
  • 循环等待

Volatile

可以保证内存可见性和禁止重排序。

访问volatile修饰的变量不会阻塞,但是只能保证变量的可见性不能保证原子性
保证可见性是因为对Volatile修饰变量进行读操作时强制从主内存中获取最新值,而进行写操作时则会立即刷新到主内存中。

CAS

Compare And Swap
这个应该算是一种编程技巧或者说是底层提供了对应机制而在上层可以实现的一种逻辑。
通过比较当前值和期望值是否相等来判断是否更新内存中的值。
ABA问题是指当前值从A改成B后又改成A,导致期望值和当前值相等,但是内存中的值实际上已经发生过变更了,可以通过加时间戳来避免这种问题。
通过这种思想Java中设计了Atomic包,其中包括AtomicInteger,AtomicIntegerArray等类。

显示锁Lock

synchronized有以下不足之处

  1. 如果临界区代码是只读操作,其实可以并行执行,但是synchronized会阻止
  2. synchronized无法知道是否成功获得锁
  3. 如果临界区的代码因为阻塞进入等待而又没有释放锁,则所有线程都会等待
public interface Lock {
    // 获得锁。 如果失败,则等待。如果成功,则必须主动释放,即使发生异常也不会释放锁
    // 所以必须按照try-catch-finally的形式,在finally中执行unlock()释放锁,防止死锁
    void lock();
  	
    void lockInterruptibly() throws InterruptedException;
  	
    // 尝试去获取锁,成功则返回true,失败则返回false。会立即返回,不会等待
    boolean tryLock();
    
    // 支持等待一段时间,超出时间之后未获取到锁则返回false,支持响应中断
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    void unlock();
	
    // Condition中的await/signal/signalAll和wait/notify/notifyAll一样
    Condition newCondition();
}

实现类是ReentrantLock(可重入锁)
可重入锁的意思是一个线程可以多次获取同一个锁,获得3次锁也得对应释放3次锁。
如果不可重入,则嵌套使用同步方法时候因为已经获得锁,而又再次去获得锁的时候导致的死锁。

// 读写锁,内部维护了两个锁,读锁共享,写锁排他。
public interface ReadWriteLock {
    // 返回读锁,当读锁被占用时,可以再次获得读锁,但是不能获得写锁
    Lock readLock();
    // 返回写锁
    Lock writeLock();
}

线程池

public ThreadPoolExecutor(
  int corePoolSize, 
  int maximumPoolSize,
  long keepAliveTime,
  TimeUnit unit,
  BlockingQueue<Runnable> workQueue,
  ThreadFactory threadFactory,
  RejectedExecutionHandler handler)
线程池参数描述
corePoolSize该线程池中核心线程数最大值
maximumPoolSize该线程池中线程总数最大值
keepAliveTime非核心线程闲置超时时长
unitkeepAliveTime的单位
workQueue阻塞队列,维护着等
threadFactory创建线程的工厂类,不指定则有默认
handler拒绝处理策略

核心线程和非核心线程的区别在于,核心线程无论是否执行都会存在,但非核心线程长时间闲置会被销毁。
RejectedExecutionHandler是拒绝处理策略,当线程数量超过最大线程数量采用的策略。

  • ThreadPoolExecutor.AbortPolicy:默认拒绝处理策略,丢弃任务并抛出RejectedExecutionException异常。
  • ThreadPoolExecutor.DiscardPolicy:丢弃新来的任务,但是不抛出异常。
  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列头部(最旧的)的任务,然后重新尝试执行程序(如果再次失败,重复此过程)。
  • ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。

主要有四种 Executor:

  • CachedThreadPool:一个任务创建一个线程;
  • FixedThreadPool:所有任务只能使用固定大小的线程;
  • SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。
  • newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行。

评论