并发编程-多线程&并发设计原理

并发编程简介

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

并发编程主要设计以下几个方面:

  • 并发编程三要素
    原子性:即一个不可再分割的粒子。在Java中原子性指的是一个或者多个操作要么全部执行成功要么全部执行失败
    有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
    可见性:当多个线程访问同一个变量时,如果其中一个线程对其进行了修改,其他线程能立即获取到最新的值。
  • 线程的5大状态
    创建状态:当用new关键字创建一个线程的时候
    就绪状态:调用start方法,处于就绪状态的线程并不一定马上就会执行run方法,还需要等待CPU的调度
    运行状态:CPU开始调度线程,并开始执行run方法
    阻塞状态:线程的执行过程中由于一些原因进入阻塞状态,如:调用sleep方法、尝试去得到一个锁等等
    死亡状态:run方法执行完毕 或者 执行的过程中遇到了一个异常
  • 悲观锁与乐观锁
    悲观锁:每次操作都会加锁,会造成线程阻塞
    乐观锁:每次操作不加锁而是假设没有冲突而去完成某项操作,如果因为冲突操作失败就重试,直到成功为之,不会造成线程阻塞
  • 线程之间的协作
    wait/notify/notifyAll
  • synchronized关键字
    synchronized是java的关键字,是一种同步锁。他修饰的对象有一下几种:
    • 修饰一个代码块:被修饰的代码块称为同步语句块,其作用的范围就是大括号{}括起来的代码。作用的对象是调用这个代码块的对象
    • 修饰一个方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
    • 修饰一个静态方法:其作用的范围是整个静态方法,其作用的是这个类的所有对象
    • 修饰一个类:其作用的范围是synchronized后面括号括起来的部分,其作用的对象是这个类的所有对象
  • CAS
    CAS全称是Compare And Swap ,即比较替换,是实现并发应用的一种技术。操作包含三个操作数- 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期的原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。
  • 线程池
    如果我们使用线程的时候就去创建一个线程,虽然简单,但是存在很大的问题。如果并发的数量很多,并且每一个线程都是执行一个时间很短的任务就结束了,这样频繁的创建线程就会大大降低系统的效率,因为频繁的创建线程和销毁线程需要时间。线程池通过复用可以大大减少线程频繁创建于销毁带来的性能上的损耗。

多线程&并发设计原理

1 多线程回顾

1.1 Thread和Runnable

1.1.1 Java中的线程

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

  • 扩展Thread类
  • 实现Runnable接口

扩展Thread类的方式创建线程:

package com.hyq.concurrent.demo;

public class MyThread extends Thread{
    @Override
    public void run() {
        while (true){
            System.out.println(Thread.currentThread().getName() + "运行了...");
            try {
                Thread.sleep(800);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
package com.hyq.concurrent.demo;

public class Main {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

实现Runnable接口的方式创建线程:

package com.hyq.concurrent.demo;

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        while (true){
            System.out.println(Thread.currentThread().getName() + "运行了...");
            try {
                Thread.sleep(800);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
package com.hyq.concurrent.demo;

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());   
        thread.start();
    }   
}
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之前调用idDaemon()方法检查线程是否为守护线程,也可以使用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(String name):这两种方法允许你获取或设置Thread对象的名称。这个名称是一个String对象,也可以在Thread类的构造方法中建立
    • getPriority() / setPriority(int newPriority):你可以使用这两种方法来获取或设置Thread对象的优先级
    • idDaemon() / setDaemon():这两种方法允许你获取或建立Thread对象的守护条件
    • getState():该方法返回Thread对象的状态
  • interrupt():中断目标线程,给目标线程发送一个中断信号,线程被打上中断标记

  • interrupted():判断目标线程是否被中断,但是将清除线程的中断标记

  • isInterrupted():判断目标线程是否被中断,不会清除中断标记

  • sleep(long ms):该方法将线程的执行暂停ms毫秒

  • join():暂停线程的执行,直到调用该方法的线程执行结束为止。可以使用该方法等待另一个Thread对象结束

  • setUncaughtExceptionHandler():当线程执行出现未校验异常时,该方法用于建立未校验异常的控制器

  • currentThread():Thread类的静态方法,返回实际执行该代码的Thread对象。

    join实例程序:

    package com.hyq.concurrent.demo;
    
    public class MyThread extends Thread{
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("MyThread 线程 : " + i);
            }
        }
    }
    
    package com.hyq.concurrent.demo;
    
    public class Main {
        public static void main(String[] args) throws InterruptedException {
            MyThread thread = new MyThread();
            thread.start();
            thread.join();
            System.out.println("main 线程 OVER...");
        }
    }
    
1.1.4 Callable

Callable接口是一个与Runnable接口非常相似的接口。Callable接口的主要特征如下:

  • 接口,有简单类型参数,与call()方法的返回类型相对应
  • 声明了call()方法。执行器运行任务时,该方法会被执行器执行。他必须返回声明中指定类型的对象
  • call()方法可以抛出任何一种校验异常。可以实现自己的执行器并重载afterExecute()方法来处理这些异常。
package com.hyq.concurrent.demo;

import java.util.concurrent.Callable;

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        Thread.sleep(1000);
        System.out.println("over");
        return "hello world call() invoked ! ";
    }
}
package com.hyq.concurrent.demo;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable myCallable = new MyCallable();

        //设置Callable对象 , 泛型表示Callable的返回类型
        FutureTask<String> futureTask = new FutureTask<String>(myCallable);
        // 启动处理线程
        new Thread(futureTask).start();

        // 同步等待线程运行的结果
        String result = futureTask.get();
        // 1s 后得到结果
        System.out.println(result);
    }
}
package com.hyq.concurrent.demo;

