Day19:60 多个实例讲解,彻底搞懂Java 多线程 可查阅,可复习,可面试

Posted 每天都要努力的小颓废呀

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day19:60 多个实例讲解,彻底搞懂Java 多线程 可查阅,可复习,可面试相关的知识,希望对你有一定的参考价值。

本专栏将从基础开始,循序渐进,由浅入深讲解Java的基本使用,希望大家都能够从中有所收获,也请大家多多支持。
专栏地址:26天高效学习Java编程
相关软件地址:软件地址
所有代码地址:代码地址
如果文章知识点有错误的地方,请指正!大家一起学习,一起进步。
如果感觉博主的文章还不错的话,还请关注支持一下博主哦

JAVA 最难学的部分是哪里?很多朋友都会说:「 java 多线程 」。随着业务量和数据的增加,我们会不可避免地使用多线程的方式处理数据,同时在 Java 职位的面试中,多线程也是必考的高阶知识点之一,可以说,java多线程是衡量一名 Java 程序员是否资深的关键标准之一。今天,我们就来学习一下 Java 多线程的概念吧!本文将详细讲解Java多线程的创建方式、线程状态、高并发、线程安全、Synchronize以及Lock的使用、Volatile关键字、原子类、并发包、线程池的创建以及死锁等。

文章目录

1 Java多线程创建方式

我们在之前,学习的程序在没有跳转语句的前提下,都是由上至下依次执行,那现在想要设计一个程序,边打游戏边听歌,怎么设计?

要解决上述问题,咱们得使用多进程或者多线程来解决.

并发与并行的区别

本节讲解什么是并发和并行

讲解

  • 并行:指两个或多个事件在同一时刻发生(同时执行)。
  • 并发:指两个或多个事件在同一个时间段内发生(交替执行)。

在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。

而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率。

注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。

线程与进程的区别

本节讲解什么是线程与进程

讲解

  • 进程:进程是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;
    • 进程其实就是应用程序的可执行单元(.exe文件)
    • 每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;
  • 线程:是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
    • 线程:其实就是进程的可执行单元
    • 每条线程都有独立的内存空间,一个进程可以同时运行多个线程;
  • 多线程并行: 多条线程在同一时刻同时执行
  • 多线程并发:多条线程在同一时间段交替执行
  • 在java中线程的调度是:抢占式调度
  • 在java中只有多线程并发,没有多线程并行

进程

线程

进程与线程的区别

  • 进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。
  • 线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。

下面内容为拓展知识点

1:因为一个进程中的多个线程是并发运行的,那么从微观角度看也是有先后顺序的,哪个线程执行完全取决于 CPU 的调度,程序员是干涉不了的。而这也就造成的多线程的随机性。

2:Java 程序的进程里面至少包含两个线程,主进程也就是 main()方法线程,另外一个是垃圾回收机制线程。每当使用 java 命令执行一个类时,实际上都会启动一个 JVM,每一个 JVM 实际上就是在操作系统中启动了一个线程,java 本身具备了垃圾的收集机制,所以在 Java 运行时至少会启动两个线程。

3:由于创建一个线程的开销比创建一个进程的开销小的多,那么我们在开发多任务运行的时候,通常考虑创建多线程,而不是创建多进程。

线程调度:

  • 分时调度

    ​ 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

  • 抢占式调度

    ​ 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

Thread类构造方法和常用方法

本节讲解Thread类的构造方法和常用方法。

讲解

Thread类的概述

  • 表示线程,也叫做线程类,创建该类的对象,就是创建线程对象(或者说创建线程)
  • 线程的任务: 执行一段代码
  • Runnable : 接口,线程任务接口

Thread类的构造方法

