JUC并发编程:并发基础与Java多线程

0. 前言&名词解释

       并发编程的目的是为了让程序运行得更快,但是,并不是启动更多的线程就能让程序最大限度地并发执行。在进行并发编程时,如果希望通过多线程执行任务让程序运行得更快,会面临非常多的挑战,比如上下文切换问题死锁的问题,以及受限于硬件和软件的资源限制问题。 – 来自《Java并发编程的艺术》

0.1 上下文切换

       即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常的短,所以CPU通过不停地切换线程执行,让我们感觉到多个线程是同时执行的,时间片一般是几十毫秒(ms)

       CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。

       并发执行多线程并非会比串行快,因为存在上下文切换的开销。

0.1.1 如何减少上下文切换

       减少上下文切换的方法无锁并发编程、CAS算法、使用最少线程和使用协程。

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

0.2 什么是并发?什么是并行?

       关于并发最被人们认可的定义是,在单个处理器上采用单核执行多个任务即为并发。在这种情况下,操作系统的任务调度程序会很快从一个任务切换到另一个任务,因此看起来所有任务都是同时运行的。

       对于并行来说也有同样的定义:同一时间在不同的计算机、处理器或处理器核心上同时运行多个任务,就是所谓的“并行”。

       另一个关于并发的定义是,在系统上同时运行多个任务(不同的任务)就是并发。而另一个关于并行的定义是:同时在某个数据集的不同部分之上运行同一任务的不同实例就是并行。

上述来自《精通Java并发编程》

在这里插入图片描述
上述来自《码出高效:Java开发手册》

0.3 什么是进程?是什么线程?

  1. 线程是处理器任务调度和执行的基本单位。
  2. 进程是操作系统资源分配的基本单位。
  3. 进程是程序的一次执行过程,是系统运行的基本单位。
  4. 线程是一个比进程更小的执行单位,一个进程可以包含多个线程。

0.4 进程和线程的关系

        现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个Java程序,操作系统就会创建一个Java进程。现代操作系统调度的最小单元是线程,也叫轻量级进程(LightWeightProcess),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。-- 来自《Java并发编程的艺术》

        线程是处理器任务调度和执行的基本单位,进程是操作系统资源分配的基本单位。一个进程可以包含多个线程。

        从Java虚拟机的角度来理解:Java虚拟机的运行时数据区包含堆、方法区、虚拟机栈、本地方法栈、程序计数器。各个进程之间是相互独立的,每个进程会包含多个线程,每个进程所包含的多个线程并不是相互独立的,这个线程会共享进程的堆和方法区,但这些线程不会共享虚拟机栈、本地方法栈、程序计数器。即每个进程所包含的多个线程共享进程的堆和方法区,并且具备私有的虚拟机栈、本地方法栈、程序计数器,如图所示,假设某个进程包含三个线程。
在这里插入图片描述由上面可知以下进程和线程在以下几个方面的区别:

        内存分配:进程之间的地址空间和资源是相互独立的,同一个进程之间的线程会共享线程的地址空间和资源(堆和方法区)。

        资源开销:每个进程具备各自的数据空间,进程之间的切换会有较大的开销。属于同一进程的线程会共享堆和方法区,同时具备私有的虚拟机栈、本地方法栈、程序计数器,线程之间的切换资源开销较小。

0.5 多线程的优缺点

优点:当一个线程进入等待状态或者阻塞时,CPU可以先去执行其他线程,提高CPU的利用率。

缺点:

  1. 上下文切换:频繁的上下文切换会影响多线程的执行速度。
  2. 死锁
  3. 资源限制:在进行并发编程时,程序的执行速度受限于计算机的硬件或软件资源。在并发编程中,程序执行变快的原因是将程序中串行执行的部分变成并发执行,如果因为资源限制,并发执行的部分仍在串行执行,程序执行将会变得更慢,因为程序并发需要上下文切换和资源调度。

1. Java 线程

1.1 创建和运行线程

1.1.1 方法一. 直接使用 Thread

// 匿名内部类写法
public class MyThread {
    
    
    public static void main(String[] args) {
    
    
        new Thread() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("Hello World");
            }
        }.start();
    }
}
public class MyThread extends Thread {
    
    
	@Override
	public void run() {
    
    
		super.run();
		System.out.println("你好");
	}
}
------------------------------------------------
public class ThreadTest {
    
    
	public static void main(String[] args) {
    
    
		MyThread myThread = new MyThread();
		myThread.start();
	}
}

1.1.2 方法二 使用 Runnable 配合 Thread

