JAVA线程(状态、终止、通信、封闭)

504 阅读11分钟

线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

线程状态

线程几种状态线程的状态在java中有明确的定义,在java.lang.Thread.State中有6种。

New:线程被创建,未执行和运行的时候

Runnable:不代表线程在跑,两种:被cpu执行的线程,随时可以被cpu执行的状态。

Blocked:线程阻塞,处于synchronized同步代码块或方法中被阻塞。

Waiting:处于等待状态的线程正等待另一个线程。线程当前不执行,如果被其他唤醒后会继续执行的状态。依赖另一个线程的通知的。这个等待是一直等,没人叫你,你起不来。 例如,已经在某一对象上调用了 Object.wait() 的线程正等待另一个线程,以便在该对象上调用 Object.notify() 或 Object.notifyAll()。已经调用了 Thread.join() 的线程正在等待指定线程终止。

Time Waiting:指定等待时间的等待线程的线程状态。带超时的方式:Thread.sleep,Object.wait,Thread.join,LockSupport.parkNanos,LockSupport.parkUntil

Terminated:正常执行完毕或者出现异常终止的线程状态。

在这里插入图片描述

线程终止

停止一个线程通常意味着在线程处理任务完成之前停掉正在做的操作,也就是放弃当前的操作。

在 Java 中有以下 3 种方法可以终止正在运行的线程:

  1. 使用 stop() 方法强行终止线程,但是不推荐使用这个方法,该方法已被弃用。存在线程安全问题
  2. 使用 interrupt 方法中断线程。
  3. 使用退出标志,使线程正常退出,也就是当 run() 方法完成后线程中止。

Stop终止线程

通过查看 JDK 的 API,我们会看到 java.lang.Thread 类型提供了一系列的方法如 start()、stop()、resume()、suspend()、destory()等方法来管理线程。但是除了 start() 之外,其它几个方法都被声名为已过时(deprecated)。

虽然 stop() 方法确实可以停止一个正在运行的线程,但是这个方法是不安全的,而且该方法已被弃用,最好不要使用它。 JDK 文档中还引入用一篇文章来解释了弃用这些方法的原因:《Why are Thread.stop, Thread.suspend and Thread.resume Deprecated?》

为什么弃用stop:

  1. 调用 stop() 方法会立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 语句中的,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭。
  2. 调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。

例如,存在一个对象 u 持有 ID 和 NAME 两个字段,假如写入线程在写对象的过程中,只完成了对 ID 的赋值,但没来得及为 NAME 赋值,就被 stop() 导致锁被释放,那么当读取线程得到锁之后再去读取对象 u 的 ID 和 Name 时,就会出现数据不一致的问题。

interrupt 终止线程

如果目标线程在调用Object class的wait()、wait(long)或wait(long, int)方法、join()、join(long, int)或sleep(long, int)方法时被阻塞,那么Interrupt会生效,该线程的中断状态将被清除,抛出InterruptedException异常。

在这里插入图片描述

如果目标线程是被I/O或者NIO中的Channel所阻塞,同样,I/O操作会被中断或者返回特殊异常值。达到终止线程的目的。

如果以上条件都不满足,则会设置此线程的中断状态。

对下示例,stop()改成interrupt()后,最终输出为"i=1 j=1",数据一致。

public class ThreadStop1 {
    public static void main(String[] args) throws InterruptedException {
        StopThread thread = new StopThread();

        thread.start();
        // 休眠1秒,确保i变量自增成功
        Thread.sleep(1000);
        // 错误的终止(强制终止可能导致i和j的结果不一致,破坏线程安全问题)
//        thread.stop();

        // 正确终止
        thread.interrupt();
        while (thread.isAlive()) {
            // 确保线程已经终止
        }
        thread.print();
    }

    static class StopThread extends Thread {
        private int i = 0, j = 0;

        @Override
        public void run() {
            synchronized (this) {
                // 增加同步锁,确保线程安全
                ++i;
                try {
                    // 休眠10秒,模拟耗时操作
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ++j;
            }
        }

        public void print() {
            System.out.println("i=" + i + " j=" + j);
        }
    }

}

使用标志位终止线程

在 run() 方法执行完毕后,该线程就终止了。但是在某些特殊的情况下,run() 方法会被一直执行;比如在服务端程序中可能会使用 while(true) { ... } 这样的循环结构来不断的接收来自客户端的请求。此时就可以用修改标志位的方式来结束 run() 方法。

public class ServerThread extends Thread {
    //volatile修饰符用来保证其它线程读取的总是该变量的最新的值
    public volatile boolean exit = false; 

