Java基础 多线程知识点(超详细且带有案例,特别容易理解版)

多线程

多线程技术概述

线程与进程
进程:
	是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间。
线程:
	是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行,一个进程最少有一个线程。
	线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程。
线程调度
分时调度
	所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间。
	
抢占式调度
	优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),java使用的为抢占式调度。
	CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核心而言,某个时刻,只能执行一个线程,而CPU在多个线程间切换速度相对我们的感觉要快,看上去就是 在同一时刻运行,其实,多线程程序(基于一个CPU)并不能提高程序的运行速度,但能够提高程序的运行效率,让CPU使用率更高。
	

扩展:数据库服务器,上千线程使用数据库,数据库只有8个CPU,同时开启1k个连接,让1k个人操作他,8个CPU交替执行效率快,还是1k个人排队用8个CPU快?
答:1k人排队用8个CPU效率更高,(A完成B开始,B完成C开始),缺少了切换的时间。
同步与异步
之前谈的线程安全是同步,线程不安全是异步。
同步:排队执行,效率低但是安全
异步:同时执行,效率高但是数据不安全
并发与并行
并发:指两个or多个事件在 同一个时间段内 发生
并行:指两个or多个事件在 同一时刻 发生(同时发生)

继承Thread

Thread是Java提供的线程类,继承了该类的就是一个线程类。

主线程和分支线程是并发执行的,谁在前谁在后  是随机的  抢占式分配

编写一个类extends Thread,重写线程类的run方法,触发方式是调用Thread对象的start()方法启动任务

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

由一个线程执行的方法,那么这个方法也会执行在这个线程里面。
每个线程都有自己的栈空间,共用一份堆内存。

实现Runnable

编写一个类implements Runnable接口,并实现run()方法

实现Runnable与继承Thread相比有如下优势:
1.通过创建任务,然后给线程分配的方式来实现的多线程,更适合多个线程同时执行相同任务的情况。即1个任务多个线程执行,很方便。
2.可以避免单继承所带来的局限性。(最有优势的点)。Java中extends只能继承一个类,单继承,implements是实现接口,可以多实现,并且还可以extends一个类。
3.任务与线程本身是分离的,提高了程序的健壮性。
4.后续学习的线程池技术(管理的是任务而不是线程),只接受Runnable类型的任务,不接受Thread类型的线程。

后续学习中用实现Runnable接口用的更多,但继承Thread类也有好处,比如下图的匿名内部类,不需要再创建其他的类文件,简洁。

Thread类

构造方法:
Thread(Runnable target) 分配新的 Thread对象。  
Thread(Runnable target, String name) 分配新的 Thread对象。不仅传一个任务,也可以传任务name。


常用方法:
getName()   返回此线程名称
getId()    返回此Thread的标识符
getPriority()    返回此线程的优先级
getPriority(int newPriority)    更改此线程的优先级,控制线程抢到时间片的几率  可以传入静态修饰的常量  MAX_PRIORITY(线程可以拥有的最大优先级)  MIN_PRIORITY(线程可以拥有的最低优先级)   NORM_PRIORITY(分配给线程的默认优先级)
start()    此线程开始执行,Java虚拟机调用此线程的run方法。
stop()    已过时,这种方式本质上不安全。比如 正在IO,停了,正在使用某些资源无法释放,资源依然被占用。
那么怎么停止线程这一单独执行路径呢?可以设置变量作为标记,线程一直监听该变量的值,一旦变了,通知线程,线程自杀,run方法return 。
sleep(long millis)    导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数。
sleep(long millis,int nanos)    导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数加上指定的纳秒数。
sleep很常见,比如想每个1秒输出一个数字,则sleep(1000)
setDaemon(boolean on)    将此线程标记为守护线程(当所有用户线程全部死亡后,守护线程自动死亡,它依附于用户线程)or用户线程(主线程和子线程都称为用户线程,所有用户线程必须全部死亡,程序才会结束)
设置和获取线程名称
Thread类有一个static方法currentThread()获取当前线程,所以可以通过 类名.方法名 调用该方法。
getName()方法返回线程名称
构造时不传name也有默认name——Thread-0、Thread-1...

也可以new出来接收一下,设置线程name,再start方法调用run

线程休眠
Thread类的static方法sleep,也是直接通过 类.方法名 方式调用。
下图运行时会每隔一秒输出一个数字

线程阻塞