把【线程】和【任务】(要执行的代码)分开

  • Thread 代表线程
  • Runnable 可运行的任务(线程要执行的代码)
public class MyThread1 implements Runnable {
    
    
	@Override
	public void run() {
    
    
		System.out.println("你好");
	}
}

--------------------------------------------------
public class MyThread1Test {
    
    
	public static void main(String[] args) {
    
    
		MyThread1 myThread1 = new MyThread1();
		Thread thread = new Thread(myThread1);
		thread.start();
	}
}
public class MyThread {
    
    
    public static void main(String[] args) {
    
    
        Runnable runnable = new Runnable() {
    
    
            @Override
            public void run() {
    
    
            	// 要执行的任务
                System.out.println("Hello World");
            }
        };

        Thread t = new Thread(runnable,"线程名");
        t.start();
    }
}

-----------------------------------------------
// lambda表达式的写法
public class MyThread {
    
    
    public static void main(String[] args) {
    
    
        Runnable runnable = () -> System.out.println("Hello World");

        Thread t = new Thread(runnable, "线程名");
        t.start();
    }
}

小节:
1. 使用Runnable接口把线程和任务分开了。
2. 使用Runnable更容易和线程池等高级API配合。
3. 用Runnable让任务类脱离了Thread继承体系,更灵活。

1.1.3 方法三 FutureTask 配合 Thread

FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况

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

public class MyThread {
    
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        // 创建任务对象
        FutureTask<Integer> task3 = new FutureTask<>(() -> {
    
    
            System.out.println("Hello World");
            return 100;
        });

        // 参数1 是任务对象;参数2 是线程名字
        new Thread(task3, "t3").start();

        Integer result = task3.get();
    }
}
import java.util.concurrent.Callable;

public class MyThread3 implements Callable<String> {
    
    

	@Override
	public String call() throws Exception {
    
    
		return "hello";
	}
}
--------------------------------------------------
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Thread3Test {
    
    
	public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
		MyThread3 myThread3 = new MyThread3();
		FutureTask futureTask = new FutureTask(myThread3);
		new Thread(futureTask).start();
		System.out.println(futureTask.get());
	}
}

1.1.3 方法四 使用线程池

public class TestCallable implements Callable<String> {
    
    
	@Override
	public String call() throws Exception {
    
    
		return "通过线程池实现多线程";
	}
}
--------------------------------------------------
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class NewTest {
    
    
	public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
		TestCallable testCallable = new TestCallable();
		ExecutorService executorService = Executors.newFixedThreadPool(3);
		// 提交任务
		Future<String> future = executorService.submit(testCallable);
		String result = future.get();
		System.out.println(result);
		
		// 也可通过下面的lambda写法
		ExecutorService executorService = Executors.newFixedThreadPool(3);
        // 提交任务
        Future<String> future = executorService.submit(()-> "Hello");
        String result = future.get();
        System.out.println(result);
	}
}

但是阿里规定不允许使用Executors类创建线程池。
在这里插入图片描述Integer.MAX_VALUE的值大小为231 -1是21亿多
– 上述来自阿里巴巴《Java开发手册》

1.2 线程的生命周期(状态)

1.2.1 五种状态

五种状态的这种说法是从操作系统层面来描述的
在这里插入图片描述

  1. NEW 新建状态,是线程被创建且未启动的状态。

  2. RUNNABLE,即就绪状态,是调用start()之后运行前的状态。线程的start()方法不能被调用多次,否则会抛出IllegalStateException异常。

  3. RUNNING,即运行状态,是run()正在执行时的线程状态。线程可能会由于某些因素而退出RUNNING,如时间、异常、锁、调度等。

  4. BLOCKE,即阻塞状态,进入此状态有以下情况

    • 同步阻塞:锁被其他线程占用。
    • 主动阻塞:调用Thread的某个方法主动让出CPU执行权,如sleep()、join()等。
    • 等待阻塞:执行了wait()
  5. DEAD, 即终止状态,是run()执行结束,或因异常退出后的状态,此状态不可逆。

1.2.2 六种状态

这是从Java API层面来描述的

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
– 表/图4-1来自《Java并发编程的艺术》

        由图4-1中可以看到,线程创建之后,调用start()方法开始运行。当线程执行wait()方法之后,线程进入等待状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而超时等待状态相当于在等待状态的基础上增加了超时限制,也就是超时时间到达时将会返回到运行状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到阻塞状态。线程在执行Runnable的run()方法之后将会进入到终止状态。

        注意: Java将操作系统中的运行和就绪两个状态合并称为运行状态。阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态,但是阻塞在java.concurrent包中Lock接口的线程状态却是等待状态,因为java.concurrent包中Lock接口对于阻塞的实现均使用了LockSupport类中的相关方法。