线程开启我们需要用到了java.lang.Thread类,API中该类中定义了有关线程的一些方法,具体如下:

  • public Thread():分配一个新的线程对象,线程名称是默认生成的。

  • public Thread(String name):分配一个指定名字的新的线程对象。

  • public Thread(Runnable target):分配一个带有指定目标新的线程对象,线程名称是默认生成的。

  • public Thread(Runnable target,String name):分配一个带有指定目标新的线程对象并指定名字。

  • 创建线程的方式有2种:

    • 一种是通过继承Thread类的方式
    • 一种是通过实现Runnable接口的方法

Thread类的常用方法

  • public String getName():获取当前线程名称。
  • public void start():导致此线程开始执行; Java虚拟机调用此线程的run方法。
  • public void run():此线程要执行的任务在此处定义代码。
  • public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
  • public static Thread currentThread() :返回对当前正在执行的线程对象的引用。

翻阅API后得知创建线程的方式总共有两种,一种是继承Thread类方式,一种是实现Runnable接口方式,

创建线程方式一_继承Thread

本节讲解如何通过继承Thread创建线程

讲解

Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建启动多线程的步骤如下:

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程

代码如下:

测试类:

public class Test 
    public static void main(String[] args) 
        /*
            补充: java程序至少有2条线程:一条为主线程,一条为垃圾回收线程
            创建线程方式一_继承方式:
                1.创建子类继承Thread类
                2.在子类中重写run方法,把线程需要执行的任务代码放在run方法中
                3.创建子类线程对象
                4.调用start()方法启动线程,执行任务代码
         */
        // 创建子类线程对象
        MyThread mt1 = new MyThread();
        // 调用start()方法启动线程,执行任务代码
        mt1.start();

        for (int j = 0; j < 100; j++) 
            System.out.println("主线程 第"+(j+1)+"次循环");
        
    


自定义线程类:

public class MyThread extends Thread 
    @Override
    public void run() 
        for (int i = 0; i < 100; i++) 
            System.out.println("子线程 第"+(i+1)+"次循环");
        
    


小结

创建线程方式一_继承方式:
	1.创建子类继承Thread2.在子类中重写run方法,把线程需要执行的任务代码放在run方法中
    3.创建子类线程对象
    4.调用start()方法启动线程,执行任务代码

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

本节讲解如何通过实现Runnable接口的方式创建线程

讲解

采用java.lang.Runnable也是非常常见的一种,我们只需要重写run方法即可。

步骤如下:

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  3. 调用线程对象的start()方法来启动线程。

代码如下:

public class MyRunnable implements Runnable 
    @Override
    public void run() 
        // 线程需要执行的任务代码
        for (int i = 0; i < 100; i++) 
            System.out.println("子线程 第"+(i+1)+"次循环");
        
    


public class Test 
    public static void main(String[] args) 
        /*
            创建线程的方式二_实现方式:
                1.创建实现类实现Runnable接口
                2.在实现类中重写run方法,把线程需要执行的任务代码放入run方法中
                3.创建实现类对象
                4.创建Thread线程对象,并传入Runnable接口的实现类对象
                5.调用start()方法启动线程,执行任务
         */
        //创建实现类对象
        MyRunnable mr = new MyRunnable();

        //创建Thread线程对象,并传入Runnable接口的实现类对象
        Thread t1 = new Thread(mr);

        //调用start()方法启动线程,执行任务
        t1.start();


        for (int j = 0; j < 100; j++) 
            System.out.println("主线程 第"+(j+1)+"次循环");
        
    


通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个执行目标。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。

在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。

实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。

tips:Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。

创建线程的方式三_实现Callable接口

目标

  • 学会使用Callable

讲解

Callable是一种可以拥有返回值的线程类。 优点: 可以获得任务执行返回值; 通过与Future的结合,可以实现利用Future来跟踪异步计算的结果。

FutureTask类简介

FutureTask是一个可获得返回值的异步计算类!可以调用方法去开始和取消一个计算,可以查询计算是否完成并且获取计算结果,只有当计算完成时才能获取到计算结果,否则就会阻塞!

Callable接口使用

1.使用FutureTask的方式

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