import java.util.concurrent.*;

public class Main2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        /**
         * 创建线程池执行器对象
         *
         * corePoolSize:指定了线程池中的线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去;
         *
         * maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量;
         *
         * keepAliveTime:当线程池中空闲线程数量超过corePoolSize时,多余的线程会在多长时间内被销毁;
         *
         * unit:keepAliveTime的单位
         *
         * workQueue:任务队列,被添加到线程池中,但尚未被执行的任务;它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列几种;
         */
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                5, 5, 1,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(10)){
            @Override
            protected void afterExecute(Runnable r, Throwable t) {
                System.out.println("线程池中任务运行完毕后执行");
                super.afterExecute(r,t);
            }
        };

        Future<String> future = executor.submit(new MyCallable());

        String result = future.get();

        System.out.println(result);

        executor.shutdown();
    }
}

1.2 synchronized关键字

1.2.1 锁的对象

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

public class MyClass {
    public synchronized void  method01(){
        // ...
    }
    public static synchronized void  method02(){
        // ...
    }
}

等价于

public class MyClass {
    public void  method01(){
        synchronized (this){
            // ...
        }
    }
    public static void  method02(){
        synchronized (MyClass.class){
            // ...
        }
    }
}

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

1.2.2 锁的本质

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

在这里插入图片描述
锁是一个对象,作用如下:

  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实现,所以对象头中的数据结构会有各种差异

1.3 wait与notify

1.3.1 生产者-消费者模型

生产者-消费者模型是一个常见的多线程编程模型,如图所示:
在这里插入图片描述
一个内存队列,多个生产者线程往内存队列中存放数据;多个消费者线程从内存队列中取数据。要实现这样一个编程模型,需要做下面几件事情:

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

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

  1. 如何阻塞?
    办法1:线程自己阻塞自己,也就是生产者、消费者各自调用wait()和notify()
    办法2:用一个阻塞队列,当取不到或者放不进去数据的时候,入队/出队函数本身就是阻塞的
  2. 如何双向通知?
    办法1:wait()与notify()机制
    办法2:Condition机制

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

package com.hyq.concurrent.demo;

public class MyQueue {
    private String[] data = new String[10];
    private int getIndex = 0;
    private int putIndex = 0;
    private int size = 0;

