转载 - Java 线程详解(下)

一、线程生命周期

《详解操作系统进程》中,从操作系统层面介绍了进程(线程)的生命周期的变迁,在操作系统中,线程的状态主要包含了五种:初始化、等待状态、就绪状态、运行状态和终止状态

但在Java中,定义了六种状态的,其中RUNNABLE状态对应运行状态和就绪状态,而等待状态在Java中细分为三种BLOCKED、WAITING、TIMED_WAITING

关于这三种状态的描述,下面的源码中的注释已经解释的很清楚

BLOCKED状态对应于对象锁,某个线程竞争对象锁失败时就处于阻塞状态

WAITING状态对应于永久等待,如果没有被唤醒,将会处于永久等待的状态

TIMED_WAITING状态对应于超时等待,超过了等待的时间之后,它们就会被主动唤醒,然后处于RUNNABLE状态,等待CPU分配时间片

public enum State {

    NEW,
    RUNNABLE,
    /**
     * Thread state for a thread blocked waiting for a monitor lock.
     * A thread in the blocked state is waiting for a monitor lock
     * to enter a synchronized block/method or
     * reenter a synchronized block/method after calling
     * {@link Object#wait() Object.wait}.
     */
    BLOCKED,

    /**
     * <ul>
     *   <li>{@link Object#wait() Object.wait} with no timeout</li>
     *   <li>{@link #join() Thread.join} with no timeout</li>
     *   <li>{@link LockSupport#park() LockSupport.park}</li>
     * </ul>
     */
    WAITING,

    /**
     * <ul>
     *   <li>{@link #sleep Thread.sleep}</li>
     *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
     *   <li>{@link #join(long) Thread.join} with timeout</li>
     *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
     *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
     * </ul>
     */
    TIMED_WAITING,

    TERMINATED;
}
 

Java中这六种线程状态的生命周期,可以用下面的图来表示:

 

 

在上图中,调用notify()和notifyAll()方法后,可能会进入到阻塞状态,因为wait()方法和notify的两个方法需要结合Synchronized关键字使用,被唤醒后,就会去竞争对象锁,如果没有拿到对象锁,就会处于Blocked状态

从JVM的层面,也定义一些针对JavaThread对象的状态

 

 


二、Thread常用方法

2.1 sleep()

当线程调用sleep()方法之后,会让当前线程从RUNNABLE状态切换到TIMED_WAITING状态,但是并不会释放对象锁

下面的代码展示了,我们在调用sleep()方法时,可能会抛出InterruptedException中断异常,这是因为在其他线程中,可以使用interrupt()方法中断正在睡眠的线程,这时调用sleep()方法的线程就会抛出中断异常,并且会清除中断标志(方法注释上说明的很清楚)

睡眠结束的线程处于RUNNABLE状态,未必会立刻得到执行

当sleep()传入的参入为0时,和yield()方法相同

RunnableTest runnableTest = new RunnableTest();
Thread thread = new Thread(runnableTest);
thread.setPriority(5);
try {
    Thread.sleep(1);
} catch (InterruptedException e) {
    e.printStackTrace();
}

/**
 * The thread does not lose ownership of any monitors.
 * @throws  InterruptedException
 *      if any thread has interrupted the current thread. The
 *      <i>interrupted status</i> of the current thread is
 *      cleared when this exception is thrown.
 */
public static native void sleep(long millis) throws InterruptedException;
 

2.2 yield()

当线程调用yield()方法时,当前线程会让出CPU,然后从RUNNING状态切换到READY状态,让优先级更高的线程获得执行机会,不会释放对象锁

假设当前JVM进程只有main线程,当调用yield方法之后,main线程会继续运行,因为没有比它优先级更好的线程

具体的实现依赖于操作系统的任务调度器,正如方法注释上说的,任务调度器可以忽略这个标志,也就是说就算当前线程执行了yield()方法,它也有可能即刻就被处理器调度

/**
 * A hint to the scheduler that the current thread is willing to yield
 * its current use of a processor. The scheduler is free to ignore this
 * hint.
 */
public static native void yield();
 

2.3 join()

