0
点赞
收藏
分享

微信扫一扫

Java学习笔记 11、快速入门多线程(详细)

飞空之羽 2022-05-09 阅读 94



文章目录

  • ​​前言​​
  • ​​一、多线程基本认识​​
  • ​​1、程序、进程、线程​​
  • ​​2、认识单核与多核CPU​​
  • ​​3、多线程优点​​
  • ​​4、一个以上的执行空间说明​​
  • ​​二、线程的创建与使用​​
  • ​​认识Thread类​​
  • ​​两种创建线程方式​​
  • ​​1、创建线程方式一:继承Thread​​
  • ​​2、创建线程方式二:实现Runnable接口​​
  • ​​比较两种创建方式​​
  • ​​常用方法​​
  • ​​修改线程名​​
  • ​​yield()方法​​
  • ​​join()方法​​
  • ​​sleep()方法​​
  • ​​线程优先级设置​​
  • ​​介绍调度​​
  • ​​线程优先级​​
  • ​​线程的分类​​
  • ​​三、线程的生命周期​​
  • ​​Thread.State中的六种状态​​
  • ​​生命周期中五种状态​​
  • ​​四、线程的同步​​
  • ​​1、多窗口卖票(引出问题)​​
  • ​​继承Thread与实现Runnable接口两种方式​​
  • ​​问题描述以及解决方案​​
  • ​​2、同步机制(解决线程安全问题)​​
  • ​​同步机制介绍​​
  • ​​方式一:同步代码块​​
  • ​​方式二:同步方法​​
  • ​​方式三:Lock锁​​
  • ​​3、同步方法的好处及坏处​​
  • ​​4、同步的范围及释放与不释放锁的操作​​
  • ​​5、小练习​​
  • ​​五、线程死锁问题​​
  • ​​1、介绍死锁问题及实例情况​​
  • ​​2、解决与避免死锁​​
  • ​​六、线程的通信​​
  • ​​1、认识线程通信​​
  • ​​2、线程通信小例子(交替打印1-100)​​
  • ​​不使用wait()、notify()实现线程通信(不推荐)​​
  • ​​使用wait()、notify()实现线程通信(推荐)​​
  • ​​3、经典例题(生产者与消费者)​​
  • ​​七、JDK5.0新增线程创建方式​​
  • ​​方式一:实现Callable接口​​
  • ​​方式二:使用线程池​​
  • ​​认识线程池的相关API​​
  • ​​实例:使用线程池创建10个线程来执行指定方法​​
  • ​​如何使用线程池的属性?​​
  • ​​相关面试题​​
  • ​​1、synchronized与Lock 的对比​​
  • ​​2、sleep()与wait()方法异同点​​
  • ​​参考文章​​

前言

      去年四月份大一下半学期正式开始学习Java,一路从java基础、数据库、jdbc、javaweb、ssm以及Springboot,其中也学习了一段时间数据结构。

      在javaweb期间做了图书商城项目、ssm阶段做了权限管理项目,springboot学了之后手痒去b站看视频做了个个人博客项目(已部署到服务器,正在备案中)。期间也不断进行做笔记,总结,但是越学到后面越感觉有点虚,觉得自己基础还有欠缺。

      之后一段时间我会重新回顾java基础、学习一些设计模式,学习多线程并发之类,以及接触一些jvm的相关知识,越学到后面越会感觉到基础的重要性,之后也会以博客形式输出学习的内容。

      现在整理的java知识基础点是在之前学习尚硅谷java课程的笔记基础之上加工汇总,部分图片会引用尚硅谷或网络上搜集或自己画,在重新回顾的过程中也在不断进行查漏补缺,尽可能将之前困惑的点都解决,让自己更上一层楼吧。

      博客目录索引:博客目录索引(持续更新)




一、多线程基本认识

1、程序、进程、线程

程序(program):指代完成指定任务并使用某种语言编写的一组指令的集合,也指代一段静态的代码。

进程(process):程序的一次执行过程,也可以用一个正在运行的程序来表示进程。它有自己的一个生命周期,自身产生、存在与消亡的过程。

线程(thread):进程中可以细化为线程,我们平时使用的就是主线程,我们也可以开辟其他线程来帮我们并行做其他事。若一个进程同一时间并行执行多个线程,就是支持多线程的!

  • 线程作为调度和执行的单位,每个线程都有自己独立的运行栈和程序计数器(pc),并且线程切换的开销小。
  • 一个进程中的多个线程共享相同的内存单元/内存地址空间,从同一堆中分配对象,可以访问相同的变量和对象,但多个线程共享的系统资源可能就会带来安全的隐患。


单线程与多线程见图

Java学习笔记 11、快速入门多线程(详细)_多线程



2、认识单核与多核CPU

单核CPU:其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。好比高度公路有多个车道但是只有一个工作人员收费。由于CPU时间单元短,我们平时运行程序时也不会感觉出来。

多核CPU:能够更好的发挥多线程的效率,现在我们一般电脑都是多核的,服务器也是。

例如我的电脑是8核的,对于暂时学习做一些普通的项目都是够得:此电脑-右击管理—设备管理器即可查看

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vyr2szlY-1612970769700)(C:\Users\93997\AppData\Roaming\Typora\typora-user-images\image-20210204215155208.png)]

补充知识点:一个Java应用程序例如java.exe,其实最少有三个线程如main()主线程,gc()垃圾回收线程,异常处理线程。一旦发生异常的话就会影响主线程。



并行:多个CPU同时执行多个任务,例如多个人同时做不同的事情。

并发:一个CPU(采用时间片)同时执行多个任务。比如秒杀项目,多个人做同一件事。



3、多线程优点

背景介绍:以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用时更短,但为什么仍需要使用多线程呢?

  • 单核时,采用多线程会比采用单线程会更慢,因为进行多线程的过程中需要来回不断切换线程。
  • 多核时,采用多线程就会比采用单线程快了,此时不需要来回进行切换。

多线程优点

  1. 提高应用程序的响应,尤其是在图形化界面中更会使用到,增强用户的体验。
  2. 提高计算机系统CPU的利用率。
  3. 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,方便于理解与修改。

何时需要使用多线程

  • 程序需要同时执行两个或多个任务。
  • 程序需要实现一些需要等待的任务时,例如用户输入、文件读写操作、网络操作、搜索等。
  • 需要后台运行的程序。


4、一个以上的执行空间说明


