JUC——Chapter01——Java Multi-Threading Skills 多线程应用技能 —— 读《Java多线程编程技术核心技术》笔记

本文目录

  • Chapter 01 —— 多线程技能
    • 进程和线程的定义与多线程的优点
    • 使用多线程
      • 继承 Thread 类
      • 使用常见的3个命令分析线程的信息
      • 线程随机性的展现
      • 实现 Runnable 接口
      • 使用 Runnable 接口实现多线程的优点
      • 实例变量共享导致的"非线程安全"问题与相应的解决方案
        • ⭐️ 不共享数据的情况
        • ⭐️ 共享数据的情况
      • 留意 i -- 与 System.out.println() 出现的 "非线程安全" 问题
    • 方法 sleep(long millis)
    • 方法StackTraceElement[] getStackTrace()
    • 方法Map `<`ThreadStackTraceElement[]`>` getAllStackTraces()
    • 停止线程
      • 停止不了的线程
      • 判断线程是不是停止状态
    • 使用LockSupport 类实现线程暂停与恢复
    • 方法 yield()
    • 线程的优先级
      • 线程优先级的继承特性
      • 线程优先级的规律性
    • 守护线程
    • 并发与并行
    • 同步与异步
    • 多核CPU 不一定比单核 CPU 运行快

Chapter 01 —— 多线程技能

所需知识点掌握

  • 线程的启动
  • 如何使线程暂停
  • 如何使线程停止
  • 线程的优先级
  • 线程安全的相关问题

进程和线程的定义与多线程的优点

什么是线程呢?

线程可以理解为在进程当中独立运行的子任务

小知识点:进程负责向操作系统申请资源在一个进程中,多个线程可以共享进程中相同的内存或者文件资源先有进程,后有线程。在一个进程中可以创建多个线程

进程和线程的总结:

1、进程虽然是相互独立的,但他们可以互相通信,较为通用的方式是使用 Socket 或 HTTP 协议。

2、进程拥有共享的系统资源,比如内存、网络端口,供其内部线程使用。

3、进程较重,因为创建进程需要操作系统分配资源,会占用内存。

4、线程存在于进程中,是进程的一个子集,先有进程,后有线程

5、虽然线程更轻,但是线程上下文切换的的时间成本非常高。

什么场景下使用多线程技术?

阻塞: 一旦系统中共出现了阻塞现象,则可以根据实际情况来使用多线程提高速率。

依赖: A、B两个业务之间不存在结果依赖关系,此时可以使用多线程来提高运行效率。

使用多线程

使用多线程编程的方式主要有两种

其一是继承 Thread 类

其二是实现 Runnable 接口

继承 Thread 类

Thread类的声明结构

public class Thread implements Runnable {}
  • 从源码中我们可以发现,Thread 类实现了 Runnable 接口,他们之间具有多态关系。

多态结构示意如下

Runnable run1 = new Thread();
Runnable run2 = new MyThread();
Thread t1 = new MyThread();

注意点:因为 Java 是单根继承的,为了支持“多继承”完全可以实现 Runnable 接口方式,一边实现一边继承。

代码演示

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/25 14:21
 * @description: 自定义线程类MyThread 以及对应的测试类 Run
 * @modified By: Alascanfu
 **/
public class MyThread extends Thread{
    @Override
    public void run() {
        super.run();
        System.out.println("MyThread");
    }
}
class Run {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start(); // 耗时长
        System.out.println("运行结束!");// 耗时短
    }
}

运行结果

在这里插入图片描述

start()方法执行较为耗时,内部执行步骤是如何的?

当执行 new Thread().start() 时

1️⃣ JVM 会告诉 OS 创建 Thread。

2️⃣OS 会开辟内存空间并且用 Windows SDK 中的 createThread() 方法创建 Thread 线程对象。

3️⃣OS 对 Thread 对象进行调度,以确定执行时机。

4️⃣Thread 在 OS 中成功执行。

