【Java多线程】01、Java线程的基本概念

一些术语

进程与线程

进程是程序运行的实例,是OS中进行资源分配和调度的基本单位
比如当启动QQ时,会将QQ相关的代码从磁盘加载至内存,这时就开启了一个进程。

线程是执行调度的基本单位,进程中包含至少一个线程,同一个进程中的所有线程共享该进程中的资源。

同步和异步

同步和异步通常用来形容方法调用。

同步方法一旦调用,调用者必须等待结果返回后才能继续后续的行为。类似去餐馆吃饭。

异步方法一旦调用,不需要等待结果返回调用者可以继续后续的操作。异步方法调用更像是一个消息传递。
如果异步调用需要返回结果,那么当该异步调用完成后,则会通知调用者。类似点外卖。

并发和并行

并发(concurrent)是同一时间应对(dealing with)多件事情的能力。偏重于多个任务交替执行。

并行(parallel)是同一时间动手做(doing)多件事情的能力 。

  • 家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这是并发
  • 家庭主妇雇了个2保姆,她们一起这些事,这时既有并发,也有并行(例如锅只有一口,一个人用锅时,另一个人就得等待)
  • 家庭主妇雇了3个保姆,一个专做饭、一个专打扫卫生、一个专喂奶,互不干扰,这是并行

栈帧

线程启动后,虚拟机就会为其分配一块栈内存

  • 每个栈由多个栈帧(Stack Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
    线程与进程

线程上下文切换

CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的资源,以便下次切换回这个任务时可以恢复现场。所以任务从保存到再加载的过程就是一次上下文切换。 上下文切换会影响多线程的执行速度 。由于多线程执行时存在上下文切换的开销,所以多线程有时候不一定就比单线程快。

线程上下文切换时机:

  • 线程的 cpu 时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

如何减少上下文切换:

  • 无锁并发编程
    • 多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
  • CAS算法
    • Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
  • 使用适量的线程
    • 避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
  • 使用协程
    • 在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换

线程常见的属性

id

每个线程有自己id,用于标识不同的线程,唯一且不允许被修改,id 从1开始自增。

name

创建线程时,可以给线程取一个唯一的name,方便后续定位问题。

priority

  • java中线程优先级有10个级别,默认为5
  • 程序设计不应该依赖优先级
    • java线程的优先级会映射到操作系统,而不同操作系统的优先级是不一样的
    • 优先级会被有的操作系统改变,甚至忽略,这会使得程序变的不可靠
public class Main {

    public static void main(String[] args) {
        MyThread thread1 = new MyThread("111");
        MyThread thread2 = new MyThread("222");
        MyThread thread3 = new MyThread("333");
        thread1.setPriority(Thread.MAX_PRIORITY);
        thread2.setPriority(Thread.NORM_PRIORITY);
        thread3.setPriority(Thread.MIN_PRIORITY);

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

输出结果,可能是:

333
111
222

isDaemon

守护线程与用户线程整体无区别,唯一区别在于是否影响 JVM 的退出。当JVM中不存在非Deamon线程(即用户线程)的时候,JVM将会退出,不会管守护线程是否运行完毕。

它们的作用通常也不同。用户线程执行主要逻辑,守护线程通常作一些程序上的服务性工作,比如心跳检测。

线程状态(生命周期)

java中的线程有六种状态:
线程状态
通常,把Blocked(被阻塞)、Waiting(等待)、Timed_waiting(计时等待)都称为阻塞状态。

创建线程

准确的说,在java中创建线程只有一种实现方式,就是实例化Thread。它有两种实现执行单元的方式:

继承Thread类并重写run方法

public class MyThread extends Thread {
    
    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        System.out.println(this.getName());
    }
}
public class Main {

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

实现Runnable接口的run方法

public class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}
public class Main {

    public static void main(String[] args) {
        Runnable task = new MyTask();
        new Thread(task).start();
        new Thread(task).start();
    }
}

两种方法的对比和本质

实现Runnable接口更好

  • 基于组合方式将线程(Thread)和任务(Runnable,要执行的代码)进行了解耦
  • 从资源利用角度,实现Runnable接口可以与线程池等高级 API 配合,节省资源
  • 从java语法角度,Java不支持双继承,实现Runnable接口后可以继承其他类,更加灵活

两种方法的本质

在这里插入图片描述
最终调用target.run()

  • 创建Thread类是将整个run方法重写
  • 实现Runnable是将自身注入Thread,并执行run方法

target变量是Thread类中Runnable对象的引用

其他写法

线程的创建方式,在代码中写法千变万化,但其本质都是通过创建Thread类或实现Runnable接口来实现的。

  • 使用线程池创建线程。底层也是通过 new Thread() 来创建线程的。
  • 定时器
  • 匿名内部类
  • lambda表达式
  • 实现Callable接口并使用FutureTask
public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "hello";
    }
}
public class Main {

