Java 线程基础

多用线程池少用 Thread 这个类,下面是各个版本的线程池:

  • < Java 5:Thread、Runnable
  • Java 5:Executor、Future、Callable
  • Java 7:ForkJoin
  • Java 8:CompletionStage、CompletionFuture
  • Java 9:Flow

Java 中线程的生命周期和线程的状态可以理解为:在其生命周期中,线程会经历各种状态。

类似于下面这张图是最常见的:

Thread status

使用 new Thread() 创建出来的线程就是 NEW 状态,这个时候线程并没有启动;当调用 start() 方法时,线程状态就会变成 RUNNABLE 状态。但是要注意,RUNNABLE 状态又分为两种,一种是准备运行(I/O 阻塞),另一种是运行。

当 t1 获得锁,t2 会等待 t1 释放锁,这个时候 t2 线程的状态就是 BLOCKED 阻塞状态。

当调用了线程的 Object#wait()、Thread#join() 或 LockSupport#park() 方法时,就会变成 WAITTING 状态;如果调用的是带有超时参数的就会变成 TIMED_WAITTING 状态。

线程执行结束就是 TERMINATED 状态。

在 Java 中线程一共就分为 6 种状态并且可以通过 state() 方法查看线程状态。

创建线程

Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Thread run");
    }
});

上面是 Java 创建一个线程的例子,下面代码展示了这个线程具体是怎么创建的,Thread 构造方法最终都会调用 init 方法。

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    /* 每一个线程必须有一个名字,默认的线程名称是:Thread- 加上线程ID。 
       线程ID 是存放在 threadInitNumber 变量中,每次创建 Thread 实例时,都会加1。*/
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }
    
    this.name = name;

    /* 假设在线程 A 中,调用了 new Thread 创建了线程 B。
       这里获取线程 A 是为了,将线程 A 的一些配置传递给线程 B。*/
    Thread parent = currentThread();
    SecurityManager security = System.getSecurityManager();
    
    /* 每一个线程必须有一个线程组,如果没有指定则会先获取安全管理器中的线程组,否则再回去父级(线程A)线程组。 */
    if (g == null) {

        if (security != null) {
            g = security.getThreadGroup();
        }

        if (g == null) {
            g = parent.getThreadGroup();
        }
    }

    /* 判断当前线程(线程A) 有没有操作线程组的权限,如果没有就会抛出 SecurityException。
       因为 Thread#start() 方法会操作线程组。*/
    g.checkAccess();
    
    if (security != null) {
        /* 主要用来判断子类有没有重写 getContextClassLoader 和 setContextClassLoader 方法,如果重写了则返回 true。*/
        if (isCCLOverridden(getClass())) {
            /* 既然重写了那么就判断一下有没有重写权限(enableContextClassLoaderOverride),如果没有则抛出 SecurityException。*/
            security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
        }
    }
    
    /* 将线程组中的 未启动线程 变量加 1。*/
    g.addUnstarted();
    
    this.group = g;
    this.daemon = parent.isDaemon();
    this.priority = parent.getPriority();
    
    /* 如果重写了就调用子类的 getContextClassLoader() 方法,否则就直接获取父类的 ClassLoader 就可以了。*/
    if (security == null || isCCLOverridden(parent.getClass()))
        this.contextClassLoader = parent.getContextClassLoader();
    else
        this.contextClassLoader = parent.contextClassLoader;
    
    /* 我也不清楚为什么要获取这个。*/
    this.inheritedAccessControlContext = acc != null ? acc : AccessController.getContext();
    
    /* 我也不清楚为什么要获取这个。*/
    this.target = target;
    
    /* 设置线程优先级。*/
    setPriority(priority);
    
    /* 继承父级的 ThreadLocal.ThreadLocalMap。
       当然自己的线程数据可以放到 ThreadLocal.ThreadLocalMap threadLocals 中。*/
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    
    this.stackSize = stackSize;

    tid = nextThreadID();
}

下面是简化的实例化流程:

  1. 设置线程名
  2. 如果没有指定线程组,则会先获取安全管理器中的线程组,否则再回去父级线程组
  3. 将 ThreadGroup#nUnstartedThreads 变量加 1
  4. 设置当前线程实例的变量 daemon、priority 和 contextClassLoader 的值为父级的值。
  5. 关联 Runnable 实例
  6. 调用 setPriority0 本地方法设置优先级
  7. 判断需不需要传递(默认需要传递) ThreadLocal
  8. 设置线程栈大小
  9. 最后设置线程ID