不只指的是线程休眠了 睡了,线程是一条执行路径,比如一个线程执行路径代码100行,可能有10行是读取某个文件,这个文件读取耗时1s,那么这1秒也是阻塞的,它停在那读文件,后面读完才会继续执行。
可以把线程阻塞简单理解为所有比较耗时间的操作,比如常见的文件读取,会导致线程等待在那个位置,代码不会继续执行,除非文件读完。
再比如说等待用户输入,用户不输入,代码就会等在那不会继续执行,线程阻塞 又称为耗时操作。

线程中断

一个线程是一个独立的执行路径,它是否应该结束,应该由其自身决定。
一个线程创建到死亡有他特定的任务,中间会占用很多资源并释放,如果由外界强制杀死该线程,极有可能导致资源没有来得及释放,从而一直占用,产生内存垃圾,不能被回收。也可能会占用硬件资源,硬件资源得不到释放,其他软件没有办法再继续使用。
所以stop方法已过时。用了就抛异常。线程对象可以给它打标记,可以简单认为它有某个属性,调用这个方法就给这个属性设置上值了,就算打上标记了。线程在某些特殊状态时会看这个标记,如果发现有你想要他中断的标记,它就会触发一个异常,程序员编写try catch,决定如何处理该线程。
在需要杀死线程的代码块处调用 线程对象.interrupt()
调用该方法会告知Thread该死亡了,Thread之后怎么办还是看Thread try到中断异常后如何处理的代码块。
就会进入该线程run方法的try catch中断异常,然后怎么做是看程序员在catch异常后的catch块里写的什么操作,一般这个位置拿来释放资源(交代线程后事),然后return,return即正常结束了run方法,Thread正常自杀。

守护线程

线程:分为守护线程和用户线程
用户线程:当一个进程不包含任何的存活的用户线程时,进程结束。
守护线程:守护用户线程的,当最后一个用户线程结束时,所有守护线程自动死亡。

直接创建的线程都是用户线程,若想设置守护线程需要创建Thread对象之后,start之前调用setDaemon方法,实参为true。
主线程输出到5,main方法还要结束,此时子线程又输出了一个6,这是在上一个中断线程的源码的基础上加了一个设置守护线程的方法。

线程安全问题

线程不安全
举个例子,不能一直空说大汉吃一碗面
卖票,实现Runnable的任务类,类的方法就是对当前票数--,sleep是故意为了让它出线程不安全的问题,演示线程不安全的事,启动三个Thread,共同执行卖票这个任务。

分析结果:单看逻辑,怎么都不会出现负数。出现负数是因为有可能A线程发现count为1(极端情况)进入while循环,还没进行count--操作,其他两个线程插足(抢到时间片)也通过count>0进入线程,所以后面进行count--,出现了-1   -2

线程安全
首先明确线程不安全的原因:
	多个线程同时执行,去同时操作一个数据,导致某个数据看到的和在自己使用时数据不一致,因为其看到的和使用的中间间隔代码执行时被其他线程插足了(其他线程进来把数据改了),所以最终导致运行不符合预期。
	
很简单,就让某个线程在执行 看到的和使用时的数据 之间的代码块时,其他线程不插足,排队执行。
大汉排队吃锅里的食物,每个人看到的和吃到嘴里的 不会有差异。

关于线程同步,有三种不同的关于让线程排队执行的方式,都是通过上锁,加锁的方式。
线程安全1-同步代码块
可以简单认为,被括住的代码块是排队执行的。
格式
	synchronized(锁对象){
	
	}
	
Java中任何对象都可以作为锁对象存在,可以认为任何对象都可以打锁标记。打锁标记我们不用管,是内存底层的机制。线程会观察锁对象是否打了锁的标记,如果被打了,说明有人正在执行,他就会等,一直等,一直看标记是不是解锁了。某个线程执行完毕后,解锁,其他人抢,抢到就上锁。一般回首掏比较nb,见下图的运行结果,大部分是Thread-0抢到了。
注意:100个线程要上同一把锁才能实现排队的效果。

下图就是在卖票的例子上加了一个private Object o = new Object()和synchronized  就实现了线程安全。 
卖票的例子   就new了一个Ticket对象(任务),所以线程用的是一个锁对象o。

所有线程要共用一把锁,线程.start()方法会调用run方法,如果在run方法里new 锁对象,则成了每个线程都有自己独立的锁,就没有线程安全的说法了。如下图,还是会出现-1  -2