    public static void main(String[] args) {
        // 创建异步任务
        FutureTask<String> futureTask = new FutureTask<String>(new MyCallable());
        // 启动线程
        new Thread(futureTask).start();
        try {
            //  等待线程执行 并返回结果
            String result = futureTask.get();
            System.out.println("result=" + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

Q:有多少种实现线程的方法?

本质上只有新建Thread类这一种方式,但是通常我们把它区分为两种形式,一种是实现Runnable接口,一种是继承Thread类。一般优先选择实现Runnable接口,因为第一它将任务的创建和执行解耦,第二线程的创建是很耗资源的,实现Runnable接口后可以与线程池等高级 API 配合,第三由于java的单继承性,实现接口会具有更好的扩展性。虽然说实现的方式是两种,但是其本质上都是一样的。都是调用了Thread类的run()方法,该方法会先判断Runnable对象是否为null,存在则执行Runnable对象的run方法。只不过实现Runnable时,是直接调用了自身的run方法,而继承Thread类是重写了run方法。除了常见的这两种方式,还有线程池、定时器、Callable等多种表现形式,但是它们底层仍然还是刚才说的那两种实现。

策略模式

Runnable 接口将业务执行单元和线程控制解耦,通过向 Thread 类中传入不同的实现类,线程会执行不同的任务,这是一种策略模式的实现思想。

启动线程——start

start原理

线程调用 start() 方法后,线程进入就绪状态,何时能够运行由操作系统的线程调度器来决定。

重复调用 start() 方法,会抛出 IllegalThreadStateException。因为在调用start()方法时会先判断当前线程的状态是否为0,如果不是则抛出 IllegalThreadStateException。而线程只有在第一次被创建时状态才为0。

一旦run方法执行完毕,线程就结束了。这里的执行完毕包含正常结束(run 方法返回)和抛出异常而导致的中止

只有调用了start()后,JVM才会将线程对象和操作系统中实际的线程进行映射,此时才会是一个线程,拥有自己的栈帧。通过 new Thread 创建的线程,此时还不是真正的线程,只是一个线程实例。

start()方法的原理:

  • 检查线程状态(栈、PC)
  • 将该线程加入线程组
  • 调用 native 方法 start0(),即和操作系统映射起来

以上,再次说明了在java中线程只有一种实现方式,就是实例化Thread,并且提供其执行的run方法。无论是通过继承thread还是实现runnable接口,最终都是重写或者实现了run方法。而真正启动线程都是通过实例化Thread并调用其start方法实现的。

模板方法模式

在 Thread 类中,使用了类似模板方法的设计模式。固定的算法已定,将具体的任务抽象出去,交给子类去实现。

在 Thread 类中提供了 start 方法来启动一个线程来执行一个任务,而这个任务是什么,Thread 类并不关心,它只是提供了 run 方法交给外部来实现。所以创建线程是实现 run 方法,但是启动的是 start 方法。

查看线程

在java中可以使用如下命令来查看:

  1. jps:命令查看所有 Java 进程
  2. jstack :查看某个 Java 进程(PID)的所有线程状态
  3. jconsole:查看某个 Java 进程中线程的运行情况(图形界面)

每个线程都拥有自己的程序计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。
栈帧

线程休眠——sleep

sleep方法可以让线程进入Timed_Waiting状态,让出CPU资源,但是不释放锁,直到规定时间后会继续抢占CPU时间片,休眠期间如果被中断,会抛出异常并清除中断标志。

线程让步——yield

当前线程调用yield方法时,意味着该线程愿意放弃 CPU 资源,不过这只是给 CPU 一个提示,当 CPU 资源并不紧张时会无视 yield 提醒。同 sleep 一样也不释放锁,调用yield的线程状态仍然是Runnable状态,在下一次线程调度中会和其他线程一起抢占CPU的执行权。

与sleep的区别:yield可能随时被再次调度,而sleep在waiting时间时不会被再次调度

等待其他线程结束——join

作用:因为新的线程加入了我们,所以我们要等他执行完再出发

case1

下面的代码执行,打印 r 是什么?

static int r = 0;
public static void main(String[] args) throws InterruptedException {
	test1();
}
private static void test1() throws InterruptedException {
	log.debug("开始");
	Thread t1 = new Thread(() -> {
		log.debug("开始");
		sleep(1000);
		log.debug("结束");
		r = 10;
	});
	
    t1.start();
	log.debug("结果为:{}", r);
	log.debug("结束");
}

因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 r=10,而主线程一开始就要打印 r 的结果,所以只能打印出 r=0。

解决方式:在 t1.start() 后 t1.join() 即可。这样main线程会等待 t1 线程执行完后才能向下执行。

case2

下面代码 cost 大约多少秒?

static int r1 = 0;
static int r2 = 0;

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

private static void test2() throws InterruptedException {
    Thread t1 = new Thread(() -> {
        sleep(1000);
        r1 = 10;
    });
    Thread t2 = new Thread(() -> {
        sleep(2000);
        r2 = 20;
    });

    long start = System.currentTimeMillis();
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    long end = System.currentTimeMillis();
    log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}

结果(cost接近2s):

r1: 10 r2: 20 cost: 2002

分析

  • 第一个 join:等待 t1 时, t2 并没有停止, 而在运行
  • 第二个 join:1s 后, 执行到此, t2 也运行了 1s, 因此也只需再等待 1s
case3:线程在join时间结束前结束

下面代码 cost 大约多少秒?

static int r1 = 0;
static int r2 = 0;

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

public static void test3() throws InterruptedException {
    Thread t1 = new Thread(() -> {
    	sleep(1000);
    	r1 = 10;
    });
	long start = System.currentTimeMillis();
	t1.start();
	t1.join(1500);
	long end = System.currentTimeMillis();
	log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}

由于 线程执行结束会导致 join 结束,所以耗时接近1s。

如果让 t1 sleep两秒,结果如何?结果cost接近1.5s

case4:join时被中断
public class JoinInterrupt {
    public static void main(String[] args) {
        Thread mainThread = Thread.currentThread();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    mainThread.interrupt();
                    Thread.sleep(5000);
                    System.out.println("Thread1 finished.");
                } catch (InterruptedException e) {
                    System.out.println("子线程中断");
                }
            }
        });
        thread1.start();
        System.out.println("等待子线程运行完毕");
        try {
            thread1.join();
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName()+"主线程中断了");
        }
        System.out.println("子线程已运行完毕");
    }

}