class CallableDemo implements Callable<Integer> 

    //和runnable的接口比起来,这是有返回值的接口
    //还可以抛异常
    @Override
    public Integer call() throws Exception 
        System.out.println(Thread.currentThread().getName() + "==============进入callable了!");
        Thread.sleep(3000);
        return 1024;
    


public class Test 

    public static void main(String[] args) throws ExecutionException, InterruptedException 
        //把实现callable的接口当做参数传入futuretask
        FutureTask<Integer> futureTask = new FutureTask<>(new CallableDemo());
        //因为futureTask实现了Runnable接口,像普通的Runnable实现类一样传入Thread就可以了
        Thread t1 = new Thread(futureTask, "t1");
        //正常启动
        t1.start();
        //尝试获取返回结果
        System.out.println("==============result=" + futureTask.get());
    



结果如下图所示:

2.使用线程池的方式(理解即可,为了内容连贯性先做介绍,第8节会讲解线程池)


import java.util.concurrent.Callable;

public class MyCallable implements Callable<String> 
    @Override
    public String call() throws Exception 
        System.out.println("任务开始...");
        Thread.sleep(5000);
        System.out.println("任务结束...");
        return "hashnode";
    


//==========================================

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

public class Test2 
    public static void main(String[] args) throws ExecutionException, InterruptedException 
        // 1.创建线程池,初始化2条线程
        ExecutorService pools = Executors.newFixedThreadPool(2);

        // 2.创建任务
        MyCallable mc = new MyCallable();

        // 3.提交任务,执行任务
        pools.submit(mc);
        pools.submit(mc);
        pools.submit(mc);
        pools.submit(mc);
        pools.submit(mc);
        pools.submit(mc);
        pools.submit(mc);
        Future<String> f = pools.submit(mc);//提交任务并获取callable的返回值
        System.out.println(f.get());

        // 4.销毁线程池(开发中,一般不会)
        pools.shutdown();
    


2 线程状态

线程的6种状态

目标

  • 理解线程的6种状态

讲解

线程状态概述

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在API中java.lang.Thread.State这个枚举中给出了六种线程状态

这里先列出各个线程状态发生的条件,下面将会对每种状态进行详细解析

线程状态导致状态发生条件
NEW(新建)线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程对象,没有线程特征。创建线程对象时
Runnable(可运行)线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。调用了t.start()方法 :就绪(经典教法)。调用start方法时
Blocked(锁阻塞)当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。等待锁对象时
Waiting(无限等待)一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。调用wait()方法时
Timed Waiting(计时等待)同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。调用sleep()方法时
Teminated(被终止)因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。run方法执行结束时
  • 无限等待:
    • 进入无限等待: 使用锁对象调用wait()方法
    • 唤醒无限等待线程: 其他线程使用锁对象调用notify()或者notifyAll()方法
    • 特点: 不会霸占cpu,也不会霸占锁对象(释放)

线程状态的切换

我们不需要去研究这几种状态的实现原理,我们只需知道在做线程操作中存在这样的状态。那我们怎么去理解这几个状态呢,新建与被终止还是很容易理解的,我们就研究一下线程从Runnable(可运行)状态与非运行状态之间的转换问题。

等待唤醒机制介绍

目标

  • 理解等待唤醒机制

讲解
思考如下案例,加入我们想让子线程打印1000次i循环主线程打印1000次j循环,并且以如下规律进行打印:打印1次i循环,就打印1次j循环,以此类推...该如何实现?
答:假如子线程先执行,打印1次i循环,让子线程进入无限等待,执行j循环,唤醒子线程,主线程就进入无限等待

以上的机制就是等待唤醒机制。这是多个线程间的一种协作机制。就好比在公司里你和你的同事们,你们可能存在在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。就是在一个线程进行了规定操作后,就进入无限等待状态(wait()),调用notfiy()方法唤醒其他线程来执行,其他线程执行完后,进入无限等待,唤醒等待线程执行,依次类推… 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。