注意点:如果多次调用 start() 方法,则会出现异常 Exception in thread "main" java.lang.IllegalThread-StateException

使用常见的3个命令分析线程的信息

常用的三个分析线程信息的方式

  • jps + jstack.exe
  • jmc.exe
  • jvisualvm.exe

这三个工具命令都是在 %JAVA_HOME%/bin 目录中的

线程随机性的展现

多线程随机输出的原因?

CPU 将时间片分给不同的线程,线程获得时间片后就执行任务,所以这些线程在交替执行并输出,导致输出结果成乱序。

什么是时间片?

时间片即是 CPU 分配给各个程序的时间。每个线程被分配一个时间片,在当前的时间片内执行线程中的任务。

注意点:当CPU在不同的线程上进行切换时也是需要耗时的,并不是线程创建的越多,软件效率就越快,相反,线程数过多反而会降低软件的执行效率。

“thread.start()” 与 “thread.run()”

thread.run() thread.start()
同步执行 异步执行

实现 Runnable 接口

Thread.java 的构造函数

JUC——Chapter01——Java Multi-Threading Skills 多线程应用技能 —— 读《Java多线程编程技术核心技术》笔记_第1张图片

快速使用

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/25 14:50
 * @description: MyRunnable 通过实现 Runnable 接口创建线程以及测试
 * @modified By: Alascanfu
 **/
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("MyRunnable => 运行中!");
    }
}

class RunRunnable{
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start();
        System.out.println("执行结束!");
    }
}

运行结果

JUC——Chapter01——Java Multi-Threading Skills 多线程应用技能 —— 读《Java多线程编程技术核心技术》笔记_第2张图片

使用 Runnable 接口实现多线程的优点

主要原因是因为 Java 是单根继承, 不支持多继承, 所以为了改变这种限制, 可以使用实现 Runnable 接口的方式来实现多线程

我们来看一下如下这个构造函数

Thread(Runnable target) 不仅可以传入 Runnable 接口的对象,还可以传入一个 Thread 类的对象,这样做完全可以将一个 Thread 对象中的 run() 方法交由其他线程进行调用。

实例变量共享导致的"非线程安全"问题与相应的解决方案

自定义线程类中的实例变量针对其他线程可以有共享与不共享之分,这在多个线程之间交互时是很重要的。

⭐️ 不共享数据的情况

直接看示例

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/25 15:04
 * @description: NotSharingDataThread 不共享数据的线程
 * @modified By: Alascanfu
 **/
public class NotSharingDataThread extends Thread {
    private int count = 5 ;
    public NotSharingDataThread(String name){
        this.setName(name);
    }
    
    @Override
    public void run() {
        super.run();
        while (count-- > 0){
            System.out.println("由 " + currentThread().getName() + " 计算 , count =" + count);
        }
    }
}

class RunNotSharingDataThread{
    public static void main(String[] args) {
        NotSharingDataThread a = new NotSharingDataThread("A");
        NotSharingDataThread b = new NotSharingDataThread("B");
        NotSharingDataThread c = new NotSharingDataThread("C");
        a.start();
        b.start();
        c.start();
    }
}

运行结果

JUC——Chapter01——Java Multi-Threading Skills 多线程应用技能 —— 读《Java多线程编程技术核心技术》笔记_第3张图片

注意点: 此示例并不存在多个线程访问同一个实例变量的情况。

⭐️ 共享数据的情况

共享数据的情况就是多个线程可以访问同一个变量

直接看示例

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/25 15:11
 * @description: SharingDataThread 共享数据的线程
 * @modified By: Alascanfu
 **/