结果

等待子线程运行完毕
main主线程中断了
子线程已运行完毕
java.lang.InterruptedException
Thread1 finished.

注意,中断的是主线程。而且抛出异常之后过了一段时间会输出"Thread1 finished",也就是说主线程结束后,子线程此时并没有运行完毕。所以应该在主线程的catch处加上 thread1.interrupt(); 主动中断子线程,避免出现不一致的情况。

总结

join():当前线程会无限等待(Waiting状态),直到目标线程执行完毕
join(long):如果超过给定时间目标线程还在执行,当前线程也会因为等不及了而继续往下执行

原理

join原理
可见,目标线程(thread1)未结束时会调用 wait 方法来暂停等待线程(main线程),直到目标线程已终止。

疑惑:没有notify,等待线程是怎么被唤醒的呢?

实际上JVM会在线程的 run 方法运行结束后执行该线程的 notifyAll 方法来通知所有的等待线程。

扩展

让一个线程等待其他线程,除了join,还可以使用成熟的工具类 CountDownLatchCyclicBarrier。在实际使用中,最好不要使用底层的方法,而是使用提供的工具类。

停止线程

interrupt

在无外界干涉的情况下,代码运行结束或抛出异常线程就停止了。

正确的停止线程:使用interrupt来通知某个线程停止,而不是强制停止该线程

