Java线程

3/23/2022 线程

# 线程状态转换(线程生命周期)

  1. New :新创建的一个线程,处于等待状态。

  2. Runnable :可运行状态,并不是已经运行,具体的线程调度各操作系统决定。在Runnable 中包含了 Ready 、 Running 两个状态,当线程调用了 start() 方法后,程则处于就绪 Ready 状态,等待操作系统分配 CPU 时间片,分配后则进入 Running 运行状态。此外当调用 yield() 方法后,只是 谦让 的允许当前线程让出 CPU ,但具体让不让不一定,由操作系统决定。如果让了,那么当前线程则会处于 Ready 状态继续竞争 CPU ,直至执行。

  3. Timed_waiting :指定时间内让出 CPU 资源, 此时线程不会被执行,也不会被系统调度,直到等待时间到期后才会被执行。下列方法都可以触发:Thread.sleep 、 Object.wait 、 Thread.join 、LockSupport.parkNanos 、 LockSupport.parkUntil 。

  4. Wating :可被唤醒的等待状态,此时线程不会被执行也不会被系统调度。此状态可以通过 synchronized 获得锁,调用 wait 方法进入等待状态。最后通过notify 、 notifyall 唤醒。下列方法都可以触发: Object.wait 、Thread .join 、 LockSupport.park 。

  5. Blocked :当发生锁竞争状态下,没有获得锁的线程会处于挂起状态。例如synchronized 锁,先获得的先执行,没有获得的进入阻塞状态。

  6. Terminated :这个是终止状态,从 New 到 Terminated 是不可逆的。一般是程序流程正常结束或者发生了异常。

java.lang.Thread.State中定义了6种不同的线程状态,在给定的一个时刻,线程只能处于其中的一个状态。

以下是各状态的说明,以及状态间的联系:

# 新建(New)

  • 新建(New)- 尚未调用start方法的线程处于此状态。此状态意味着: 创建的线程尚未启动

# 就绪(Runnable)

  • 就绪(Runnable)- 已经调用了start方法的线程处于此状态。此状态意味着: 线程已经在 JVM 中运行 。但是在操作系统层面,它可能处于运行状态,也可能等待资源调度(例如处理器资源),资源调度完成就进入运行状态。所以该状态的可运行是指可以被运行,具体有没有运行要看底层操作系统的资源调度。

# 阻塞(Blocked)

  • 阻塞(Blocked)- 此状态意味着: 线程处于被阻塞状态 。表示线程在等待synchronized的隐式锁(Monitor lock)。synchronized修饰的方法、代码块同一时刻只允许一个线程执行,其他线程只能等待,即处于阻塞状态。当占用synchronized隐式锁的线程释放锁,并且等待的线程获得synchronized隐式锁时,就又会从BLOCKED转换到RUNNABLE状态。

# 等待(Waiting)

  • 等待(Waiting)- 此状态意味着: 线程无限期等待,直到被其他线程显式地唤醒 。 阻塞和等待的区别在于,阻塞是被动的,它是在等待获取synchronized的隐式锁。而等待是主动的,通过调用Object.wait等方法进入。
进入方法 退出方法
没有设置 Timeout 参数的Object.wait方法 Object.notify/Object.notifyAll
没有设置 Timeout 参数的Thread.join方法 被调用的线程执行完毕
LockSupport.park方法(Java 并发包中的锁,都是基于它实现的) LockSupport.unpark

# 定时等待(Timed waiting)

  • 定时等待(Timed waiting)- 此状态意味着: 无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒
