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类来创建并启动多线程的步骤如下:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
- 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的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.创建子类继承Thread类
2.在子类中重写run方法,把线程需要执行的任务代码放在run方法中
3.创建子类线程对象
4.调用start()方法启动线程,执行任务代码
创建线程的方式二_实现Runnable接口
本节讲解如何通过实现Runnable接口的方式创建线程
讲解
采用java.lang.Runnable
也是非常常见的一种,我们只需要重写run方法即可。
步骤如下:
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的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 多线程 可查阅,可复习,可面试的主要内容,如果未能解决你的问题,请参考以下文章