通过设置线程的中断标志并不能直接终止该线程的执行,而是由被中断的线程根据中断情况自行处理。也就是说,中断的并不是线程的逻辑,而是线程的阻塞。

  • void interrupt():该方法仅会设置线程的中断标志位为true,而不会中断线程。如果线程因为调用sleep、wait、join方法而被阻塞挂起,其他线程调用该线程的interrupt方法后,该线程会在调用这些方法的地方抛出 InterruptedException 异常而返回
  • **boolean isInterrupted():**通过检查中断标志位判断当前线程是否被中断
  • **static boolean interrupted():**检查当前线程是否被中断。与 isInterrupted() 不同的是,该方法如果发现线程被中断,会清除线程的中断标志位状态

在实际中,很少会有停止线程的操作。interrupt也只是起到辅助的功能,具体的停止逻辑还是需要人为来控制。

正确停止线程的好处:

  • 被中断的线程拥有如何中断线程的权利
  • 保证了数据的安全

通过中断终止线程

线程的中断状态是线程的一个标识位,表示一个运行中的线程是否被其他线程进行了中断操作。线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。

如果该线程已经处于终止状态,即使该线程被中断过,在调用该线程对象的isInterrupted()时依旧会返回false。

public class Main {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println(Thread.currentThread() + "running");
            }
        });

        thread.start();
        Thread.sleep(100);

        System.out.println("interrupt thread");
        thread.interrupt();
        thread.join();
        System.out.println("main thread end");
    }
}

输出:

// 省略更多的Thread[Thread-0,5,main]running
Thread[Thread-0,5,main]running
Thread[Thread-0,5,main]running
interrupt thread
Thread[Thread-0,5,main]running
main thread end

中断操作是一种简便的线程间交互方式,最适合用来取消或停止任务。

注意,在while内tyr/catch会遇到的问题:即使中断了线程,线程也不会停止。可以通过break或return来终止。

通过中断节省时间

假设线程sleep了10s,但是其任务有可能在10s内就完成了,此时休眠10s再返回就有点浪费了。这时通过调用线程的interrupt方法,强制sleep方法抛出异常而返回,使得线程恢复到激活状态

public class Main {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println("业务逻辑在10s内完成");
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                System.out.println("被中断了:" + Thread.currentThread().isInterrupted());
            }
        });

        thread.start();
        Thread.sleep(2000);
        thread.interrupt();
        thread.join();
        System.out.println("main is over");
    }
}

程序结束,输出结果:

业务逻辑在10s内完成
被中断了:false
main is over

可中断的方法(如sleep、join、wait)在抛出InterruptedException后,会将该线程的中断标识位清除改回 false

上例中,如果不触发中断异常,该线程在sleep时如果被中断,其中断标志位会变为true.

public class Main {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println("业务逻辑在10s内完成");
            Thread.sleep(10000);
            System.out.println("被中断了:" + Thread.currentThread().isInterrupted());
        });

        thread.start();
        Thread.sleep(2000);
        thread.interrupt();
        thread.join();
        System.out.println("main is over");
    }
}

程序结束,输出结果:

业务逻辑在10s内完成
被中断了:true
main is over