《head first java 2.0》中的一个问题:有一个以上的执行空间代表什么?


      当我们创建多个线程,有超过一个以上的执行空间时,看起来会像是有好几件事情同时发生。实际上,只有真正的多处理器才能够同时执行多件事情(前面也提到了)。

      对于使用在​​Java​​中的线程可以让它看起来好像同时都在执行中,实际上执行动作在执行空间中非常快速的来回交换,所以我们会有错觉每项任务都在同时进行,这里说个数字在100个毫秒内目前执行程序代码会切换到不同空间上的不同方法。

这里又有一个问题:Java在操作系统中也只是个在底层操作系统上执行的进程。一旦轮到Java执行时,Java虚拟机会执行什么?

  • 目前执行空间最上面的会执行。


书中的截图:描述线程执行空间

Java学习笔记 11、快速入门多线程(详细)_ide_02



二、线程的创建与使用

认识Thread类

Java的JVM允许程序运行多个线程,jdk也提供了相应的API,​​Thread​​类

​Thread​​类:

  • 我们想让一个线程做指定的事情是要通过某个特定​​Thread​​​对象的​​run()​​​方法来完成操作的,将​​run()​​方法的主体成为线程体。
  • 想让run()中的内容执行并不是直接调用run()方法,而是调用其​​Start()​​方法。
  • 为啥不调用run()方法呢?因为run()方法是我们进行重写的,若是直接调用run()方法来启动会当做普通方法调用的,那么就是主线程来执行了;调用start()方法启动线程,整个线程处于就绪状态,等待虚拟机调度,执行run方法,一旦run方法结束,此线程终止。

​Thread​​类的构造器

  • ​Thread()​​:创建新的Thread对象
  • ​Thread(String threadname)​​:创建线程并指定线程实例名
  • ​Thread(Runnable target)​​:指定创建线程的目标对象,它实现了Runnable接 口中的run方法
  • ​Thread(Runnable target, String name)​​:创建新的Thread对象

常用方法

  • ​void start()​​:启动线程,并执行对象的run()方法。
  • ​String getName()​​:返回线程的名称。
  • ​void setName()​​:设置该线程名称。
  • ​static native Thread currentThread()​​:静态⽅法,返回对当前正在执⾏的线程对象的引⽤。
  • ​static native void yield()​​:表示放弃的意思,表示当前线程愿意让出对当前处理器的占用,需要注意就算当前线程调用其方法,程序在调度时,也还是可能会执行该线程的。
  • ​static native void sleep(long millis)​​:指定millis毫秒数让当前线程进行睡眠,睡眠的状态是阻塞状态。
  • ​final void join()​​:在线程a中可以调用线程b的join()方法,此时线程a进入阻塞状态,知道线程b执行完之后,a才结束阻塞状态。内部调用的是Object的wait()方法。



两种创建线程方式

1、创建线程方式一:继承Thread

为什么要继承Thread类呢?我们想要让其他线程执行自己的事情那么就需要先继承Thread类并重写run()方法,这样我们创建自定义线程类实例,调用start()方法即可。

  • 这个run()方法其实是Thread类实现​​Runnable​​接口里的方法。

继承Thread方式

class MyThread extends Thread{
@Override
public void run() {
for (int i = 0;i<100;i++){
System.out.println(i);
}
}
}

public class Main {

//本身main方法是主线程
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();//①虚拟机中创建一个线程 ②调用线程的run()方法

//主线程下执行语句
for(int i=0;i<50;i++)
System.out.println("主线程中循环:"+i);
}

}

Java学习笔记 11、快速入门多线程(详细)_多线程_03

  • 可以看到主线程与自己创建的线程进行交互执行任务

注意点

  1. 在程序中两个线程在不断的切换执行,他们的打印顺序不会一样,run()方法由JVM调用,什么时候调用执行过程控制都有操作系统的CPU调度(CPU调度策略,主要执行进程的顺序)决定。
  2. 记住要使用run()方法需要调用start()方法,若是调用run(),那么就只是普通方法,并没有启动多线程。
  3. 一个线程对象只能调用一次start()方法启动,如果重复调用了,会抛异常​​IllegalThreadStateException​​。当调用了start()后,虚拟机jvm会为我们创建一个线程,然后等这个线程第一次得到时间片再调用run()方法。


还可以使用Thread的匿名实现类来创建线程

//本身main方法是主线程
public static void main(String[] args) {
//使用Java8的函数式编程
new Thread(()->{
System.out.println("Thread的匿名实现类");
}).start();

//普通重写方法
new Thread(){
@Override
public void run() {
System.out.println("匿名实现类");
}
}.start();
}



2、创建线程方式二:实现Runnable接口

通过实现Runnable接口方式来创建对象,实现该接口的类作为参数传递到构造器中:

class MyRunnable implements Runnable{

@Override
public void run() {
System.out.println("实现Runnable接口");
}
}

public class Main {
public static void main(String[] args) throws InterruptedException {
//方式一:通过实现接口类,并创建实例作为参数放置到Thread类中
Thread thread = new Thread(new MyRunnable());
thread.start();

//方式二:通过给Thread类传入匿名接口类
new Thread(new MyRunnable(){
@Override
public void run() {
System.out.println("匿名接口类runnable");
}
}).start();

}
}
  • 这里演示了两种使用实现Runnable接口方式来创建线程


源码解析:对于非继承方式实现Runnable接口在进行start()时进行调用作为参数中的run()方法

//构造器传入接口实现类,采用多态方式,调用了init()方法将target传入
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}

//init()方法
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
....
}

//当调用start()方法实际上也会调用run()方法,看一下实际上就是调用的target.run()方法也就是之前传入的target
@Override
public void run() {
if (target != null) {
target.run();
}
}



对于实现​​Runnable​​接口来创建多个线程方式

class MyRunnable implements Runnable{

@Override
public void run() {
System.out.println("实现Runnable接口");
}
}

public class Main {
public static void main(String[] args) throws InterruptedException {
MyRunnable runnable = new MyRunnable();
//若是要多线程调用同一个接口实现类的run()方法,就需要创建多个Thread实例
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
}
}



比较两种创建方式

对于线程中共享的属性声明介绍

  • 继承​​Thread​​类实现的,想要共享其中的属性,那么需要使用static来进行修饰
  • 实现​​runnable​​接口的,只要是new Thread(runnable)中的runnable实现类是同一个时,其中属性默认就是共享的。

那么哪个方式我们更常使用呢

开发中优先选择实现​​runnable​​接口的方式

  • 实现方式没有类的单继承局限性。
  • 实现方式更适合来处理多个线程有共享数据的情况。
  • 降低了线程对象和线程任务的耦合性。


常用方法

修改线程名

//方式一:使用setName(threadname)方法更改,需要创建实例之后
new Thread(){
...run()
}.setName("线程一");