终止一个线程

Thread#stop() 方法已经不推荐使用了,但可以使用 Thread#interrupt() 方法停止一个线程。

但是当调用目标线程的 interrupt() 方法后只会将目标线程打上中断标记,而目标线程并不会立即终止。也就是说,只是一个中断标记,JVM 不会强制要求终止线程。下面就是一个不会立即终止的例子:

public class ThreadWithoutIntterupt {
    public static void main(String[] args) {
        Thread t = new Thread(new PrintEvenNumbers());
        t.start();

        // 调用目标线程 t 的 interrupt() 方法。
        t.interrupt();
    }
}

class PrintEvenNumbers implements Runnable {
    @Override
    public void run() {
        System.out.println("Printing even numbers till 20 :");
        for (int i = 2; i <= 20; i = i + 2) {
            System.out.println(i);
        }
    }
}

上面这个例子中,目标线程并没有立即终止,而是正常停止(打印完了所有偶数)。也就是说,终止目标线程的执行,必须要目标线程自己终止,这样才能保证一致性。目标线程可以使用 Thread#interrupted() 方法来判断是否要终止自身。将 PrintEvenNumbers 类改成如下:

class PrintEvenNumbers implements Runnable {
    @Override
    public void run() {
        System.out.println("Printing even numbers till 20 :");
        for (int i = 2; i <= 20; i = i + 2) {
            System.out.println(i);
            if (Thread.interrupted()) {
                System.out.println("保证数据一致性,并终止线程。");
                return;
            }
        }
    }
}

值得注意的是:当调用 Thread.interrupted() 方法时,会清除中断标记(也就是再次设置为 false),再次调用时该方法会返回 false。但是调用 t1.isInterrupted() 方法则不会清除中断标记。

还有一点就是无论是否先调用了目标线程的 interrupt() 方法,Object#wait(...) Thread#join(...) Thread#sleep(...) 都会抛出 InterruptedException 并且会清空 interrupt 状态(只要 interrupt 状态是 true,这三个方法就会抛出异常)。如果是 I/O 操作的话则会抛出 ClosedByInterruptException 异常。

class PrintEvenNumbers implements Runnable {
    @Override
    public void run() {
        System.out.println("Printing even numbers till 20 :");
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            throw new InternalError("Thraed is interrupted");
        }
    }
}

值得注意的是:如果 interrupt() 方法在 start() 方法之前调用则无效。

join

在当前线程中调用目标线程的 join(...) 方法时,当前线程进入等待状态,直到目标的线程终止、 join 超时或其它线程调用了当前线程的 interrupt() 方法,当前线程才会继续执行。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t2 = new SampleThread(1);
        t2.start();
        System.out.println("Invoking join");
        t2.join();
        System.out.println("Returned from join");
        System.out.println(t2.isAlive());
    }
}

class SampleThread extends Thread {
    public int processingCount = 0;

    SampleThread(int processingCount) {
        this.processingCount = processingCount;
        System.out.println("Thread Created");;
    }

    @Override
    public void run() {
        System.out.println("Thread " + this.getName() + " started");
        while (processingCount > 0) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                System.out.println("Thread " + this.getName() + " interrupted");
            }
            processingCount--;
        }
        System.out.println("Thread " + this.getName() + " exiting");;
    }
}

下面是一个带有超时时间的 join 代码示例:

Thread t2 = new SampleThread(10);
t2.start();
System.out.println("Invoking join");
t2.join(1000);
System.out.println("Returned from join");
System.out.println(t2.isAlive());

值得注意的是:join(...) 方法满足 Happens-before 关系。

wait 和 notify

当我们调用 wait() 方法时,会使当前线程进行等待并释放锁,直到其他的某个线程在同一对象上调用 notify()、notifyAll() 或 interrupt() 方法为止。

notify() 和 notifyAll() 方法的区别在于,前者只会随机唤醒其中一个等待的线程,而后者会唤醒所有等待的线程。注意:被唤醒的线程有可能不能立即执行,因为这些线程还会争夺锁。

值得注意的是:notify() 和 wait() 方法只能从同步方法或同步代码块中被调用。另外,wait 方法也可以设置超时时间,当设置超时时间为 wait(0) 时与调用 wait() 的作用是相同的。