进入方法 退出方法
Thread.sleep方法 时间结束
获得synchronized隐式锁的线程,调用设置了 Timeout 参数的Object.wait方法 **时间结束 /Object.notify/**Object.notifyAll
设置了 Timeout 参数的Thread.join方法 时间结束 / 被调用的线程执行完毕
LockSupport.parkNanos方法 LockSupport.unpark
LockSupport.parkUntil方法 LockSupport.unpark

# 终止(Terminated)

  • 终止(Terminated) - 线程执行完 run 方法,或者因异常退出了 run 方法。此状态意味着:线程结束了生命周期。

# 线程实现方式

  1. 有三种使用线程的方法:

    • 实现 Runnable 接口;
    • 实现 Callable 接口;
    • 继承 Thread 类。
  2. 实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用 。可以说任务是通过线程驱动从而执行的。

# 实现Runnable接口

public class MyRunnable implements Runnable {
    public void run() {
        // ...
    }
}
public static void main(String[] args) {
    MyRunnable instance = new MyRunnable();
    Thread thread = new Thread(instance);
    thread.start();
}

# 实现Callable接口

public class MyCallable implements Callable<Integer> {
    public Integer call() {
        return 123;
    }
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
    MyCallable mc = new MyCallable();
    FutureTask<Integer> ft = new FutureTask<>(mc);
    Thread thread = new Thread(ft);
    thread.start();
    System.out.println(ft.get());
}

# 继承Thread类

public class MyThread extends Thread {
    public void run() {
        // ...
    }
}
public static void main(String[] args) {
    MyThread mt = new MyThread();
    mt.start();
}

# 线程基本用法

方法 描述
run 线程的执行实体。
start 线程的启动方法。
currentThread 返回对当前正在执行的线程对象的引用。
setName 设置线程名称。
getName 获取线程名称。
setPriority 设置线程优先级。Java 中的线程优先级的范围是 [1,10],一般来说,高优先级的线程在运行时会具有优先权。可以通过thread.setPriority(Thread.MAX_PRIORITY)的方式设置,默认优先级为 5。
getPriority 获取线程优先级。
setDaemon 设置线程为守护线程。
isDaemon 判断线程是否为守护线程。
isAlive 判断线程是否启动。
interrupt 中断另一个线程的运行状态。
interrupted 测试当前线程是否已被中断。通过此方法可以清除线程的中断状态。换句话说,如果要连续调用此方法两次,则第二次调用将返回 false(除非当前线程在第一次调用清除其中断状态之后且在第二次调用检查其状态之前再次中断)。
join 可以使一个线程强制运行,线程强制运行期间,其他线程无法运行,必须等待此线程完成之后才可以继续执行。
Thread.sleep 静态方法。将当前正在执行的线程休眠。
Thread.yield 静态方法。将当前正在执行的线程暂停,让其他线程执行。

# Thread.sleep-线程休眠

  1. 使用Thread.sleep**方法可以使得当前正在执行的线程进入休眠状态。

  2. 使用 Thread.sleep 需要向其传入一个整数值,这个值表示线程将要休眠的毫秒数。

  3. Thread.sleep 方法可能会抛出 InterruptedException,因为异常不能跨线程传播回 main 中,因此必须在本地进行处理 。线程中抛出的其它异常也同样需要在本地进行处理。

public class ThreadSleepDemo {
    public static void main(String[] args) {
        new Thread(new MyThread("线程A", 500)).start();
        new Thread(new MyThread("线程B", 1000)).start();
        new Thread(new MyThread("线程C", 1500)).start();
    }

    static class MyThread implements Runnable {

        /** 线程名称 */
        private String name;

        /** 休眠时间 */
        private int time;

        private MyThread(String name, int time) {
            this.name = name;
            this.time = time;
        }
        @Override
        public void run() {
            try {
                // 休眠指定的时间
                Thread.sleep(this.time);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(this.name + "休眠" + this.time + "毫秒。");
        }
    }
}

# Thread.yield-线程礼让

  1. Thread.yield 方法的调用声明了 当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行

  2. 该方法 只是对线程调度器的一个建议 ,而且也只是建议具有相同优先级的其它线程可以运行。

public class ThreadYieldDemo {

    public static void main(String[] args) {
        MyThread t = new MyThread();
        new Thread(t, "线程A").start();
        new Thread(t, "线程B").start();
    }

    static class MyThread implements Runnable {

        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "运行,i = " + i);
                if (i == 2) {
                    System.out.print("线程礼让:");
                    Thread.yield();
                }
            }
        }
    }
}

# Thread.interrupt-线程终止

Thread 中的 stop 方法有缺陷,已废弃。