//方式二:创建实例时,使用有参构造器修改线程名 Thread(String name)
new Thread("线程一")

//方式三:双参构造(实现runnable接口情况下)
new Thread(myRunnable, "线程1");

修改了线程名,我们总要输出线程名吧:

class MyThread extends Thread{
@Override
public void run() {
//方式一:在线程中直接调用getName()
System.out.println(this.getName());
}
}

public class Main {
public static void main(String[] args) {
new MyThread().start();
//方式二:通过Thread的静态方法currentThread()获取当前线程实例,调用方法即可
System.out.println(Thread.currentThread().getName());
}
}



yield()方法

class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if(i%4==0){
//使用该方法会交出当前cpu的占用,让其他线程执行
yield();
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}

public class Main {
public static void main(String[] args) {
new MyThread().start();

for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
  • 这里测试的是当i%4==0时让出占用的cpu资源,那么就会执行其他线程了

Java学习笔记 11、快速入门多线程(详细)_java_04

注意:使用yield()方法并不是每一次都有效的,也有可能继续执行当前线程,理解含义即可。



join()方法

class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}

public class Main {
public static void main(String[] args) {
MyThread mythread = new MyThread();
mythread.start();

for (int i = 0; i < 10; i++) {
if(i == 5){
try{
//此时主线程阻塞,开始执行b线程,b线程执行完以后,主线程阻塞结束
mythread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
  • 当i==5时,我们让主线程阻塞,来执行指定线程中内容,当指定线程执行完,主线程阻塞结束继续执行

Java学习笔记 11、快速入门多线程(详细)_ide_05



sleep()方法

class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
if(i%2 == 1){
try {
//让该线程睡眠(阻塞)1秒
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}

public class Main {
public static void main(String[] args) {
MyThread mythread = new MyThread();
mythread.start();

for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
  • 这里是当i%2==0时该线程进入睡眠(阻塞)1秒。

Java学习笔记 11、快速入门多线程(详细)_java_06

注意点:sleep()是Thread类中静态方法,在哪个线程中调用该方法哪个线程就会进入睡眠,就算你在a线程中调用b线程的sleep()也还是a线程进入睡眠。



线程优先级设置

介绍调度

CPU通过为每个线程分配CPU时间⽚来实现多线程机制,对于操作系统分配时间片给每个线程的涉及到线程的调度策略。

Java学习笔记 11、快速入门多线程(详细)_ide_07

  • 抢占式:高优先级的线程抢占CPU

针对于Java的调度方法

  • 同优先级线程组成先进先出队列(先到先服务),使用时间片策略
  • 对于高优先级,使用优先调度的抢占式策略


线程优先级

Java中线程的优先级等级如下,定义在​​Thread​​类中的静态属性:

  • ​MAX_PRIORITY​​:10
  • ​MIN _PRIORITY​​:1
  • ​NORM_PRIORITY​​:5

设置与获取当前优先级方法

  • ​setPriority(int newPriority)​​ :改变线程的优先级
  • ​getPriority()​​ :返回线程优先值
//优先值可以直接使用Thread类中的静态变量
myThread.setProority(Thread.MAX_PRIORITY);

对于优先级的说明

  1. 线程创建时继承父线程的优先级
  2. 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才调用。


线程的分类

Java中的线程分为两类:守护线程与用户线程

  • 用户线程:我们平常创建的普通线程。
  • 守护线程:用来服务于用户线程,不需要上层逻辑介入。当线程只剩下守护线程时,JVM就会退出,若还有其他用户线程在,JVM就不会退出。

这两种线程几乎是一样的,唯一的区别是判断JVM何时离开!

如何设置线程为守护线程呢

  • 在调用start()方法前,调用​​Thread.setDaemon(true)​​。就可以把一个用户线程变成一个守护线程。

java垃圾回收就是一个典型的守护线程,若是jvm中都是守护线程时,JVM就会退出。

守护线程应用场景

  1. 在任何情况下,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用,免去了有些时候主线程结束子线程还在执行的情况,当jvm只剩下守护进程时,JVM就会退出。
  2. 相对于用户线程,通常都是些关键的事务,这些操作不能中断所以就不能使用守护线程。


三、线程的生命周期

Thread.State中的六种状态

线程也具有生命周期,在JDK中使用​​Thread.State​​类来定义线程的几种状态,下图是六种状态:

Java学习笔记 11、快速入门多线程(详细)_多线程_08

  • ​NEW​​(初始):新创建的一个线程对象,还没有调用start()。
  • ​RUNNABLE​​(运行):Java线程中将就绪(ready)和运行中(running)两种状态笼统称为"运行"。若调用了satrt()方法,该状态线程位于可运行线程池中,等待被线程调度选中,获取CPU使用权限,此时处于就绪状态(ready)。就绪状态线程获得CPU时间片后变为运行中状态(running)。
  • ​BLOCKED​​(阻塞):线程处于阻塞状态,等待监视锁。
  • ​WAITING​​(等待):在该状态下的线程需要等待其他线程做出一些特定动作(通知或中断)。
  • ​TIMED_WAITING​​(超时等待):调用sleep()、join()、wait()方法可能导致线程处于等待状态,不同于WAITING,它可以在指定时间后自行返回。
  • ​TERMINATED​​(终止):表示线程执行完毕。


参考文章:Java线程的6种状态及切换(透彻讲解)


详细说明

  1. 初始状态(NEW)
  • 实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。
  1. 就绪状态(RUNNABLE之READY)
  1. 就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
  2. 调用线程的start()方法,此线程进入就绪状态。
  3. 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
  4. 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
  5. 锁池里的线程拿到对象锁后,进入就绪状态。
  1. 运行中状态(RUNNABLE之RUNNING)
  • 线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一的一种方式。
  1. 阻塞状态(BLOCKED)
  • 阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。
  1. 等待(WAITING)
  • 处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。
  1. 超时等待(TIMED_WAITING)
  • 处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。
  1. 终止状态(TERMINATED)
  • 当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
  • 在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。


生命周期中五种状态

Java语言使用​​Thread​​类及其实现类的对象来创建使用线程,完整的生命周期中通常要经历如下的五种状态

​新建—就绪—运行—阻塞—死亡​

  • 新建:当一个Thread类或其子类的对象被声明并被创建时,新生的线程对象处于新建状态。
  • 就绪:处于新建状态的线程被​​start()​​后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源。
  • 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,​​run()​​方法定义了线程的操作和功能。
  • 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态。
  • 死亡:线程完成了它的全部工作(run方法结束)或线程被提前强制性地中止或出现异常导致结束。

周期图如下

Java学习笔记 11、快速入门多线程(详细)_java_09

四、线程的同步

1、多窗口卖票(引出问题)

继承Thread与实现Runnable接口两种方式

多窗口卖票问题描述:我们在run()方法中模拟卖票的过程,一旦进入while第一条输出语句表示售出一张票,在多线程中若是进行就可能会出现下面的卖出重复票、错票的问题。


继承Thread的多窗口卖票问题


class MyThread extends Thread{

//继承Thread方式想要共享属性:设置为static
private static int ticket = 100;

@Override
public void run() {
while(true){
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+"的票数出售:"+ticket);
ticket--;
}else{
break;
}
}
}

public MyThread(String name) {
super(name);
}
}

public class Main {
public static void main(String[] args) throws InterruptedException {
MyThread thread1 = new MyThread("线程一");
MyThread thread2 = new MyThread("线程二");
thread1.start();
thread2.start();
}
}
  • 使用继承Thread方式创建线程想要共享属性就需要设置static静态,因为都是new的同一个类的对象。

Java学习笔记 11、快速入门多线程(详细)_多线程_10




实现Runnable接口的多窗口卖票问题


class MyRunnable implements Runnable{

//实现runnable接口方式想要共享属性:默认权限
private int ticket = 100;

@Override
public void run() {
while(true){
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+"的票数出售:"+ticket);
ticket--;
}else{
break;
}
}
}

}

public class Main {
public static void main(String[] args) throws InterruptedException {
MyRunnable runnable = new MyRunnable();
//使用相同的接口实现类runnable
Thread thread1 = new Thread(runnable, "线程一");
Thread thread2 = new Thread(runnable, "线程二");
thread1.start();
thread2.start();
}
}
  • 在实现runnable接口类中对于想要共享的属性不需要设置static,因为该类创建的实例都作为Thread构造器参数传入,使用的是一个runnable。

Java学习笔记 11、快速入门多线程(详细)_ide_11



问题描述以及解决方案

问题:在卖票过程中,出现了重票、错票,出现了线程的安全问题。当某个线程操作车票的过程中,尚未操作完成时,其他操作与此同时参与进来执行导致共享数据的错误。

解决办法:对于多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中其他线程不可以参与执行。



2、同步机制(解决线程安全问题)

同步机制介绍


同步机制


同步机制:Java对于线程安全问题也出同步机制,当线程调用一个方法时在没有得到结果之前,其他线程无法参与执行。

有两种方式解决

  • 同步代码块:​​synchronized(同步监视器){ ...操作共享数据 }​​,单独在方法中声明
  • 同步方法:​​public synchronized void show (String name){ ​​,直接声明在方法上



synchronized介绍


​synchronized​​锁是什么?

  1. 任意对象都可以作为同步锁,所有对象都自动含有单一的锁(监视器)。
  2. 同步代码块:同步监视器可以是单一的对象,很多时候可以将this或类.class作为锁。
  3. 同步方法的锁:可以看到只需要在方法权限修饰前加入synchronized即可,它对应的锁是根据它的方法状态,若其方法是static静态方法(​​类.class​​​);非静态方法(​​this​​)

synchronized充当角色如图

Java学习笔记 11、快速入门多线程(详细)_多线程_12

同步锁机制:对于并发工作,你需要某种方式来防 止两个任务访问相同的资源(其实就是共享资源竞争)。 防止这种冲突的方法 就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须 锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。 —《Thinking in Java》

注意

  1. 必须要确保使用同一个资源的多个线程共用一把锁,否则无法保证操纵共享资源的安全。
  2. 针对于同步方法中一个线程类中的所有静态方法都会共用同一把锁(​​类.class​​​),非静态方法都会使用​​this​​充当锁;同步代码块一定要谨慎。


方式一:同步代码块

语法

synchronized(同步监视器){ 
//需要被同步的代码
}
  • 共享数据:多个线程共同操作的变量,例如之前的ticket。
  • 被同步代码:操作共享数据的代码。
  • 同步监视器:俗称锁,可以是任意一个对象,都可以充当锁,多个线程必须共用一把锁。


解决之前两种方式进行多线程的线程安全问题

  1. 继承Thread方式
class MyThread extends Thread{

private static int ticket = 100;

//这里单独创建一个静态对象
private static Object object = new Object();

@Override
public void run() {
while(true){
//这里使用Object静态实例对象作为锁
synchronized (object){
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+"的票数出售:"+ticket);
ticket--;
}else{
break;
}
}
}
}

public MyThread(String name) {
super(name);
}
}
  • 这里的锁是使用的类中一个静态对象实例object,对于这种继承Thread方式进行多线程的话是要new多个​​MyThread​​类的,而Synchronized中的锁必须是指定单独一把锁,所以将该object作为锁。
  1. 实现runnable接口
//解决实现runnable接口的线程安全问题
class MyRunnable implements Runnable{

private int ticket = 100;

@Override
public void run() {
while(true){
//同步代码块:this本身指的是runnable的实现类MyRunnable
synchronized (this){
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+"的票数出售:"+ticket);
ticket--;
}else{
break;
}
}
}
}

}
  • 对于实现runnable方式的,我们可以直接使用​​this​​来充当锁,因为创建多个线程使用的是同一个Runnable。

注意:不管什么形式来创建多线程的,若是想要避免出现线程安全问题那么就要确保synchronized里的锁是一把锁也就是同一个对象。



方式二:同步方法

语法

//非静态方法:锁为this
public synchronized void show(){
}

//静态方法:锁为类.class
public synchronized static void show(){
}

解决之前两种方式进行多线程的线程安全问题