join()方法主要用于等待调用join()方法的线程执行完成之后,在执行后面的逻辑。一般用于等待异步线程执行结果之后再运行的场景

public class ThreadJoinDemo {

    public static void main(String[] sure) throws InterruptedException {

        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("t begin");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t finished");
            }
        });
        long start = System.currentTimeMillis();
        t.start();
        //主线程等待线程t执行完成
        t.join();

        System.out.println("执行时间:" + (System.currentTimeMillis() - start));
        System.out.println("Main finished");
    }
}
所有重载的join()方法,最后都会调用下面的方法,可以看到,join()方法就是判断当前线程是否还在存活,如果存在就一直调用wait()方法进行等待(特别注意:不论哪个线程调用的join()方法,最终都是执行join()方法的代码段所在的线程在真正调用wait()方法,进行等待,比如上面的t.join()是在主线程执行的,那么就是主线程去执行wait()方法,如果t.join()是在另外一个线程中执行的,那么就有该主线程去执行wait()方法)

join()方法会使当前所在线程一直等待下去,直到被其他线程中断或者join的线程执行完毕,如果指定了时间,当前线程也会在时间到达后,退出阻塞状态


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

    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}
 

2.4 stop

stop()方法已经被JDK弃用,原因是stop()方法太过暴力,它可以强行把执行到一般的线程终止。

比如下面的例子,线程执行一半被终止后,锁也被释放了,就可能导致thread线程的run()方法执行出现数据不一致的情况

public class ThreadStopDemo {

    private static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    System.out.println(Thread.currentThread().getName() + "获取锁");
                    try {
                        Thread.sleep(60000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "执行完成");
            }
        });
        thread.start();
        Thread.sleep(2000);
        // 停止thread,并释放锁
        thread.stop();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "等待获取锁");
                synchronized (lock) {
                    System.out.println(Thread.currentThread().getName() + "获取锁");
                }
            }
        }).start();

    }
}
 

那么Java中有没有比较优雅的停止线程的方式呢?其实Java本身并没有提供一种简单、直接的方式来停止线程,而是提供了一种中断机制来实现优雅的停止线程

三、Java线程中断机制

中断机制是一种协作机制,也就是说它并不能直接中断某个线程,而是需要被中断的线程自己决定是否中断。

被中断的线程拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停止

主要实现就是通过中断方法将线程的中断标志为设置为true,然后线程可以在自己run()方法里面去判断当前线程的标志位决定是否退出

相关API:

  • interrupt():将线程中的中断标志为设置为true,不会停止线程
  • isInterrupted():判断当前线程的标志位是否位true,不会清除中断标志位
  • Thread.interrupted():判断当前线程的中断标志位是否位true,并清除中断标志,重置为false
public class ThreadInterruptTest {

    static int i = 0;

    public static void main(String[] args)  {
        System.out.println("begin");
        Thread t1 = new Thread(new Runnable() {
            @Override
            public  void run() {
                while (true) {
                    i++;
                    System.out.println(i);
                    //Thread.interrupted()  清除中断标志位
                    //Thread.currentThread().isInterrupted() 不会清除中断标志位
                    if (Thread.currentThread().isInterrupted() ) {
                        System.out.println("=========");
                    }
                    if(i==10){
                        break;
                    }

                }
            }
        });

        t1.start();
        //不会停止线程t1,只会设置一个中断标志位 flag=true
        t1.interrupt();
    }
}
利用中断机制优雅的停止线程
while (!Thread.currentThread().isInterrupted() && more work to do) {
    do more work
 
public class StopThread implements Runnable {

    @Override
    public void run() {
        int count = 0;
        while (!Thread.currentThread().isInterrupted() && count < 1000) {
            System.out.println("count = " + count++);
        }
        System.out.println("线程停止: stop thread");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new StopThread());
        thread.start();
        Thread.sleep(5);
        thread.interrupt();
    }
}
注:使用中断机制时一定要注意是否存在中断标志为被清除的情况

线程sleep期间能够感受到中断,修改上面的代码,线程执行期间有休眠需求

