文章目录
Java多线程详解
多线程概述
- 多任务:
例如,边吃饭边玩手机。虽然看似同时间做了很多事,但本质依然是某一时刻做一件事,只是两件事交替的很快,所以看起来像同时进行。 - 多线程:
例如,一条单向的路,车很多,一辆车出事全部走不了。为了解决这个问题,开多两条同向的路,可以并排走三辆车,大家互不干扰,一条出事还有两条可以走。
又例如,编程中,如果不使用多线程,在执行main函数时如果需要另一个函数的结果,要调用该函数并使其运行结束返回后,再执行main函数,但如果加多一个线程,跟main函数刚开始就一起跑,结果等到main函数执行到位直接获得,就大大节省了程序运行时间。
- 多进程
在操作系统中运行的一个程序就是一个进程,比如播放器。
一个进程可以有多个线程,比如播放器中可以同时听到声音(一个线程),看到画面(一个线程),看到弹幕(一个线程)等等。
进程(Process) VS 线程 (Thread)
进程
- 进程是针对程序(Program)而言的。程序是指令和数据的有序集合,其本身不会运行,是一个静态的概念。
- 进程则是针对程序执行的一次执行过程,是动态的概念,是系统资源分配的单位。
- 一个进程可以包含一个或一个以上的线程。
线程
- 线程是CPU调度和执行的单位。
- 多线程有两种情况;第一种是真实的多线程,即多个处理器多核,每个处理器处理一个线程。第二种是模拟的多线程,一个CPU,在多个线程之间左右横跳,切换的很快达到同时执行的错觉,但本质上在一个时间点依然只有一个代码在执行,“模拟多线程” 类似 “多任务” 的情景假设。
- 线程是独立的执行路径。
- main()为主线程。
- 即使没主动创建线程,程序运行依然有多个线程,如主线程和垃圾回收线程。
- 多线程的运行顺序由调度器(CPU)安排,与操作系统相关,无法人为干预。
- 线程间的资源抢夺,需要加入并发控制来解决。
- 并发控制和CPU调度会带来额外程序开销。
- 每个线程在自己的内存区域交互,内存控制不当会造成数据不一致。
线程的创建
通过Thread Class
- 自定义类继承Tread 类
- 重写run()方法,编写线程执行体
- 在main()线程中,创建线程对象,调用start() 方法启动新线程
- 线程不一定立即执行,由CPU调度
- 不建议使用,避免OOP单继承的局限性
通过Runnable Interface
- 自定义类 “实现” (implements) Runnable接口
- 重写run() 方法
- 在主线程中,创建自定义类的对象,将对象丢入 Thread thread = new Thread(对象名).start() 开启执行
- 推荐使用,可以避免单继承的问题,方便同一个对象被多个线程类使用,但这会触发并发问题,(例如,一个火车票Runnable对象,有10张票的类属性,每个Thread类是一个买票者,每买一张票,票属性–,这时他们可能同时买第n张票,因为票属性是公共资源,在run()方法内的资源才是线程私有资源)
龟兔赛跑例子核心代码(i<=100而不是<100):
通过Callable Interface
- 自定义类继承Callable<?>,“?”为返回类型
- 重写(Override)call()方法,call() 与 run() 不同,需要与Callable<?>中的“?”相同的返回类型
- 创建自定义类对象,一个对象需要一个提交
- 创建 “执行服务”,输入参数为n个线程
- 提交执行,n个线程需要提交n次
- 获取返回结果,n次提交有n个返回结果
- 关闭服务
- Callable的好处:可以定义返回值;可以抛出异常;
静态代理模式
“静态代理模式” 与 “Thread” 的关系:
此处插入代理模式的原因主要是想表达,Thread类其实就是一个代理对象,Thread自身实现了Runnable接口,输入一个实现了Runnable接口的自定义类来实现多线程,底层原理就是代理模式。
- 真实对象和代理对象都要实现同一接口
- 代理对象要代理真实角色
这样做的好处:
- 代理对象可以做很多真实对象做不了的事情(例如结婚中的,婚庆公司帮你策划,你自己只用结婚)
- 真实对象专注做自己的事.
图中Marry为接口,You为真实对象,WeddingCompany为代理对象。
Lambda表达式
“Lambda表达式” 与 “Thread”的关系:
此处插入Lambda表达式主要是因为Runnable接口就是一个函数式接口,可以用lambda表达式简洁的实现出一个对象而不用写class类。
格式:
(parameters) -> expression
(parameters) -> statement
(parameters) -> {statements}
parameter的参数类型可以省略,要加都加,不加都不加,参数的括号在只有一个参数的时候可以省略
为什么要使用lambda表达式:
- 避免匿名内部类定义过多
- 可以让代码看起来简洁
- 去掉冗余的部位,只保留核心逻辑
使用lambda表达式必须先理解 “函数式接口”(functional interface)
函数式接口(Functional Interface)
定义:任何接口,只包含唯一一个抽象方法,那么他就是一个函数式接口,对于函数式接口,我们可以通过lambda表达式来创建该接口的对象。
例如:Runnable Interface
/*
推导lambda表达式
*/
public class TestLambda {
//3.静态内部类
static class Like2 implements ILike{
@Override
public void lambda() {
System.out.println("I Like Lambda2");
}
}
public static void main(String[] args) {
ILike iLike = new Like();
iLike.lambda();
iLike = new Like2();
iLike.lambda();
//4.局部内部类
class Like3 implements ILike{
@Override
public void lambda() {
System.out.println("I Like Lambda3");
}
}
iLike = new Like3();
iLike.lambda();
//5.匿名内部类,没有类的名称,必须借助接口或者父类
iLike = new ILike() {
@Override
public void lambda() {
System.out.println("I Like Lambda4");
}
};
iLike.lambda();
//6.用lambda简化,简单来说就是将函数式接口直接在代码里实现其唯一的抽象方法后,跳过实现类,直接获得一个该接口的实现后的对象
//如果是带参数的话,就在小括号内加参数(如:int a),调用时输入一个int即可,甚至int 都可以省略掉
iLike = () -> {
System.out.println("I Like Lambda5");
};
iLike.lambda();
}
}
//1.定义一个函数式接口
interface ILike{
void lambda();
}
//2.实现类
class Like implements ILike{
@Override
public void lambda() {
System.out.println("I Like Lambda1");
}
}
线程的五大状态
线程停止(flag)
- 不推荐使用JDK的stop(),destroy()方法,已过时
- 推荐线程自己停止下来,通过代码的条件判断
- 建议使用一个Boolean标志位,并在Runnable类内添加一个开关方法,当main线程内条件符合时,调用该开关方法跳出run()内循环,使run()方法跑完结束并结束子线程。
线程休眠(sleep)
- sleep( milliseconds ),指定当前线程阻塞的毫秒数
- sleep() 存在异常 InterruptedException 需要抛出
- sleep() 时间达到后线程进入就绪状态
- sleep() 可以模拟网络延时,倒计时等
- 每一个对象都有一个锁,sleep() 不会释放对象的锁
- Thread.sleep()
线程礼让(yield)
- 礼让线程,让当前正在执行的线程从 “运行状态” 转为 “就绪状态”,不阻塞,一旦调度器调度上,立刻开始运行
- 礼让不一定成功,准确来说叫重新竞争,线程全部就绪状态,CPU仍然可能选择之前的线程运行,看CPU调度器的决定
- Thread.yield()
线程强制执行(join)
- 可以想象成清场,某线程调用该方法后,它保持运行或开始运行,命令其他正在运行的线程阻塞,等待它执行完成
- Thread.join()
观测线程状态
- NEW:刚刚new出来的线程,创建状态
- RUNNABLE:在Java虚拟机中执行的线程处于此状态
- BLOCKED:被阻塞等待监视器锁定的线程处于此状态
- WAITING:正在等待另一个线程执行特定动作的的线程处于此状态
- TIMED_WAITING:正在等待另一个线程执行特定时间的线程处于此状态
- TERMINATED:已退出的线程出于此状态,死亡后的线程不可通过start再次启动
如何监测线程状态:
线程的优先级
- Java提供线程调度器来监控所有就绪的线程,调度器按照优先级决定执行哪个线程,优先级高的大概率先获得系统资源,不是绝对的,优先级对不同操作系统意义不同。
- 优先级用数字来表示,范围1-10,有三个常数,Thread.MIN_PRIORITY =1.
Thread.MAX_PRIORITY=10.
Thread.NORM_PRIORITY=5.
默认为5 - Thread方法getPriority()/setPriority(int x)可以获取和改变优先级
- 优先级的设定要在start()方法调度之前
守护线程(Daemon)
- 线程分为用户线程和守护线程
- JVM必须等待用户线程运行完毕
- JVM不必等待守护线程运行完毕
- 守护线程主要做一些后台工作,包括操作日志,内存监控,垃圾回收
- Thread.setDaemon(true)可以将线程设置为守护线程
线程的同步(Synchronized)
- 并发:同一个对象被多个线程操作
- 例子:网上抢火车票,夫妻同时取钱
- 处理并发问题,我们就需要线程同步,即一种等待机制:一个对象如果有多个线程要访问,则所有线程进入该对象的等待池,形成 “队列”,当前面线程使用完毕,下一个线程开始使用。然而光有队列还是无法保证安全性,我们还需要 “锁”(想象一下公共厕所,光有队列还不够,门还要上锁)。
- 性能和安全是补全关系,保证线程安全则一定会损失一定的性能,想要更高的性能就要牺牲一定的安全
三个不安全案例:
- 不安全的买票
- 不安全的取钱
- 多个线程同时对一个List添加元素,可能占用同一个位置
同步方法和同步块
如同private关键字,我们可以用一个关键字synchronized来解决同步问题,该关键字有两种用法:
- synchronized method (同步方法)
- synchronized block(同步代码块)
同步方法(Synchronized Method)
- 同步方法(synchronized method)就在method声明中加入synchronized关键字。
- 每个synchronized method在执行前都必须获得调用该方法的对象的锁,否则阻塞。
- 同步方法无需指定同步监视器,因为默认是this
- 将大方法设置为synchronized会大大影响效率,因为方法内有些代码是是只读代码,并没有做任何修改。方法内需要修改内容的代码才需要锁。因此我们引进一个新的概念,同步代码块
同步代码块(Synchronized Block)
- 同步代码块(synchronized block)在方法内需要同步的区域加入synchronized (Obj) { }
- 同步代码块执行完后就会释放锁
- 此处Obj称为同步监视器,可以为任何对象,推荐使用公共资源作为同步监视器,例如夫妻取钱例子中,把夫妻共有账户锁住即可
JUC线程安全集合
JUC即java.util.concurrent API。在不安全例子中,我们讲到多个线程对同一个List操作存在线程不安全问题。JUC提供了一个CopyOnWriteArrayList的ArrayList是自带线程安全的。不需要自己增加同步方法。
线程的死锁
两个玩具车和枪,小孩A和小孩B各拿一个,他们都想要两个玩具,都不给对方,这种情况就称为死锁。
在代码中,如果一个同步代码块需要两个或两个以上的锁时,就有可能发生死锁。两个线程都拿到了其中部分锁且等待剩下的锁,同步方法和同步代码块执行完后就会释放锁。
产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用(单资源单线程用)
- 请求与保持条件:一个进程因请求资源阻塞时,不释放已获得的资源(线程不主动放资源)
- 不剥夺条件:进程已获得的资源,在未使用完前不能强行剥夺(线程不被动放资源)
- 循环等待条件:若干进程之间形成一种环形资源等待关系(资源需求形成闭环)
破除一条以上则可避免死锁发生
Lock接口(锁)
Lock接口锁是JDK 5.0开始提供的一种与线程的同步synchronized关键字相对的加锁方式
Lock接口 简介
Lock在代码中具体实现方式
Synchronized同步 与 Lock接口 的对比
线程的协作(生产者消费者问题)
生产者消费者模式:
这不是设计模式,是一个线程同步中的问题。
生产者和消费者共用一个仓库资源。
仓库没东西时,消费者等待,生产者生产后放入仓库,并通知消费者消费。
仓库有东西时,生产者等待,消费者消费后,通知生产者再次生产。
Synchronized同步 在这问题中,只可以解决并发问题,但是无法做到通知传递。
于是乎,Java在Object类中提供了几个解决线程间通信问题的method。
等待:
wait() 表示线程一直会等待到被其他线程唤醒,会释放锁。
wait(long timeout) 等待指定的毫秒数
唤醒:
notify() 唤醒一个处于等待的线程
notifyAll() 唤醒同一个对象上所有调用wait()方法的线程,优先级高的优先调度
无论是等待还是唤醒,只能在synchronized method或synchronized block中使用,否则会抛出IllegalMonitorStateException异常
管程法
信号灯法
信号灯法就是管程长度为1的管程法,通过一个flag判断是否通知其他线程启动
线程池