  1. 继承Thread方式
class MyThread extends Thread{

//继承Thread方式想要共享属性:设置为static
private static int ticket = 100;

@Override
public void run() {
while(true){
if(ticket<=0 || operate()){
break;
}
}
}

//同步方法:静态方法 锁为MyThread.Class 单独一份
public synchronized static boolean operate(){
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+"的票数出售:"+ticket);
ticket--;
return ticket==0;
}else{
return true;
}
}

public MyThread(String name) {
super(name);
}
}
  • 这里就需要使用静态方法了,此时锁为​​MyThread.class​​单独一份,若是非静态的话就是this,而下面创建多线程就是多个实例,就会出现安全问题。
  1. 实现Runnable接口方式
class MyRunnable implements Runnable{

private int ticket = 100;

@Override
public void run() {
while(true){
//一旦ticket<=0 马上退出
if(ticket<=0 || operate()){
break;
}
}
}

//非静态方法使用this来作为锁
public synchronized boolean operate(){
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+"的票数出售:"+ticket);
ticket--;
return ticket==0;
}else{
return true;
}
}

}
  • 这里将判断操作单独抽成一个同步方法,这里是非静态的,锁就是​​this​

注意:继承Thread方式中应使用静态方法来实现同步方法;实现runnable接口实现同步方法时,该方法是否为静态都可以,因为多个线程始终使用的是一个runnable,锁始终只有一份。