public class SharingDataThread extends Thread{
    private int count = 5 ;
    @Override
    public void run() {
        super.run();
        /** 此示例不要用 while 语句,会造成其他线程得不到运行的机会
         * 因为第一个执行 while 的语句的线程就会将 count 值减为 0
         * 一直由一个线程进行减法运算
         * */
        count--;
        System.out.println("由 " + currentThread().getName() + " 计算 , count =" + count);
    }
}
class RunSharingDataThread{
    public static void main(String[] args) {
        SharingDataThread sharingDataThread = new SharingDataThread();
        Thread a = new Thread(sharingDataThread, "A");
        Thread b = new Thread(sharingDataThread, "B");
        Thread c = new Thread(sharingDataThread, "C");
        Thread d = new Thread(sharingDataThread, "D");
        Thread e = new Thread(sharingDataThread, "E");
        
        a.start();
        b.start();
        c.start();
        d.start();
        e.start();
    }
}

运行结果

JUC——Chapter01——Java Multi-Threading Skills 多线程应用技能 —— 读《Java多线程编程技术核心技术》笔记_第4张图片

从图中结果可以看到线程 B 和 线程 D 输出的 count 值都是 1 说明 A 和 B 同时对 count 进行了处理,产生了 “非线程安全” 问题。而我们想要得到的输出结果确实不重复的,应该是一次递减的。

出现上述非线程安全的情况是因为在某些 JVM 中,count-- 的操作要分解成如下三步(执行这3个步骤的过程中会被其他线程锁打断)

1️⃣ 获取原有的 count 值

2️⃣ 计算 count - 1

3️⃣ 对 count 进行重新赋值

i -- 操作对应的字节码

getstatic # 获取 static 变量
iconst_1 # 产生整数 1
isub # 对 static 变量进行减 1 操作
putstatic # 对 static 变量进行赋值

注意点:出现非线程安全的情况是多个线程操作同一个对象的同一个实例变量

如何更改上述代码使得多个线程之间进行同步的操作

public class SharingDataThread extends Thread{
    private int count = 5 ;
    @Override
    synchronized public void run() {
        super.run();
        count--;
        System.out.println("由 " + currentThread().getName() + " 计算 , count =" + count);
    }
}

通过在 run 方法前 加入 synchronized 关键字,使得多个线程在执行 run 方法时,以排队的方式进行处理。

使用 synchronized 关键字修饰的方法称之为"同步方法",可以用来对方法内部的全部代码进行加锁,而加锁的这段代码称之为"互斥区" 或 “临界区”。

实际的 synchronized 过程

当一个线程想要执行同步方法里面的代码时,会首先尝试去拿这把锁,如果能够拿到,那么该线程就会执行 synchronized 里面的代码,反之如果不能拿到,那么这个线程就会不断尝试去获得这把锁,直到拿到为止。

注意:Servlet 技术也会引起 “非线程安全问题”,在Web开发中,Servlet 对象本身就是单例的,所以为了不出现的非线程安全,建议不要在 Servlet 中出现实例变量。

留意 i – 与 System.out.println() 出现的 “非线程安全” 问题

虽然 println()方法在内部是 synchronized 同步的,但 i – 操作是在进入 println()之前发生的,所以还是可能发生非线程安全问题。

注意点:不要看到 synchronized 就认为代码是安全的,在 synchronized 之前执行的代码也有可能是不安全的。

方法 sleep(long millis)

sleep() 方法的作用是在指定的毫秒数内 让当前 “正在执行的线程” 休眠(暂停执行),这个"正在执行的线程" 是指 this.currentThread()返回的线程。

如果调用 sleep() 方法所在的类 是 Thread.java 则如下两种方式的效果是一致的

Thread.sleep(3000);
this.sleep(3000);

如果调用sleep()方法所在的类不是 Thread.java 则必须使用如下代码实现暂停功能

Thread.sleep(3000);

实验代码1

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/25 17:28
 * @description: SleepThread
 * @modified By: Alascanfu
 **/
