【Java基础】9.多线程
9.1 基础概念
9.1.1 并发与并行
并发:CPU同一时间段执行多种不同的进程
并行:CPU同一时刻同时执行不同进程
9.1.2 进程
进程:进入到内存的程序。
打开Windows任务管理器可以查看合结束当前内存中的进程。【从内存中清除】
9.1.3 线程
线程:进程中的一个执行单元。一个进程中至少有一个线程。
举例: Inter Core i7 8866 4核心8线程
8线程:同时执行8个任务
【单核心单线程】cpu会在多个线程之间做高速切换,轮流执行多个线程,效率低,切换速度(1/n 毫秒)
【4核心8线程】cpu可同时执行8个线程,8个线程在多个任务之间做高速切换,速度是单线程的8倍
多线程的优势:
1.效率高
2.多个线程之间互不影响
9.1.4 线程调度
分时调度:所有线程轮流使用CPU,平均分配时间
优先级调度(抢占式调度):优先级高的优先使用,同优先级随机选择【java】
9.1.5 主线程
主线程:执行main方法的线程
单线程程序:java程序中只有一个线程
执行从main方法开始,从上到下顺序执行
【java程序过程分析】
JVM执行main方法,main方法会依次进入到栈内存
JVM会找操作系统开辟一条main方法通向cpu的执行路径
cpu就可以通过这个路径来执行main方法
而这个路径有一个名字,叫主线程
9.2 创建线程类
9.2.1 Thread类
JVM允许应用程序【并发】执行多线程程序
- run()方法内布置线程任务
- start()方法用于开启该线程任务,总动执行run()内任务
创建多线程程序方式一:创建Thread类的子类
实现步骤:
- 创建一个Thread类的子类
- 在Thread类的子类中重写Thread类中的run方法,设置线程任务(开启线程要做什么)
- 创建Thread类的子类对象
- 调用Thread类中的方法start方法,开启新的线程,执行run方法
多线程原理
【开辟新的栈空间】用于执行thread的子类执行run方法
当存在多个栈空间时,cpu就有个选择的权利,可以选择执行其它线程(其它栈空间)
常用方法
public final String getName()//返回该线程的名称
public static Thread currentThread()//返回当前正在执行线程对象的引用
public final void setName(String name) //设置线程名称
【设置线程名称】除了使用setName来改名以外,还可以在创建线程对象时,用重写后的带参构造方法来取名。
public static void sleep(long millis)//暂停millis毫秒
【应用】可以用于模拟时钟
9.2.2 Runnable接口
public Thread(Runnable target)
public Thread(Runnable target,String name)
创建多线程程序方式二:实现Runnable接口
Runnable
接口应该由打算通过某一线程执行其实例的类来实现。
实现步骤:
- 创建一个Runnable接口的实现类
- 在实现类中重写Runnable接口run方法,设置线程任务
- 创建一个Runnable接口的实现类对象
- 创建Thread类对象,构造方法中传递Runnable接口实现类对象
- 调用Thread类中的start方法,开启新线程,执行run方法
Runnable接口创建多线程程序的优势
- 避免了单继承的局限性
- 增强了程序的扩展性,降低程序耦合性(解耦)。一个接口实现类就可以实现一个需求。
9.2.3 匿名内部类实现多线程
匿名:没有名字
内部类:写在其他类内部的类
【作用】简化代码
- 把子类继承父类,重写父类方法,创建子类对象合为一步完成
- 把实现类实现类接口,重写接口中方法,创建实现类对象一步完成
匿名对象的最终产物:子类/实现类对象,该对象的类没有名字
格式:
new 父类/接口(){
//重写父类/接口中方法
}
调用格式:
new 父类/接口(){
//重写父类/接口中方法
}.start
9.3 线程安全
9.3.1 线程安全问题的产生
【引例】电影院买票问题
若有一场电影,一个窗口卖所有的票。【单线程】无安全问题
若有一场电影,共100张票,售票窗口1: 1-33号票,售票窗口2: 34-67号票,售票窗口3: 68-100号票。【多线程】没有共享数据,无安全问题
若有一场电影,共100张票,三个售票窗口,都可以卖所有的票。【多线程】访问了共享数据,会产生线程安全问题。
代码论证实现如下:
package cn.itcast.day13.demo03;
public class RunnableImpl implements Runnable {
//定义一个多线程共享票源
private int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
//提高安全问题出现的概率,让程序睡眠一下
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//判断有票
System.out.println(Thread.currentThread().getName() + "--> 正在卖剩余第" + ticket + "张票");
ticket--;
}
}
}
}
package cn.itcast.day13.demo03;
import javax.swing.plaf.TableHeaderUI;
/*
模拟卖票
创建3 个进程,同时开启,对共享的票进行出售
*/
public class Demo01Ticket {
public static void main(String[] args) {
//创建接口实现类对象
RunnableImpl run = new RunnableImpl();
//创建Thread类对象
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
//开启多线程
t0.start();
t1.start();
t2.start();
//产生了线程安全问题,卖出了重复的票,甚至卖出了第-1张票
}
}
【产生的安全问题】
- 卖出了重复的票
- 卖出了不存在的票
9.3.2 同步代码块
解决线程安全问题的第一种方案是同步代码块
格式
synchronized(锁对象){
//访问共享数据的代码块
}
【注意】
- 通过代码块中的锁对象,可以使用任意对象
- 必须保证多个线程共用一个锁对象
- 锁对象的作用:把同步代码块锁住,只让一个线程在同步代码块中执行
加入锁对象后的卖票修正代码:
package cn.itcast.day13.demo04.demo03;
import java.util.Objects;
public class RunnableImpl implements Runnable {
//定义一个多线程共享票源
private int ticket = 100;
//创建一个锁对象,就是一个普通对象就行
Object object = new Object();
@Override
public void run() {
while (true) {
//同步代码块
synchronized (object){
if (ticket > 0) {
//提高安全问题出现的概率,让程序睡眠一下
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//判断有票
System.out.println(Thread.currentThread().getName() + "--> 正在卖剩余第" + ticket + "张票");
ticket--;
}
}
//同步代码块
}
}
}
同步技术的原理
使用了一个锁对象,这个锁对象叫【同步锁】,也叫对象锁,或对象监视器
【简而言之】
同步中的线程,没有执行完毕,不会释放同步锁。
同步外的线程,没有同步锁,不会进入同步。
同步保证了只有一个线程在同步中对共享数据进行操作,保证了安全,但降低了程序效率。
9.3.3 同步方法
解决线程安全问题的第二种方案:使用同步方法
使用步骤:
- 把访问了共享数据的代码抽取出来,放到一个方法中
- 在方法上添加synchronize修饰符
格式:
修饰符 synchronized 返回值类型 方法名(参数列表){
//访问共享数据的代码块
}
利用同步方法执行的代码块:
public synchronized void payTicket(){
if (ticket > 0) {
//提高安全问题出现的概率,让程序睡眠一下
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//判断有票
System.out.println(Thread.currentThread().getName() + "--> 正在卖剩余第" + ticket + "张票");
ticket--;
}
}
【本质】与同步代码块思想一致,不过是将线程实现类对象作为了锁对象
静态同步方法
修饰符 static synchronized 返回值类型 方法名(参数列表){
//访问共享数据的代码块
}
【锁对象】本类的class文件对象,不是this,this是创建对象之后产生的
9.3.4 Lock锁
Lock
实现提供了比使用 synchronized
方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 [Condition
] 对象。
ReentrantLock实现类
lock方法
void lock()
获取锁
unlock方法
void unlock()
释放锁
Lock锁的使用步骤
- 在成员位置创建一个ReentrantLock对象
- 在可能会出现安全问题的代码前调用lock方法
- 在可能会出现安全问题的最后一行代码后调用unlock方法
9.4 等待唤醒
9.4.1 线程状态
线程状态。线程可以处于下列状态之一:
- [
NEW
]
至今尚未启动的线程处于这种状态。 - [
RUNNABLE
]
正在 Java 虚拟机中执行的线程处于这种状态。 - [
BLOCKED
]
受阻塞并等待某个监视器锁的线程处于这种状态。 - [
WAITING
]
无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。 - [
TIMED_WAITING
]
等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。 - [
TERMINATED
]
已退出的线程处于这种状态。
9.4.2 线程间通信
定义:多个线程处理同一资源,但线程任务却各不相同。
【为什么要处理线程间通信】
多线程并发执行时,若希望他们有规律地执行,需要协调一下工作,以帮助达到多线程工作目的。
【如何保证有效利用资源】
等待与唤醒机制
9.4.3 等待唤醒
【引例】卖包子
顾客消费线程和包子铺卖包子线程之间的通信
-
顾客线程:告知包子铺老板要包子的种类和数量,然后调用wait方法进入无限等待
-
包子铺线程:花时间做包子,做好之后调用notify方法唤醒顾客,完成包子交易
注意事项:
- 顾客和包子铺线程必须保证线程安全,等待和唤醒只能有一个在执行。
- 同步使用的锁对象唯一
- 只有锁对象才能调用wait和notify方法
Timewait状态方法
进入到TimeWaiting(计时等待)状态有两种方式
- 使用sleep(long m)方法,在毫秒结束后,使得线程唤醒,进入Runnable/Blocked状态
- 使用wait(long m)方法,在毫秒结束后,若还没被notify唤醒,会自动恢复Runnable/Blocked状态
notifyAll方法
public final void notifyAll()
唤醒当前所有正在等待状态的线程
等待唤醒机制
又称为线程间通信。【重点】有效利用资源
在多线程任务中,若多个线程进入等待状态,将会存在一个等待调度队列。notify唤醒时,优先唤醒队列中等待时间最长的线程。
【注意】即线程被唤醒,也不一定直接进入Runnable状态。若此时锁还没有被上一线程释放,就获取不到锁对象。于是线程会先进入Blocked状态堵塞,等待获取到锁对象。
需求分析
【资源类】:包子类
设置包子的属性:皮,馅
设置包子的状态:有 true 没有false
【生产者类】:包子铺类 (是一个线程类,继承Thread)
设置线程任务(run):生产包子
生产流程:
-
对包子状态进行判断
-
true:有包子 包子铺进入等待状态
-
false:没有包子 包子铺生产包子,交替生产两种包子
有两种状态(i%20) A包子 (i%21) B包子
包子铺生产好了包子,修改状态为true
唤醒消费者类(吃货类)
【消费者类】:吃货类,线程类
设置线程任务(run):吃包子
对包子的状态进行判断
false:没有包子 吃货线程调用wait方法进入等待状态
true:有包子 吃货吃包子,吃完包子修改包子状态为false,唤醒包子铺线程生产包子
【测试类】:包含main方法的类,启动程序
创建包子对象,创建包子铺线程,开始营业。
创建吃货线程,开始交易。
9.5 线程池
为了避免频繁地创建线程和销毁线程,有一种办法可以实现线程复用,那就是线程池。
9.5.1 概念
线程池:一个容纳多个线程的容器,其中的线程可以反复使用。
【本质】一种容器:集合(ArrayList , HashSet,LinkedList,HashMap)
推荐使用LinkedList 集合
在JDK 5之后,java内置有线程池,不需要手动创建集合
【好处】
- 降低资源消耗
- 提高响应速度
- 提高线程的可管理性
9.5.2 线程池的使用
java.util.concurrent 类 Executors
newCachedThreadPool方法
public static ExecutorService newCachedThreadPool()
创建线程池对象
java.util.concurrent 接口 ExecutorService
submit方法
Future<?> submit(Runnable task)
【线程池接口】用来从线程池中调用线程,执行start方法执行线程任务
shutdown方法
用于关闭/销毁线程池
线程池的使用步骤
- 使用newCachedThreadPool方法产生一个指定线程数量的线程池
- 创建一个类,实现Runnnable接口,重写run方法,设置线程任务
- 调用ExecutorService接口中submit方法,调用线程
- 调用ExecutorService接口中shutdown方法销毁线程池【一般不执行】
9.6 Lambda 表达式
9.6.1 函数式编程思想
面向对象的思想:做一件事情,找一个能解决这件事的对象,调用对象的方法,完成这件事。【找对象】
函数式编程思想:只要能获取到结果,谁去做的,怎么做的都不重要,重视结果不重视过程。【找结果】
9.6.2 冗余的Runnable代码
传统写法
实现Runnable接口的方式,实现多线程程序
步骤:
- 创建Runnable实现类,重写run方法,设置线程任务。
- 创建实现类对象,创建Thread类对象,构造方法中传递实现类对象
- 调用start方法开启新线程,执行run方法
简化写法
匿名内部类
9.6.3 编程思想转换
做什么,而不是怎么做
java 8 发布了新特性:Lambda 表达式
9.6.4 匿名内部类的本质
制定了一种做事情的方案(函数):
无参数,无返回值,只有代码块
同样的语义体现在Lambda语法中,更加简单:
() -> System.out.println("多线程任务执行!");
9.6.5 Lambda表达式标准格式
(参数列表) -> {一些重写方法的代码};
三要素:
- 一些参数
- 一个箭头
- 一段代码(函数体)
【格式解释】
- 小括号内就是接口中抽象方法的参数列表。多个参数用逗号分隔。
- 箭头表达传递的意思。把参数传递给方法体。
- 大括号内是重写接口的抽象方法的方法体。
9.6.7 练习
(无参无返回值)
给定一个厨子cook接口,内部唯一抽象方法makeFood()
(有参有返回值)
9.6.8 Lambda表达式省略形式
原则:【可推导,可省略】
凡是根据上下文推导出来的内容,都可以省略书写。
可以省略的内容包括:
-
(参数列表) :括号中参数列表的数据类型可以省略
-
(参数列表):括号中的参数如果只有一个,那么数据类型和括号都可以省略
-
{代码}:若{}中代码只有一行,无论是否有返回值,都可以省略大括号{},return,分号
注意:{} return ; 分号 这三个要省略,必须一起省略
9.6.9 Lambda的使用前提
- 必须有接口,而且要求接口中有且仅有一个抽象方法
- 使用Lambda必须具有上下文推断
【注意】有且仅有一个抽象方法的接口,成为"函数式接口"