第11章 多线程
11.1 线程概述
一个程序包含一个进程,一个进程包含多个线程
11.1.1 线程和进程
当一个程序进入内存运行时,即变为一个进程,具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位
进程的特征
-
独立性,进程是系统中独立存在的实体,可以拥有自己独立的资源,拥有自己私有的地址空间
-
动态性,程序只是一个静态的指令集,进程是一个正在系统中活动的指令集,进程具有生命周期和状态,程序无
-
并发性,多个进程可在单个处理器上并发执行,不会互相影响
并发(concurrency)和并行(parallel)
并发:在同一时刻只有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多进程同时执行的效果
并行:在同一时刻,有多条指令在多个处理器上同时执行
现在大部分系统基本都支持多进程并发运行,常用的策略有
-
共用式的多任务操作策略
-
抢占式多任务操作策略(效率高一点)
线程(Thread)也称为轻量级进程(Lightweight Process),线程是进程的组成部分,线程是进程的执行单元
线程的特点
-
每个线程独立的
-
可并发执行
-
进程初始化时,主线程被创建
-
线程拥有自己的堆栈,程序计数器和局部变量(不拥有系统资源)
-
多个线程共享父进程资源
-
线程之间相互隔离(不知道对方是否存在)
-
线程之间抢占运行
-
线程之间可互相创建和撤销
对多线程的调度和管理,以及资源分配,由进程本身负责
操作系统可以同时执行多个任务,每个任务就是进程;进程可以同时执行多个任务,每个任务就是线程。
11.1.2 多线程的优势
与分隔的进程相比,进程中线程之间的隔离程度要小。它们共享内存、文件句柄和其他每个进程应有的状态。
线程比进程有更高的性能,因为多个线程共享一块内存区域,通信成本很小,
线程创建的成本低
因此,使用多线程来实现并发比使用多进程并发的性能要高
多线程编程的优点
-
共享内存
-
创建线程代价小
-
java对多线程的支持
线程调度
-
分时调度
- 所有线程轮流 使用cpu,每个线程平分cpu的时间片
-
抢占式调度(Java)
- 让优先级高的线程首先使用cpu,如线程优先级相同,随机选择一个线程
11.2 线程的创建和启动
java使用Thread类来代表线程,所有线程对象都必须是Thread类或其子类的实例
11.2.1 继承Thread类创建线程类
通过继承Thread类来启动多线程
- 创建thread类的子类,并重写run()方法,该方法体代表了线程需要完成的任务,因此把run()也称为线程执行体
- 创建该子类实例(创建线程对象)
- 调用该实例的start()方法,启动线程
注意,如果不适用start()方法,而是直接调用run(),那么其他线程,在run()方法返回之前无法并发执行,系统会将run()方法当作一个普通方法来执行
Thread类的方法:
-
Thread.currentThread():返回当前正在执行的线程对象
-
getName():返回调用该方法的线程名字
可以通过setName(字符串)来设置线程的名字
通过getName()来取得指定线程的名字
如果继承Thread,直接调用run()方法,那么this.getName()方法返回的该对象的名字
Thread.currentThread().getName()总是返回当前线程的名字
默认情况下
-
主线程名为main
-
用户的多个线程名字依次为Thread-0,Thread-1……
通过继承Thread来实现多线程时,多个线程之间无法共享线程类的实例变量
11.2.2 实现Runnable接口创建线程类
实现Runnable接口来实现多线程,步骤如下:
-
定义Runnable的实现类,重写run()方法
-
创建该实现类的实例对象,用该对象作为构造器形参,来创建Thread对象,
//创建实现类对象 Runnableimpl op=new Runnableimpl(); //创建Thread对象 //将Runnable实现类对象作为形参传入 //并且可以为新线程指定名字 Thread o1=new Thread(op,"线程1");
-
调用对象的start()方法来启动线程
说明:实际执行时,还是需要执行Thread对象的run方法,只是该方法调用了Runnable实现类的run方法,但使用这种方法,能使多线程共享一个线程类的实例属性
11.2.3 使用Callable和Future创建线程
java在JDK1.5时,受倒c#启发(c#可以将任意方法包装为线程执行体),提供了Callable接口,该接口提供了一个call()方法可以作为线程执行体
-
call()方法可以有返回值(该返回值和接口泛型,类型相同)
-
call()方法可以声明抛出异常
java在JDK1.5时,提供了Future接口,该接口代表了Callable接口中 call 方法的返回值,该接口有实现类FutureTask,该实现类实现了Tutrre接口和Runnable接口,因此,这个实现类FutureTask可以作为Thread构造器的形参
Future接口中定义了公共方法来控制它关联的Callable任务 -
boolean cancel(boolean may):试图取消该Future里关联的Callable任务
-
V get():返回Callable任务里call()方法的返回值,调用该方法将导致程序阻塞
-
V get(long timeout,TimeUnit unit):返回Callable任务里call()的返回值,该方法最多让程序阻塞timeout和unit指定的时间,指定时间过后Callable还是没有返回值,将抛出TimeoutException异常
-
boolean isCancelled():如果在Callable任务正常完成前被取消,则返回true
-
trueboolean isDone():如果Callable任务已完成,则返回true
使用Callable的步骤
-
创建Callable的实现类,重写call()方法
-
创建Callable实现类实例,用FutureTask包装该实例(该类封装了Callable对象call()方法的返回值)
-
使用FutureTask对象作为Thread构造器的形参,启动线程
-
调用FutureTask实例的get()方法来获得子线程执行结束后的返回值
public class ThirdThread implements Callable<Integer>{ // 实现call()方法,作为线程执行体 public Integer call(){ int i = 0; for ( ; i < 100 ; i++ ){ System.out.println(Thread.currentThread().getName() + " 的循环变量i的值:" + i); } // call()方法可以有返回值 return i; } public static void main(String[] args){ // 创建Callable对象 ThirdThread rt = new ThirdThread(); // 使用FutureTask来包装Callable对象 FutureTask<Integer> task = new FutureTask<Integer>(rt); for (int i = 0 ; i < 100 ; i++){ System.out.println(Thread.currentThread().getName() + " 的循环变量i的值:" + i); if (i == 20){ // 实质还是以Callable对象来创建并启动线程 new Thread(task , "有返回值的线程").start(); } }try{ // 获取线程返回值 System.out.println("子线程的返回值:" + task.get()); }catch (Exception ex){ ex.printStackTrace(); } } }
-
11.2.4 创建线程的三种方式对比
Runnable接口和Callable接口(可以有返回值)的优劣:
好处:
- 线程类不止实现了上述两个接口,还可以继承其他类
- 可以共享一个Thread对象,所以可以使多个线程来处理同一份资源
坏处:
- 使编程复杂,要访问当前线程,必须使用Thread.currentThread()方法
继承Thread实现多线程的优劣
好处:编写简单,访问当前线程可直接使用this关键字
坏处:继承了Thread类,不能继承别的类
11.3 线程的生命周期
线程生命周期有:
- 新建(New)
- 就绪(Runnable)
- 运行(Running)
- 阻塞(Blocked)
- 死亡(Dead)
在一个线程的生命周期内,会多次在运行和阻塞之间切换
11.3.1 新建和就绪状态
新建(New):当使用new关键字创建一个线程后,JVM会为其分配内存,初始化成员变量,该线程处于新建状态
就绪(Runnable):当线程对象调用start()方法后,JVM会为其创建方法调用栈,创建程序计数器,这时线程还没有运行,具体运行时间要等JVM,里的线程调度器调度,这时线程处于就绪阶段
如果直接调用run()方法,则线程不再处于新建状态,会被当作普通方法执行
只能对处于新建状态的线程,调用start()方法,否则将引发lllegalThreadStatrException(指示线程没有处于请求操作所要求的适当状态)
如果想让子线程在,它的start()方法被调用后,马上执行,可以执行Thread.sleep(1),让当前线程(主线程)睡眠一毫秒,这时子线程会马上抢占CPU,子线程开始执行
11.3.2 运行和阻塞状态
对于CPU的调度
- PC使用抢占式调度策略
- 系统会将cpu的时间分片,一个线程执行完一个时间片后,系统会剥夺该线程所占用的资源,其他线程开始抢占下一个时间片,在这时,系统也会考虑线程的优先级
- PE使用协作模式
- 线程主动调用Sleep()或yield(),放弃它所占的资源
线程进入阻塞状态
- 线程调用sleep()方法,主动放弃所占资源
- 线程调用了一个阻塞IO方法,在该方法返回前,线程被阻塞
- 线程试图获得一个同步监视器,但该监视器正在被其他线程所持有
- 线程在等待某个通知(notify)
- 程序挂起了线程(调用了suspend()方法,该方法会导致死锁,谨慎使用)
解除阻塞
- 调用sleep()方法的线程,已经经过了指定时间
- 阻塞IO方法已返回
- 线程成功获得了同步监视器
- 等待通知的线程,得到了通知
- 被挂起的线程,调用了resume()恢复方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bWcNvkSI-1661949179367)(D:\学习相关\images\线程生命周期.jpg)]
线程在就绪和运行状态之间的转换不受程序控制,但有一个方法例外,就是调用yield()方法,可以让运行状态的线程转入就绪状态
11.3.3 线程死亡
线程结束方式
-
run()或call()方法执行完毕,正常结束
-
线程抛出一个未捕获的异常Exception或Error
-
调用线程的stop()方法来结束线程(会导致死锁,谨慎使用)
当主线程结束时,其他线程不受任何影响,并不会随之结束。一旦子线程启动起来后,它就拥有和主线程相同的地位,它不会受主线程的影响
线程状态测试方法
isAlive()
返回True——线程处于就绪,运行,阻塞状态时返回
返回False——线程处于新建,死亡状态时返回
对死亡的线程调用start()方法,将引发lllegalThreadState Exception(非法的线程状态异常)异常
11.4 控制线程
11.4.1 join线程
join()方法,让一个线程等待另一个线程执行完毕
当一个线程调用了其他线程的join()方法后,主调线程将被阻塞,直到被调线程执行完毕
join方法
-
join() 等待被join的线程执行完毕
-
join(long millis) 等待被join的线程,最长millis毫秒,如果在这个时间内线程还没有执行结束,则不再等待
-
join(long millis,int nanos) 等待被join的线程最多millis毫秒加nanos毫微秒(毫微秒一般不常用,因为太小了,计算机硬件,操作系统都无法精确到毫微秒)
该方法需要理清楚调用join()后,谁等待,谁运行
11.4.2 后台线程
后台线程(守护线程/精灵线程,我喜欢这个名字): 在后台运行,它的任务是为其他的线程提供服务
JVM的垃圾回收线程就是典型的守护线程
特征:如果所有的前台的线程都死亡,后台线程会自动死亡
设置后台线程:调用Thread对象的setDaemon(true)
//格式
线程名.setDaemon(true)
判断线程是否为后台线程:isDaemon()
一些需要知道的点
-
主线程默认是前台线程
前台线程创建的线程,默认为前台线程 后台线程创建的线程,默认是后台线程
-
在JVM通知后台线程死亡时,后台线程需要一定的时间响应
-
设置某个线程为后台线程时,需要在该线程启动之前,否则,将引发IllegalThreadStateException(非法的线程状态)异常
11.4.3 线程睡眠:sleep
sleep()方法,将当前正在执行的线程暂停一段时间,并进入阻塞状态,该方法是Thread类的静态方法
重载sleep
-
static void sleep(long millis)让当前线程暂停millis毫秒,并进入阻塞状态,该方法受到系统计时器和线程调度器的精度和准确度的影响
-
static void sleep(long millis,int nanos)让当前线程暂停millis毫秒加nanos毫微秒,并进入阻塞
在线程睡眠的时间段内,该方法不会获得执行机会,即使CPU空闲(举例子说明,不会出现这种情况)
11.4.4 线程让步(暂停):yield
Thread类的静态方法yield(),让当前正在执行的线程暂停,它不会阻塞线程,只是让该线程转入就绪状态
实际上,该方法只会让线程暂停,但是暂停后,只有优先级与该线程相同,或更高,才能获得执行的机会
sleep和yield方法的区别
-
sleep方法给其他线程运行机会时,不理会其他线程的优先级,yield方法,只给优先级相同,或更高的线程运行机会
-
sleep会将线程转入阻塞,并且限定了阻塞时间,yield将线程强制转为就绪状态(所以完全有可能在yield(暂停)之后,马上又被运行)
-
sleep会抛出InterruptedException(中断异常),yield没有声明抛出任何异常
-
sleep具有更好的移植性,
11.4.5 改变线程优先级
每个线程都具有一定的优先级,默认情况下,main线程具有普通优先级,每个被创建的线程,拥有与它父线程相同的优先级
高优先级的线程,有更多被执行的机会
修改线程优先级
-
setPriority(int newPriority)
-
设置线程优先级,范围1-10之间,该方法可以使用Thread类的三个静态常量
-
MAX_PRIORITY:值为10
-
MIN_PRIORITY:值为1
-
NORM_PRIORITY:值为5
-
-
-
getPriority()
- 获取线程的优先级
//改变主线程的优先级
Thread.currentThread().setPriority(整数)
//改变子线程的优先级,该操作可在线程启动后执行
线程名.setPriority(整数)
不同操作系统对线程优先级的支持不一样,所以尽量使用Thread的常量来为线程赋优先级,这样具有更好的移植性
11.5 线程同步
当使用多线程来访问同一个数据,很容易“偶然”出现线程的安全问题
1.5.1 线程安全问题
这里涉及一个非常经典的线程同步案例,银行取钱案例
两个人对同一账户执行取钱操作,假设在A取钱后,到账户扣钱之间的这段时间内,B又取钱(可以取到,因为扣钱操作还未发生),则会发生错误
11.5.2 同步代码块(加锁)
在java的多线程中,引入了同步监视器(线程锁)来解决多线程带来的问题,
同步监视器
//锁对象
private static Object obj=new Object();
//obj就是同步监视器(或者说锁对象,该锁对象是任意的,只需保证多个线程拿到的是同一把就可)
//线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。
synchronized(obj){
//同步代码块
……
}
同步监视器的目的:阻止两个线程对同一个共享资源的并发访问
同一时刻,只能有一个线程,可以获得对同步监视器的锁定,当同步代码块执行完毕后,该线程会释放同步监视器的锁定
obj意味着被加锁的资源,在同步代码块内,被加锁的资源不能被别的线程访问,直到同步代码块结束
11.5.3 同步方法
使用synchronized关键字修饰的方法,称为同步方法
在书中只说了同步方法的锁对象为this,而没有分静态非静态,应该是一个疏漏
静态/非静态同步方法的锁对象
-
静态方法的锁对象是,类名.class(表示的是当前类对应的字节码文件对象)
-
非静态方法的锁对象是,this(当前类的一个对象)
线程安全类的特点
-
该类的对象可以被多线线程安全的访问
-
每个线程调用该对象的任意方法,都将得到正确的结果
-
每个线程调用该对象的任意方法,该对象仍处在合理状态
面向对象内的设计方法:Domain Driven Design(领域驱动设计,DDD)
java中的不可变类不存在线程竞争问题,而可变类存在
可变类牺牲了部分运行效率,来保证线程安全
应该为一个类提供两种环境的类,单线程和多线程
如JDK提供的StringBuilder在单线程时使用
StringBuffer在多线程使用
11.5.4 释放同步监视器的锁定(解锁)
在进入同步代码块之前,需要先获得对同步监视器的锁定,但释放时,程序无法显示的释放对同步监视器的锁定
以下情况,线程将解锁(释放同步监视器)
-
当前线程在同步方法/同步代码块执行结束,线程释放同步监视器
-
当前线程在同步代码块,同步方法中遇到了break,return终止了该代码块或同步方法,当前线程会释放同步监视器
-
当前线程在同步代码块内/同步方法内,出现了未处理的Error或Exception,导致该代码块异常结束,线程释放同步监视器
-
当前线程在同步代码块/同步方法内,程序执行了同步监视器对象的wait()方法,当前线程暂停,并释放同步监视器
以下情况,线程将不会解锁
-
线程执行同步代码块时,程序调用了Thread.sleep()睡眠方法,Thread.yield()暂停方法
-
线程执行同步代码块时,其他线程将该线程挂起(调用了该线程的suspend()方法)
11.5.5 同步锁(Lock)
java5提供的线程同步机制——通过显示定义同步锁对象,来实现同步,同步锁就是Lock对象
Lock实现允许更灵活的结构,可以具有差别很大的属性,支持多个相关的Condition对象
Lock接口的实现类ReentrantLock(可重入锁)
ReadWriteLock(读写锁)接口的实现类ReentrantReadWriteLock,该实现类允许对共享资源的并发访问
使用Lock
//在一个类中定义锁对象
private final ReentrantLock lock = new ReentrantLock();
//在一个需要保持同步的方法内,加锁
lock.lock();
……
//同步代码
//使用完毕之后在finally块中释放锁
finally{
lock.unlock();
}
我的问题
-
是否可以将lock锁放到增强try语句的括号中,使它在try语句结束之后自动释放锁呢,
- 查了API,发现ReentrantLock类并没有实现Autocloseable或Closeable接口,所以直接放肯定时不行的,如果继承然后重写该类呢?
-
lock解锁操作为什么要放在finally中呢?
- 原因是如果加锁和解锁中间出现了异常,会导致unlock()操作无法执行,导致死锁,所以放在finally块中,避免死锁问题
-
加锁操作为什么要放在try块外?
- 考虑效率缘故,如果让在try块内,加锁产生异常时,解锁也会产生异常,但如果放在try块,就算加锁异常,解锁不会触发异常
当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的范围内释放所有锁。(就是释放锁时,要在加锁的作用域内)
Lock提供的方法
-
用于非块结构的tryLock()
-
视图获取可中断锁的lockInterruptibly()
-
获取超时失效锁的tryLock(long TimeUnit)
ReentrantLock锁称为可重入锁,他具体表现在,一个线程可以对,已被加锁的ReentrantLock锁,再次加锁
ReentrantLock对象会维护一个计数器,来追踪lock()方法的嵌套调用,线程在每次调用lock()后,必须显式调用unlock()来释放锁,所以一段被锁保护的代码,可以调用另一个被相同锁保护的方法
11.5.6 死锁
当两个线程,相互等待对方释放手里的资源时,就会发生死锁
java对死锁没有检测,也没有对应的处理措施,一但出现死锁,程序不会有任何异常,也没有任何提示,只是所有线程都处于阻塞状态,无法继续
这种错误是一种逻辑错误,应该在编程阶段避免这种错误
这里有一个非常经典的死锁案例,如有需要可以翻看这一小节
11.6 线程通信
因为线程的调度由JVM的线程调度器操作,具有一定的透明性,程序通常无法准确控制线程的轮转(就如同无法控制GC一样)
11.6.1 传统的线程通信
为了实现程序对线程轮转的控制,Object类提供了一些方法,但这些方法要由同步监视器对象来调用,所以以下三个方法,只有在使用synchronized来实现同步时,才能使用
使用情况
-
synchronized修饰的同步方法,因为该类默认实例,就是同步监视器,所以可以在该方法中,直接调用这三个方法
-
synchronized修饰的同步代码块,同步监视器是synchronized后,括号里的对象,要用该对象来调用这三个方法
Object类的线程协调方法
-
wait()让调用该方法的线程等待,并释放对该同步监视器的锁定(解锁),其他线程可调用,该同步监视器的notify()方法,或notifyAlly()方法来唤醒该线程
-
无参,一直等待被唤醒
-
毫秒,等待指定毫秒值后,自动苏醒
-
毫秒,毫微秒,等待指定时间后,自动苏醒
-
-
notify()唤醒此同步监视器上,等待的单个线程,如果有多个线程在此同步监视器上等待,则任意选择其中一个线程唤醒(请注意此处的任意选择一个线程被唤醒,后面要讨论)
- 只有当前线程放弃对该同步监视器的锁定后,也就是使用了wati()方法后,才可以执行被唤醒的线程
-
notifyAll()唤醒此同步监视器上,等待的所有线程
- 只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程
notify方法在唤醒线程时,是从所有线程中随机唤醒一个线程,但这里的唤醒,其实并不是完全随机,这个随机,具体依赖JVM的实现,而hotspot,是对第一个元素进行出队操作,是遵循”先进先出“原则
具体参考:notify方法的随机唤醒
11.6.2 使用Condition控制线程通信
如果使用了Lock对象来保证线程同步,则不能使用wait(),notify()和notifyAll()方法来控制线程,要使用Condition类
Condition对象可让线程释放Lock对象,也可唤醒处于等待的线程
获得Condition实例
Condition实例被绑定再一个Lock对象上,要获得该实例,要调用Lock对象的newCondition()方法
Condition的方法
-
await():类似wait()方法,可使当前线程等待,该方法呦很多变体,如awaitNanos,awaitUninterruptibly,awaitUntil等
-
signal():唤醒此Lock对象上等待的单个线程,如有多个线程等待,随机唤醒其中一个
-
signalAll():唤醒在此Lock对象上等待的所有线程
11.6.3 使用阻塞队列(BlockingQueue)控制线程通信
Java5提供了一个BlockingQueue接口,该接口是Queue的子接口,该接口的主要作用是作为线程同步工具
BlockingQueue的特点
当生产者向BlockingQueue放入元素时,如果队列已满,则该线程被阻塞,
当消费者视图同BlockingQueue中取出元素时,如果队列已空,则该线程被阻塞
BlockingQueue的阻塞方法
-
put(E e):将E元素放入BlockingQueue中,如队列元素已满,则阻塞该线程
-
take():尝试从BlockingQueue的头部取出元素,如队列已空,则阻塞该线程
因为该接口继承了Queue接口,所以也可以使用Queue接口中的方法
-
在队尾插入元素
-
以下方法,当队列已满时
-
add(E e) 该方法会抛出异常
-
offer(E e) 返回false
-
put(E e)阻塞队列
-
-
在队列头删除并返回元素
-
以下方法,当队列已空时,
-
remove() 抛出异常
-
poll() 返回false
-
take() 阻塞队列
-
-
从队列头取出但不删除元素
-
以下方法,当队列已空时
-
element() 抛出异常
-
peek() 返回false
-
BlockingQueue的实现类
-
ArrayBlockingQueue:基于数组实现的BlockingQueue队列
-
LinkedBlockingQueue:基于链表实现的BlockingQueue队列
-
PriorityBlockingQueue:该类取出元素时,并不是遵循队列的规则,而是每次取出元素值最小的,该大小按照自然排序,或比较器排序(定制排序)
-
SynchronousQueue:同步队列,该队列的存取操作必须交替进行
-
DelayQueue:该类底层基于PriorityBlockingQueue实现,该类要求集合元素实现Delay接口,该类根据元素的getDalay()方法的返回值排序(这个方法就是Delay接口中唯一的方法)
package java相关小实验;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
/**
* 一些声明信息
*
* @author 谢柯
* @date: 2022/8/16 20:32
* @description: 使用BlockingQueue阻塞队列,来实现线程通信(生产者消费者)
* @since JDK 1.8
*/
class Producer extends Thread {
private BlockingQueue<String> bq;
public Producer(BlockingQueue<String> bq) {
this.bq = bq;
}
public void run() {
String[] strArr = new String[]
{ "Java",
"Struts",
"Spring"};
for (int i = 0; i < 999999999; i++) {
System.out.println(getName() + "生产者准备生产集合元素!");
try {
Thread.sleep(200);
// 尝试放入元素,如果队列已满,则线程被阻塞
bq.put(strArr[i % 3]);
} catch (Exception ex) {
ex.printStackTrace();
}
System.out.println(getName() + "生产完成:" + bq);
}
}
}
class Consumer extends Thread {
private BlockingQueue<String> bq;
public Consumer(BlockingQueue<String> bq) {
this.bq = bq;
}
public void run() {
while (true) {
System.out.println(getName() + "消费者准备消费集合元素!");
try {
Thread.sleep(200);
// 尝试取出元素,如果队列已空,则线程被阻塞
bq.take();
} catch (Exception ex) {
ex.printStackTrace();
}
System.out.println(getName() + "消费完成:" + bq);
}
}
}
public class BlockingQueueTest2 {
public static void main(String[] args) {
// 创建一个容量为1的BlockingQueue
BlockingQueue<String> bq = new ArrayBlockingQueue<>(1);
// 启动3个生产者线程
new Producer(bq).start();
new Producer(bq).start();
new Producer(bq).start();
// 启动一个消费者线程
new Consumer(bq).start();
}
}
11.7 线程组和未处理的异常
java使用ThreadGroup来表示线程组,可以对一批线程进行分类管理,java允许程序直接对线程组进行控制,对线程组的控制,相当于同时控制这批线程
程序创建的线程,属于默认的线程组,子线程默认,和创建它的父线程处在同一组内
一旦线程加入某个指定的线程组后,将不能更改它所属的线程组
Thread类的构造器,创建线程,指定线程组
-
Thread(ThreadGroup group,Runable target):以target的run()方法,作为线程执行体,创建新线程,属于group线程组
-
Thread(ThreadGroup group,Runnable target,String name):以target的run()方法作为线程执行体,创建新线程,该线程属于group线程组,线程名为name
-
Thread(ThreadGroup group,String name):创建新线程,属于group线程组
获取线程所属线程组
- getThreadGroup()
该方法会返回一个ThreadGroup对象,表示一个线程组
创建线程组(创建ThreadGroup实例)
-
ThreadGroup(String name):创建线程组,指定线程组名
-
ThreadGroup(ThreadGroup parent,String name):以指定名字,指定父线程组,创建一个新的线程组
操作线程组内的线程
-
int activeCount():返回此线程组中,活动的线程数目
-
interrupt():中断此线程组中的所有线程
-
isDaemon():判断该线程组是否是后台线程组
-
setDaemon(boolean daemon):把该线程组设置为后台线程组,当后台线程组的最后一个线程执行结束或最后一个线程被销毁后,该线程组将自动销毁
-
setMaxPriority(int pri):设置线程组饿最高优先级
-
uncaughtException(Thread t,Throwable e):该方法可以处理线程组内,任意线程所抛出的未处理的异常(该方法是,Thread的内部静态接口,Thread.UncaughtExceptionHandler的抽象方法,也就是说,ThreadGroup实现了该接口)
从java5开始,java加强了线程的异常处理,如果线程执行过程中,抛出了一个未处理的异常,JVM会在结束该线程之前,会自动查找是否有对应的Thread.UncaughtExceptionHandler对象,如果找到了,会调用该对象的uncaughtException(Thread t,Throwable e)方法来处理该异常
Thread.UncaughtExceptionHandler是Thread类的一个静态内接口,该接口内只有一个方法
-
void uncaughtException(Thread t,Throwable e)
t 代表出现异常的线程,e代表该线程抛出的异常
Thread类,设置异常处理器的方法
-
setDefaultUncaughtExceptionHandler(Thread.UncaughtException eh):为该线程类的所有线程实例,设置默认的异常处理器,(默认处理器2)
-
setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh):为指定的线程实例,设置异常处理器(默认处理器1)
线程异常处理流程
当一个线程抛出未处理的异常时
-
JVM查找对应的异常处理器(默认处理器1),如没有,向下走
-
JVM调用该线程所属线程组的方法来处理(unacughtException()方法设置的组默认处理器)
-
如该线程有父类线程组,调用父类线程组的方法来处理(父类线程组的,组默认处理器)
-
找该线程实例所属的线程类的默认异常处理器(默认处理器2)
-
如该异常时ThreadDeath的对象,不做任何处理,否则,打印异常跟踪栈,结束线程
-
需要说明的时,异常处理器不同于try……catch,异常处理器在处理完异常之后,该异常还是会传播给调用者
11.8 线程池
线程池的好处
-
系统创建一个新线程的成本比较高,所以可使用线程池来提高性能
-
控制系统中,并发线程的数量,因为线程池规定了最大线程数
线程池在程序启动时,不创建线程,而是程序将一个Runnable对象或Callable对象传递给线程池时,线程池才会创建线程,当run()方法或call()方法执行结束后,该线程不会死亡,而是再次返回线程池中,称为空闲状态,等待下一个Runnable对象或Callable对象(也就是下一个任务)
11.8.1 Java 5实现的线程池
java新增了一个Executors工厂,来产生线程池
以下方法返回一个ExecutorService对象
-
newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程
-
newFixedThreadPool(int):创建一个线程数固定的线程池
-
newSingleThreadExecutor():创建一个只有单线程的线程池
ExecutorService对象代表一个线程池,它可以执行Runnable对象或Callable对象所代表的线程,该线程池代表尽快执行线程,程序只要将一个Runnable对象或Callable对象提交给线程池,该线程池就会尽快执行该任务
ExecutorService提供的方法
-
Future<?> submit(Runnable task)
- 将一个Runnable对象提交给线程池,线程池将在有空闲线程时,执行该线程任务,其中Future对象代表Runnable任务的返回值,又因为Runnable的run()方法没有返回值,所以该Future对象将在run()方法执行结束后,返回null
-
Future submit(Runnable task,T result)
- 将一个Runnable对象提交给指定的线程池,线程池将在有空闲线程时,执行Runnable对象代表的任务,result显式指定线程执行结束后的返回值,所以Future对象将在run()方法执行结束后,返回result
-
Future submit(Callable task)
- 将一个Callable对象提交给指定的线程池,线程池将在有空闲线程时执行Callable对象代表的任务。其中Future代表Callable对象里call()方法的返回值。
以下方法返回一个ScheduledExecutorService线程池
-
newScheduledThreadPool(int):创建一个具有指定线程数的线程池,它可以在指定延迟后,执行线程任务,方法参数为线程池中所保存的线程数
-
newSingleThreadScheduledExecutor():创建只有一个线程的线程池,它可以在指定延迟后,执行线程任务
ScheduleExecutorService线程池,它是ExecutorService的子类,它可以在指定延迟后,或周期性的,执行线程任务
ScheduledExecutorService 提供的方法
-
ScheduledFuture schedule(Callable callable,long delay, TimeUnit unit)
- 指定callable任务将在delay延迟后执行
-
ScheduledFuture<?> schedule(Runnable command,long delay,TimeUnit unit)
- 指定command任务将在delay延迟后执行
-
ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit)
-
指定command任务将在delay延迟后执行,而且以设定频率重复执行,TimeUnit为时间单位
-
在initialDelay后开始执行,依次在initialDelay+period,initialDelay+2*period……处重复执行
-
-
ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit)
- 创建并执行一个在给定初始延迟后,首次启用的定期操作,随后的每一次执行终止,和下一次执行开始之间,都会存在给定的延迟。如果任务在任一次执行时遇到异常,将会取消后续执行,否则,只能通过程序来显式取消或终止该任务
关闭线程池
-
shutdown():当前线程池不再接收新任务,但会将以前所有已提交的任务执行完成,所有任务完成之后,池中的所有线程都会死亡
-
shutdownNow():该方法试图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表
11.8.2 Java 7新增的ForkJoinPool
java7提供了ForkJoinPool,来支持将一个任务拆分成多个”小任务“并行计算(需要cpu为多核),再把多个”小任务“的结果合并为总的计算结果。
ForkJoinPool是ExecutorService的实现类,因此是一种特殊的线程池
ForkJoinPool的构造方法
-
ForkJoinPool(int):创建一个指定数量线程并行的线程池
-
ForkJoinPool():以Runntime.availableProcessors()方法的返回值作为参数,创建一个ForkJoinPool
加载线程任务
-
submit(ForkJoinTask)
-
incoke(ForkJoinTask)
- 该方法中的ForkJoinTask表示一个可以并行,合并的任务
ForkJoinTask是一个抽象类,它有两个抽象子类
-
RecursiveAction:无返回值的任务
-
RecursiveTask:有返回值的任务
以下内容书上没有
自定义线程池
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数解释🌈
corePoolSize——核心线程最大值,不能小于0
maximumPoolSize——最大线程数,不能小于等于0 maximumPoolSize>=corePoolSize
keepAliveTime——空闲线程最大存活时间,不能小于0
unit——时间单位
workQueue——任务队列,不能为null
threadFactory——创建线程工厂,不能为null
handler——任务拒绝策略,不能为null
参数解释
-
unit时间单位在TimeUnit类中定义了常量,直接用这些值就可
-
任务队列也就是阻塞队列,需要用该接口的实现类,也就是
ArrayBlockkingQueue和LinkedBlockingQueue等
-
线程工厂,Executors的defaultThreadFactory方法
-
拒绝策略有四种,是ThreadPoolExecutor的内部类,默认为AbortPolicy()
拒绝策略
-
AbortPolicy —— 丢弃任务并抛出RehectedExecutionException异常,默认
-
DiscardPolicy —— 丢弃任务,不抛出异常
-
DiscardLdestPolicy —— 抛弃 队列中等待最久的任务,然后将当前任务加入队列
-
CallerRunaPolicy —— 调用任务的run()方法,绕过线程池直接执行(直接执行run()方法)
该方法最大能处理线程数为,maximumPoolSize+workQueue
使用该方法更容易对线程池控制,调优,并且该方法在实际开发中更常用
11.9 线程相关类
java提供了ThreadLocal类,它代表一个线程局部变量,通过把数据放在ThreadLocal中,可以让每个线程创建一个该变量的副本,从而避免并发访问的线程安全问题
11.9.1 ThreadLocal类
ThreadLocal,是Thread Local Variable(线程局部变量),该类为每个使用此变量的线程都提供了一个变量值的副本,每个线程独立的修改自己的副本,从而避免冲突问题
ThreadLocal的方法
-
T get():获取线程副本的值
-
void remove():删除此线程局部变量中,当前线程的值
-
void set(T value):设置副本值
使用场景
ThreaLocal隔离了多个线程的数据贡献,避免了多个线程对共享资源的竞争(所以也就不需要同步了),所以如果仅需要隔离多个线程之间的共享冲突,可以考虑使用ThreadLocal
同步机制是为了同步多个线程对相同资源的并发访问,如果多个线程之间需要共享资源,以达到线程之间通信,就要考虑同步机制
11.9.2 包装线程不安全的集合
使用Collections的方法,将集合包装成线程安全的集合,从而保证多线程访问同一集合时,该集合数据的完整性
Collections的静态方法
-
Collection synchronizedCollection(Collection c):返回指定collection对应的,线程安全的collection
-
static List synchronizedList(List list):返回指定List对象,对应的线程安全的版本
-
static<K,V> Map<K,V> synchronizedMap(Map<K,V>m):返回指定Map对象对应的线程安全的Map对象。
-
static Set synchronizedSet(Set s):返回指定Set对象对应的线程安全的Set对象。
-
static<K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m):返回指定SortedMap对象对应的线程安全的SortedMap对象。
-
static SortedSet synchronizedSortedSet(SortedSet s):返回指定SortedSet对象对应的线程安全的SortedSet对象
如果需要把某个集合包装成线程安全的集合,则应该在创建之后立即包装
11.9.3 线程安全的集合类
从java5开始,java在java.util.concurrent包下提供了大量支持高效并发访问的集合接口和实现类,分为两类
- Concurrent开头的集合(支持并发访问)
- ConcurrentHashMap
- ConcurrentSkipListMap
- ConcurrentSkipSet
- ConcurrentLinkedQueue
- ConcurrentLikedDeque
- CopyOnWrite开头的集合
- CopyOnWriteArrayList(读快,写慢)
- CopyOnWriteArraySet
Concurrent开头的类支持并发访问集合,写入的线程的所有操作都是安全的,读取操作不必锁定,以Concurrent开头的集合类,采用了更加复杂的算法来保证,永远不会锁住整个集合
Concurrent类集合的特点
- 当多线程共享访问一个集合时,可使用ConcurrentLinkedQueue,该集合不允许使用null元素,实现了多线程高效访问,多个线程访问时,无需等待
- 当多线程写入时,可用ConcurrentHashMap,该集合支持16个线程并发写入,可通过设置 concurrentcyLevel 构造参数,来支持更多的并发写入线程
- ConcurrentLinkedQueue和ConcurrentHashMap支持多线程并发访问,如果在获取了这两个集合的迭代器后,对集合元素修改,则不会抛出任何异常,迭代器也无法反应,创建迭代器之后所作的修改
Copy类集合特点
- CopyOnWriteArraySet的底层封装了CopyOnWriteArrayList
- CopyOnWriteArrayList采用复制底层数组的方式,来实现写操作
- CopyOnWriteArrayList
- 读会直接读集合本身,无需加锁与阻塞
- 写时,会在底层复制一份当前数组,作为副本,接下来对副本执行写入操作,所以线程安全t
也是因为CopyOnWriteArrayList执行写时,要频繁的复制数组,所以性能较差,但读操作不需要加锁,所以读操作就很快,还安全,所以该集合适合用在缓存等,这种读操作>写操作的场景
后言
我突然有些慰藉,我身边竟然已经有了这么多朋友,他们走到了好远的地方,我们却还保持着联系,是否说明,我也走了很远的路才到达这里,今天下午梦见了我的一位朋友,种种原因我们不再熟络,我常常想,如果当初的那个选择,选另一条路现在会是什么样,现在的我有很多的遗憾,也怀有很多的希望,恐惧,和快乐,迷雾始终围绕在我身边,未来的道路也不知在何方,只是,不敢奢求,也不甘如此,我该如何认识世界,世界又如何对待我,