打断park线程

private static void test3() throws InterruptedException {
    Thread t1 = new Thread(() -> {
    	log.debug("park...");
    	LockSupport.park();	//执行到此处后不会继续向下执行
    	log.debug("unpark...");
    	log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
    }, "t1");
}

使用interrupt可以打断 park 线程, 不会清空打断状态

private static void test3() throws InterruptedException {
    Thread t1 = new Thread(() -> {
    	log.debug("park...");
    	LockSupport.park();
    	log.debug("unpark...");
    	log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
    }, "t1");
   
    t1.start();
    Thread.sleep(500);
    t1.interrupt();
}

输出

[t1] c.TestInterrupt - park...
[t1] c.TestInterrupt - unpark...
[t1] c.TestInterrupt - 打断状态:true

如果打断标记已经是 true, 则 park 会失效

private static void test4() {
    Thread t1 = new Thread(() -> {
        log.debug("park...");
        	LockSupport.park();
            log.debug("unpark...");
        	log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
        
        	LockSupport.park();
        	log.debug("unpark...");
    });
    
    t1.start();
    Thread.sleep(1000);
    t1.interrupt();
}

输出:

[Thread-0] c.TestInterrupt - park...
[Thread-0] c.TestInterrupt - unpark...
[Thread-0] c.TestInterrupt - 打断状态:true
[Thread-0] c.TestInterrupt - unpark...

如果第6行的代码改为调用Interrupted,打断标记会被还原为fasle,park仍然生效,最后的unpark不会被输出

理解interrupted()和isInterrupted()

案例1

public class Main {

    public static void main(String[] args) throws InterruptedException {
        Thread a = new Thread(() -> {
            while (true) {
            }
        });

        a.start();
        a.interrupt();
        System.out.println("isInterrupted:" + a.isInterrupted());
        System.out.println("isInterrupted:" + a.interrupted());
        System.out.println("isInterrupted:" + Thread.interrupted());
        System.out.println("isInterrupted:" + a.isInterrupted());
        a.join();
        System.out.println("main is over");
    }
}

程序不终止,输出结果:

isInterrupted:true
isInterrupted:false
isInterrupted:false
isInterrupted:true

interrupted 方法内部是获取当前线程的中断状态,这里虽然调用了a线程的interrupted()方法,但是获取的是主线程的标志,因为主线程是当前线程。即 a.interrupted() 与 Thread.interrupted() 的作用是一样的。

public class Main {

    public static void main(String[] args) throws InterruptedException {
        Thread a = new Thread(() -> {
            while (!Thread.currentThread().interrupted()) {
            }
            System.out.println("a isInterrupted:" + Thread.currentThread().isInterrupted());
        });

        a.start();
        a.interrupt();
        a.join();
        System.out.println("main is over");
    }
}

程序终止,输出结果:

a isInterrupted:false
main is over

案例2

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (true) {
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(500);
                t1.interrupt();
                System.out.println("打断线程t1");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2");

        t1.start();
        t2.start();

        t1.join();
    }

预期启动后,线程 t2 会打断线程 t1,实际结果却是t2未打断t1。这是为什么呢?

同上,在main线程中调用 t1.join()时的当前线程是main线程,而t2打断的线程是t1线程,所以不生效。

将其改为如下代码,即可打断 t1 线程

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (true) {
            }
        }, "t1");

        Thread main = Thread.currentThread();

        Thread t2 = new Thread(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(500);
                main.interrupt();
                System.out.println("打断线程t1");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2");

        t1.start();
        t2.start();

        t1.join();
    }

错误的停止线程

使用过时的API

  • stop():导致线程运行时突然停止,可能会产生脏数据问题
  • supend():带着锁挂起线程,容易造成线程死锁
  • resume()

使用volatile修饰的标志位

case1:正确的场景
// 线程每隔1s输出0~100000中是100的倍数的数字,5s后程序结束
public class WrongWayVolatile implements Runnable {

    private volatile boolean canceled = false;