线程安全2-同步方法
同步代码甚至可以以一行代码为单位进行加锁,但同步方法以方法为单位进行加锁。
格式
	给方法添加synchronized修饰词

这里的锁就是任务Ticket对象,一个任务多个线程调用,才会安全。
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();

同步方法用的是一个this这把锁,要是有多个同步方法,or有个同步代码是传的this为锁对象,则若有一个同步方法在执行,其他同步方法都执行不了(Thread-0上锁之后,其他Thread都不能执行别的同步方法),都是排队的,因为上的是同一把锁。

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

线程安全3-显式锁Lock
同步代码块和同步方法都属于隐式锁,就是具体是怎么锁的我们不用管,只需把格式写好,它就自己锁,自己开,这就叫隐式锁。
显式锁就是自己创建锁对象,自己上锁,自己解锁。

在Ticket类里定义一个属性显式锁l
private lock l = new ReentrantLock();
在需要上锁的地方 l.lock()
在需要解锁的地方 l.unlock()

因为是new一个任务(Ticket)对象 所以用的是同一把显式锁l

显式锁比隐式锁更好一些,更能体现程序员在控制 锁的概念

while true里的代码如下,就是法1同步代码块的地方

显式锁与隐式锁有什么区别?可自己整理,面试可能会用得到。

公平锁与非公平锁

公平锁
	排队 先来先到,A线程先来等,那么解锁之后A线程先执行并上锁
	
非公平锁
	一旦解锁,大家一起抢
	
Java里默认的,上述三种线程安全方法都是非公平锁。

显式锁l里,fair参数为true,表示是公平锁,构造方法的实参:
private lock l = new ReentrantLock(true)

线程死锁

死锁  互相等待的一种僵局

避免的话  在开发时,在任何有锁的方法(点播里的是给方法加synchronized修饰词)里尽量不要编写另一个带锁的方法

多线程通信问题

生产者与消费者问题(经典)
食物类,为了让他在做菜时更容易出问题,设置name和taste之间sleep一会,执行设置name后,时间片被占,再次抢占到时间片时回来执行设置taste

厨师   声明为线程,就做饭。

服务员,因为厨师的run方法里的set方法sleep了100ms,所以这里也sleep一下。

main方法
代码里写的是老干妈小米粥香辣味,运行结果是甜辣味。就说明出问题了,厨师刚生产设置了name,sleep 的时候,服务员线程把菜端走了,所以taste没有设置,还是之前的甜辣味。
出现了两个线程进行协作时不协调的问题。

对于这个问题,多数人认为可以给食物类的get set上锁,给方法加synchronized修饰词,都是用的this锁,所以厨师在做菜的时候,服务员不能端走,但厨师会一直回手掏,会出更大的问题,所以不能解决上述问题。

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

解决方案:
	厨师做饭时,服务员睡着,厨师做完饭,叫醒服务员,服务员去上菜然后厨师睡着,服务员上完菜把盘子端回来再把厨师叫醒,服务员睡着...

可以在上锁后(synchronized)再加以设置标记实现。
给Food类加一个private boolean flag = true
假设true表示可以生产
flag判完设置为FALSE,确保不会连续执行第二次,一次做饭,一次端盘子,notifyAll是唤醒当前处于等待的所有线程,然后自己等待。

运行结果   没有任何问题了

线程的六种状态

  • New

    刚被创建 尚未启动的线程处于此状态

  • Runnable

    Java虚拟机中执行的线程处于此状态

  • Blocked

    被阻塞等待监视器锁定的线程处于此状态

    线程安全里提过上锁的概念,多个线程从一起执行到排队执行,当处于排队的时候就处于Blocked状态,排完队进入Runnable状态

  • Waiting

    无限期等待另一个线程执行特定操作的线程处于此状态 比如没有指定休眠时间的休眠,一直睡,知道被唤醒则进入Runnable状态

  • Timed_Waiting

    无限期等待另一个线程执行最多指定等待时间的操作的线程处于此状态 可以不用等别人唤醒,毕竟是指定时间的,倒也可以被唤醒,醒了直接进入Runnable状态

  • Terminated

    已退出的线程处于此状态

带返回值的线程Callable