使用Thread.stop停止线程会导致它解锁所有已锁定的监视器(由于未经检查的ThreadDeath异常会在堆栈中传播,这是自然的结果)。 如果先前由这些监视器保护的任何对象处于不一致状态,则损坏的对象将对其他线程可见,从而可能导致任意行为。

stop() 方法会真的杀死线程,不给线程喘息的机会,如果线程持有 ReentrantLock 锁,被 stop() 的线程并不会自动调用 ReentrantLock 的 unlock() 去释放锁,那其他线程就再也没机会获得 ReentrantLock 锁,这实在是太危险了 。所以该方法就不建议使用了,类似的方法还有 suspend() 和 resume() 方法,这两个方法同样也都不建议使用了,所以这里也就不多介绍了。Thread.stop 的许多用法应由仅修改某些变量以指示目标线程应停止运行的代码代替。 目标线程应定期检查此变量,如果该变量指示要停止运行,则应按有序方式从其运行方法返回。如果目标线程等待很长时间(例如,在条件变量上),则应使用中断方法来中断等待。

  1. 通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。 但是不能中断 I/O 阻塞和 synchronized 锁阻塞。
public class InterruptExample {

    private static class MyThread1 extends Thread {
        @Override
        public void run() {
            try {
                Thread.sleep(2000);
                System.out.println("Thread run");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public static void main(String[] args) throws InterruptedException {
    Thread thread1 = new MyThread1();
    thread1.start();
    thread1.interrupt();
    System.out.println("Main run");
}
  1. 如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。

  2. 但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此 可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程

# 安全终止线程的方法

  1. 定义 volatile 标志位,在 run 方法中使用标志位控制线程终止
public class ThreadStopDemo2 {
    public static void main(String[] args) throws Exception {
        MyTask task = new MyTask();
        Thread thread = new Thread(task, "MyTask");
        thread.start();
        TimeUnit.MILLISECONDS.sleep(50);
        task.cancel();
    }

    private static class MyTask implements Runnable {
        private volatile boolean flag = true;

        private volatile long count = 0L;

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " 线程启动");
            while (flag) {
                System.out.println(count++);
            }
            System.out.println(Thread.currentThread().getName() + " 线程终止");
        }
        /**
         * 通过 volatile 标志位来控制线程终止
         */
        public void cancel() {
            flag = false;
        }
    }
}
  1. 使用 interrupt 方法和 Thread.interrupted 方法配合使用来控制线程终止
public class ThreadStopDemo3 {

    public static void main(String[] args) throws Exception {
        MyTask task = new MyTask();
        Thread thread = new Thread(task, "MyTask");
        thread.start();
        TimeUnit.MILLISECONDS.sleep(50);
        thread.interrupt();
    }

    private static class MyTask implements Runnable {

        private volatile long count = 0L;

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " 线程启动");
            // 通过 Thread.interrupted 和 interrupt 配合来控制线程终止
            while (!Thread.interrupted()) {
                System.out.println(count++);
            }
            System.out.println(Thread.currentThread().getName() + " 线程终止");
        }
    }
}

# thread.join

首先join() 是一个synchronized方法, 里面调用了wait(),这个过程的目的是让持有这个同步锁的线程进入等待,那么谁持有了这个同步锁呢?答案是主线程,因为主线程调用了threadA.join()方法,相当于在threadA.join()代码这块写了一个同步代码块,谁去执行了这段代码呢,是主线程,所以主线程被wait()了。然后在子线程threadA执行完毕之后,JVM会调用lock.notify_all(thread);唤醒持有threadA这个对象锁的线程,也就是主线程,会继续执行。

# 守护线程

  1. 什么是守护线程?

    • 守护线程(Daemon Thread)是在后台执行并且不会阻止 JVM 终止的线程 当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程

    • 与守护线程(Daemon Thread)相反的,叫用户线程(User Thread),也就是非守护线程。

  2. 为什么需要守护线程?

    • 守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务 。典型的应用就是 垃圾回收器
  3. 如何使用守护线程?

  • 可以使用isDaemon方法判断线程是否为守护线程。

  • 可以使用setDaemon方法设置线程为守护线程。

    • **正在运行的用户线程无法设置为守护线程,所以 ** setDaemon 必须在 thread.start 方法之前设置,否则会抛出 llegalThreadStateException 异常

    • 一个守护线程创建的子线程依然是守护线程

    • 不要认为所有的应用都可以分配给守护线程来进行服务,比如读写操作或者计算逻辑。