下面是一个使用例子,首先创建一个 Data 类,该类由将要从 Sender 发送到 Receiver 的数据包组成。我们将使用 wait() 和 notifyAll() 在它们之间建立同步:

public class Data {
    private String packet;
    
    // True if receiver should wait
    // False if sender should wait
    private boolean transfer = true;
 
    public synchronized void send(String packet) {
        while (!transfer) {
            try { 
                wait();
            } catch (InterruptedException e)  {
                Thread.currentThread().interrupt(); 
                Log.error("Thread interrupted", e); 
            }
        }
        transfer = false;
        
        this.packet = packet;
        notifyAll();
    }
 
    public synchronized String receive() {
        while (transfer) {
            try {
                wait();
            } catch (InterruptedException e)  {
                Thread.currentThread().interrupt(); 
                Log.error("Thread interrupted", e); 
            }
        }
        transfer = true;

        notifyAll();
        return packet;
    }
}

在 wait 写在 while 循环中是为了防止还没有满足条件的时候,线程就被唤醒了并得到错误的结果。

讨论

1. 为什么要弃用 stop()、suspend()、resume() 方法?

当调用 Thread#stop() 方法时会导致立即释放掉它持有的锁(这是不安全操作),并且还会抛出 ThreadDeath 异常。

Thread t1 = new Thread(() -> {});
t1.stop(); // t1 这个线程会立即释放掉它持有的锁

假设,同步代码块中一共有五行代码,t1 线程执行到第三行的时候就被 t2 线程调用了 t1.stop() 方法,这时 t1 会立即释放掉锁,那么 t2 所看到的数据就有可能会不一致(也可以说这些数据是被毁坏的)。

suspend() 和 resume() 方法必须要配合使用,如果 resume() 在 suspend() 之前执行会导致死锁,所以一定要保证顺序;还有就是 suspend() 会挂起线程,但是不会释放锁,所以也会导致死锁。

还有一点要注意,虽然可以使用 wait() 和 notify() 代替 suspend() 和 resume(),但是:

Thread t1 = new Thread(() -> {});
// 可以用在没有同步的地方使用
t1.suspend();
t1.resume();

synchronized (Object.class) {
    // 必须要在同步的地方使用
    Object.class.wait();
    Object.class.notify();
}

这里有产生了另一个问题,为什么 wait 和 notify 要放到同步代码块或同步方法中。有关这个为题可以阅读 Java Monitor。

2. wait & join

wait 和 join 之间的一个最重要的区别就是 wait 要写在同步代码块或同步方法中,否则就会抛出 IllegalMonitorStateException。其他的就是用途不一样,wait 用于线程间的通信,而 join 可以控制线程执行顺序;再者他们两个都可以通过 interrupt 方法来中断,并且都有代超时参数的重载。

还有就是 join 方法也是靠 wait 实现的:

public final synchronized void join(long millis) throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        // 如果线程还在活动,则返回true。这里写入 while 循环,是为了防止没有满足条件就被唤醒了。
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

总结

对于 Thread 类中,主要方法 interrupt、wait、notify、notifyAll、join。

还有就是建议使用 TimeUnit sleep 代替 Thread sleep 方法

知识拓展-Runnable 接口

Runnable 这个接口可以理解成线程的任务接口,它里面只有一个无参的 run 方法,线程启动后就会执行这个方法中的代码。

还可以使,Thread 实例通过另一个 Thread 的构造方法进行传递。

知识拓展-FutureTask 配合 Thread

FutureTask 能够接收 Callable 类型的参数,用来处理又返回结果的情况:

// 创建任务对象
FutureTask<Integer> task = new FutureTask<>(() -> {
        System.out.println("hello");
        return 100;
});

// 参数1 是任务对象;参数2 是线程名字,推荐
new Thread(task, "tk").start();

// 主线程阻塞,同步等待 task 执行完毕的结果。
Integer result = task.get();
System.out.printf("结果是: %d\n", result);

知识拓展-主线程与守护线程

默认情况下,Java进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,就是守护线程的代码没有执行完,也会强制结束。

// 设置 t1 线程为守护线程,需要在 start 之前设置。
t1.setDaemon(true);

参考资料

Life Cycle of a Thread in Java

Java Thread Primitive Deprecation

Difference Between Wait and Sleep in Java

Differences between wait() and join() methods in Java

posted @ 2021-05-21 10:54  代号-G  阅读(126)  评论(0)    收藏  举报