public class SleepThread extends Thread {
    @Override
    public void run() {
        try {
            super.run();
            System.out.println("run threadName = " + currentThread().getName() + " begin ");
            Thread.sleep(2000);
            System.out.println("run threadName = " + currentThread().getName() + "end ");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class RunSleepThread{
    public static void main(String[] args) {
        SleepThread sleepThread = new SleepThread();
        System.out.println("begin = " + System.currentTimeMillis());
        sleepThread.run();
        System.out.println("end = " + System.currentTimeMillis());
    }
}

执行结果

JUC——Chapter01——Java Multi-Threading Skills 多线程应用技能 —— 读《Java多线程编程技术核心技术》笔记_第5张图片

实验代码2

class StartSleepThread{
    public static void main(String[] args) {
        SleepThread sleepThread = new SleepThread();
        System.out.println("begin = " + System.currentTimeMillis());
        sleepThread.start();
        System.out.println("end = " + System.currentTimeMillis());
    }
}

执行结果

JUC——Chapter01——Java Multi-Threading Skills 多线程应用技能 —— 读《Java多线程编程技术核心技术》笔记_第6张图片

方法StackTraceElement[] getStackTrace()

public StackTraceElement[] getStackTrace() 方法的作用是返回一个表示该线程的堆栈跟踪元素数组。如果该线程尚未启动或者已经终止,则该方法将返回一个零长度的数组。如果返回的数组不是零长度的,则其第一个元素代表堆栈顶,它是该数组中最新的方法调用。最后一个元素代表堆栈底,是该数组中最旧的方法调用。

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/25 17:41
 * @description: GetStackTraceTest
 * @modified By: Alascanfu
 **/
public class GetStackTraceTest {
    void a(){
        b();;
    }
    
    void b(){
        c();
    }
    
    void c(){
        d();
    }
    
    void d(){
        e();
    }
    
    void e(){
        StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
        if (stackTrace != null){
            for (int i = 0 ; i < stackTrace.length; i++){
                StackTraceElement eachElement = stackTrace[i];
                System.out.println("className => " + eachElement.getClassName()
                    + "methodName => " + eachElement.getMethodName() + "lineNumber =>"
                + eachElement.getLineNumber());
            }
        }
    }
    
    public static void main(String[] args) {
        GetStackTraceTest getStackTraceTest = new GetStackTraceTest();
        getStackTraceTest.a();
    }
}

运行结果

JUC——Chapter01——Java Multi-Threading Skills 多线程应用技能 —— 读《Java多线程编程技术核心技术》笔记_第7张图片

如上便是可以在控制台中查看到当前线程的堆栈跟踪信息。

方法Map <ThreadStackTraceElement[]> getAllStackTraces()

Map getAllStackTraces() 方法的作用是返回所有活动线程的堆栈信息的一个映射。

实验代码

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/25 17:52
 * @description: GetAllStackTracesTest
 * @modified By: Alascanfu
 **/
public class GetAllStackTracesTest {
    void a() {
        b();
        ;
    }
    
    void b() {
        c();
    }
    
    void c() {
        d();
    }
    
    void d() {
        e();
    }
    
    void e() {
        Map<Thread, StackTraceElement[]> map =
            Thread.getAllStackTraces();
        if (map != null && map.size() != 0) {
            Iterator<Thread> iterator = map.keySet().iterator();
            while (iterator.hasNext()) {
                Thread eachThread = (Thread) iterator.next();
                StackTraceElement[] stackTraceElements = map.get(eachThread);
                System.out.println("------每个线程的基本信息");
                System.out.println("  线程名称: " + eachThread.getName());
                System.out.println(" StackTraceElement[].length = " + stackTraceElements.length);
                System.out.println("  线程的状态:" + eachThread.getState());
                if (stackTraceElements != null) {
                    System.out.println("  打印 StackTraceElement[] 数组的具体信息:");
                    for (int i = 0; i < stackTraceElements.length; i++) {
                        StackTraceElement eachElement = stackTraceElements[i];
                        System.out.println("className => " + eachElement.getClassName()
                            + "methodName => " + eachElement.getMethodName() + " lineNumber =>"
                            + eachElement.getLineNumber());
                    }
                }else {
                    System.out.println("  没有StackTraceElement[] 信息, 因为线程" + eachThread.getName()
                    +"中的 没有StackTraceElement[].length == 0");
                    System.out.println();
                    System.out.println();
                }
            }
        }
    }
    
    public static void main(String[] args) {
        GetAllStackTracesTest getAllStackTracesTest = new GetAllStackTracesTest();
        getAllStackTracesTest.a();
    }
}

运行结果

停止线程

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

在 Java 中有三种方法可以使正在运行的线程终止运行

1️⃣ 使用退出标志使线程正常退出

2️⃣ 使用 stop() 方法强行终止线程,但是这个方法不推荐使用,因为 stop() 和 suspend() 和 resume()一样,都是作废过期的方法,使用它们可能发生不可预料的结果。

3️⃣ 使用 interrupt() 方法中断线程。

停止不了的线程

通过 interrupt() 方法来停止线程,但是 interrupt() 方法的使用效果并不像 for + break 语句那样,可以马上停止循环,该方法仅仅是在当前线程中打了一个作业的标记,并不是真正的停止线程。

实验代码

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/25 23:04
 * @description: InterruptThread   Test
 * @modified By: Alascanfu
 **/
public class TestInterruptThread extends Thread {
    @Override
    public void run() {
        super.run();
        for (int i = 1 ; i <= 50000 ; i ++){
            System.out.println("i = " + i);
        }
    }
}
class TestInterrupt{
    public static void main(String[] args) throws InterruptedException {
        TestInterruptThread thread = new TestInterruptThread();
        thread.start();
        TimeUnit.MILLISECONDS.sleep(10);
        thread.interrupt();
        System.out.println("zzzzzzzzzzzzzz");
    }
}

从运行结果来看

...
i = 566
i = 567
i = 568
i = 569
i = 570
zzzzzzzzzzzzzz
i = 571
i = 572
i = 573
i = 574
...

调用 interrupt()方法 并没有将线程停止,那么如何停止线程呢?

判断线程是不是停止状态

Thread.java 类里提供了两种判断方法 进行判断线程的状态是否已经是停止的

1、public static boolean interrupted() : 测试 currentThread()是否已经中断,执行后具有清除状态标志值为 false 的功能。

2、public boolean this.isInterrupted(): 测试 this 关键字所在线程类的对象是否已经中断,不会清除状态标志。

使用LockSupport 类实现线程暂停与恢复

创建测试项目

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/25 23:36
 * @description: LockSupport
 * @modified By: Alascanfu
 **/
public class LockSupportThread extends Thread{
    @Override
    public void run() {
        super.run();
        System.out.println("begin " + System.currentTimeMillis());
        LockSupport.park();
        System.out.println(" end " + System.currentTimeMillis());
    }
}
class TestLockSupportThread{
    public static void main(String[] args) throws InterruptedException {
        LockSupportThread lockSupportThread = new LockSupportThread();
        lockSupportThread.start();
    
        TimeUnit.SECONDS.sleep(2);
        LockSupport.unpark(lockSupportThread);
    }
}

执行结果

JUC——Chapter01——Java Multi-Threading Skills 多线程应用技能 —— 读《Java多线程编程技术核心技术》笔记_第8张图片

方法介绍

  • public static void park()方法的作用是将线程暂停
  • public static void unpark(Thread thread) 方法的作用是恢复线程的运行。

方法 yield()

方法 yield() 的作用是放弃当前的 CPU 资源 , 让其他任务去占用 CPU 执行时间 ,放弃的时间不确定,有可能刚刚放弃,马上又获得 CPU 时间片 。

实验代码

/***
 * @author: Alascanfu
 * @date : Created in 2022/6/25 23:58
 * @description: YieldThread
 * @modified By: Alascanfu
 **/
public class YieldThread extends Thread{
    
    @Override
    public void run() {
        super.run();
        long beginTime = System.currentTimeMillis();
        int count = 0 ;
        for (int i = 0 ;  i < 50000000 ; i++){
             Thread.yield();
            count = count + (i + 1);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("用时: " + (endTime - beginTime) + "毫秒 ! ");
    }
}

class TestYieldThread{
    public static void main(String[] args) {
        YieldThread thread = new YieldThread();
        thread.start();
    }
}

运行结果

JUC——Chapter01——Java Multi-Threading Skills 多线程应用技能 —— 读《Java多线程编程技术核心技术》笔记_第9张图片

  • 将 CPU 资源让给其他资源 , 导致速度变慢

  • 如果此时将 Thread.yield() 注释掉,则此时CPU独占时间片,速度很快。

线程的优先级

在操作系统当中,线程可以划分优先级,优先级较高的线程得到的CPU资源就较多,即CPU优先执行优先级较高的线程对象中的任务,其实就是让高优先级的线程获得更多的CPU时间片

JDK 中源码

	public final void setPriority(int newPriority) {
        ThreadGroup g;
        checkAccess();
        if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
            throw new IllegalArgumentException();
        }
        if((g = getThreadGroup()) != null) {
            if (newPriority > g.getMaxPriority()) {
                newPriority = g.getMaxPriority();
            }
            setPriority0(priority = newPriority);
        }
    }

在 JAVA 中线程的优先级分为 10 个等级 , 即 1~10 如果小于 1 或者 大于 10 则 JDK 抛出异常throw new IllegalArgumentException()

线程优先级的继承特性

线程的优先级具有继承性,比如 A 线程 启动 B 线程 ,则 B 线程 的优先级与 A 是一样的

线程优先级的规律性

高优先级的线程总是大部分先执行完,但不代表高由县级的线程全部先执行完。

当线程优先级的等级差距很大时,谁先执行完和代码的调用顺序无关

线程优先级具有一定的规律性,也就是 CPU 会尽量将执行资源让给优先级比较高的线程

守护线程

什么是守护线程?

守护线程是一种特殊的线程,当进程中不存在非守护线程时,则守护线程自动销毁。典型的守护线程就是垃圾回收线程,当进程中没有非守护线程了,则垃圾回收线程也就没有存在的必要了,自动销毁。

守护Daemon线程的作用是为了其他线程的运行提供便利服务,最典型的应用就是 GC (垃圾回收器)

小结:综上所述,当最后一个用户线程销毁了,守护线程会退出,进程也随之结束。

并发与并行

什么是并发?

可以简单理解为一个 CPU 同时处理多个任务。比如使用 单核 CPU ,那么工作中的 多个线程之间其实还是可以按顺序的方式被CPU 执行。

为什么在平时使用的过程中感受不到这种处理呢?

OS 中的线程调度器将 CPU 时间片 分配给不同的线程使用,由于CPU在线程间的切换速度非常快,所以使用者会认为多个任务在同时运行,这种线程轮流使用 CPU 时间片的处理方式称之为并发。

什么是并行?

并行是指多个CPU 或者 多核的CPU 同时处理多个不同的任务。

小结:并发是逻辑上的同时发生,而并行是物理上的同时发生.

同步与异步

什么是同步?

同步是指需要等待处理的结果才能继续运行,比如 a 方法调用 b 方法 ,直到 b 方法调用结束后 才能调用 c 方法。

什么是异步?

不需要等待处理的结果还能继续运行的就是异步。比如 a 方法 调用 b 方法,不需要 b 方法调用结束就能继续调用 c 方法。

实际开发中常常会出现使用异步解耦 提高执行效率。

多核CPU 不一定比单核 CPU 运行快

Redis 就是典型的案例

CPU 在线程上进行上下文切换的时间成本非常高。当多线程在执行计算密集型任务时,类似于 while(true) 这类的任务,则多核 CPU 的执行效率反而会 慢一些,因为多核 CPU 需要处理内存中的共享数据以及多核 CPU 之间的通信和任务的调度等,而单核 CPU 在执行这种计算密集型任务时相当的“专注”,没有多余的操作需要处理,所以执行效率反而更快。

你可能感兴趣的:(JUC,并发编程理解与实战,java,开发语言,juc,并发编程)