一,多线程的介绍
二,多线程的四种实现方式
三,多线程的五大状态
四,多线程的调度
五,线程的同步(例:多口售票问题)
六,线程的协作(例:生产者-消费者模型)
七,线程的中断
>>百度中多线程的介绍(multithreading):
是指从软件或者硬件上实现多个线程并发执行的技术。
>>这里就需要了解一下什么是进程?
是操作系统进行资源分配的最小单位;
同一进程中的多个线程共享该进程中的全部系统资源,
而进程和进程之间是相互独立的,是系统进行资源分配和调度的一个独立单位;
进程是具有一定功能的程序关于某个数据集合上的一次运行活动;
>>什么是线程?
线程是cpu调度和分派的基本单位,
同一进程中的多个线程共享该进程中的全部系统资源,
线程是进程运行和执行的最小调度单位
>>为什么要使用多线程?
1. 从计算机底层来说:线程是程序执行的最小单元,线程间的切换,调度成本远远小于进程,
另外多核CPU时代意味着多个线程可以同时运行,减少了线程上下文切换的开销;
上下文切换:多线程的上下文切换是指CPU控制权由已经正在运行的线程切换到另一个
就绪并等待获取CPU执行权的线程的过程;
2. 从互联网趋势来说:当下的系统频繁需要百万级,甚至是千万级的并发量,而多线程并发
编程正是开发高并发的基础,利用多线程机制可以大大提高系统整体的并发能力和性能;
>>多线程可能带来的问题:
并发编程的目的是为了提高程序的执行效率,但并发编程并不总能提高程序运行效率
而且还有可能带来其它问题,如:内存泄漏,上下文切换,死锁,还受限于硬件和软件的资源闲置问题
用关键字extends继承Thead类;
重写run
方法;(获得执行线程的线程名:Thread.currentThread().getName()
)
在测试类中创建类的实例,调用start
方法;
>>线程介绍:
继承了Thread类,编写简单,
如果需要访问当前线程,无需Thread.currentTread()方法,直接使用this,即可获得当前线程。
但由于类的单继承原则,该类不能继承其他类,灵活度不高
public class TheadDemo extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
public static void main(String[] args) throws InterruptedException {
TheadDemo demo=new TheadDemo();
demo.start();
}
}
用关键字implements实现Runnable接口;
重写run
方法;
在测试类中创建类的实例;
创建Thead类的实例,传入上面创建的实例对象;
用Thead类的实例,调用start
方法;
>>线程介绍:
实现Runnable接口比较容易实现多个线程共享一个对象,
非常适合多线程处理同一份资源的场景,
因而可以将代码,数据分开,形成清晰的代码模型,体现着Java面向对象的思想
public class RunnableDemo implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
public static void main(String[] args) {
RunnableDemo demo=new RunnableDemo();
Thread thread=new Thread(demo);
thread.start();
}
}
>>start方法和run方法的区别:
调用start方法后,线程会被放到等待队列,等待JVM调度,并不一定马上开始执行,此时线程处于就绪状态
该方法用来启动线程,调用后子线程就绪;
run方法用来封装子线程所需要的业务代码,run方法运行结束,子线程终止
用关键字implements实现Callable接口,并规定泛型;
重写call
方法;
在测试类中创建类的实例;
创建FutureTask的实例,并传入上面类的实例对象;
创建Thead的实例,传入FutureTask的实例对象;
用Thead的实例对象调用start
方法;
线程介绍:
实现Runnable接口和实现Callable接口
比较容易实现多个线程共享一个对象,非常适合,多线程处理同一份资源的场景,
因而可以将代码,数据分开,形成清晰的代码模型,体现着Java面向对象的思想
而实现Runnable接口中的call方法,可以有返回值,灵活度更高
public class CallableTask implements Callable<Integer> {
@Override
public Integer call() throws InterruptedException {
int sum = 0;
for (int i = 0; i < 100; i++) {
sum += i;
System.out.println(Thread.currentThread().getName() + ":" + sum);
}
return sum;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
CallableTask callableTask = new CallableTask();
FutureTask<Integer> futureTask = new FutureTask<>(callableTask);
Thread thread = new Thread(futureTask, "子线程");
futureTask.get();//得到CallableTask中call方法的返回数据
thread.start();
}
}
来看看自定义的线程池构造函数(参数最多的)
public ThreadPoolExecutor(
int corePoolSize, //线程池中核心线程的数量
int maximumPoolSize, //线程池可创建的最大线程数量
long keepAliveTime, //线程最大等待时间
TimeUnit unit, //指定keepAliveTime的单位
BlockingQueue<Runnable> workQueue, //指定存储未执行任务的队列
ThreadFactory threadFactory, //创建线程的工厂
RejectedExecutionHandler handler //指定拒绝策略
)
public class 自定义线程池 {
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 100;
private static final Long KEEP_ALIVE_TIME = 1L;
public static void main(String[] args) {
//通过ThreadPoolExecutor构造函数自定义参数创建
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 10; i++) {
//创建RunableTaskForPool对象(RunableTaskForPool类实现了Runnable 接口)
Runnable worker = new RunableTaskForPool();
//执行Runnable使用execute方法
//执行Runnable和Callable类型任务用submit方法
executor.execute(worker);
}
//终止线程池
executor.shutdown();
}
}
>>线程池获取线程优势:
Tread/Runnable/Callable创建线程如果频繁创建和关闭会消耗大量系统资源,影响性能;
因此更建议使用线程池来创建线程;
由于线程池创建并管理多个线程,当程序需要使用线程时,直接从线程池获取即可,使用完成会再次放回线程池
不需要频繁创建和销毁线程,可以大大提高系统性能,项目开发主要使用线程池的方式
>>线程池的执行流程:
1. 提交一个线程任务,当线程池里存活的核心线程数小于线程数时,线程会创建一个核心线程去处理提交的任务
2. 如果核心线程数已满,提交的线程就会放在任务队列中排队等待
3. 当核心线程已满,任务队列已满,此时判断线程数是否到达最大数量,如果没有到达,则创建非核心线程执行提交的任务
4. 如果核心线程数已到达,任务队列也已满,非核心线程数也到达,则采用拒绝策略处理
>>线程池的几种工作队列:
ArrayBlockingQueue(有界队列):
是一个基于数组结构的有界阻塞队列,此队列按先进先出原则排序
LinkedBlockingQueue(可设置容量队列):
一个基于链表结构的阻塞队列,也按照先进先出原则排序,吞吐量高于ArrayBlockingQueue
SynchronousQueue(同步队列):
一个不存储元素的阻塞队列,每个插入操作,必须等到另一个线程调用移除操作 否则插入操作一直处于
阻塞状态,吞吐量通常高于LinkedBlockingQueue,CachedTreadPool线程池使用该队列
PriorityBlockingQueue(优先级队列):
一个具有优先级的无限阻塞队列
DelayQueue(延迟队列):
是一个任务定时周期的延迟执行队列,根据执行时间从小到大排序,
否则根据插入到队列的先后顺序,newScheduledTreadPool线程池使用该队列
>>线程池的状态:
RUNNING:线程池一旦被创建就处于该状态,任务数为0,能接收新任务,对以排队的业务进行处理
SHUTDOWN:不接收新任务,但能处理已排队的业务;
调用线程池的shutdown()方法,线程池由RUNNING转变为SHUTDOWN状态
STOP:不接收新任务,不处理已排队业务,并且会中断正在处理的任务;
调用线程池的shutdownNow()方法,线程池由RUNNING或SHUTDOWN转变为STOP状态
TIDYING:线程在SHUTDOWN状态下任务数为空,其它所有任务已中止,线程池会变为TIDYING状态
线程在STOP状态,线程池中执行任务为空时就会转变为TIDYING状态
TERMINATED:线程池彻底中止,线程池在TIDYING执行完terminated()方法就会转变为TERMINATED状态
>>线程池的四种拒绝策略:
AbortPolicy:默认策略,丢弃任务并抛出异常RejectedExectionExecption
DiscardPolicy:丢弃任务,不抛异常
DiscardOldestPolicy:丢弃队列最前面的任务,然后从新提交被拒绝的任务
CallerRunPolicy:交给调用线程池的线程处理
(《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过
ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规
避资源耗尽的风险)
>>Executors工具类可创建的几种常见线程池:
newCachedThreadPool:可缓存线程池
newFixedThreadPool:固定线程数量的线程池
newScheduledThreadPool:定时线程池,定时任务和周期任务使用
ThreadPoolExecutor:自定义线程池,可控制:核心线程数(核心线程数不会被释放)/最大线程数
/最大空闲时间(到达最大空闲时间会自动回收的线程)/时间单位/等待队列
public class CallableTaskForPool implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":线程任务被执行"+i);
}
return "Callable线程任务被执行";
}
}
public class 带有缓冲区的线程池 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建一个带有缓冲区的线程池,返回一个ExecutorService对象,该对象用于对线程池进行管理
//如:向线程池提交线程任务,关闭线程池等操作
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 100; i++) {
Future<String> future= executorService.submit(new CallableTaskForPool());//向线程池提交任务
}
executorService.shutdown();//关闭线程池
}
}
public class 固定数量的线程池 {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);//传入线程数
for (int i = 0; i < 20; i++) {
executorService.submit(new CallableTaskForPool());
}
executorService.shutdown();
}
}
public class RunableTaskForPool implements Runnable {
@Override
public void run() {
System.out.println("执行周期线程任务");
}
}
public class 定时任务线程池 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
//五个参数:线程任务/延迟时间/间隔时间/时间单位
ScheduledFuture<?> scheduledFuture = scheduledExecutorService.scheduleAtFixedRate(new RunableTaskForPool(), 0, 1000, TimeUnit.MILLISECONDS);
scheduledFuture.get();//得到RunableTaskForPool()中的run方法返回的数据
scheduledExecutorService.shutdown();
}
}
public class 自定义线程池 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建等待队列
ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(10);
//参数:核心线程数(核心线程数不会被释放)/最大线程数/最大空闲时间(到达最大空闲时间会自动回收的线程)/时间单位/等待队列
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(4, 10, 20, TimeUnit.MILLISECONDS, queue);
for (int i = 0; i < 100; i++) {
Future<?> future = threadPoolExecutor.submit(new RunableTaskForPool());
future.get();//获得返回结果
}
threadPoolExecutor.shutdown();
}
}
新建:当一个Thread类或者其子类被调用时,新生的线程对象处于新建状态,
此时它已经有了相应的内存空间和其他资源,在新建状态下的线程不会被执行;
就绪:当线程被创建,并调用了start方法,该线程就进入了就绪状态,
该状态下的线程位于可运行池(线程池)等待获得cpu的使用权;
运行:处于该状态的线程专用cpu
只有处于就绪状态的线程才有机会转为运行状态;
阻塞:放弃cpu资源,让其他资源获取,
五种阻塞原因:
1.位于等待池:执行wait方法,jvm就会将该线程放于等待池;
2.位于锁池:试图获得某个对象同步锁时,如果该对象的同步锁已经被其他线程占用,
jvm会将这个线程放在这个对象的锁池中;
3.执行了sleep方法;
4.调用其他线程join方法;
5.发出IO请求时;
死亡:run方法结束,线程结束;
wait
:线程等待(该线程会被放在等待池,等待被唤醒,在同步锁中使用);notify
:唤醒一个处于等待的线程,在同步锁中使用;notifyAll
:唤醒所有等待的线程,在同步锁中使用;sleep
(静态方法):线程休眠,指定休眠一定时间,休眠期间线程处于堵塞状态,休眠结束,线程进入就绪状态等待获取cpu资源;(传入数字,默认单位为毫秒);join
:在一个线程中调用另一个线程的该方法,则该线程进入等待状态,等待调用线程执行结束,该线程才会继续执行;yield
(静态方法):线程让步,中止本次线程执行,让出cpu资源,回到就绪状态继续抢占cpu资源;setPriority
:设置线程优先级,只能相对的增大抢到cpu资源的概率(传入数字1-10);setDaemon
:守护线程,又叫精灵或者幽灵线程,当其它线程结束后该线程自动结束,如果尚未结束,jvm强行终止。通常情况可能会超出当然也就i体现了jvm强行中止(传入boolean值);interrupt
:其实它的作用就是传出一个线程应该中断的信号,至于该怎样中断线程,设计者来实现>>百度的解释就很好:
线程有可能和其他线程共享一些资源,比如,内存,文件,数据库等。
当多个线程同时读写同一份共享资源的时候,可能会引起冲突。这时候,我们需要引入线程“同步”机制,
即各位线之间要有个先来后到(抢占cpu资源),不能一窝蜂挤上去抢作一团。
线程同步的真实意思和字面意思恰好相反。
线程同步的真实意思,其实是“排队”:几个线程之间要排队,一个一个共享资源进行操作,而不是同时进行操作。
>>线程安全:
当多个线程访问时,采用加锁机制,当一个线程访问某个类的数据时进行保护,其它线程不能访问
直到该线程结束,其它线程才可访问,不会出现数据不一致和或者数据污染
>>保证线程安全的方式:
1. 使用线程安全的类,如:CopyOnWriteArrayList,ConcurrentHashMap等
2. synchronized同步代码块
3. 同步方法
4. 使用LOCK锁
>>线程死锁:
多个线程同时被堵塞,他们中的一个或全部都在等待某个资源的释放,由于线程被无限期的阻塞,
程序不能中止和运行的一种状态;
例:A需要x,y两个资源,B也需要x,y两个资源;而抢占CPU时,A得到了x资源,B得到了y资源
此时,A等待获取y资源,B等待获取x资源,线程未结束前又不能释放已获得的资源,
因此造成了相互等待,形成死锁
>线程死锁的四个条件:
互斥条件:该资源任意一个时刻只能由一个线程占用
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:线程已获得的资源在未使用前不能被其他线程强行剥夺,只有自己使用完毕后才能释放资源
循环等待条件:若干线程之间形成一种头尾相接的循环等待资源的关系
>避免线程死锁: 破坏以上四个条件即可
破坏互斥条件:无法破坏,因为我们用锁本身就是为了互斥访问
破坏请求与保持条件:一次申请所有资源
破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源
破坏循环等待条件:使用按序申请资源来预防,按某一顺序申请资源,释放资源则反序释放,这样就可以破坏该条件
public class SynchronizedDemo {
//方法1
public void test1() {
synchronized (this) {
//使用this实现对象级同步
}
}
//方法2(LOCK锁)
private static final Object LOCK = new Object();
public void test2() {
synchronized (LOCK) {
//方法级同步
}
}
//方法3
public synchronized void test3() {
//对象级的方法同步
}
//方法4
public static synchronized void test4() {
//同步的静态方法,静态方法属于类,所以也可以实现对象级的同步
}
}
描述:我只有10张票,给两个销售点去售卖,不能卖重复了,用多线程的思想解决
public class SaleTask {
private int ticketNum;//定义票数
//构造器设置票数
public SaleTask(int ticketNum) {
this.ticketNum = ticketNum;
}
//卖票方式
public void doSale() {
if (ticketNum <= 0) {
System.out.println("没票了");
return;
}
System.out.println(Thread.currentThread().getName() + ":第" + ticketNum + "张票");
ticketNum--;
}
public static void main(String[] args) {
//创建对象
SaleTask sale1 = new SaleTask(10);
//销售点一,匿名内部类形式实现线程的创建的法一
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
sale1.doSale();
try {
Thread.sleep(10);//让线程睡一会,方便销售点2和它抢占cpu
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "销售点1").start();
//销售点二,匿名内部类形式实现线程的创建的法二
new Thread("销售点2") {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
sale1.doSale();
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
public void doSale() {
synchronized (this) {
if (ticketNum <= 0) {
System.out.println("没票了");
return;
}
System.out.println(Thread.currentThread().getName() + ":第" + ticketNum + "张票");
ticketNum--;
}
}
多个对象时:
//LOCK锁,指定了唯一对象
private static final Object LOCK = new Object();
//卖票方式
public void doSale() {
synchronized (LOCK) {
if (ticketNum <= 0) {
System.out.println("没票了");
return;
}
System.out.println(Thread.currentThread().getName() + ":第" + ticketNum + "张票");
ticketNum--;
}
}
//优化卖票方法
public synchronized void doSale() {
if (ticketNum <= 0) {
System.out.println("没票了");
return;
}
System.out.println(Thread.currentThread().getName() + ":第" + ticketNum + "张票");
ticketNum--;
}
多个对象时
public static synchronized void doSale() {
if (ticketNum <= 0) {
System.out.println("没票了");
return;
}
System.out.println(Thread.currentThread().getName() + ":第" + ticketNum + "张票");
ticketNum--;
}
wait
,notify
,notifyAll
三个方法的灵活使用synchronized
代码中调用,而且只有锁对象才能调用,即持有锁对象监视器的线程才能调用锁对象的这三个方法wait()
notify( )
或 notifyAll()
方法来唤醒。notify( )
notifyAll()
描述:我有一个面包店,有一个师傅生产面包,有一个师傅销售面包,怎样达到一种产销平衡
面包类
public class Bread {
private String bread;
public Bread() {
}
}
操作类
public class BreadCabinet {
private Bread[] breads;//定义一个面包数组,用于规定容器大小
private int num;//用于统计当前面包数量
public BreadCabinet(int initSize) {
breads=new Bread[initSize];//初始化容器大小
}
public synchronized void doBread(Bread bread){
while (breads.length==num){//说明面包箱满了
System.out.println("面包箱满了,快卖");
try {
wait();//容器装满了,就让该线程进入等待状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}
breads[num]=bread;
num++;//做出一个面包
System.out.println(Thread.currentThread().getName()+":"+"面包"+num+"出炉");
notifyAll();//唤醒线程
}
public synchronized void saleBread(){
while (num==0){
System.out.println("面包箱没了,快做");
try {
wait();//没面包了,就让该线程进入等待状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+":"+"面包"+num+"售出");
num--;//卖出一个面包
notifyAll();//唤醒线程
}
}
测试类
public class BreadTest {
public static void main(String[] args) {
BreadCabinet breadCabinet = new BreadCabinet(10);
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
Bread bread = new Bread();
breadCabinet.doBread(bread);
}
}
},"生产者1").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
breadCabinet.saleBread();
}
}
},"消费者1").start();
}
}
首先了解一下方法interrupt
, interrupted
, isInterrupted
,sleep
三个方法
很明显上面的interrupt方法可以中断,但是需要特殊情况
而interrupted
, isInterrupted
貌似只能测试这个线程是否被中断
方法一:先看上面四张图介绍:经测试后,发现只要调用了上面的几个方法,就可以通过异常实现打断线程
方法二:futureTask.cancel()
方法传入true
(该方法底层调用了interrupted()
方法)
方法三: 通过接收信号的方式在子线程内主动结束线程(根据具体业务运用return
,break
等,)
描述:现有主线程一个,子线程一个,如何让主线程中断子线程
描述:让主线程在i=20时,结束子线程
建立子线程
public class CallableTask implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum=0;
for (int i = 0; i < 100; i++) {
sum+=i;
System.out.println(Thread.currentThread().getName()+":"+sum);
}
return sum;
}
}
开启子线程,主线程
public class CallableTaskTest {
public static void main(String[] args) {
//开启子线程
CallableTask callableTask = new CallableTask();
FutureTask<Integer> futureTask = new FutureTask<>(callableTask);
Thread thread = new Thread(futureTask, "线程一");
thread.start();
//主线程
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
if (i==20){
thread.interrupt();
if (thread.isInterrupted()){
System.out.println("线程中断成功");
}
}
}
}
}
详解:
当我们将sleep的异常throws出的时候,是能够实现中断的
但当把该异常由throws改成try-catch再看看结果
得到不仅没有打断成功,而且还报出了睡眠被吵醒的异常
可见这是一种通过异常打断线程的方法,实现线程的打断的
因此给子线程加上sleep等方法配合使用interrupt方法就可以实现线程打断
//子线程的循环加sleep
for (int i = 0; i < 100; i++) {
sum+=i;
System.out.println(Thread.currentThread().getName()+":"+sum);
Thread.sleep(0);
}
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
if (i==20){
thread.interrupt();
if (thread.isInterrupted()){
System.out.println("线程中断成功");
}
}
}
详解:内部调用了interrupt()方法,原理与法一类似
//子线程的修改方式
for (int i = 0; i < 100; i++) {
sum += i;
System.out.println(Thread.currentThread().getName() + ":" + sum);
Thread.sleep(0);
}
//主线程的修改方式
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
if (i==20){
futureTask.cancel(true);//发出线程中断信号
if (thread.isInterrupted()){
System.out.println("线程中断成功");
}
}
}
方法三详解:
详解:既然interrupted()可以发信号,isInterrupted()可以收信号,那就利用这一点来主动结束线程
//子线程修改代码
for (int i = 0; i < 100; i++) {
sum += i;
System.out.println(Thread.currentThread().getName() + ":" + sum);
if (Thread.interrupted()){
System.out.println("线程中断成功");
break;//结束循环,相当于手动结束线程
}
}
//主线程修改代码
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
if (i==20){
thread.interrupt();//发出终断信号
if (thread.isInterrupted()){
System.out.println("线程中断信号发出");
}
}
}