2. Java线程池

        Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处。

  1. 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  2. 提高响应速度。 当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的可管理性。 线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

2.1 线程池的实现原理

        当向线程池提交一个任务之后,线程池处理流程如图9-1所示。从图中可以看出,当提交一个新任务到线程池时,线程池的处理流程如下。

  1. 线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。
  2. 线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
  3. 线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。

在这里插入图片描述
– 上述来自《Java并发编程的艺术》

2.2 Executor 框架

       在Java中,使用线程来异步执行任务。Java线程的创建与销毁需要一定的开销,如果我们为每一个任务创建一个新线程来执行,这些线程的创建与销毁将消耗大量的计算资源。同时,为每一个任务创建一个新线程来执行,这种策略可能会使处于高负荷状态的应用最终崩溃。
       Java的线程既是工作单元,也是执行机制。从JDK5开始,把工作单元与执行机制分离开来。工作单元包括Runnable和Callable,而执行机制由Executor框架(import java.util.concurrent.Executor )提供。

2.2.1 Executor框架简介之Executor框架的两级调度模型 (TODO)

// TODO

2.2.1 Executor 框架结构(主要由三大部分组成)

2.1 三大方法

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Thread3Test {
    
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        // 单个线程
        ExecutorService threadPool1 = Executors.newSingleThreadExecutor();

        // 创建一个固定大小的线程池
        ExecutorService threadPool2 = Executors.newFixedThreadPool(5);

        // 可自动适应大小的线程池
        ExecutorService threadPool3 = Executors.newCachedThreadPool();
    }
}

3. 查看进程和线程的方法

学习课程地址

3.1 Windows

  • 任务管理器可以查看进程和线程数,也可以用来杀死进程
  • tasklist 查看进程
  • taskkill 杀死进程

3.2 Linux

  • ps -fe 查看所有进程

  • ps -fe|grep java 查看Java所有进程

  • ps -fT -p <PID> 查看某个进程(PID)的所有线程

  • kill 杀死进程
    在这里插入图片描述

  • top -H -p <PID>动态的查看某个进程(PID)的所有线程
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

3.3 Java

  • jps 命令可以查看所有Java进程
  • jstack <PID> 查看某个Java进程(PID)的所有线程状态

4. 线程运行的原理以及常用方法

4.1 栈与栈帧

Java Virtual Machine Stacks (Java 虚拟机栈)

我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟
机就会为其分配一块栈内存。

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

4.2 线程上下文切换(Thread Context Switch)

因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码

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

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念
就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
  • Context Switch 频繁发生会影响性能

4.3 常见方法

方法名 功能说明 注意
start() 启动一个新线程,在新的线程运行run方法中的代码 start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException
run() 新线程启动后会调用的方法 如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为
join() 等待线程运行结束
join(long n) 等待线程运行结束,最多等待 n 毫秒
getId() 获取线程长整型的 id id 唯一
getName() 获取线程名
setName(String) 修改线程名
getPriority() 获取线程优先级
setPriority(int) 修改线程优先级 java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率
getState() 获取线程状态 Java 中线程状态是用 6 个 enum 表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
isInterrupt() 打断线程 如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除 打断标记 ;如果打断的正在运行的线程,则会设置 打断标记 ;park 的线程被打断,也会设置 打断标记 public void interrupt()
interrupted() 判断当前线程是否被打断 static的 会清除打断标记
isInterrupted() 判断是否被打断 static的 public static boolean interrupted()
isAlive() 线程是否存活(还没有运行完毕)
currentThread() 获取当前正在执行的线程 static的
sleep(long n) 让当前执行的线程休眠n毫秒,休眠时让出 cpu 的时间片给其它线程 static的
yield() 提示线程调度器让出当前线程对CPU的使用 static的 主要是为了测试和调试

4.3.1 start() 与 run()

import ch.qos.logback.core.util.FileUtil;
import cn.itcast.Constants;
import cn.itcast.n2.util.FileReader;
import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.TestStart")
public class TestStart {
    
    
    public static void main(String[] args) {
    
    
        Thread t1 = new Thread("t1") {
    
    
            @Override
            public void run() {
    
    
                log.debug(Thread.currentThread().getName());
                FileReader.read(Constants.MP4_FULL_PATH);
            }
        };

        t1.start();
        log.debug("do other things ...");
    }
}
19:39:14 [main] c.TestStart - main
19:39:14 [main] c.FileReader - read [1.mp4] start ...
19:39:18 [main] c.FileReader - read [1.mp4] end ... cost: 4227 ms
19:39:18 [main] c.TestStart - do other things ...

