并发编程从零开始(一)

并发编程从零开始(一)

简介

java是一个支持多线程的开发语言。多线程可以在包含多个CPU核心的机器上同时处理多个不同的任务,优化资源的使用率,提升程序的效率。在一些对性能要求比较高场合,多线程是java程序调优的重要方面。

Java并发编程主要涉及以下几个部分:

  • 并发编程三要素:原子性:即一个不可再被分割的颗粒。在Java中原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。

    有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)

    可见性:当多个线程访问同一个变量时,如果其中一个线程对其作了修改,其他线程能立即获取到最新的值。

  • 线程的五大状态:

    创建状态:当用 new 操作符创建一个线程的时候

    就绪状态:调用 start 方法,处于就绪状态的线程并不一定马上就会执行 run 方法,还需要等待CPU的调度

    运行状态:CPU 开始调度线程,并开始执行 run 方法

    阻塞状态:线程的执行过程中由于一些原因进入阻塞状态比如:调用 sleep 方法、尝试去得到一个锁等等

    死亡状态:run 方法执行完 或者 执行过程中遇到了一个异常

  • 悲观锁与乐观锁:

    悲观锁:每次操作都会加锁,会造成线程阻塞。

    乐观锁:每次操作不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止,不会造成线程阻塞。

  • 线程之间的协作:

    线程间的协作有:wait/notify/notifyAll等 。

  • synchronized 关键字:

    synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:

    1. 修饰一个代码块:被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象
    2. 修饰一个方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
    3. 修饰一个静态的方法:其作用的范围是整个静态方法,作用的对象是这个类的所有对象
    4. 修饰一个类:其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
  • CAS:

    CAS全称是Compare And Swap,即比较替换,是实现并发应用到的一种技术。操作包含三个操作数—内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。CAS存在三大问题:ABA问题,循环时间长开销大,以及只能保证一个共享变量的原子操作。

  • 线程池:

    如果我们使用线程的时候就去创建一个线程,虽然简单,但是存在很大的问题。如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。线程池通过复用可以大大减少线程频繁创建与销毁带来的性能上的损耗。


第一部分:多线程&并发设计原理

1. 多线程

1.1 Thread 和 Runnable

1.1.1 java中的线程