Java中的第三种线程实现方式
创建线程一般就继承Thread和实现Runnable,这两种方法创建的线程和主线程是一块执行的,两个一块走。
Callable实现线程的话既可以两个一块走,也可以实现主线程等子线程执行完毕返回结果
Runnable与Callable
接口定义
//Callable接口
public interface Callable<V> {
	V call() throws Exception;
}
//Runnable接口
public interface Runnable {
	public abstract void run();
}
Callable使用步骤
1. 编写类实现Callable接口 , 实现call方法
    class XXX implements Callable<T> {
        @Override
        public <T> call() throws Exception {
        	return T;
        }
    }
2. 创建FutureTask对象 , 并传入第一步编写的Callable类对象
	FutureTask<Integer> future = new FutureTask<>(callable);
3. 通过Thread,启动线程
	new Thread(future).start();
	
Callable相对麻烦一点,实现Callable接口的类需要使用泛型,任务执行完毕以后进行return返回什么类型的结果给启动线程的人。

FutureTask有get()方法获取线程执行的结果,但要知道,比如说主线程调用了线程A,线程A需要花10s完成某件事情并返回结果,那么主线程如果没调用get方法,它就不会等这10s,并发执行两个线程,但如果调了这个方法,主线程就会等这10s,等待线程A执行完毕,主线程获取结果之后再往下执行。
还有一个get(long timeout,TimeUnit unit),给一个最多能能带的时间,要是超时,算了不要了。

FutureTask对象名.isDone()可以判断任务是否执行完毕
FutureTask对象名.cancel(true)  取消任务,其返回值为true 取消成功,任务还没完成被干掉了。return FALSE的情况绝大多数是因为任务已经执行完毕,执行成功了,没有办法再取消了。

该方法作为了解,用得并不多。

线程池

概述
创建线程->创建任务->执行任务->销毁线程
有时候创建任务和执行任务只占整个过程大概5%的时间,真正花时间的是创建线程和销毁线程。

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间. 线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。

合理设置线程池的长度也很有必要,依次领任务。如果是非定长线程池,见到任务来了没相应闲状态的线程,就会new一个Thread并且线程池长度+1了。既然能自动扩容也有自动清缓存,如果有多个线程一直处于闲,就销毁了。

Java中自己主动使用线程池的还是少的,后端开发本来就是基于多线程的,已经有池缓存的概念了,不需要额外加线程池了。

线程池的好处
  • 降低资源消耗
  • 提高响应速度
  • 提高线程的可管理性
Java中的四种线程池 ExecutorService
1.缓存线程池
/**
    * 缓存线程池.
    * (长度无限制)
    * 执行流程:
        * 1. 判断线程池是否存在空闲线程
        * 2. 存在则使用
        * 3. 不存在,则创建线程 并放入线程池, 然后使用    [非定长线程池]
*/
    ExecutorService service = Executors.newCachedThreadPool();
    //向线程池中 加入 新的任务
    service.execute(new Runnable() {
        @Override
        public void run() {
        	System.out.println("线程的名称:"+Thread.currentThread().getName());
        }
    });
    service.execute(new Runnable() {
        @Override
        public void run() {
        	System.out.println("线程的名称:"+Thread.currentThread().getName());
        }
    });
    service.execute(new Runnable() {
        @Override
        public void run() {
        	System.out.println("线程的名称:"+Thread.currentThread().getName());
        }
    });

2.定长线程池
/**
    * 定长线程池.
    * (长度是指定的数值)
    * 执行流程:
        * 1. 判断线程池是否存在空闲线程
        * 2. 存在则使用
        * 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
        * 4. 不存在空闲线程,且线程池已满的情况下,则排队等待线程池存在空闲线程
*/
    ExecutorService service = Executors.newFixedThreadPool(2);
    service.execute(new Runnable() {
        @Override
        public void run() {
        	System.out.println("线程的名称:"+Thread.currentThread().getName());
        }
    });
    service.execute(new Runnable() {
        @Override
        public void run() {
        	System.out.println("线程的名称:"+Thread.currentThread().getName());
        }
    });

3.单线程线程池
效果与定长线程池 创建时传入数值1 效果一致.
不管多少任务,就这1个线程在干!
应用领域:有时候某些任务要子线程执行,任务还要求排队执行,就可以是单线程执行这些任务;如果是要并发执行,则需要定长or不定长(缓存)线程池
    /**
    * 单线程线程池.
    * 执行流程:
        * 1. 判断线程池 的那个线程 是否空闲
        * 2. 空闲则使用
        * 4. 不空闲,则等待 池中的单个线程空闲后 使用
    */
    ExecutorService service = Executors.newSingleThreadExecutor();
    service.execute(new Runnable() {
        @Override
        public void run() {
        	System.out.println("线程的名称:"+Thread.currentThread().getName());
        }
    });
    service.execute(new Runnable() {
        @Override
        public void run() {
        	System.out.println("线程的名称:"+Thread.currentThread().getName());
        }
    });