将上述代码的 t1.run() 改为t1.start()

19:41:30 [main] c.TestStart - do other things ...
19:41:30 [t1] c.TestStart - t1
19:41:30 [t1] c.FileReader - read [1.mp4] start ...
19:41:35 [t1] c.FileReader - read [1.mp4] end ... cost: 4542 ms

程序在 t1 线程运行, FileReader.read() 方法调用是异步的

小结:

  • 直接调用 run 是在主线程中执行了 run,没有启动新的线程
  • 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码

4.3.2 sleep 与 yield

sleep

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  3. 睡眠结束后的线程未必会立刻得到执行
  4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test7")
public class Test7 {
    
    

    public static void main(String[] args) throws InterruptedException {
    
    
        Thread t1 = new Thread("t1") {
    
    
            @Override
            public void run() {
    
    
                log.debug("enter sleep...");
                try {
    
    
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
    
    
                    log.debug("wake up...");
                    e.printStackTrace();
                }
            }
        };
        t1.start();

        Thread.sleep(1000);
        log.debug("interrupt...");
        t1.interrupt();
    }
}

在这里插入图片描述


yield

  1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
  2. 具体的实现依赖于操作系统的任务调度器
  3. 会放弃 CPU 资源,锁资源不会释放

线程优先级

  • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
  • 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
package main;

public class Test1 {
    
    

    public static void main(String[] args) {
    
    
        Runnable task1 = () -> {
    
    
            int count = 0;
            for (; ; ) {
    
    
                System.out.println("---->1 " + count++);
            }
        };
        Runnable task2 = () -> {
    
    
            int count = 0;
            for (; ; ) {
    
    
                // Thread.yield();
                System.out.println(" ---->2 " + count++);
            }
        };
        Thread t1 = new Thread(task1, "t1");
        Thread t2 = new Thread(task2, "t2");
//        t1.setPriority(Thread.MIN_PRIORITY);
//        t2.setPriority(Thread.MAX_PRIORITY);
        t1.start();
        t2.start();
    }
}

TimeUnit 进行sleep

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j(topic = "c.Test8")
public class Test8 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        log.debug("enter");
        TimeUnit.SECONDS.sleep(1);
        log.debug("end");
//        Thread.sleep(1000);
    }
}

4.3.2.1案例::防止CPU占用100%

课程地址
在没有利用cpu来计算时,不要让while(true)空转浪费cpu,这时可以使用yieldsleep来让出cpu的使用权给其他程序。

@Slf4j(topic = "c.Test8")
public class Test8 {
    
    
    public static void main(String[] args) {
    
    
        while (true) {
    
    
            try {
    
    
                Thread.sleep(50);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}
  • 可以用wait或条件变量达到类似的效果。
  • 不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景。
  • sleep适用于无需锁同步的场景。

4.3.3 join 方法详解

为什么需要join方法

下面的代码执行,打印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("开始");
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            log.debug("结束");
            r = 10;
        });
        t1.start();
        log.debug("结果为:{}", r);
        log.debug("结束");
    }

在这里插入图片描述
分析

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

解决方法

  1. 用 sleep 行不行?为什么?
  2. 用 join,加在 t1.start() 之后即可

4.3.4 interrupt 方法详解

===>打断 sleep、wait、join 的线程

sleep、wait、join这几个方法都会让线程进入阻塞状态

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test11")
public class Test11 {
    
    

    public static void main(String[] args) throws InterruptedException {
    
    
        Thread t1 = new Thread(() -> {
    
    
            log.debug("sleep...");
            try {
    
    
                Thread.sleep(5000); // wait, join
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        },"t1");

        t1.start();
        Thread.sleep(1000);
        log.debug("interrupt");
        t1.interrupt();
        log.debug("打断标记:{}", t1.isInterrupted());
    }
}

在这里插入图片描述

4.3.4.1 interrupt 打断正常
import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test12")
public class Test12 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Thread t1 = new Thread(() -> {
    
    
            while(true) {
    
    
                boolean interrupted = Thread.currentThread().isInterrupted();
                if(interrupted) {
    
    
                    log.debug("被打断了, 退出循环");
                    break;
                }
            }
        }, "t1");
        t1.start();

        Thread.sleep(1000);
        log.debug("interrupt");
        t1.interrupt();
    }
}
4.3.4.2 interrupt 设计模式-两端终止

终止模式之两阶段终止模式:Two Phase Termination