    public synchronized void put(String element){
        if (size == data.length){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        data[putIndex] = element;
        putIndex++;
        if (putIndex == data.length){
            putIndex = 0;
        }
        size++;
        notify();
    }

    public synchronized String get(){
        if (size == 0){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        String result = data[getIndex];
        getIndex++;
        if (getIndex == data.length){
            getIndex = 0;
        }
        size--;
        notify();
        return result;
    }
}
package com.hyq.concurrent.demo;

import java.util.Random;

public class ProductThread extends Thread{

    private final MyQueue myQueue ;
    private final Random random = new Random();
    private int index = 0;

    public ProductThread(MyQueue myQueue) {
        this.myQueue = myQueue;
    }


    @Override
    public void run() {
        while (true){
            String temp = "ele-"+ index;
            myQueue.put(temp);
            System.out.println("添加元素:" + temp);
            index++;
            try{
                Thread.sleep(random.nextInt(1000));
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}
package com.hyq.concurrent.demo;


import java.util.Random;

public class ConsumerThread extends Thread{

    private final MyQueue myQueue ;
    private final Random random = new Random();

    public ConsumerThread(MyQueue myQueue) {
        this.myQueue = myQueue;
    }

    @Override
    public void run() {
        while (true){
            String s = myQueue.get();
            System.out.println("\t 消费元素:" + s);
            try {
                Thread.sleep(random.nextInt(1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
package com.hyq.concurrent.demo;

public class Main {
    public static void main(String[] args)  {
        MyQueue myQueue = new MyQueue();

        ProductThread productThread = new ProductThread(myQueue);
        ConsumerThread consumerThread = new ConsumerThread(myQueue);

        productThread.start();
        consumerThread.start();
    }
}

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

package com.hyq.concurrent.demo;

public class MyQueue2 {
    private String[] data = new String[10];
    private int getIndex = 0;
    private int putIndex = 0;
    private int size = 0;

    public synchronized void put(String element){
        if (size == data.length){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            put(element);//唤醒之后 重新存入该元素
        }else {
            put0(element);
            notify();
        }
    }

    public void put0(String element){
        data[putIndex] = element;
        putIndex++;
        if (putIndex == data.length){
            putIndex = 0;
        }
        size++;
    }

    public synchronized String get(){
        if (size == 0){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return get(); // 唤醒之后重新获取
        }else {
            String result = get0();
            notify();
            return result;
        }


    }

    public String get0(){
        String result = data[getIndex];
        getIndex++;
        if (getIndex == data.length){
            getIndex = 0;
        }
        size--;
        return result;
    }
}
package com.hyq.concurrent.demo;

import java.util.concurrent.*;

public class Main2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyQueue2 myQueue = new MyQueue2();

        for (int i = 0; i < 3; i++) {
            new ProductThread2(myQueue).start();
        }

        for (int i = 0; i < 3; i++) {
            new ConsumerThread2(myQueue).start();
        }
    }
}

package com.hyq.concurrent.demo;

import java.util.Random;

public class ProductThread2 extends Thread{

    private final MyQueue2 myQueue ;
    private final Random random = new Random();
    private int index = 0;

    public ProductThread2(MyQueue2 myQueue) {
        this.myQueue = myQueue;
    }


    @Override
    public void run() {
        while (true){
            String temp = "ele-"+ index;
            myQueue.put(temp);
            System.out.println("添加元素:" + temp);
            index++;
            try{
                Thread.sleep(random.nextInt(1000));
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

package com.hyq.concurrent.demo;


import java.util.Random;

public class ConsumerThread2 extends Thread{

    private final MyQueue2 myQueue ;
    private final Random random = new Random();

    public ConsumerThread2(MyQueue2 myQueue) {
        this.myQueue = myQueue;
    }

    @Override
    public void run() {
        while (true){
            String s = myQueue.get();
            System.out.println("\t 消费元素:" + s);
            try {
                Thread.sleep(random.nextInt(1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
1.3.2 为什么必须和synchronized一起使用?

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

先看为什么wait()和notify()必须和synchronized一起使用?请看下面的代码:

public class MyClass1 {

    private Object obj1 = new Object();

    public void method1() throws InterruptedException {
        synchronized (obj1){
            //...
            obj1.wait();
            //...
        }
    }

    public void method2(){
        synchronized (obj1){
            //...
            obj1.notify();
            //...
        }
    }
}

或者下面的代码:

public class MyClass1 {
    
    public synchronized void method1() throws InterruptedException {
        //...
        this.wait();
        //...
    }

    public synchronized void method2(){
        //...
        this.notify();
        //...
    }
}

然后开两个线程,线程A调用method1(),线程B调用method2()。答案已经很明显:两个线程之间要通信,对于同一个对象来说,一个线程调用该对象的wait(),另一线程调用该对象的notify(),该对象本身就需要同步!所以,在调用wait()、notify()之前,要先通过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()内部的伪代码块如下:

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

如此则可以避免死锁。

1.3.4 wait()与notify()的问题

以上述生产者-消费者模型来看,其伪代码大致如下:

public void enqueue(){
	synchronized(queue){
		while(queue.full()){
			queue.wait();
		}
		// ... 数据入列
		queue.notify(); //通知消费者,队列中有数据了。
	}
}

public void dequeue(){
	synchronized(queue){
		while(queue.empty()){
			queue.wait();
		}
		// 数据出队列
		queue.notify(); // 通知生产者,队列中有空间了,可以继续放数据了。
	}
}

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

1.4 InterruptedException与interrupt()方法

1.4.1 Interrupted异常

什么情况下会抛出Interrupted异常
假设while循环中没有调用任何的阻塞函数,就是通常的算术运算,或者打印一行日志,如下所示:

package com.hyq.concurrent.demo;

public class MyThread extends Thread{

    @Override
    public void run() {
        while (true){
            boolean interrupted = isInterrupted();
            System.out.println("中断标记:" + interrupted);
        }
    }
}

这个时候,在主线程中调用一句 thread.interruppt()。请问该线程会不会抛出异常?不会

package com.hyq.concurrent.demo;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        myThread.start();
        Thread.sleep(10);
        myThread.isInterrupted();
        Thread.sleep(100);
        System.exit(0);
    }
}

只有那些声明了会抛出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_WAITONG;而像synchronized这种不能被称为中断的阻塞称为重量级阻塞,对应的状态是BLOCKED。如图所示:调用不同的方法后,一个线程的状态迁移过程。
在这里插入图片描述
初始线程处于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,并且线程被唤醒。而如果线程此时并没有被阻塞,则线程什么都不会做。但在后续,线程可以判断自己是否收到过其他线程发来的中断信号,然后做一些对应的处理。

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

package com.hyq.concurrent.demo;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        myThread.start();
        Thread.sleep(10);
        myThread.interrupt();
        Thread.sleep(7);
        System.out.println("main中断状态检查-1:" + myThread.isInterrupted());
        System.out.println("main中断状态检查-2:" + myThread.isInterrupted());
    }
}
package com.hyq.concurrent.demo;

public class MyThread extends Thread{

    @Override
    public void run() {
        int i= 0;
        while (true){
            boolean interrupted = isInterrupted();
            System.out.println("中断标记:" + interrupted);

            i++;
            if (i > 20){
                // 检查并重置中断标志
                boolean interrupted1 = Thread.interrupted();
                System.out.println("重置中断状态:" + interrupted1);
                interrupted1 = Thread.interrupted();
                System.out.println("重置中断状态:" + interrupted1);
                interrupted = isInterrupted();
                System.out.println("中断标记:" + interrupted);
                break;
            }
        }
    }
}

1.5线程的优雅关闭

1.5.1 stop与destory函数

线程是“一段运行中的代码”,一个运行中的方法。运行到一半的线程能否强制杀死?
不能。在java中,有stop()、destory()等方法,但这些方法官方明确不建议使用。原因很简单,如果强制杀死线程,则线程中所使用的资源,例如文件描述符、网络连接等无法正常关闭。

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

1.5.2 守护线程

daemon线程和非daemon线程的对比:

package com.hyq.concurrent.demo;

public class MyDaemonThread extends Thread{
    @Override
    public void run() {
        while (true){
            System.out.println(Thread.currentThread().getName());
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
package com.hyq.concurrent.demo;

public class MyThread extends Thread{

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("非daemon线程...");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
package com.hyq.concurrent.demo;

public class Main {
    public static void main(String[] args) {
        MyDaemonThread myDaemonThread = new MyDaemonThread();
        //设置为守护线程
        myDaemonThread.setDaemon(true);
        myDaemonThread.start();
        //启动非守护线程,当非守护线程结束,不管守护线程是否结束,都会结束JVM进程
        new MyThread().start();
    }
}

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

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

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

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

1.5.3 设置关闭的标志位

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

package com.hyq.concurrent.demo;

public class MyThread extends Thread{

    private boolean running = true;

    @Override
    public void run() {
        while (running){
            System.out.println("线程正在运行...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

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

    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        myThread.start();
        Thread.sleep(5000);
        myThread.stopRunning();
    }
}

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

教编程的侯老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值