创建执行线程的两种方法:

  • 扩展Thread 类。继承Thread类实现多线程,覆盖run()方法。

    public class ThreadCreatingByThread extends Thread{
        @Override
        public void run() {
            while(true){
                System.out.println(Thread.currentThread().getName()+" is running");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    在Main中调用测试:

    public class Main {
        public static void main(String[] args) {
            //调用线程创建类创建线程
    
            //通过继承Thread类,重写run()方法的方式创建线程
            ThreadCreatingByThread thread = new ThreadCreatingByThread();
            thread.start();
        }
    }
    
  • 实现Runnable 接口。实现run()方法。

    public class ThreadCreatingByRunnable implements Runnable{
        @Override
        public void run() {
            while(true){
                System.out.println(Thread.currentThread().getName()+" is running");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    在Main中调用测试:

    public class Main {
        public static void main(String[] args) {
            //调用线程创建类创建线程
    
            //通过继承Runnable接口,实现run()方法的方式创建线程
            Thread thread = new Thread(new ThreadCreatingByRunnable());
            thread.start();
        }
    }
    
  • 覆写Callable接口实现多线程(jdk1.5),实现call()方法,有返回值。

    public class ThreadCreatingByCallable implements Callable<String> {
        @Override
        public String call() throws Exception {
            Thread.sleep(3000);
            return "hello world call() invoked!";
        }
    }
    

    在Main中进行测试:

    public class Main {
        public static void main(String[] args) throws InterruptedException, ExecutionException {
            //调用线程创建类创建线程
            //覆写Callable接口实现多线程(jdk1.5),实现call()方法,有返回值。
            ThreadCreatingByCallable threadCreatingByCallable = new ThreadCreatingByCallable();
            //定义FutureTask代表了一个由Callable定义的未来的工作
            // 设置Callable对象,泛型表示Callable的返回类型
            FutureTask<String> futureTask = new FutureTask<>(threadCreatingByCallable);
            // 启动处理线程
            new Thread(futureTask).start();
            // 同步等待线程运行的结果
            String result = futureTask.get();
            System.out.println(result);
    
        }
    }
    
  • 通过线程池启动多线程。

1.1.2 Java中的线程: 特征和状态
  1. 所有的Java 程序,不论并发与否,都有一个名为主线程的Thread 对象。执行该程序时, Java虚拟机( JVM )将创建一个新Thread 并在该线程中执行main()方法。这是非并发应用程序中唯一的线程,也是并发应用程序中的第一个线程。

  2. Java中的线程共享应用程序中的所有资源,包括内存和打开的文件,快速而简单地共享信息。但是必须使用同步避免数据竞争

  3. Java中的所有线程都有一个优先级,这个整数值介于Thread.MIN_PRIORITY(1)和Thread.MAX_PRIORITY(10)之间,默认优先级是Thread.NORM_PRIORITY(5)。线程的执行顺序并没有保证,通常,较高优先级的线程将在较低优先级的钱程之前执行。

  4. 在Java 中,可以创建两种线程:

    • 守护线程。
    • 非守护线程。

    区别在于它们如何影响程序的结束。

    Java程序结束执行过程的情形:

    • 程序执行Runtime类的exit()方法, 而且用户有权执行该方法。
    • 应用程序的所有非守护线程均已结束执行,无论是否有正在运行的守护线程

    守护线程通常用在作为垃圾收集器或缓存管理器的应用程序中,执行辅助任务。在线程start之前调用isDaemon()方法检查线程是否为守护线程,也可以使用setDaemon()方法将某个线程确立为守护线程。

  5. Thread.States类中定义线程的状态如下:

    • NEW:Thread对象已经创建,但是还没有开始执行。
    • RUNNABLE:Thread对象正在Java虚拟机中运行。
    • BLOCKED : Thread对象正在等待锁定。
    • WAITING:Thread 对象正在等待另一个线程的动作。
    • TIME_WAITING:Thread对象正在等待另一个线程的操作,但是有时间限制。
    • TERMINATED:Thread对象已经完成了执行。

    getState()方法获取Thread对象的状态,可以直接更改线程的状态。

    在给定时间内, 线程只能处于一个状态。这些状态是JVM使用的状态,不能映射到操作系统的线程状态。

1.1.3 Thread类和Runnable接口

Runnable接口只定义了一种方法:run()方法。这是每个线程的主方法。当执行start()方法启动新线程时,它将调用run()方法。

Thread类其他常用方法:

  • 获取和设置Thread对象信息的方法。
    • getId():该方法返回Thread对象的标识符。该标识符是在钱程创建时分配的一个正整数。在线程的整个生命周期中是唯一且无法改变的。
    • getName()/setName():这两种方法允许你获取或设置Thread对象的名称。这个名称是一个String对象,也可以在Thread类的构造函数中建立。
    • getPriority()/setPriority():你可以使用这两种方法来获取或设置Thread对象的优先级。
    • isDaemon()/setDaemon():这两种方法允许你获取或建立Thread对象的守护条件。
    • getState():该方法返回Thread对象的状态
  • interrupt():中断目标线程,给目标线程发送一个中断信号,线程被打上中断标记。
  • interrupted():判断目标线程是否被中断,但是将清除线程的中断标记。
  • isinterrupted():判断目标线程是否被中断,不会清除中断标记。
  • sleep(long ms):该方法将线程的执行暂停ms时间。
  • join():暂停线程的执行,直到调用该方法的线程执行结束为止。可以使用该方法等待另一个Thread对象结束。也可以理解为:当我们调用某个线程的这个方法时,这个方法会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行。
  • setUncaughtExceptionHandler():当线程执行出现未校验异常时,该方法用于建立未校验异常的控制器。
  • currentThread():Thread类的静态方法,返回实际执行该代码的Thread对象。

Thread类常用方法以及join()方法示例:

public class ThreadCreatingForJoinByThread extends Thread{
    @Override
    public void run() {
        for (int i = 1 ; i<=10 ; i++ ){
            System.out.println(Thread.currentThread().getName()+i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Main {
    public static void main(String[] args) throws InterruptedException {
        //调用线程创建类创建线程

        //thread对象常用方法
        ThreadCreatingForJoinByThread thread = new ThreadCreatingForJoinByThread();
        System.out.println(thread.getName());
        System.out.println(thread.getId());
        System.out.println(thread.getContextClassLoader());
        System.out.println(thread.getPriority());
        System.out.println(thread.getState());
        System.out.println(thread.isDaemon());
        thread.start();
        System.out.println(thread.getState());
        //join()方法调用
        thread.join();
        System.out.println("main running finished");
    }
}
1.1.4 Callable

Callable 接口是一个与Runnable 接口非常相似的接口。一般用于Future模式。Callable 接口的主要特征如下:

  • 接口。有简单类型参数,与call()方法的返回类型相对应。

  • 声明了call()方法。执行器运行任务时,该方法会被执行器执行。它必须返回声明中指定类型的对象。

  • call()方法可以抛出任何一种校验异常。可以实现自己的执行器并重载afterExecute()方法来处理这些异常。

    ThreadCreatingByCallable threadCreatingByCallable = new ThreadCreatingByCallable();
            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                    5,5,1, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10)
            ){
                //如果call方法执行过程中存在异常,则可以在此处处理
                @Override
                protected void afterExecute(Runnable r, Throwable t) {
                    System.out.println("mission has been execute successfully:"+t);
                }
            };
    
            Future<String> future = threadPoolExecutor.submit(threadCreatingByCallable);
            String result = future.get();
            System.out.println(result);
            //关闭线程池
            threadPoolExecutor.shutdown();
    

1.2 synchronized 关键字

1.2.1 锁的对象

synchronized关键字“给某个对象加锁”,示例代码:

public class MyClass {
    //实例方法 (两个方法相互等价)
    public synchronized void method1(){
        // ...
    }

    public void method2(){
        synchronized (this){
            // ...
        }
    }
    //静态方法 (两个方法相互等价)
    public static synchronized void method3(){
        // ...
    }
    
    public static void method4(){
        synchronized (MyClass.class){
            // ...
        }
    }
}

实例方法的锁加在对象myClass上;静态方法的锁加在MyClass.class上。

1.2.2 锁的本质

如果一份资源需要多个线程同时访问,需要给该资源加锁。加锁之后,可以保证同一时间只能有一个线程访问该资源。资源可以是一个变量、一个对象或一个文件等。

image-20211025154048676

锁是一个“对象”,作用如下:

  1. 这个对象内部得有一个标志位(state变量),记录自己有没有被某个线程占用。最简单的情况是这个state有0、1两个取值,0表示没有线程占用这个锁,1表示有某个线程占用了这个锁。
  2. 如果这个对象被某个线程占用,记录这个线程的thread ID。
  3. 这个对象维护一个thread id list,记录其他所有阻塞的、等待获取拿这个锁的线程。在当前线程释放锁之后从这个thread id list里面取一个线程唤醒。

要访问的共享资源本身也是一个对象,例如前面的对象myClass,这两个对象可以合成一个对象。代码就变成synchronized(this) {…},要访问的共享资源是对象a,锁加在对象a上。当然,也可以另外新建一个对象,代码变成synchronized(obj1) {…}。这个时候,访问的共享资源是对象a,而锁加在新建的对象obj1上。

资源和锁合二为一,使得在Java里面,synchronized关键字可以加在任何对象的成员上面。这意味着,这个对象既是共享资源,同时也具备“锁”的功能!

1.2.3 实现原理

修饰对象:在对象头里,有一块数据叫Mark Word。在64位机器上,Mark Word是8字节(64位)的,这64位中有2个重要字段:锁标志位和占用该锁的thread ID。因为不同版本的JVM实现,对象头的数据结构会有各种差异。

修饰同步代码块:中是在对象头中有一个monitor对象,对应着monitorenter和monitorexit指令,当执行enter指令时尝试获取monitor的持有权,获取成功将计数器从0设为1,如果获取失败就阻塞等待别的线程释放。

修饰方法:的话是ACC_SYNCHRONIZED标识,标明是一个同步方法。JVM通过这个标识才执行相应的同步调用。

1.2.4 优化

在jdk 1.6后,对synchronized锁进行了优化。

偏向锁:JVM认为只有某个线程才会执行同步代码(没有竞争环境),所以在MarkWord会直接记录线程ID,只要线程来执行代码就会对比线程ID是否相等,相等则直接获取到锁,不相等就CAS来尝试修改当前的线程ID,如果CAS修改成功就继续,如果失败说明有竞争环境,升级为轻量级锁。简单来说就是:如果存在竞争环境,则升级为轻量级锁。

轻量级锁:轻量级锁是相对于重量级锁而言,轻量级锁不需要申请互斥量,只需要将markwork中的部分字节CAS更新指向线程的id,如果更新成功则表示已经成功的获取了锁,否则说明已经有线程获取了轻量级锁,发生了锁竞争,轻量级锁开始自旋。

在jdk1.6之前,设置了自旋锁自旋次数为10次。1.6及之后,优化为自适应自旋锁。可以根据加锁的代码来决定要自选几次. 如果自旋超过一定次数,或者此时有第三个线程来竞争该锁时,锁膨胀为重量级锁。

重量级锁:Jvm每次从队列中取出一个线程来用于锁竞争候选者即竞争线程.但是并发情况下,尾部list会被大量的并发线程的访问为了降低竞争,提高获取线程的速度,JVM将竞争的list拆为了两份,获取竞争线程时只从头部获取,而新进入的竞争线程则被放到尾部.提高了竞争时的效率.当Owner线程在unlock时会将尾部线程的部分线程迁移到头部线程中,并且制定头部线程的某一个线程作为竞争线程,但是并没有直接将锁交给竞争线程,而是让竞争线程自己来获取锁,这样做虽然会牺牲公平性,但是会极大的提升系统的吞吐量。

synchronized是非公平锁.当线程在进入尾部队列之前,会尝试着先自旋获取锁,如果获取失败才选择进入尾部队列。之后的操作参考重量级锁。

公平锁底层为将线程放入一个先进先出的队列中,按照顺序一次获取资源。


1.3 wait与notify

wait方法会让线程进入等待队列,若要执行wait方法,线程必须持有锁,但如果线程进入等待队列,便会释放实例的锁。

notify()方法会将等待队列中的一个线程取出。那么在等待队列中的那个线程便会被选中唤醒,然后退出等待队列。这里需要注意的是,在执行notify唤醒的线程并不会在执行notify的一瞬间重新运行。因为在执行notify的那一瞬间,执行notify的线程还持着锁,所以其他线程还无法获取这个实例的锁。

notifyAll()方法会将等待队列中的所有线程都取出来,所有等待的线程都会被唤醒。有意思的是,在执行notifyAll()方法时,谁持着锁呢?当然是执行notifyAll()的线程正持着锁,因此,唤醒的线程虽然都退出了等待队列,但都在等待获取锁,处于阻塞状态,只有在执行notifyAll之后的线程释放锁以后,其中的一个幸运儿才能够实际运行。

在调用之前,先判定该线程是否持有该锁。

1.3.1 生产者-消费者模型

image-20211025162411308

一个内存队列,多个生产者线程往内存队列中放数据;多个消费者线程从内存队列中取数据。要实现这样一个编程模型,需要做下面几件事情:

  • 内存队列本身要加锁,才能实现线程安全。
  • 阻塞。当内存队列满了,生产者放不进去时,会被阻塞;当内存队列是空的时候,消费者无事可做,会被阻塞。
  • 双向通知。消费者被阻塞之后,生产者放入新数据,要notify()消费者;反之,生产者被阻塞之后,消费者消费了数据,要notify()生产者。

第1件事情必须要做,第2件和第3件事情不一定要做。例如,可以采取一个简单的办法,生产者放不进去之后,睡眠几百毫秒再重试,消费者取不到数据之后,睡眠几百毫秒再重试。但这个办法效率低下,也不实时。所以,我们只讨论如何阻塞、如何通知的问题。

如何阻塞?

办法1:线程自己阻塞自己,也就是生产者、消费者线程各自调用wait()和notify()。

办法2:用一个阻塞队列,当取不到或者放不进去数据的时候,入队/出队函数本身就是阻塞的。

如何双向通知?

办法1:wait()与notify()机制。

办法2:Condition机制。

单个生产者单个消费者线程的情形:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        MessageQueue messageQueue = new MessageQueue();
        new ProducerThread(messageQueue).start();
        new ConsumerThread(messageQueue).start();
        Thread.sleep(2000);
        System.exit(0);
    }
}
public class ProducerThread extends Thread{
    private final MessageQueue messageQueue;
    private final Random random = new Random();
    private int index = 0;
    public ProducerThread(MessageQueue messageQueue) {
        this.messageQueue = messageQueue;
    }


    @Override
    public void run() {
        while (true){
            String message = String.valueOf(index++);
            messageQueue.put(message);
            System.out.println("生产数据: "+message);
            try {
                Thread.sleep(random.nextInt(100));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}
public class ConsumerThread extends Thread{
    private final MessageQueue messageQueue;
    private final Random random = new Random();
    public ConsumerThread(MessageQueue messageQueue) {
        this.messageQueue = messageQueue;
    }

    @Override
    public void run() {
        while (true){
            String result = null;
            try {
                result = messageQueue.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("consumer data is "+result);
            try {
                Thread.sleep(random.nextInt(100));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}
@SuppressWarnings("all")
public class MessageQueue {
    private String[] data = new String[10];
    //下一条存储记录的下标
    private int putIndex = 0;
    //下一条要获取的记录的下标
    private int getIndex = 0;
    //data中元素的个数
    private int size = 0;

    public synchronized void put(String element){
        if (size == data.length){
            try{
                //阻塞,等待
                wait();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
        data[putIndex++] = element;
        //唤醒消费者
        notify();
        size++;
        if (putIndex == data.length){
            putIndex = 0;
        }
    }

    public synchronized String get() throws InterruptedException {
        if (size == 0){
            wait();
        }
        String result = data[getIndex++];
        if(getIndex == data.length) getIndex=0;
        size--;
        //唤醒生产者
        notify();
        return result;
    }
}

多个生产者多个消费者的情形:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        //多个生产者和多个消费者
        MessageQueue2 messageQueue = new MessageQueue2();
        for (int i = 0 ; i < 3 ; i++ ){
            new ConsumerThread(messageQueue).start();
        }
        for (int i = 0 ; i<5 ; i++ ){
            new ProducerThread(messageQueue).start();
        }
    }
}
@SuppressWarnings("all")
public class MessageQueue2 extends MessageQueue{
    private String[] data = new String[10];
    //下一条存储记录的下标
    private int putIndex = 0;
    //下一条要获取的记录的下标
    private int getIndex = 0;
    //data中元素的个数
    private int size = 0;

    private void commonPut(String element){
        data[putIndex++] = element;
        //唤醒消费者
        notify();
        size++;
        if (putIndex == data.length){
            putIndex = 0;
        }
    }

    @Override
    public synchronized void put(String element) {
        if (size == data.length){
            try{
                //阻塞,等待
                wait();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            //利用迭代,重新获取共享锁
            put(element);
        }else {
            commonPut(element);
        }

    }

    private String commonGet(){
        String result = data[getIndex++];
        if(getIndex == data.length) getIndex=0;
        size--;
        //唤醒生产者
        notify();
        return result;
    }

    @Override
    public synchronized String get() throws InterruptedException {
        if (size == 0){
            wait();
            //利用迭代,重新获取共享锁
            return get();
        }else{
            return commonGet();
        }

    }
}
1.3.2 为什么必须要和synchronized一起使用

在Java里面,wait()和notify()是Object的成员函数,是基础中的基础。为什么Java要把wait()和notify()放在如此基础的类里面,而不是作为像Thread一类的成员函数,或者其他类的成员函数呢?

两个线程之间要通信,对于同一个对象来说,一个线程调用该对象的wait(),另一个线程调用该对象的notify(),这两个操作需要协调,所以该对象本身就需要同步!所以,在调用wait()、notify()之前,要先通过synchronized关键字同步给对象,也就是给该对象加锁。并且调用这些方法之前需要确定是否获得了该锁,所以需要和synchronized关键字一起使用。

synchronized关键字可以加在任何对象的实例方法上面,任何对象都可能成为锁。因此,wait()和notify()只能放在Object里面了。

1.3.3 为什么wait()的时候必须要释放锁

当线程A进入synchronized(obj1)中之后,也就是对obj1上了锁。此时,调用wait()进入阻塞状态,一直不能退出synchronized代码块;那么,线程B永远无法进入synchronized(obj1)同步块里,永远没有机会调用notify(),发生死锁。

在wait()的内部,会先释放锁obj1,然后进入阻塞状态,之后,它被另外一个线程用notify()唤醒,重新获取锁!其次,wait()调用完成后,执行后面的业务逻辑代码,然后退出synchronized同步块,再次释放锁。

wait(){
	//释放锁
    //阻塞,等待被其他线程notify
    //重新获取锁
}

如此则可以避免死锁。

1.3.4 wait()与notify()的问题

生产者在通知消费者的同时,也通知了其他的生产者;消费者在通知生产者的同时,也通知了其他消费者。原因在于wait()和notify()所作用的对象和synchronized所作用的对象是同一个,只能有一个对象,无法区分队列空和列队满两个条件。这正是Condition要解决的问题


1.4 InterruptedException和interrupt()方法

1.4.1 Interrupted异常

什么情况下会抛出Interrupted异常

只有那些声明了会抛出InterruptedException的函数才会抛出异常,也就是下面这些常用的函数:

public static native void sleep(long millis) throws InterruptedException {...} 
public final void wait() throws InterruptedException {...} public final void join() throws InterruptedException {...}
1.4.2 轻量级锁阻塞与重量级阻塞

能够被中断的阻塞称为轻量级阻塞,对应的线程状态是WAITING或者TIMED_WAITING;而像synchronized 这种不能被中断的阻塞称为重量级阻塞,对应的状态是 BLOCKED。如图所示:调用不同的方法后,一个线程的状态迁移过程。

image-20211025190129064

初始线程处于NEW状态,调用start()开始执行后,进入RUNNING或者READY状态。如果没有调用任何的阻塞函数,线程只会在RUNNING和READY之间切换,也就是系统的时间片调度。这两种状态的切换是操作系统完成的,除非手动调用yield()函数,放弃对CPU的占用。

一旦调用了图中的任何阻塞函数,线程就会进入WAITING或TIMED_WAITING状态,两者的区别只是前者为无限期阻塞,后者则传入了一个时间参数,阻塞一个有限的时间。如果使用了synchronized关键字或者synchronized块,则会进入BLOCKED状态。

不太常见的阻塞/唤醒函数,LockSupport.park()/unpark()。这对函数非常关键,Concurrent包中Lock的实现即依赖这一对操作原语。

因此thread.interrupted()的精确含义是“唤醒轻量级阻塞”,而不是字面意思“中断一个线程”。

thread.isInterrupted()Thread.interrupted()的区别

因为 thread.interrupted()相当于给线程发送了一个唤醒的信号,所以如果线程此时恰好处于WAITING或者TIMED_WAITING状态,就会抛出一个InterruptedException,并且线程被唤醒。而如果线程此时并没有被阻塞,则线程什么都不会做。但在后续,线程可以判断自己是否收到过其他线程发来的中断信号,然后做一些对应的处理。

这两个方法都是线程用来判断自己是否收到过中断信号的,前者是实例方法,后者是静态方法。二者的区别在于,前者只是读取中断状态,不修改状态;后者不仅读取中断状态,还会重置中断标志位。


1.5 线程的优雅关闭

1.5.1 stop与destory函数

线程是“一段运行中的代码”,一个运行中的方法。运行到一半的线程能否强制杀死?

不能。在Java中,有stop()、destory()等方法,但这些方法官方明确不建议使用,并在jdk 11中废除。原因很简单,如果强制杀死线程,则线程中所使用的资源,例如文件描述符、网络连接等无法正常关闭。

因此,一个线程一旦运行起来,不要强行关闭,合理的做法是让其运行完(也就是方法执行完毕),干净地释放掉所有资源,然后退出。如果是一个不断循环运行的线程,就需要用到线程间的通信机制,让主线程通知其退出。

1.5.2 守护线程
public class Main {
    public static void main(String[] args) {
        MyDaemonThread myDaemonThread = new MyDaemonThread();
        myDaemonThread.setDaemon(true);
        myDaemonThread.start();
        new MyThread().start();

    }
}
public class MyDaemonThread extends Thread{
    @Override
    public void run() {
        while (true){
            try{
                System.out.println("waking ...");
                Thread.sleep(500);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}
public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("normal thread running");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

对于上面的程序,在thread.start()前面加一行代码thread.setDaemon(true)。当main(...)函数退出后,线程thread就会退出,整个进程也会退出。

当在一个JVM进程里面开多个线程时,这些线程被分成两类:守护线程和非守护线程。默认都是非守护线程。

在Java中有一个规定:当所有的非守护线程退出后,整个JVM进程就会退出。意思就是守护线程“不算作数”,守护线程不影响整个 JVM 进程的退出。

例如,垃圾回收线程就是守护线程,它们在后台默默工作,当开发者的所有前台线程(非守护线程)都退出之后,整个JVM进程就退出了。

1.5.3 设置关闭的标志位

开发中一般通过设置标志位的方式,停止循环运行的线程。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        StopFlagThread stopFlagThread = new StopFlagThread();
        stopFlagThread.start();
        Thread.sleep(500);
        stopFlagThread.changeRunning();
        stopFlagThread.join();
    }
}
public class StopFlagThread extends Thread{
    private boolean running = true;

    @Override
    public void run() {
        while(running){
            System.out.println("thread is running");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void changeRunning(){
        this.running = false;
    }
}

但上面的代码有一个问题:如果MyThread t在while循环中阻塞在某个地方,例如里面调用了object.wait()函数,那它可能永远没有机会再执行 while( !stopped)代码,也就一直无法退出循环。

此时,就要用到InterruptedException()与interrupt()函数。

posted @ 2021-10-25 19:44  会编程的老六  阅读(260)  评论(0编辑  收藏  举报