public class ThreadDaemonDemo {
    public static void main(String[] args) {
        Thread t = new Thread(new MyThread(), "线程");
        t.setDaemon(true); // 此线程在后台运行
        System.out.println("线程 t 是否是守护进程:" + t.isDaemon());
        t.start(); // 启动线程
    }

    static class MyThread implements Runnable {

        @Override
        public void run() {
            while (true) {
                System.out.println(Thread.currentThread().getName() + "在运行。");
            }
        }
    }
}

# 线程间通信

# wait/notify/notifyAll

# join

# 管道

  1. 管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于, 它主要用于线程之间的数据传输,而传输的媒介为内存 **。

  2. 管道输入/输出流主要包括了如下 4 种具体实现:PipedOutputStreamPipedInputStreamPipedReaderPipedWriter,前两种面向字节,而后两种面向字符。

public class Piped {

    public static void main(String[] args) throws Exception {
        PipedWriter out = new PipedWriter();
        PipedReader in = new PipedReader();
        // 将输出流和输入流进行连接,否则在使用时会抛出IOException
        out.connect(in);
        Thread printThread = new Thread(new Print(in), "PrintThread");
        printThread.start();
        int receive = 0;
        try {
            while ((receive = System.in.read()) != -1) {
                out.write(receive);
            }
        } finally {
            out.close();
        }
    }

    static class Print implements Runnable {

        private PipedReader in;

        Print(PipedReader in) {
            this.in = in;
        }

        public void run() {
            int receive = 0;
            try {
                while ((receive = in.read()) != -1) {
                    System.out.print((char) receive);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

# 常见问题

# sleep、yield、join 方法有什么区别

  • yield方法

    • yield方法会让线程从Running状态转入Runnable 状态

    • 当调用了yield方法后,只有与当前线程相同或更高优先级的Runnable 状态线程才会获得执行的机会

  • sleep方法

    • sleep方法会让线程从Running状态转入Waiting 状态

    • sleep方法需要指定等待的时间,超过等待时间后,JVM 会将线程从Waiting状态转入Runnable 状态

    • 当调用了sleep方法后, 无论什么优先级的线程都可以得到执行机会

    • sleep方法不会释放“锁标志”,也就是说如果有synchronized同步块,其他线程仍然不能访问共享数据。

  • join

    • join方法会让线程从Running状态转入Waiting 状态

    • 当调用了join方法后,当前线程必须等待调用join 方法的线程结束后才能继续执行

# 为什么 sleep 和 yield 方法是静态的

Thread类的sleepyield方法将处理Running状态的线程。

所以在其他处于非Running状态的线程上执行这两个方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。

# Java 线程是否按照线程优先级严格执行

即使设置了线程的优先级,也 无法保证高优先级的线程一定先执行

原因在于线程优先级依赖于操作系统的支持,然而,不同的操作系统支持的线程优先级并不相同,不能很好的和 Java 中线程优先级一一对应。

# 一个线程两次调用 start()方法会怎样

Java 的线程是不允许启动两次的,第二次调用必然会抛出 IllegalThreadStateException,这是一种运行时异常,多次调用 start 被认为是编程错误。

# start 和 run 方法有什么区别

  • run方法是线程的执行体。
  • start方法会启动线程,然后 JVM 会让这个线程去执行run方法。

# 可以直接调用 Thread 类的 run 方法么

  • 可以。但是如果直接调用Threadrun方法,它的行为就会和普通的方法一样。
  • 为了在新的线程中执行我们的代码,必须使用 Threadstart 方法。

# 参考

Java 线程基础 | JAVACORE (opens new window)

Java 并发 - 线程基础 | Java 全栈知识体系 (opens new window)

Last Updated: 3/28/2022, 9:29:49 PM