@Override
public void run() {
    int count = 0;
    while (!Thread.currentThread().isInterrupted() && count < 1000) {
        System.out.println("count = " + count++);

        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    System.out.println("线程停止: stop thread");
}
 

处于休眠中的线程被中断,线程是可以感受到中断信号的,并且会抛出一个InterruptedException异常,同时清除中断信号,将中断信号设置为false。这样就会导致while条件Thread.currentThread().isInterrupted()为false,程序会在不满足count < 1000时退出。如果不再catch语句中重新添加中断信号,不做任何处理,中断信号就被屏蔽掉了,可能导致while循环里面的代码会一直执行,无法正确退出

try {
    Thread.sleep(1);
} catch (InterruptedException e) {
    e.printStackTrace();
    //重新设置线程中断状态为true
    Thread.currentThread().interrupt();
 

sleep和wait的线程都可以被中断,抛出中断异常:InterruptedException,同时清除中断标志位

四、线程间通信

4.1 Volatile

volatile有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程之间进行通信。

public class VolatileDemo {

    private static volatile boolean flag = true;

    public static void main(String[] args) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    if (flag){
                        System.out.println("trun on");
                        flag = false;
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    if (!flag){
                        System.out.println("trun off");
                        flag = true;
                    }
                }
            }
        }).start();
    }
}
 

4.2 等待唤醒机制

等待唤醒机制是通过wait和notify方法来实现的,在一个线程内调用该线程锁对象的wait()方法,线程将进入等待队列进行等待直到被唤醒

下面的代码,线程执行wait()方法后,会释放lock对象锁,其他线程可以竞争该对象锁,notify()方法只能唤醒一个等待lock对象锁的线程,而notifyAll()方法会去唤醒所有等待lock对象锁的线程

并且notify()方法只能随意唤醒某一个等待lock对象锁的线程,不能指定唤醒某一个线程

public class WaitDemo {
    private static Object lock = new Object();
    private static  boolean flag = true;

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock){
                    while (flag){
                        try {
                            System.out.println("wait start .......");
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                    System.out.println("wait end ....... ");
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                if (flag){
                    synchronized (lock){
                        if (flag){
                            lock.notify();
                            System.out.println("notify .......");
                            flag = false;
                        }

                    }
                }
            }
        }).start();
    }
}
 

注:wait于notify()必须结合synchronized关键字使用,否则会抛出IllegalMonitorStateException异常

等待唤醒机制的另一种实现是LockSupport,LockSupport是JDK中用来实现线程阻塞和唤醒的工具,线程调用park()方法则等待”许可“,调用unpark()方法则为指定线程提供”许可“。使用LockSupport的好处是,可以在任何场景使线程阻塞(不需要像wait()方法需要结合synchronized关键),并且不用担心阻塞和唤醒的顺序,但是要注意多次唤醒的效果和一次唤醒的效果一样

public class LockSupportTest {

    public static void main(String[] args) {
        Thread parkThread = new Thread(new ParkThread());
        parkThread.start();

        System.out.println("唤醒parkThread");
        LockSupport.unpark(parkThread);
    }

    static class ParkThread implements Runnable{

        @Override
        public void run() {
            System.out.println("ParkThread开始执行");
            LockSupport.park();
            System.out.println("ParkThread执行完成");
        }
    }
}
 

4.3 管道输入输出流

管道输入/输出流和普通文件输入/输出流或者网络输入/输出流的不同指出在于,它主要用于线程之间的数据传输,而传输的媒介为内存。管道输入/输出流主要包括如下四种具体实现:

PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前面两种面向字节,而后面两种面向字符

public class PipedTest {

    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;

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

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

4.4 Thread.join()

join()方法可以理解为线程合并,将并行的线程通过join()方法合并成串行的,因为当前线程会一直阻塞等待调用join()方法的线程执行完毕才能继续执行(如果没有指定时间),所以join()方法的好处是可以保证线程的执行顺序,但是如果调用线程的join()方法其实已经失去了并行的意义,虽然存在多个线程,但这些线程本质上还是串行的。join()的实现也是基于等待唤醒机制的。

 
posted @ 2023-03-28 11:42  r1-12king  阅读(15)  评论(0编辑  收藏  举报