目标:在一个线程 T1 中如何优雅终止线程 T2?优雅指的是给 T2 一个后置处理器

错误思想:

  • 使用线程对象的 stop() 方法停止线程:stop 方法会真正杀死线程,如果这时线程锁住了共享资源,当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁
  • 使用 System.exit(int) 方法停止线程:目的仅是停止一个线程,但这种做法会让整个程序都停止

两阶段终止模式图示:
在这里插入图片描述
打断线程可能在任何时间,所以需要考虑在任何时刻被打断的处理方法:

public class Test {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        TwoPhaseTermination tpt = new TwoPhaseTermination();
        tpt.start();
        Thread.sleep(3500);
        tpt.stop();
    }
}
class TwoPhaseTermination {
    
    
    private Thread monitor;
    // 启动监控线程
    public void start() {
    
    
        monitor = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                while (true) {
    
    
                    Thread thread = Thread.currentThread();
                    if (thread.isInterrupted()) {
    
    
                        System.out.println("后置处理");
                        break;
                    }
                    try {
    
    
                        Thread.sleep(1000);					// 睡眠
                        System.out.println("执行监控记录");	// 在此被打断不会异常
                    } catch (InterruptedException e) {
    
    		// 在睡眠期间被打断,进入异常处理的逻辑
                        e.printStackTrace();
                        // 重新设置打断标记,打断 sleep 会清除打断状态
                        thread.interrupt();
                    }
                }
            }
        });
        monitor.start();
    }
    // 停止监控线程
    public void stop() {
    
    
        monitor.interrupt();
    }
}

在这里插入图片描述

4.3.4.3 打断 park

park 作用类似 sleep,打断 park 线程,不会清空打断状态(true)

import java.util.concurrent.locks.LockSupport;

public class Test2 {
    
    
    public static void main(String[] args) throws Exception {
    
    
        Thread t1 = new Thread(() -> {
    
    
            System.out.println("park...");
            LockSupport.park();
            System.out.println("unpark...");
            System.out.println("打断状态:" + Thread.currentThread().isInterrupted());//打断状态:true
        }, "t1");
        t1.start();
        Thread.sleep(2000);
        t1.interrupt();
    }
}
park...
unpark...
打断状态:true

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

LockSupport.park();
System.out.println("unpark...");
LockSupport.park();//失效,不会阻塞
System.out.println("unpark...");//和上一个unpark同时执行

可以修改获取打断状态方法,使用 Thread.interrupted(),清除打断标记

4.3.5 不推荐的方法

不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁:

  • public final void stop():停止线程运行

废弃原因:方法粗暴,除非可能执行 finally 代码块以及释放 synchronized 外,线程将直接被终止,如果线程持有 JUC 的互斥锁可能导致锁来不及释放,造成其他线程永远等待的局面

  • public final void suspend():挂起(暂停)线程运行

废弃原因:如果目标线程在暂停时对系统资源持有锁,则在目标线程恢复之前没有线程可以访问该资源,如果恢复目标线程的线程在调用 resume 之前会尝试访问此共享资源,则会导致死锁

  • public final void resume():恢复线程运行

4.3.6 主线程与守护线程

       默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

	log.debug("开始运行...");
	
	Thread t1 = new Thread(() -> {
    
    
	log.debug("开始运行...");
	sleep(2);
	log.debug("运行结束...");
	}, "daemon");
	
	// 设置该线程为守护线程
	t1.setDaemon(true);
	t1.start();
	
	sleep(1);
	log.debug("运行结束...");
08:26:38.123 [main] c.TestDaemon - 开始运行... 
08:26:38.213 [daemon] c.TestDaemon - 开始运行... 
08:26:39.215 [main] c.TestDaemon - 运行结束...

附录1

1.Java并发指南-黄小邪
2.其他人的笔记
3.其他人的笔记1

附录2

Java监视和管理平台 jconsole

jconsole 远程监控配置

  • 需要以如下方式运行你的 java 类
java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote -
Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接 -
Dcom.sun.management.jmxremote.authenticate=是否认证 java类
  • 修改 /etc/hosts 文件将 127.0.0.1 映射至主机名

如果要认证访问,还需要做如下步骤

  • 复制 jmxremote.password 文件
  • 修改 jmxremote.password 和 jmxremote.access 文件的权限为 600 即文件所有者可读写
  • 连接时填入 controlRole(用户名),R&D(密码)

在这里插入图片描述
现在Linux服务器上设置
在这里插入图片描述
本地连接
在这里插入图片描述
关闭防火墙
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/Blue_Pepsi_Cola/article/details/132526326