方式三:Lock锁


认识Lock锁


从JDK5.0开始,Java提供了更强大的线程同步机制—使用显示定义同步锁对象来实现同步,同步锁使用​​Lock​​对象充当。

该Lock接口是属于​​java.util.concurrent.locks​​包,它用来控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。

如何使用Lock接口呢

  • 我们使用其Lock接口实现类​​ReentrantLock​​类(重进入锁)。
  • ​ReentrantLock​​​类:它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是​​ReentrantLock​​​类,可以调​​lock()​​​方法显式加锁,调​​unlock()​​方法释放锁。



实际使用:解决线程同步


解决之前两种方式进行多线程的同步问题

  1. 解决继承​​Thread​​的同步问题
class MyThread extends Thread{

//继承Thread方式想要共享属性:设置为static
private static int ticket = 100;
//静态实例:使用Lock锁
private static Lock lock = new ReentrantLock();

@Override
public void run() {
while(true){

//手动上锁
lock.lock();

if(ticket > 0){
System.out.println(Thread.currentThread().getName()+"的票数出售:"+ticket);
ticket--;
}else{
lock.unlock();
break;
}

//手动解锁
lock.unlock();
}
}
}
  • 针对于继承Thread方式的lock锁在创建实例时定义为​​static​​。之后再在操作共享数据外显示上锁与解锁。
  1. 解决实现Runnable接口同步问题
class MyRunnable implements Runnable{

//实现runnable接口方式想要共享属性:默认权限
private int ticket = 100;
private Lock lock = new ReentrantLock();

@Override
public void run() {
while(true){
//上锁
lock.lock();
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+"的票数出售:"+ticket);
ticket--;
}else{
lock.unlock();
break;
}
//解锁
lock.unlock();
}
}
}
  • 针对于实现Runnable的情况,我们就创建一个普通实例对象即可。多个线程使用的是同一个Runnable实现类。

注意:如果同步代码有异常,要将​​unlock()​​写入finally语句块。



3、同步方法的好处及坏处

好处:解决了线程的安全问题。

坏处(局限性):在操作同步代码时,只能有一个线程参与,其他线程需要等待开锁之后才能进入,相当于是一个单线程的过程,效率低。



4、同步的范围及释放与不释放锁的操作


同步范围


对于寻找代码是否存在线程安全问题几个关键点

  • 明确哪些方法是多线程运行的代码。
  • 明确多个线程是否有共享数据。
  • 明确多线程代码中是否有多条语句操作共享数据。

解决策略:对于多条操作共享数据的语句,只能让一个线程执行完之后再让下个线程执行,即所有操作共享数据的这些语句都要放在同步范围中。对于同步范围的大小也要有个度,范围小太的话往往可能会没有锁住所有安全的问题;范围太大的话没发挥多线程的功能。




释放锁与不会释放锁的操作


释放锁的操作

  • 当前线程的同步方法或同步代码块执行结束会自动释放锁。
  • 当前线程在同步方法或同步代码块中遇到​​break​​​、​​return​​终止了该代码块、该方法的执行。
  • 当前线程在同步方法或同步代码块中出现了未处理的​​Error​​​或​​Exception​​,导致异常结束。
  • 当前线程在同步方法或同步代码块中执行了线程对象的​​wait()​​方法,当前线程暂停,并释放锁。

不会释放锁的操作

  • 线程执行同步方法或同步代码块时,程序调用​​Thread.sleep()​​​、​​Thread.yield()​​方法暂停当前线程的执行。
  • 其他线程调用了该线程的​​suspend()​​方法将该线程挂起,该线程不会释放锁(同步监视器)
  • ​suspent()​​​与​​resume()​​方法已弃用。


5、小练习

问题描述:银行有一个账户。 有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打 印账户余额。

程序如下

class Account{
private double wallet;

//将该方法设置为同步方法,锁为this指的是该Account类,这里是可以的
public synchronized void deposit(double money){
if(wallet>=0){
wallet += money;
System.out.println(Thread.currentThread().getName()+"向账户存储了"+money+"元,账户余额为:"+wallet);
}
}
}

class Customer extends Thread{

private Account account;

public Customer(Account account,String name) {
super(name);
this.account = account;
}

@Override
public void run() {
for (int i = 0; i < 3; i++) {
account.deposit(1000);
}
}
}

public class Main {
public static void main(String[] args) throws InterruptedException {
Account account = new Account();
new Customer(account,"用户A").start();
new Customer(account,"用户B").start();
}
}
  • 这里直接将Account操控数据的方法设置为同步方法,不需要使用static,因为多个线程共同操作一个Account

Java学习笔记 11、快速入门多线程(详细)_多线程_13



五、线程死锁问题

1、介绍死锁问题及实例情况

死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

  • Java语言通过synchronized关键字来保证原子性,其中每一个Object都有一个隐含锁,这个也称为监视器对象,在进入到synchronized之前自动获取此内部锁,一旦离开此方式会自动释放锁。

使用1个例子来描述死锁如何形成