    @Override
    public void run() {
        ServerSocket serverSocket = new ServerSocket(8080);
        while(!exit){
            serverSocket.accept(); //阻塞等待客户端消息
            ...
        }
    }
    
    public static void main(String[] args) {
        ServerThread t = new ServerThread();
        t.start();
        ...
        t.exit = true; //修改标志位,退出线程
    }
}

线程通信

线程通信(如:线程执行先后顺序,获取某个线程执行的结果等)有多种方式:

  • 文件共享: 线程1 --写入--> 文件 < --读取-- 线程2
  • 网络共享
  • 变量共享: 线程1 --写入--> 主内存共享变量 < --读取-- 线程2
  • jdk提供的线程协调API suspend/resume wait/notify park/unpark。

下面内容中主要对jdk中提供的线程协作API进行梳理讲解。

线程协作的典型场景:生产者-消费者 模型(线程阻塞、线程唤醒)。如:线程1去买包子,没有包子,则不再执行,线程2生产包子,通知线程1继续执行 在这里插入图片描述

API - 被弃用 suspend/resume

调用suspend挂起目标线程, 通过resume可以恢复线程执行, 对调用顺序有要求,也要开发者自己注意锁的释放。这个被弃用的API, 容易死锁,也容易导致永久挂起。

public class ThreadInteration {
    public static Object baozidian =null;
    public static void main(String[] args) throws InterruptedException {
        new ThreadInteration().suspendResumeTest();
    }
    /**
     * 使用弃用的API suspend和resume 来挂起目标线程和恢复线程执行
     * 这两个api容易写出死锁的代码。
     * 1,使用同步锁的时候,因为suspend不会释放锁,这样会导致死锁。
     * 2,suspend 和 resume 的执行顺序颠倒,会导致死锁。
     */
    //正常suspend 和 resume
    public void suspendResumeTest() throws InterruptedException {
        Thread consumerThread =new Thread(()->{
            if (baozidian==null){
                System.out.println("1 进入等待,线程被挂起");
                Thread.currentThread().suspend();
                System.out.println("线程被唤醒了");
            }
            System.out.println("3 买到包子了,回家!");
        });
        consumerThread.start();
        Thread.sleep(3000L);
        //生产者创建
        baozidian=new Object();
        consumerThread.resume();
        System.out.println("2 通知消费者,消费者线程被唤醒");
    }
}    

死锁情况:

public class ThreadInteration {
    public static Object baozidian =null;
    public static void main(String[] args) throws InterruptedException {
        new ThreadInteration().suspendResumeDeadLockTest();
//        new ThreadInteration().suspendResumeDeadLockTest2();
    }
    
    /**使用同步锁导致死锁,suspend和resume不会像wait一样释放锁**/
    public void suspendResumeDeadLockTest() throws InterruptedException {
        //创建线程
        Thread consumerThread=new Thread(()->{
            if (baozidian==null){//如果没有包子,就进入等待
                //当前线程拿到锁,线程被挂起
                synchronized (this){
                    System.out.println("1 进入等待,线程被挂起");
                    Thread.currentThread().suspend();
                    System.out.println("线程被唤醒了");
                }
                
            }
            System.out.println("3 买完包子,回家");
        });
        consumerThread.start();
        Thread.sleep(2000L);
        //产生包子
        baozidian=new Object();
        //争取到锁后,再恢复consumerThread()。
        synchronized (this){
            consumerThread.resume();
        }
        System.out.println("2 通知消费者,消费者线程被唤醒");
    }
    