    @Override
    public void run() {
        int num = 0;
        try {
            while (num <= 100000 && !canceled) {
                if (num % 100 == 0) {
                    System.out.println(num + "是100的倍数。");
                }
                num++;
                Thread.sleep(1);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        WrongWayVolatile r = new WrongWayVolatile();
        Thread thread = new Thread(r);
        thread.start();
        Thread.sleep(5000);
        r.canceled = true;
    }
}

在这种情景下,确实会使得线程停止。

case2:不正确的场景

生产者

class Producer implements Runnable {

    public volatile boolean canceled = false;

    BlockingQueue storage;

    public Producer(BlockingQueue storage) {
        this.storage = storage;
    }


    @Override
    public void run() {
        int num = 0;
        try {
            while (num <= 100000 && !canceled) {
                if (num % 100 == 0) {
                    storage.put(num);
                    System.out.println(num + "是100的倍数,被放到仓库中了。");
                }
                num++;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("生产者结束运行");
        }
    }
}

消费者

class Consumer {

    BlockingQueue storage;

    public Consumer(BlockingQueue storage) {
        this.storage = storage;
    }

    public boolean needMoreNums() {
        if (Math.random() > 0.95) {
            return false;
        }
        return true;
    }
}

主程序

public class WrongWayVolatileCantStop {

    public static void main(String[] args) throws InterruptedException {
        ArrayBlockingQueue storage = new ArrayBlockingQueue(10);

        Producer producer = new Producer(storage);
        Thread producerThread = new Thread(producer);
        producerThread.start();
        Thread.sleep(1000);

        Consumer consumer = new Consumer(storage);
        while (consumer.needMoreNums()) {
            System.out.println(consumer.storage.take()+"被消费了");
            Thread.sleep(100);
        }
        System.out.println("消费者不需要更多数据了。");

        //一旦消费不需要更多数据了,应该让生产者也停下来
        producer.canceled=true;
        System.out.println(producer.canceled);
    }
}

上例中,生产者的生产速度很快,消费者消费速度慢,所以阻塞队列满了以后,生产者会阻塞,等待消费者进一步消费。也就是生产者在 storage.put(num); 时阻塞了,无法进行下一次的循环判断。在此种情况下,通过volatile来停止线程是无效的。

在此情况下,想要停止线程,应该使用中断:

  • 主线程 r.canceled = true; 改为 producer.interrupt()

  • 生产者 !canceled改为Thread.currentThread().isInterrupt()

Q:如何停止线程

思路:

  1. 先回答结果
  2. 再说明这种方式的好处
  3. 扩展说说错误的方式,以及错误的原因

可以用interrupt来停止线程。

这种方式一方面可以保证数据的安全,另一方面应该把线程停止的主动权交给被中断的线程,由它自身根据情况来处理。要想达到这种效果,需求请求方、被停止方、子方法被调用方相互配合。具体的说,请求方发送中断请求信号,被停止方必须在每次循环或适当的时候去检查这个中断信号,并且在可能抛出InterruptException的时候去处理这个信号,以便于自己可以被停止。如果我们是写子方法的,有两种最佳实践,优先是在方法层抛出InterruptException,以便其他人去做进一步的处理,或者在收到中断信号之后将其再次设为中断状态。

如果不用interrupt,也可以使用stop,但是它会带来数据安全问题;或者使用suspend,但是它会使得线程带锁挂起导致死锁;或者使用volatile修饰的boolean,但是它无法处理长时间阻塞的情况。

Q:如何处理不可中断的阻塞

比如抢锁时ReentrantLock.lock()或者Socket I/O时无法响应中断,那应该怎么让该线程停止呢?

没有通用的方法,要具体场景具体分析。比如 ReentrantLock.lock() ,在 lock 过程中发生阻塞时是没有办法让其及时响应的,但是可以使用该类的 lockInterrupt() 方法来响应中断。也就是说,在编码应该选择这种可以响应中断的方法,尽可能地让线程可以响应中断。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值