wait/notify 就是线程间的一种协作机制。

  • 实现等待唤醒机制程序:
    • 必须使用锁对象调用wait方法,让当前线程进入无限等待状态
    • 必须使用锁对象调用notify\\notifyAll方法唤醒等待线程
    • 调用wait\\notfiy\\notfiyAll方法的锁对象必须一致
  • 分析的等待唤醒机制程序:
    • 线程的调度依然是抢占式调度
    • 线程进入无限等待状态,就不会霸占cpu和锁对象(释放),也不会抢占cpu和锁对象
    • 如果是在同步锁中\\Lock锁中,调用sleep()方法进入计时等待,不会释放cpu和锁对象(依然占用)

等待唤醒机制相关方法介绍

  • public void wait() : 让当前线程进入到无限等待状态 此方法必须锁对象调用.
  • public void notify() : 唤醒当前锁对象上等待状态的线程 此方法必须锁对象调用.
  • 案例一: 进入无限等待
public class Test 
    static Object obj = new Object();
    public static void main(String[] args)  
        // 步骤1 : 子线程开启,进入无限等待状态, 没有被唤醒,无法继续运行.
        new Thread(new Runnable() 
            @Override
            public void run() 
                System.out.println("准备进入无限等待状态...");
                synchronized (obj)
                    try 
                        obj.wait();
                     catch (InterruptedException e) 
                        e.printStackTrace();
                    
                
            
        ).start();
    

  • 案例二: 等待和唤醒
public class Test1 
    // 锁对象
    static Object lock = new Object();

    public static void main(String[] args) 
        // 无限等待线程
        new Thread(new Runnable() 
            @Override
            public void run() 
                synchronized (lock)
                    System.out.println("无限等待线程:准备进入无限等待状态...");
                    // 进入无限等待状态
                    try 
                        lock.wait();// 被唤醒-->锁阻塞
                     catch (InterruptedException e) 
                        e.printStackTrace();
                    
                    System.out.println("无限等待线程:被其他线程唤醒...");
                
            
        ).start();

        // 唤醒线程
        new Thread(new Runnable() 
            @Override
            public void run() 
               synchronized (lock)
                   System.out.println("唤醒线程: 准备唤醒无限等待线程...");
                   lock.notify();
                   try 
                       Thread.sleep(10000);
                    catch (InterruptedException e) 
                       e.printStackTrace();
                   
                   System.out.println("唤醒线程: 唤醒完毕");
               // 释放锁
            
        ).start();
    


  • 案例三
public class Test 
    // 锁对象
    static Object lock = new Object();

    public static void main(String[] args) 
        // 无限等待线程
        new Thread(new Runnable() 
            @Override
            public void run() 
                while (true)
                    synchronized (lock)
                        System.out.println("无限等待线程1:准备进入无限等待状态...");
                        // 进入无限等待状态
                        try 
//                            System.out.println("无限等待线程:被其他线程唤醒1=========================================");
                            lock.wait();// 等待被唤醒,如果是notify,则多个wait的线程竞争一个执行机会,如果是notifyAll则每个wait的线程都能执行
                            //注意,只有在notify的互斥代码段执行完才会开始竞争资源,并且没有wait的synchronized互斥块不受其他wait和notify的影响,正常竞争
                            //lock的使用
                            System.out.println("无限等待线程1:被其他线程唤醒=========================================");
                         以上是关于Day19:60 多个实例讲解,彻底搞懂Java 多线程 可查阅,可复习,可面试的主要内容,如果未能解决你的问题,请参考以下文章

彻底搞懂JDBC的运行过程

Java 多态 ——一个案例 彻底搞懂它

一文彻底搞懂 Python中的描述器反射

一文彻底搞懂 Python中的描述器反射

3天拿到网易Java岗offer,彻底帮你搞懂

熬夜彻底搞懂Cookie Session Token JWT