    /** 由于suspend/resume的调用顺序,导致程序永久死锁 **/
    public void suspendResumeDeadLockTest2() throws InterruptedException {
        Thread consumerThread=new Thread(()->{
            if (baozidian==null){
                System.out.println("1 进入线程,线程被挂起");
                try {
                    Thread.sleep(5000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //这里的suspend是运行在resume之后
                Thread.currentThread().suspend();
                System.out.println("线程被唤醒了");
            }
            System.out.println("3 买完包子,回家");
        });
        consumerThread.start();
        Thread.sleep(2000L);
        baozidian=new Object();
        consumerThread.resume();
        System.out.println("2 通知消费者,消费者线程被唤醒");
        consumerThread.join();
    }
}
suspendResumeDeadLockTest: 
		1、进入等待 (一直处于等待状态,由于suspend未释放锁)
suspendResumeDeadLockTest2:
		1、没包子,进入等待
		3、通知消费者 (一直处于等待状态,由于suspend/resume的调用顺序)

API - wait/notify

这些方法只能由同一对象锁的线程持有者调用,也就是写在同步代码块里面, 否则会抛出IllegalMonitorStateException异常。 wait: 方法导致当前线程等待, 加入该对象的等待集合中, 并且放弃当前持有的对象锁 notify/notifyAll: 唤醒一个/所有正在等待这个对象锁的线程 注意: 虽然wait会自动解锁, 但对顺序有要求, 如果在notify被调用之后, 才开始wait方法的调用, 线程会永远处于WAINTING状态

public class ThreadInteration {
    public static Object baozidian =null;
    public static void main(String[] args) throws InterruptedException {
        new ThreadInteration().waitNotifyTest();
//        new ThreadInteration().waitNotifyDeadLockTest();
    }
    /**
     * API推荐的 wait/notify 机制来挂起线程和唤醒线程
     * 这些方法一定要是在同一锁对象的持有者线程调用。也就是写在同步代码块里面,否则会抛出IllegalMonitorStateException.
     * wait方法就是将线程等待,调用wait就是把对象加入到 等待集合 中。并且放弃当前持有的锁对象
     * notify/notify唤醒一个或者所有正在等待这个对象锁的进程。
     *
     * wait虽然会释放锁,但是对调用的顺序有要求。如果notify先与wait调用,线程会一直处于waiting状态。
     */
    public void waitNotifyTest() throws InterruptedException {
        new Thread(()->{
            if (baozidian==null){
                System.out.println("1 进入等待,线程将会被挂起");
                synchronized (this){//顺序1 获取到锁
                    try {
                        this.wait();//顺序2 线程挂起,释放了锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("线程被唤醒了");
            }
            System.out.println("3 买到包子,回家");
        }).start();
        Thread.sleep(2000L);
        baozidian=new Object();
        synchronized (this){//顺序3 主线程拿到了锁
            this.notify();//顺序4 主线程进行唤醒
        }
        System.out.println("2 通知消费者,消费者线程被唤醒");
    }
    /** wait虽然会释放锁,但是对调用的顺序有要求。如果notify先与wait调用,线程会一直处于waiting状态。 **/
    public void waitNotifyDeadLockTest() throws InterruptedException {
        new Thread(()->{
            if (baozidian==null){
                try {
                    Thread.sleep(5000L);//顺序1 
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("1 进入等待,线程被挂起");
                synchronized (this){
                    try {
                        this.wait();//顺序4 先唤醒了,再进行休眠。导致死锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("线程被唤醒了");
            }
            System.out.println("3 买到包子,回家");
        }).start();
        
        Thread.sleep(2000L);//顺序2
        baozidian=new Object();
        synchronized (this){
            this.notify();//顺序3 
        }
        System.out.println("2 通知消费者,消费者线程被唤醒");
    }
}

API - park/unpark

线程调用park则等待“许可”, unpark方法为指定线程提供“许可”。 不要求park和unpark方法的调用顺序。 多次调用unpark后再调用park, 线程会直接运行, 但不会叠加, 也就是说, 连续多次调用park方法, 第一次会拿到“许可”直接运行, 后续调用会进入等待。 注意: park/unpark 对调用顺序没有要求, 但是并不会释放锁

public class ThreadInteration {
    public static Object baozidian =null;
    public static void main(String[] args) throws InterruptedException {
        new ThreadInteration().parkUnparkTest();
//        new ThreadInteration().parkUnparkDeadLockTest();
    }
    public void parkUnparkTest() throws InterruptedException {
        Thread consumerThread=new Thread(()->{
            if (baozidian==null){
//                try {
//                    Thread.sleep(5000L);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
                System.out.println("1 进入等待,线程被挂起");
                LockSupport.park();
                System.out.println("线程被唤醒了");
            }
            System.out.println("3 买到包子,回家");
        });
        consumerThread.start();
        Thread.sleep(2000L);
        baozidian=new Object();
        LockSupport.unpark(consumerThread);
        System.out.println("2 通知消费者,消费者线程被唤醒");
    }
    
    /** park/unpark 不能自动释放锁**/
    public void parkUnparkDeadLockTest() throws InterruptedException {
        Thread consumerThread=new Thread(()->{
            if (baozidian==null){
                System.out.println("1 进入等待,线程被挂起");
                synchronized (this){//这个时候park获取了锁,然后挂起了。没有及时释放锁导致后面的unpark获取不到锁,就执行不了unpark
                    LockSupport.park();
                }
                System.out.println("线程被唤醒了");
            }
            System.out.println("3 买到包子,回家");
        });
        consumerThread.start();
        Thread.sleep(2000L);
        baozidian=new Object();
        synchronized (this){
            LockSupport.unpark(consumerThread);
        }
        System.out.println("2 通知消费者,消费者线程被唤醒");
    }
}

伪唤醒

前面使用了if (baozidian==null) 来判断是否进入等待状态,是错误的。是指并非由notify/unpack来唤醒的,由更底层的原因被唤醒。官方建议使用while () 来判断是否进入等待状态。 因为:处于底层的线程可能会收到错误警报和伪唤醒,如果不在循环中检查,程序可能会在没有满足条件的情况下退出解决方案就是将上面的if全部改成while

/**
     * 伪唤醒:前面使用了if (baozidian==null) 来判断是否进入等待状态,是错误的。是指并非由notify/unpack来唤醒的,由更底层的原因被唤醒。
     * 官方建议使用while (baozidian==null) 来判断是否进入等待状态。
     * 因为:处于底层的线程可能会收到错误警报和伪唤醒,如果不在循环中检查,程序可能会在没有满足条件的情况下退出
     * 解决方案就是将上面的if全部改成while
     */
    public void waitNotifyGoodTest() throws InterruptedException {
        new Thread(()->{
            synchronized (this){
                //将while放入同步锁中判断
                while (baozidian==null){
                    System.out.println("1 进入等待,线程将会被挂起");
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("线程被唤醒了");
            
            }
            System.out.println("3 买到包子,回家");
        }).start();
    
        Thread.sleep(2000L);
        baozidian=new Object();
        synchronized (this){
            this.notify();
        }
        System.out.println("2 通知消费者,消费者线程被唤醒");
    }

线程封闭

多线程访问共享可变数据时,涉及到线程间数据同步的问题。并不是所有时候,都要用到共享数据,所以线程封闭概念就提出来了。数据被封闭在各自的数据线程之中,就不需要同步,这种通过将数据封闭在线程中而避免使用同步的技术称之为线程封闭。 JAVA中线程封闭具体的实现由:ThreadLocal、局部变量。

ThreadLocal

ThreadLocal它是一个线程级别的变量,每一个线程都有一个ThreadLocal就是每一个线程都有了自己独立的一个变量。竞争条件被彻底消除了,在并发模式下是绝对安全的变量。

用法: ThreadLocal var = new ThreadLocal(); 会自动在每一个线程上创建一个T的副本,副本之间彼此独立,互不影响。可以用ThreadLocal存储一下参数,以便在线程中多个方法中使用,用来代替方法传参的做法。

public class ThreadClose {
    /**
     * threadLocal变量,每个线程都有一个副本,互不干扰
     */
    public static ThreadLocal<String> value = new ThreadLocal<>();

    /** 线程封闭测试代码 **/
    public void threadLocalTest() throws Exception {

        // 主线程设置值
        value.set("这是主线程设置的123");
        String v = value.get();
        System.out.println("线程1执行之前,主线程取到的值:" + v);

        new Thread(() -> {
            String v1 = value.get();
            System.out.println("线程1取到的值:" + v1);
            // 设置 threadLocal
            value.set("这是线程1设置的456");

            v1 = value.get();
            System.out.println("重新设置之后,线程1取到的值:" + v1);
            System.out.println("线程1执行结束");
        }).start();

        // 等待所有线程执行结束
        Thread.sleep(5000L);
        v = value.get();
        System.out.println("线程1执行之后,主线程取到的值:" + v);

    }

    public static void main(String[] args) throws Exception {
        new ThreadClose().threadLocalTest();
    }
}

可以理解为,JVM维护了一个Map<Thread,T>,每个线程要用到这个T的时候,用当前线程去Map里面取。

栈封闭 局部变量的固有属性之一就是封闭在线程中。 它位于执行线程的栈中(栈中由局部变量表),其他线程无法访问这个栈