概述
- 即使程序编码时没有主动自定义线程,运行时仍然会有一些默认线程,如主线程【main()】、gc线程【jvm提供的垃圾回收】。
- 多线程操作同一份资源,存在资源抢夺,需要加入并发控制。(每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。)。每个拿到资源的线程都上一把锁,就算是Thread.sleep()也不会释放这把锁。保证安全性。
- 线程的运行由调度器安排,调度器与操作系统紧密相关,因此线程运行的先后顺序无法人为干预。
- 额外性能开销:CPU调度时间、并发控制开销
程序、进程、线程关系
- 程序:没有运行的含义,静态的代码
- 进程Process:运行的程序【系统分配的】
- 线程Thread:一个进程中包含>=1个线程,真正程序的执行由线程来完成【由CPU调度和执行】
多线程类别
- 真正的多线程,有多个CPU(多核)
- 模拟的多线程,仅一个CPU,(在同一时间点CPU仍然只能做一件事),但因为cpu执行的任务切换的非常快,从而产生了同时执行的错觉。
线程创建
三种方式:
Thread类
Runnable接口
- Thread类实际上是实现了Runnable接口。
- 建议使用,接口实现
1
class Thread implements Runnable {}
Callable接口
Thread
- 继承Thread类
- 重写run()方法,run方法为线程执行体
- 调用start()方法,启动线程。
- 线程不一定立即执行,由CPU安排调度。线程调用顺序不是同步一次执行(多线程是同时执行的)
1 | // 定义 |
下载文件案例
使用依赖包commons-io
实现文件下载。如果是maven项目,直接引入依赖;否则,手动引入。
1 | <!-- https://mvnrepository.com/artifact/commons-io/commons-io --> |
可以看到已经成功引入。
1 | import org.apache.commons.io.FileUtils; |
Runnable
- 实现Runnable接口
- 实现run()方法
- 借助Thread类,调用start()方法,启动线程。
- 线程不一定立即执行,由CPU安排调度。线程调用顺序不是同步一次执行(多线程是同时执行的)
1 | public class RunnableTest implements Runnable { |
Callable
- 实现
Callable<数据类型>
接口 - 实现
call(数据类型)
方法 - 启动线程
- 创建执行服务 ExecutorService executorService = Executors.newFixedThreadPool(1);//线程个数
- 提交执行 Future
future = executorService.submit(f); // 每个线程一次 - 获取结果 Boolean r1 = future.get(); // 每个线程一次
- 关闭服务 executorService.shutdownNow(); // //所有线程关闭
- 线程不一定立即执行,由CPU安排调度。线程调用顺序不是同步一次执行(多线程是同时执行的)
- 好处:
- 可以定义返回值
- 可以抛出异常
1 | import org.apache.commons.io.FileUtils; |
线程生命周期
生命周期:
环节 | 描述 | 拟人 |
---|---|---|
创建状态(新生状态) | Thread thread = new Thread() 线程对象实例创建。尚未启动。 | 来了 |
就绪状态 | thread.start() 线程变为就绪状态。 | 准备工作了 |
运行状态 | 真正执行线程体、真正工作。 | 在工作 |
阻塞状态 | thread.sleep() / thread.wait() / 同步锁定时,代码不再执行。阻塞事件解除后,重新进入就绪状态,等待cpu调度。 | 午休 |
死亡状态 | 线程中断或结束。一旦死亡不能再次启动。 | 离开了 |
线程状态
https://docs.oracle.com/javase/8/docs/api/index.html
Thread.state,一个线程在给定时间点,必定处于一个状态。这个状态是虚拟机状态,而不能反映操作系统线程状态。
使用方式:
- Thread.State.TERMINATED
- thread.getState()
状态 | 描述 |
---|---|
NEW | 线程创建,但尚未启动 |
RUNNABLE | 线程正在运行 |
BLOCKED | 线程被阻塞 |
WAITING | 正等待另一线程执行特定动作 |
TIMED_WAITING | 正等待另一线程执行特定动作,且并已经到了指定的等待时间 |
TERMINATED | 线程已退出 |
案例
1 | public class ThreadStateTest { |
线程优先级
优先级低,只是意味着,获得调度的概率比较低。
Thread类:
public final static int MIN_PRIORITY = 1;最小优先级
public final static int NORM_PRIORITY = 5; 默认优先级
public final static int MAX_PRIORITY = 10;最大优先级
setPriority(int newPriority),建议在start()调度前设置
getPriority()
1 | public class ThreadPriorityTest { |
线程方法
方法 | 说明 |
---|---|
boolean isAlive() | 获取是否活动状态 |
setPriority(int priority) | 修改线程优先级 |
static void yield() | Thread.yield() 礼让线程。礼让不一定成功,看CPU。暂停当前执行的线程实例,并执行其他线程。 |
static void sleep(long millis) | Thread.sleep(200) 暂停线程,让线程休眠,毫秒数。sleep能够放大问题的发生性。 |
void interrupt() | 中断线程。不建议使用。 |
void join() | myThread.join() 插队。myThread线程插队先执行。其他线程阻塞。等插队进行执行完成后,再执行其他线程。 |
线程停止
- 【废弃】Thread提供的stop()\destroy()方法不要使用,已废弃@Deprecated。
- 【办法】手动设置标志位flag = false,让线程代码不真正运行。或其他手动停止的办法。
1 | public class ThreadStopTest implements Runnable { |
线程休眠
sleep
- 当前线程阻塞的毫秒数。
- 时间到达后进入就绪状态。
- 存在异常InterruptedException。
- 可以模拟网络延时、倒计时等。
模拟网络延时
1 | System.out.println("a"); |
模拟倒计时
1 | for (int i = 10; i > 0; i--) { |
打印当前时间
1 | SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); |
System.currentTimeMillis()获取当前时间的13位时间戳。new Date(System.currentTimeMillis())。
线程礼让
yield
- 礼让,让线程从运行状态,转为就绪状态。
- 如,线程a执行到中途,不执行了,变成就绪状态。让线程b执行。
- 礼让不一定成功!看CUP的心情。
1 | public class ThreadYieldTest { |
结果:
1 | // 礼让,曹植没有一直执行到完毕。而是礼让了一下曹丕。 |
线程强制执行
join
- 线程到其他线程去插队执行,当插队线程执行完成后,再执行其他线程。
1 | package com.luxia.thread; |
守护线程daemon
线程的分类:
- 用户线程:thread.setDaemon(false)。虚拟机必须确保用户线程执行完毕。main()。
- 守护线程:thread.setDaemon(true)。虚拟机不会等待守护线程执行完毕。 后台记录操作日志,监控内存,垃圾回收gc()等待…
1 | package com.luxia.thread; |
线程Thread是一种静态代理模式
静态代理模式
- 真实对象,和代理对象,都实现了同一个接口。
- 代理对象要代理真实对象
new Thread(真实执行实例)。
其中
- Thread类,继承了Runnable接口,是代理。
- 真实执行类,也继承了Runnable接口,是真正的执行人。
1 | // 创建实现类实例 |
一个静态代理示例
消费者要买车,4s店可以代理消费者买车。
都实现了买车这个行为,4s店代理消费者完成。
1 | public class StaticProxy { |
线程同步
并发问题
同一份资源,需要被多个线程同时操作。
- 抢火车票
- 同时取钱
但各线程都有自己的内存,当拿到资源之后,都会以为自己处理了资源。导致资源处理出错。
线程a认为还剩1张票,线程b也认为还剩1张票,在各自内存进行了-1操作,导致实际上取了2张票。
同步机制
用来解决并发问题。
线程同步机制,本质是一种【等待】机制,多个想同时访问此对象的线程进入这份资源的等待池,形成【队列】,等待前一个线程使用完毕【锁】,下一个线程再使用。
- 访问同一资源的多个线程形成【队列】,等待。
- 当时占用资源的线程,上【锁】,保证安全,直到使用完成后释放。
锁机制
synchronized隐式锁。
出了作用域自动释放。
可以锁方法、锁代码块。
lock显式锁。
手动开锁、关锁。
只锁代码块。
- 性能更好(jvm花费更少时间来调度线程)
- 扩展性更好(提供了更多子类)
优先级顺序:lock锁 > 同步代码块 > 同步方法
synchronized隐式锁
一个线程一旦获得资源,就获得一把排它锁,独占资源。此时,其他线程必须等待,直到使用完成释放锁。
- 一个线程拥有了锁,就会导致其他需要此资源的线程挂起。
- 加锁、释放锁,会导致上下文切换和调度延时,引起性能问题。
- 优先级高的线程,等待优先级低的线程释放锁,会导致优先级倒置,引起性能问题。
synchronized锁如何修改对象资源?
每个对象资源都有一把锁,谁来访问谁拥有。
因为数据对象已经被private控制,只能通过方法进行修改,所以只需要对方法进行锁机制,就可保证同步。
方法里有需要修改的资源,就需要锁。其他不需要,否则浪费资源。
- synchronized方法。将一个大方法申明为synchronized会影响效率。
synchronized方法,默认锁的是this(对象本身,或者反射class)
1 | public synchronized void method(int args) {//含修改共享资源的代码} |
- synchronized块。
synchronized块,需要指定监听器Obj,可以是任何对象,推荐使用共享资源作为监听器。
synchronized方法,其实就是监听的对象本身。
1 | synchronized(Obj) {//含修改共享资源的代码} |
lock显式锁
- jdk5.0开始引入
- 拥有与synchronized相同的并发性和内存语义,但更强大。
- 显式定义【同步锁对象(Lock对象)】,来实现同步。
- 接口:java.util.concurrent.locks.Lock。
- Lock接口实现类:ReentrantLock,可以显式加锁、释放锁。
1 | private ReentrantLock lock = new ReentrantLock(); |
锁示例
取票
不安全取票
买票时候,会拿到0票和-1票。
因为每个线程都把资源数量copy到自己的线程中了,比如,只剩1张票的情况下,小红、小明和黄牛都看到了1张票,都放到了自己的线程内存中,继续对ticketCount进行了处理。
1 | public class TicketTest implements Runnable { |
安全锁:synchronized方法
1 | public class TicketTest implements Runnable { |
安全锁:synchronized块
1 | public class TicketTest implements Runnable { |
安全锁:lock对象
1 | import java.util.concurrent.locks.ReentrantLock; |
取钱
synchronized块
1 | public class BankTest { |
操作List资源
synchronized块
1 | import java.util.ArrayList; |
线程安全的方法
CopyOnWriteArrayList相对于List方法,本身就是一种线程安全的方法。自带LOCK锁机制
1 | package com.luxia.thread; |
死锁
死锁,多个线程互相占着对方需要的资源,形成僵持,导致所有线程都运行停止。
- 线程1,占用资源a。但等待线程2释放资源b。
- 线程2,占用资源b。但等待线程1释放资源a。
某【一个同步块】(synchronized块),同时拥有【两个以上资源的锁】,就可能发生死锁。
产生死锁的四个必要条件
- 资源独占,即每次只能被一个进程调用。
- 资源霸占,一个进程绝不主动释放已获资源,
- 资源不剥夺,在进程使用完资源前,绝不能强行剥夺资源。
- 资源循环等待:若干进程之间,形成头尾相接的循环等待资源关系。
死锁案例
拥有各自资源的情况下,又试图占用对方的资源。
想把多个资源都同时锁住,但是有资源已经被别人锁了,导致线程停止。
1 | /** |
不死锁写法
用完就释放,而不是一直锁住。
1 | /** |
线程协作
目的是实现多线程之间进行通信。
通信方法
是Object类的方法(意味着所有类都有该方法),且只能在同步方法/同步代码块中使用,否则存在异常IIIegalMonitorStateException
。
方法名 | 描述 | |
---|---|---|
等待 | wait() | 线程一直等待,直到收到其他线程的通知。 |
等待 | wait(long timeout) | 指定毫秒数 |
唤醒 | notify() | 唤醒一个处于等待状态的线程 |
唤醒 | notifyAll() | 唤醒同一对象上所有调用wait()方法的线程,优先唤醒优先级高的线程 |
生产者消费者模型的通信方式
- 管程法
- 信号灯法
graph LR a(生产者) --> B{数据缓存区} B --> C(消费者)
生产者消费者模型:
- 生产者:负责生产数据
- 消费者:负责处理数据
- 缓冲区:生产者将数据放入缓冲区,消费者从缓冲区拿出数据
管程法
- 缓冲区
- notify() / wait()
1 | /** |
信号灯法
- 标志位
1 | /** |
线程池
- jdk5.0引入线程池API
- 可提前创建很多线程,放在线程池,使用时直接取用,用完放回池中。避免频繁创建销毁线程,实现重复利用。提高响应速度,降低资源消耗,便于线程管理。
- ExecutorService:线程池【接口】,常见子类ThreadPoolExecutor。
- void execute() 执行任务,没有返回值,一般用来执行【Runnable】线程类
- submit() 执行任务,有返回值,一般用来执行【Callable】线程类
- void shutdown() 关闭连接池
- Executors:工具类、线程池的【工厂类】,用于创建并返回不同类型的线程池。
- corePoolSize 核心池大小
- maximumPoolSize 最大线程数
- keepAliveTime 线程没有任务时,最多保持多长时间终止
1 | // 创建 |
1 | import java.util.concurrent.ExecutorService; |