public class Main {
public static void main(String[] args) throws InterruptedException {
StringBuffer buffer1 = new StringBuffer();
StringBuffer buffer2 = new StringBuffer();

new Thread(){
@Override
public void run() {
synchronized (buffer1){
buffer1.append("A");
buffer2.append("A");

//睡眠2秒
sleep2Sec();

synchronized (buffer2){
buffer1.append("B");
buffer2.append("B");
}
}
System.out.println("线程1中:buffer1="+buffer1);
System.out.println("线程1中:buffer2 = "+buffer2);
}
}.start();

new Thread(){
@Override
public void run() {
synchronized (buffer2){
buffer1.append("C");
buffer2.append("C");

//睡眠2秒
sleep2Sec();

synchronized (buffer1){
buffer1.append("D");
buffer2.append("D");
}
}
System.out.println("线程2中:buffer1="+buffer1);
System.out.print("线程2中:buffer2 = "+buffer2);
}
}.start();
}

//延时2秒
public static void sleep2Sec(){
//增加延时
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
  • 注意看两个线程中的run()方法里有两个synchronized同步代码块,第一个线程run()中第一个同步代码块将buffer1作为锁,第二个线程的第一个同步代码块将buffer2对象作为锁,两个同步代码块的内部同步代码块也是各自设置buffer1或buffer2为锁。
  • 为了造出死锁的情况,在两个线程都进入到第一个同步代码块时都使用​​sleep()​​来让线程阻塞一会(不会释放锁),待两个线程都同时进入到第一个同步代码块中,第一个线程拿到buffer1内部锁,第二个线程拿到buffer2内部锁,一旦sleep()结束阻塞,那么就会出现死锁状况,各个都在等待对方资源被释放。

Java学习笔记 11、快速入门多线程(详细)_ide_14

即不断的在阻塞中…

这个例子是因为多线程访问共享资源由于访问顺序原因所造成阻塞情况,一个线程锁住资源A,由想去锁住资源B;在另一个线程中先锁住B,又想锁住A来完成操作,一旦两个线程同时先后锁住A与B时,就会造成两个线程都在等待情况,程序进入阻塞。



2、解决与避免死锁

通过专门的算法,尽量避免同不资源定义以及避免嵌套同步。

这里介绍三个技术避免死锁问题

  1. 加锁顺序(线程按照一定的顺序上锁)
  2. 加锁时限(线程尝试获取锁时加上一定时限,超过时限则放弃对该锁请求,并释放自己所占有锁)
  3. 死锁检测


方式一:加锁顺序


//第一个线程
synchronized (buffer1){
....

synchronized (buffer2){
.....
}
//第二个线程
synchronized (buffer1){
....

synchronized (buffer2){
.....
}

说明:对上锁的顺序作适当排序,这样就不会进入到死锁情况,因为无论哪个线程先进入,另一个线程会一直等待锁的释放,直到第一个使用该锁的释放再进行。




方式二:加锁时限


介绍:就是在获取锁的时候加一个超时时间,一旦超过了这个时限则会放弃该锁的请求,并释放自己所占用的锁。

在Java中不能对synchronized同步块设置超时时间。你需要创建一个自定义锁,或使用​​java.util.concurrent​​包下的工具



六、线程的通信

1、认识线程通信


引出线程通信


为什么要线程通信

  • 多个线程并发执行时默认是根据CPU调度策略随机分发时间片,对于任务的执行其实是随机的,当我们需要多线程来共同完成一件事,并且希望它能够有规律的执行,那么就需要一些协同通信,来达到多线程共同操纵一份数据。
  • 多线程中若是我们不使用线程通信的方式也是可以实现共同完成一件事,但是在很大程度上多线程会对共享变量进行争夺造成损失,所以引出线程通信,目的是能让多线程之间的通信避免同一共享资源的争夺

什么是线程通信

  • 多个线程在处理同一个共享变量,且任务不同时需要线程通信来解决对一个变量使用与操作的随机性,使其变得有规律,具有可控性,避免对同一共享变量进行争夺。
  • 想要实现线程通信这里就引出等待唤醒机制,如​​wait()​​​、​​notify()​​​、​​notifyAll()​​方法。



认识三个方法,三个方法都是Object对象提供。

Object声明三个方法原因:这三个方法必须由锁对象调用,而任何对象都可以作为synchronized的同步锁。


前提:三个方法只有在​​synchronized方法​​​或​​synchronized代码块​​​中才能使用,否则会报​​IllegalMonitorStateException​​异常(如果当前线程不是此对象的监视器所有者)。

​wait()​​:让当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,此时当前线程会进行排队等候其他线程调用notify()与notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能够执行。

  • 简述:该线程暂停等待,释放此监视器的所有权(指锁),等待指定方法唤醒重新获得监视器所有权,从断点处开始执行。

​notify()​​:唤醒正在排队等待同步资源的线程中优先级最高者结束等待。

  • 简述:唤醒正在等待对象监视器的单个线程,如之前使用wait()方法的线程,需要注意不能唤醒sleep()的线程。

​notifyAll()​​:唤醒正在排队等待资源的所有线程结束等待。

  • 简述:唤醒所有等待对象监视器的线程,如通过调用wait()方法之一等待对象的监视器,非sleep()方法。

注意

  1. 这几个方法配合使用需要使用同一个对象来进行调用。
  2. 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)。


2、线程通信小例子(交替打印1-100)

不使用wait()、notify()实现线程通信(不推荐)

这里仅使用继承​​Thread​​方式来实现线程通信:

class MyThread extends Thread{

private static int i;

@Override
public void run() {
while(true){
synchronized (MyThread.class){
//指定只有偶数情况且当前线程为线程一时才执行
if(i<100 && i%2 == 0){
if(Thread.currentThread().getName() == "线程一"){
i++;
System.out.println(Thread.currentThread().getName()+":"+i);
}
}else if(i<100 && i%2 == 1){//指定只有奇数情况且当前线程为线程二时才执行
if(Thread.currentThread().getName() == "线程二"){
i++;
System.out.println(Thread.currentThread().getName()+":"+i);
}
}

if(i>=100){
break;
}
}

}
}
}

public class Main {
public static void main(String[] args) throws InterruptedException {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
thread1.setName("线程一");
thread2.setName("线程二");
thread1.start();
thread2.start();
}
}
  • 通过双重判断来达到线程交替打印,不过这种方式会有大量无效情况以及可能会出现问题,更消耗资源。

Java学习笔记 11、快速入门多线程(详细)_java_15



使用wait()、notify()实现线程通信(推荐)

【1】实现Runnable接口方式

class MyRunnable implements Runnable{

private int i;

@Override
public void run() {
while(true){
//同步代码块
synchronized (this){
if(i<100){
//进行唤醒排队等待监视器的线程,此时继续向下执行相应操作
this.notify();
i++;
System.out.println(Thread.currentThread().getName()+":"+i);

//进入等待
try {
if(i<100) //加一个判断防止最后出现阻塞情况
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
}

public class Main {
public static void main(String[] args) throws InterruptedException {
MyRunnable runnable = new MyRunnable();
Thread thread1 = new Thread(runnable, "线程一");
Thread thread2 = new Thread(runnable, "线程二");
thread1.start();
thread2.start();
}
}
  • 这里因为是实现runnable接口,在创建多个线程时使用的是同一个Runnable实现类,所以我们可以直接使用该对象作为监视器。

Java学习笔记 11、快速入门多线程(详细)_java_16



【2】继承Thread方式

class MyThread extends Thread{

private static int i;

@Override
public void run() {
while(true){
//将MyThread.class作为锁,只有一个类
synchronized (MyThread.class){

//作为锁的类调用唤醒方法
MyThread.class.notify();

if(i<100){
i++;
System.out.println(Thread.currentThread().getName()+":"+i);

try {
if(i<100)
MyThread.class.wait();//释放监视器并进行等待
} catch (InterruptedException e) {
e.printStackTrace();
}

}else{
break;
}
}

}
}
}

public class Main {
public static void main(String[] args) throws InterruptedException {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
thread1.setName("线程一");
thread2.setName("线程二");
thread1.start();
thread2.start();
}
}
  • 对于继承Thread来实现多线程的,共享变量为static,这里锁为自定义类的class类

Java学习笔记 11、快速入门多线程(详细)_ide_17



3、经典例题(生产者与消费者)


介绍操作系统中的生产者与消费者


操作系统中的问题:系统中有一组生产者进行与一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区中取出一个产品并使用(这里"产品"理解为某种数据)。

①生产者、消费者共享一个初始为空、大小为n的缓冲区。

②只有缓冲区没满时,生产者才能把产品放入缓冲区中,否则必须等待;

③只有缓冲区不空时,消费者才能从中取出产品,否则必须等待。

④缓冲区是临界资源,葛金城必须互斥地访问。




模拟生产者与消费者案例


案例描述:生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处 取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。

出现问题描述

  1. 生产者比消费者快时,消费者会漏掉一些数据没有取到。
  2. 消费者比生产者快时,消费者会取相同的数据。

程序如下:

//店员类:有生产产品与消费产品功能
class Clerk{

//设置初始产品为0,最高产品数量为20
private int product;

//生产产品
public synchronized void addProduct(){
//产品数量够了
if(product>=20){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
product++;//生产产品
System.out.println("产品+1,当前产品数量为"+product);
notify();//唤醒操作说明生产了新的产品了
}

}

//消费产品
public synchronized void consumeProduct(){
//如果产品为0了,那么无法进行消费
if(product<=0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
product--;
System.out.println("产品-1,当前产品数量为"+product);
notify();
}
}

}

//生产者
class Product extends Thread{

private Clerk clerk;

public Product(Clerk clerk) {
this.clerk = clerk;
}

@Override
public void run() {
//不断进行生产操作
while(true){

//为了让效果更加明显这里对线程使用延时
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

clerk.addProduct();
}
}
}


//消费者
class Consumer extends Thread{

private Clerk clerk;

public Consumer(Clerk clerk) {
this.clerk = clerk;
}

@Override
public void run() {
//不断进行消费操作
while(true){
clerk.consumeProduct();
}
}
}

public class Main {
public static void main(String[] args) throws InterruptedException {
Clerk clerk = new Clerk();
Product proThread = new Product(clerk);
Consumer conThread = new Consumer(clerk);
proThread.start();
conThread.start();
}
}

程序分析

​Clerk​​店员类来负责生产产品与消费产品的进行

  • 生产方法:一旦产品数量>=20,则进入阻塞状态(表示已满无法生产);若<=20就进行生产,并唤醒使用wait()等待的消费者(通知它我生产出产品了你可以进行消费了)。
  • 消费方法:一旦产品数量<=0,则进入阻塞状态(表示无产品暂时无法消费);若>0则进行消费,并唤醒使用wait()等待的生产者(告知它我已经消费产品了,快去生产)。

​Product​​​作为生产者线程,​​Consumer​​作为消费者线程。

  • 这里给生产者线程加了sleep()方法,生产速递慢,消费速度快,体现的更加明显。

Java学习笔记 11、快速入门多线程(详细)_java_18



七、JDK5.0新增线程创建方式

方式一:实现Callable接口


介绍Callable接口


​Callable​​接口:与使用Runnable相比, 其功能更加强大

  • 相比run()方法,其实现的callable接口方法​​call()​​中可以有返回值
  • 方法可抛出异常。
  • 支持泛型的返回值。
  • 需要借助​​FutureTask​​类的get()方法获取call()的返回值。

Java学习笔记 11、快速入门多线程(详细)_java_19




案例演示


案例描述:使用多线程获取到0-99和的值

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

class MyCallable implements Callable<Integer>{

@Override
public Integer call() throws Exception {
int sum = 0;
for(int i = 0;i<100;i++){
sum+=i;
}
return sum;
}
}

public class Main {
public static void main(String[] args) throws InterruptedException {
MyCallable callable = new MyCallable();
//将MyCallable创建的实例放置到FutureTask的有参构造器中,futureTask实现了Run()方法,其中就调用了callable的call()方法
FutureTask<Integer> futureTask = new FutureTask<>(callable);
//启动线程
new Thread(futureTask).start();

try {
//通过FutureTask类的get()方法调用获取call()方法的返回值
Integer sum = futureTask.get();
System.out.println(sum);
} catch (ExecutionException e) {
e.printStackTrace();
}

}
}
  • FutureTask实现了​​RunnableFuture​​​接口,并且该接口又多继承了​​Runnable​​​,​​Future<V> ​​这两个接口。
  • Future接口的get()方法:能够获取到实现​​Callable​​​的​​call()​​方法的返回值。
  • 为什么要将​​MyCallable​​​实例放到​​FutureTask​​中?
  • ​FutureTask​​​中实现了Runnable接口,其中也包含run()方法,run()方法里调用了实现Callable实例类中的​​call()​​​方法,并且在run()过程中获取到了call()的返回值,使用其中的​​set()​​​方法赋值到自己类中属性里。所以我们下面也就可以看到使用其类的​​get()​​方法获取到了call()的返回值。
  • 为什么将​​FutureTask​​实例放到Thread中?
  • 之前也说到了​​FutureTask​​​实现了runnable接口,符合Thread类中的一个有参构造,一旦调用start()就会执行​​FutureTask​​的run()方法。

Java学习笔记 11、快速入门多线程(详细)_java_20




源码分析一波


首先看一下​​Callable​​接口类:

//函数式接口,允许使用Lambda表达式
@FunctionalInterface
public interface Callable<V> {

//支持自定义泛型返回值,可以抛出异常
V call() throws Exception;
}

接着看​​RunnableFuture​​类:见下面1.1

//只列举Future<V>接口,runnable接口中只有一个run()抽象方法这里不展开
public interface Future<V> {
...
V get() throws InterruptedException, ExecutionException;
}

//1.2 RunnableFuture接口 多继承Runnable接口以及Future接口
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}

//1.1实现RunnableFuture接口 (1.2见RunnableFuture接口)
public class FutureTask<V> implements RunnableFuture<V> {
//使用outcome来接收call()方法返回值
private Object outcome;

//有参构造器,使用Callable多态
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}

//看一下run()方法,之后Thread类中使用start()方法会调用该run()方法
public void run() {
...
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
//这里调用了之前有参构造器中传入的Callable接口实现类的call()方法,使用V来接收返回值
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
//调用set()方法将返回值赋值到
set(result);
}
....
}

//set()方法:本身类自己实现
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v;//赋值操作
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
finishCompletion();
}
}

//通过调用get()方法获取到返回值:实现Future接口
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
//调用方法返回
return report(s);
}

//该方法用于返回outcome的值也就是调用call()的返回值
private V report(int s) throws ExecutionException {
Object x = outcome;
if (s == NORMAL)
return (V)x;
if (s >= CANCELLED)
throw new CancellationException();
throw new ExecutionException((Throwable)x);
}
}
  • 关注一下其中的run()方法以及get()方法,简单来说该类中run()方法实际上就是调用了实现Callable类的call()方法,get()方法获取到了call()方法的返回值,具体内容见上。

最后看一下​​Thread​​的构造器:

public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
  • 由于​​RunnableFuture​​也实现了Runnable接口,所以能够传入到Thread的构造器中。

上面只是粗略看了一下源码找出了关键信息,对于具体内容实现并没有太过深入了解,仅大概方法调用也有了些思路。



方式二:使用线程池

认识线程池的相关API


线程池背景及好处


背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程, 对性能影响很大,我们可以使用现成的线程池。

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。

好处

  1. 提高响应速度(减少了创建新线程的时间)。
  2. 降低资源消耗(重复利用线程池中线程,不需要每次都创建)。
  3. 便于线程管理,例如:​​corePoolSize​​​:核心池的大小​​maximumPoolSize​​​:最大线程数​​keepAliveTime​​:线程没有任务时最多保持多长时间后会终止。这些都可直接设置。



认识了解线程池相关API


同样是​​JDK5.0​​​,提供了线程池的相关的API:​​ExecutorService​​​ 和​​ Executors​

​ExecutorService​​:真正的线程池接口。常见子类​​ThreadPoolExecutor ​

  • ​void execute(Runnable command)​​ :执行任务/命令,没有返回值,一般用来执行 Runnable。
  • ​Future submit(Callable task)​​:执行任务,有返回值,一般又来执行 Callable。
  • ​void shutdown() ​​:关闭连接池。

​Executors​​:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

  • ​ExecutorService Executors.newCachedThreadPool()​​:创建一个可根据需要创建新线程的线程池
  • ​ExecutorService Executors.newFixedThreadPool(n)​​: 创建一个可重用固定线程数的线程池
  • ​ExecutorService Executors.newSingleThreadExecutor() ​​:创建一个只有一个线程的线程池
  • ​ScheduledExecutorService Executors.newScheduledThreadPool(n)​​:创建一个线程池,它可安排在给定延迟后运 行命令或者定期地执行。

说明:我们主要使用​​Executors​​​工具类来获取到线程池,上面列举到的前三个实际上返回的是​​ThreadPoolExecutor​​这个实现类,ExecutorService是该实现类实现的接口。

我们想要执行我们自定义的线程任务就可以使用上面​​ExecutorService​​列举到的方法。

Java学习笔记 11、快速入门多线程(详细)_java_21



实例:使用线程池创建10个线程来执行指定方法

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

class MyRunnable implements Runnable{

@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}

public class Main {
public static void main(String[] args) {
//1、使用工具类创建10个线程
ExecutorService pool = Executors.newFixedThreadPool(10);
//2、在将来某个时候执行给定的任务,这里submit()方法需要提供Runnable接口实现类
pool.submit(new MyRunnable());
//3、启动有序关闭,其中先前提交的任务将被执行,但不会接受任何新任务。
pool.shutdown();

}
}

Java学习笔记 11、快速入门多线程(详细)_多线程_22


如何使用线程池的属性?

首先列举三个属性

  • ​corePoolSize​​:核心池的大小
  • ​maximumPoolSize​​:最大线程数
  • ​keepAliveTime​​:线程没有任务时最多保持多长时间后会终止

Java学习笔记 11、快速入门多线程(详细)_ide_23

  • 我们需要向下转型为ThreadPoolService才能调用指定方法


查看源码


首先看​​Executors.newFixedThreadPool(10)​​方法

//这里实际上使用了多态,ExecutorService是ThreadPoolExecutor的接口
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

再其ExecutorService接口中并没有设置线程池属性的方法:

Java学习笔记 11、快速入门多线程(详细)_多线程_24

所以我们需要向下转型,这是允许的,因为在本身返回的时候就是返回的​​ThreadPoolExecutor​​实例对象。


相关面试题

1、synchronized与Lock 的对比

相同点:二者都可以解决线程安全问题

不同点

  1. ​synchronized​​机制再执行完相应的同步代码以后,会自动的释放同步监视器。
  2. ​Lock​​​锁需要手动上锁以及解锁,结束同步需要手动调用​​unlock()​​方法。
  3. 使用​​Lock​​​锁,​​JVM​​将会花费较少的时间来调度线程,性能会更好,并且具有更好的扩展性(提供了更多的子类)。

优先使用顺序:​​Lock​​锁 > 同步代码块(进入方法体分配了相应资源) -> 同步方法



2、sleep()与wait()方法异同点

相同点:这两个方法都能够让线程进入到阻塞状态。

不同点

  1. 两个方法声明不同,sleep()方法声明在Thread类中,wait()方法声明在Object类中。
  2. 调用位置不同,sleep()方法在任何需要的场景下都可以使用,而wait()方法只能在同步代码块或同步方法中使用。
  3. 关于是否释放监视器,sleep()不会释放锁,wait()会释放锁。


参考文章

[1]. 书籍《head first java 2.0》

[2]. ​​尚硅谷-Java30天-多线程篇(宋红康主讲)​​

[3]. 多线程:创建Thread为什么要调用start启动,而不能直接调用run方法

[4]. ​​什么是CPU调度?​​

[5]. 主线程调用子线程对象的 sleep() 方法,会造成子线程睡眠吗?

[6]. Java守护线程的理解和使用场景

[7]. ​​什么是守护线程?​​

[8]. Java线程的6种状态及切换(透彻讲解)

[9]. Java多线程:死锁

[10]. 线程通信的例子:使用线程交替打印1-100

[11]. 操作系统——生产者消费者问题




我是长路,感谢你的阅读,如有问题请指出,我会听取建议并进行修正。
欢迎关注我的公众号:长路Java,其中会包含软件安装等其他一些资料,包含一些视频教程以及学习路径分享。
学习讨论qq群:891507813 我们可以一起探讨学习
注明:转载可,需要附带上文章链接




举报

相关推荐

0 条评论