4.周期性任务定长线程池
它的执行不像上面的——给了立马执行,他可以是在某个时机触发时执行
比如说多少秒以后(定时执行),or每间隔多少秒(定时间隔周期重复执行)
缓存线程池
/**
    * 缓存线程池.
    * (长度无限制)
    * 执行流程:
        * 1. 判断线程池是否存在空闲线程
        * 2. 存在则使用
        * 3. 不存在,则创建线程 并放入线程池, 然后使用    [非定长线程池]
*/

方法为newCachedThreadPool()
下图第一句话为创建线程池。execute为执行任务,Runnable对象是传参时直接定义了。

为测试线程池复用已创建的空闲线程,可以执行完三个线程任务后sleep一下,再执行一次,这样就可以观察到复用了线程池里原先创建的Thread-1 如下图二

定长线程池
/**
    * 定长线程池.
    * (长度是指定的数值)
    * 执行流程:
        * 1. 判断线程池是否存在空闲线程
        * 2. 存在则使用
        * 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
        * 4. 不存在空闲线程,且线程池已满的情况下,则排队等待线程池存在空闲线程
*/
方法为newFixedThreadPool(size)
因为是定长,还需要添加一个线程池里能存放多少线程的一个int变量
下图的例子中,调用了三次execute方法,前两次都sleep(3000) 即sleep 3s,所以打印结果为首先输出前两句话,第三句话是在三秒后,有线程空闲了(Thread-1)复用了。

单线程线程池
/**
    * 单线程线程池.
    * 执行流程:
        * 1. 判断线程池 的那个线程 是否空闲
        * 2. 空闲则使用
        * 4. 不空闲,则等待 池中的单个线程空闲后 使用
*/

效果与定长线程池 创建时传入数值1 效果一致.
方法为newSingleThreadExecutor()
执行结果可以看出,无论多少任务,都是Thread-1在做!

这几个代码跑的结果会发现一直不结束,因为线程池在等传任务,若有一个用户线程没有关闭,应用程序就不会被关闭,当然,一定时间后会自动关闭。

周期定长线程池
public static void main(String[] args) {
/**
    * 周期任务 定长线程池.
    * 执行流程:
        * 1. 判断线程池是否存在空闲线程
        * 2. 存在则使用
        * 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
        * 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
    *
    * 周期性任务执行时:
    * 		定时执行, 当某个时机触发时, 自动执行某任务 .
    */
    //返回类型不再是之前的ExecutorService类型了
    ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
/**
    * 1.定时执行1次
        * 参数1. 定时执行的runnable类型的任务
        * 参数2. 时长数字
        * 参数3. 时长数字的时间单位,TimeUnit的常量指定
*/
	//以下任务在5s后执行
    /*service.schedule(new Runnable() {
        @Override
        public void run() {
        	System.out.println("俩人相视一笑~ 嘿嘿嘿");
        }
    },5,TimeUnit.SECONDS);
    */
    
    
    /**
    * 2.周期性执行任务
        * 参数1. runnable类型的任务
        * 参数2. 延迟时长数字(延迟执行的时长,即第一次执行是在什么时间以后)
        * 参数3. 周期时长数字(每次执行的间隔时间)
        * 参数4. 时长数字的单位
    */    
    // 5s之后开始执行,每隔2s执行1次
    service.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
        	System.out.println("俩人相视一笑~ 嘿嘿嘿");
        }
    },5,2,TimeUnit.SECONDS);
    
}

Lambda表达式

Lambda表达式属于函数式编程思想,注重方法本身,与面向对象有很多冲突。
面向对象:创建对象调用方法 解决问题

接口必须只有一个抽象方法  才能使用Lambda表达式

下图new对象是为了什么,为了调用里面的run方法
Lambda表达式能让实现接口的这类方法变得更简单

方法名后面不是有()吗,Thread的参数就是(),->右边就是方法体的内容,也可以把sout这个语句整个加{sout...  },这是合法的。

再举一个例子,下图一是匿名内部类,正常面向对象的思想走的流程

下图二是对下图一做些许改动,就是匿名内部类部分(实现接口的类),留形参和方法体,运行结果是一样的。即Lambda表达式。
()->{}

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值