导图社区 Java多线程编程核心技术
这是一篇关于Java多线程编程核心技术的思维导图
提示: 本内容由社区用户上传并分享。平台不对内容的真实性、合法性、知识产权归属及是否侵害第三方权利进行事前审核或保证。本内容可能包含受版权保护的图片、字体或其他第三方素材,使用前请自行确认授权范围。
Java多线程编程核心技术
Java多线程技能
进程及多线程概述
进程:正在运行的程序,受操作系统管理的基本运行单元
进程负责向操作系统申请资源,一个进程中的多个线程可以共享相同的内存或文件资源
线程:可以理解为在进程中独立运行的子任务
使用多线程有什么好处?可以在同一时间执行多个任务,提升CPU利用率
何时使用多线程?
阻塞,当系统中出现阻塞现象,则可以使用多线程提高效率
依赖,两个业务A和B,当B业务的执行不依赖A业务的结果时可以使用多线程提高效率,否则按顺序执行业务
同步和异步
同步:当调用某个东西时,等待调用返回结果后才继续往下执行
异步:和同步相反,当调用发出后不等待返回结果,继续执行后续操作,被调用者通过回调函数等来处理这个调用
单任务的特点就是同步执行,而使用多线程就是在使用异步
使用多线程
Java中实现多线程编程主要的两种方式
继承Thread类,Thread类实现了Runnable接口,Java语言中是单根继承,为了支持多继承也可以实现Runnable接口,但这两种方式没有本质区别
1.继承Thread类,重写run()方法,该方法中就是线程对象要执行的任务
2.创建线程对象,调用start()方法启动一个线程,线程启动后会自动调用线程对象中的run()方法
main线程执行start()方法时并不会等待以下步骤都执行完毕,而是立即执行start()后面的代码
如果直接调用代码thread.run()而不是thread.start()那么就不是异步执行了,而是同步执行
start()方法执行比较耗时,其步骤如下:
1.通过JVM告诉操作系统创建Thread
2.操作系统开辟内存,使用Windows SDK中的createThread()函数创建Thread对象
3.操作系统对Thread对象进行调度,以确定执行时机
4.Thread在操作系统中被执行
实现Runnable接口
1.实现Runnable接口,重写run()方法
2.使用Thread类构造方法,传入Runnable实现
线程的执行顺序具有随机性,在使用多线程时,代码的运行结果与代码的执行顺序或调用顺序是无关的,执行start()的顺序不代表执行run()的顺序,CPU以一种随机的方式调用run()方法
多线程随机输出的原因是CPU将时间片分给不同线程,线程获得时间片就执行任务,所以线程交替执行,导致结果乱序
时间片:CPU分配给各个程序的时间
CPU在不同线程上切换是需要耗时的,所以线程过多也会降低执行效率
使用常见命令分析线程的信息(位于jdk/bin文件夹下)
jps+jstack.exe
使用jps命令查看Java进程,使用jstack命令查看进程下线程的状态
jmc.exe
jvisualvm.exe
多个线程之间实例变量的共享与不同享
共享数据就是多个线程可以访问同一个变量,非线程安全问题是指多个线程对同一对象的同一实例变量进行操作时,出现值被更改,值不同步而影响程序流程的情况
synchronized关键字,可以对任意对象或方法加锁,让线程以排队的方式进行处理,synchronized加锁的这段代码称为"互斥区"或者"临界区"
当线程要执行同步方法中的代码时,会先去申请这把锁,申请到则执行否则会尝试重复申请,并且多个线程会同时争夺这把锁
Servlet的非线程安全问题
在Web开发中,Servlet是单例的,为了不出现非线程安全问题,尽量不要在Servlet中使用实例变量
i--和println()方法
println()方法是在内部同步的,在进入prinln()方法之前仍然可能出现非线程安全问题
Thread.currentThread(),返回当前代码段正在被哪个线程调用
isAlive(),判断当前线程是否存活,活动状态:线程已经启动且尚未终止的状态为活动状态。如果线程处于正在运行或准备开始运行就认为该线程是"存活"的
注意:如果线程对象是以构造方法的形式传递给Thread对象进行start(),那么和直接建线程对象调用start()运行结果是有差异的,主要是因为this和Thread.currentThread()的差异
sleep(long millis),让当前正在执行的线程(Thread.currentThread()返回的线程)休眠(暂停执行)一段时间(毫秒)
sleep(long millis,int nanos)让当前正在执行的线程休眠一段时间(毫秒数+纳秒数),此操作受操作系统计时器和调度程序的精度和准确度的影响
StackTraceElement getStackTrace() ,返回一个表示该线程堆栈跟踪的数组,如果该线程未启动或已终止则返回一个零长度的数组,否则数组第一个元素代表栈顶,最后一个元素代表栈底
static void dumpStack(),将当前线程的堆栈跟踪信息输出至标准错误流,一般仅用于调试
static Map<Thread,StackTraceElement[]> getAllStackTrace(),返回所有活动线程的堆栈跟踪信息,键为线程,值为对应的StackTraceElement[],调用该方法时线程可能正在执行,堆栈跟踪仅代表一个快照
getId(),返回线程的唯一标识
停止线程,停止一个线程意味着线程在处理完任务之前停止正在做的操作,也就是放弃当前操作
Java中停止线程的3种方式
1.使用退出标志使线程正常退出
2.使用stop()方法强行终止线程,此方法已弃用,使用它们可能会引起不可预料的结果
该方法已弃用,由于被停止的线程不能确定在哪里被停止了,容易造成数据不一致
调用stop()方法时会抛出java.lang.ThreadDeath异常,但一般不需要显式的捕捉
释放锁会使数据得不到同步处理,进而使数据不一致,stop()方法释放锁就有可能出现数据不一致的情况
3.使用interrupt()方法中断线程
调用interrupt()方法,仅仅是在当前线程中做了一个停止标记,并不是真正的停止线程
如何判断线程是否已经停止?
public static boolean interrupted(),测试当前线程(Thread.currentThread()返回的线程)是否已经中断,此方法会清除线程的中断状态标志,也就是说如果连续两次调用此方法,第二次调用将返回false
public boolean isInterrupted(),测试线程对象是否已经中断,此方法不会清除中断状态标志
异常法,在run()方法中使用isInterruped()方法判断线程的状态并抛出异常以停止线程
在sleep状态下停止线程,不管先调用interrupt()还是先调用sleep(),只要这两个方法碰到一起便会抛出异常,并清除中断状态标志
使用return;和interrupt()方法停止线程的缺点及解决方案
使用interrupt()方法结合return;语句也可以停止线程,但此实现方式不利于异常信息的统一处理
使用异常法在catch块中统一处理
暂停线程,暂停线程意味着此线程还可以恢复运行
suspend()和resume(),suspend()方法用来暂停线程,线程对象并不销毁,只是在当前执行到的代码处暂停,将来还可以恢复。resume()方法用来恢复线程的执行。这两个方法是已经过期的
缺点
独占
如果这两个方法使用不当,容易造成公共同步对象被独占,其他线程无法访问公共同步对象的情况,即一个线程获得锁后永远的suspend了,则其他线程就无法获得这把锁了
当main线程销毁,而某个线程一直处于暂停状态,则进程也不会销毁
当执行到System.out.println()方法内部时,线程暂停,会导致println()方法内部的锁未释放,而其他需要执行println()方法的线程一直无法获得锁
数据不完整,当一个线程正在修改共享的数据但是还没有修改完成时,该线程被暂停,则其他线程访问该共享的数据时,会出现数据不完整的情况
yield(),放弃当前CPU资源,让其他任务去占用CPU的时间,放弃的时间不确定,有可能刚放弃,马上又获得了CPU时间片
线程的优先级,线程可以划分优先级,优先级高的线程可以获得更多的CPU时间片
使用setPriority()方法设置线程优先级
线程优先级为1-10,小于1或者大于10,则抛出IllegalArgumentException异常
JDK使用三个常量预置优先级的值
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
线程优先级具有继承性,A线程启动B线程,则B线程的优先级和A线程是一样的
优先级的规律性:优先级高的线程总是大部分先执行完,但不代表优先级高的线程全部先执行完;当线程的优先级等级差距很大时,谁先执行完和调用顺序无关
优先级的随机性:优先级较高的线程不一定每次都先执行完run()中的任务,它们具有随机性,不确定性
优先级对线程运行速度的影响:优先级高的线程运行速度快
守护线程,Java中有两种线程,用户线程(也称非守护线程)和守护线程,当进程中不存在非守护线程时,守护线程自动销毁。守护Daemon线程主要是为其他线程的运行提供便利服务
setDaemon(true),设置线程为守护线程
要在调用start()方法之前调用setDaemon()方法,否则会抛出IllegalThreadStateException异常
对象及变量的并发访问
synchronized同步方法,synchronized关键字可以保障原子性、可见性和有序性
方法内的变量不存在非线程安全问题,是线程安全的,因为方法内部的变量具有私有性
实例变量非线程安全问题与解决方案
多个线程同时访问对象的实例变量,则有可能出现非线程安全问题,可使用synchronized加锁,多个线程访问同一个对象的同步方法时,一定是线程安全的
synchronized在字节码指令中的原理
synchronized修饰方法时,使用了flag标记ACC_SYNCHRONIZED,调用指令检查如果设置了此标志,则执行线程必须先持有锁,并且在执行完后释放锁
synchronized同步代码块,则是使用了monitorenter和monitorexit指令进行同步处理
同步:按顺序执行A和B两个业务。异步:A业务执行时,B业务也在同时执行
多个对象多个锁
当多个线程访问同一个对象的同步方法时,线程和业务对象是多对一的关系,为了避免出现非线程安全问题,所以使用synchronized加锁
synchronized取得的锁是对象锁,多个线程访问同一个对象的同步代码时才会有锁争抢的情况,如果是多个线程都访问自己所属的业务对象的方法,即线程和业务对象是一对一的关系,则JVM会创建多个锁,不存在锁争抢的情况,这种情况下业务对象代码加不加synchronized都是线程安全的
将synchronized方法与对象作为锁
synchronized关键字修饰的方法一定是排队运行的
只有共享的资源读写访问才需要同步,如果不是共享的资源,则没有必要同步
对象作为锁时
1.A线程先持有object对象的Lock锁,此时B线程可以以异步的方式调用object对象的非synchronized方法,也就是不需要等待
2.A线程先持有object对象的Lock锁,B线程如果此时也调用object对象的synchronized方法,则需要等待,也就是同步
3.synchronized修饰方法不是锁方法,而是锁当前类的对象
4.Java中只有“将对象作为锁”的说法,没有“锁方法”这种说法
5.Java中,锁就是对象,对象可以映射成锁,哪个线程拿到锁,就可以执行该对象的synchronized方法
6.如果在X对象中使用synchronized修饰非静态方法,则X对象就被当成锁
脏读(dirty read)
读取变量时,变量的值已经被其他线程更改过了
原因:当A线程调用对象的synchronized方法X修改值时,A线程就获得了X所在对象的锁,但此时其他线程仍然可以调用其他非synchronized方法获取值
解决:在设置值和取值方法上加上synchronized同步关键字
多个线程调用同一个业务对象的不同synchronized方法时,是按同步的顺序调用的,因为非静态的synchronized方法的锁是当前对象
synchronized锁重入
synchronized拥有重入锁功能,当一个线程获得了对象锁后,再次请求此对象锁是可以获得该对象锁的。即一个线程再次获取自己内部的锁(该线程已获得该锁并且尚未释放)
synchronized方法/块内部调用本类的其他synchronized方法/块时,是永远可以得到锁的
锁重入支持继承环境,即子类可以通过锁重入调用父类的同步方法
出现异常,锁自动释放
当一个线程执行的代码出现异常时,其所持有的锁自动释放
注意:Thread.suspend()和sleep(millis)并不释放锁
重写方法时,如果不使用synchronized关键字则是非同步方法,使用后则是同步方法
public static native boolean holdsLock(Object obj),Thread.currentThread()在指定的对象(obj)上保持锁定时才返回true
synchronized同步语句块,synchronized方法将当前对象作为锁,而synchronized代码块将任意对象作为锁。锁可以看做一个标识,哪个线程持有这个标识,就可以执行同步方法
synchronized方法的弊端:当一个线程调用同步方法执行一个长时间的任务时,其他需要调用该方法的线程等待的时间比较长
synchronized同步代码块的使用
synchronized(锁对象){需要同步的代码}
当两个并发线程访问同一个对象中的synchronized(this)同步代码块时,一段时间内只能有一个线程得到执行,另一个线程必须等待当前线程执行完这个代码块后才能执行该代码块
当一个线程访问object的synchronized代码块时,另一个线程仍然可以访问该object对象中的非synchronized(this)代码块
一个线程访问object的synchronized(this)代码块时,其他线程访问同一个object中的其他synchronized(this)代码块时会被阻塞,因为使用的锁是同一个
println()方法中使用了synchronized(this)同步代码块,保证了输出的数据是完整的,不会出现信息交叉混乱的情况
和synchronized方法一样,synchronized(this)代码块也是锁定当前对象的
将任意对象作为锁
synchronized方法或synchronized(this)块会使同一个对象其他synchronized方法或synchronized(this)块的调用呈同步效果,并且同一时间只有一个线程可以执行synchronized方法/块中的代码
synchronized代码块还支持将任意对象作为锁
优点:当对象有多个synchronized方法,会出现争抢this对象锁,影响效率,而如果使用其他对象,则synchronized(非this对象)会与synchronized方法异步执行,因为不是同一把锁
多个锁就是异步执行,使用synchronized同步代码块时,必须是同一把锁,否则就是异步执行
synchronized块可以保证同步块中的代码是顺序执行的,但不能保证其所在的非synchronized方法被多个线程调用的顺序,方法被线程调用的时机是随机的
synchronized(x)得出的三个结论
1.当多个线程同时执行synchronized(x)同步代码块时呈同步效果
2.当其他线程执行x对象中的synchronized方法时呈同步效果
3.当其他线程执行x对象方法里面的synchronized(this)同步代码块时呈同步效果
静态同步synchronized方法和synchronized(class)代码块
synchronized关键字修饰静态方法时,锁对象是静态方法所在类的Class对象,并且Class对象是单例的
Class对象锁对类的所有实例都起作用
synchronized(类名.class)代码块对所有的对象实例都起作用
当使用synchronized(String)时应注意String的常量池会有缓存,多个线程使用相同的锁,可能会导致一个线程持有锁后未释放,其他线程一直无法执行的情况。所以大多数情况下synchronized代码块不使用String作为锁对象,可以使用new Object()等
使用实例对象的synchronized方法时(锁对象为this),可能会出现一个线程获取锁后执行一个耗时较长的任务而一直没有释放锁,导致其他使用该锁的线程一直处于等待状态,此时可以使用synchronized代码块创建不同的锁对象来解决
多线程的死锁,双方互相持有对方的锁,并且等待对方释放锁,不同的线程都在等待不可能释放的锁
使用jps+jstack查看
死锁的四个必要条件,同时满足这四个条件,才会发生死锁
1.互斥,一个资源只能同时被一个线程使用
2.请求与保持,请求第二把锁的时候,保持自身的锁不释放
3.不剥夺,获得锁后,没有外界力量剥夺锁
4.循环等待,你等我,我等你或头尾相接的等待,即A等B,B等C,C等A
避免死锁的一些方法
1.保证线程按照一定的顺序获得锁
2.获取锁时设置超时时间,超过时限就放弃对该锁的请求,并释放自己已获得的锁
内部类和同步
内部类中的同步方法,使用不同的锁,也是异步执行的
synchronized(lock),对lock对象上锁后,其他线程只能以同步的方式调用lock对象中的同步方法
锁对象改变导致异步执行
如果多个线程同时争抢相同的多对象,则这些线程之间就是同步的,如果每个线程分别获得自己的锁,则这些线程之间是异步的
如果锁对象在线程执行过程中被改变,会造成多个线程的锁对象不同,导致线程异步执行
所以一旦持有锁对象尽量不要更改(对象的属性可以修改,只要对象不变)
synchronized关键字的几种写法比较
volatile关键字
可见性
A线程修改变量的值后,B线程马上就能看到修改后的值
volatile关键字强制线程使用变量时从主内存取值
synchronized关键字也具有使线程工作内存中的私有变量与公共内存中的变量同步的特性
原子性
在32位系统中,未使用volatile修饰的double和long数据类型没有实现写原子性,如果想实现则需要使用volatile修饰。在64位系统中,原子性取决于具体的实现,在X86架构64位JDK版本中,写double或long是原子的。
volatile不支持原子性,多个线程对volatile变量i执行i++时会出现非线程安全问题
i++操作
1.从内存中取出i的值
2.计算i值
3.将i的值写到内存中
Atomic原子类实现i++操作的原子性
原子操作是一个不能分割的整体,没有其他线程可以中断或者检查处于原子操作中的变量
一个原子类型就是一个原子操作可用的类型,它可以在没有锁的情况下做到线程安全
Atomic原子类型只能确保单个变量的原子操作
禁止代码重排序
Java程序运行时,JIT(Just-In-Time-Compiler,即时编译器),可以动态改变代码的运行顺序,以提升运行效率
重排序发生在没有依赖关系时,比如代码A、B、C、D,B、C、D不依赖于A的结果,C、D不依赖于代码A、B的结果,D不依赖于代码A、B、C的结果;这种情况下就会发生代码重排序,如果代码之间有依赖关系,则不会发生重排序
使用volatile关键字可以禁止代码重排序
volatile变量之前的和之后的代码都可以重排序
volatile变量之前的代码不能重排到volatile变量之后
volatile变量之后的代码不能重排到volatile变量之前
synchronized之前的代码不能重排到synchronized之后
synchronized之后的代码不能重排到synchronized之前
总结
synchronized保证同一时刻只有一个线程执行某个方法或代码块,保证了可见性、原子性和禁止代码重排序
使用场景:多个线程对同一对象的同一个实例变量进行修改时,为了避免出现非线程安全问题,就使用synchronized
volatile,保证可以让其他线程获取到最新的值
可见性:B线程马上能看到A线程修改的数据
原子性:在32位系统中,针对未使用volatile声明的long或double数据类型没有实现写原子性,如果想实现,则声明变量时添加volatile,而在64位系统中,原子性取决于具体的实现,在X86架构64位JDK版本中,写double或long是原子的。另外,针对用volatile声明的int i变量进行i++操作时是非原子的
volatile禁止代码重排序
使用场景:当一个变量的值被更改时,让其他线程能取到最新的值
线程间通信
wait/notify机制
使用while(true)语句轮询机制实现线程间的通信比较浪费CPU资源或因轮询间隔时间较长而取不到想要的数据,所以需要引入wait/notify(等待/通知)机制减少CPU资源的浪费并实现线程间的通信
在此过程中出现了wait/notify机制
wait/notify机制原理
前提:拥有相同锁的线程才可以实现wait/notify机制
wait()方法,是Object类的方法,使当前执行wait()方法的线程在wait()所在的代码行处等待(暂停执行)并释放锁,直到接到通知或被中断。
调用wait()时线程需要先获得该对象的锁,即只能在synchronized方法或synchronized块中调用wait()
通过通知机制使某个线程继续执行wait()之后的代码时,对线程选择是按执行wait()的顺序确定的,并且需要重新获得锁。如果调用wait()时没有获得适当的锁,则抛出IllegalMonitorStateException异常
notify()方法,通知暂停的线程继续执行
调用notify()时必须获得锁,即必须在synchronized方法或synchronized块中执行,如果调用notify()时没有获得适当的锁则抛出IllegalMonitorStateException异常
notify()按照执行wait()的顺序向等待该锁的线程发送一个通知(notify),然后在执行notify()方法的线程退出synchronized区域后,处于wait状态的线程重新获取锁。当第一个获取到锁的wait线程运行完毕并释放锁后,此时如果没有再次调用notify(),那么其他处于wait状态的线程将因为没有得到通知而继续处于wait状态
notify()方法只会按照调用wait()方法的顺序唤醒等待同一个锁的一个线程,使其进入可运行状态,即notify()方法仅通知一个线程
notifyAll()方法会按照调用wait()方法的相反顺序依次唤醒全部线程
线程状态的切换
创建线程对象调用start()方法,系统为线程分配CPU资源此时线程处于runnable(可运行状态),这是一个准备运行的阶段
线程抢到CPU资源进入running(运行状态)
runnable和running状态可互相切换,线程进入runnable大体分为4种情况
1.调用sleep()方法到达指定的休眠时间后
2.线程成功获得了试图同步的监视器
3.线程正在等待某个通知,其他线程发出了通知
4.处于挂起状态的线程调用了resume()恢复方法
blocked(阻塞状态)例如遇到I/O操作,线程由runnable转为blocked状态,等待I/O的结果,此时操作系统会将CPU时间片分配给其他线程,等I/O操作结束后,线程由blocked转为runnable
出现阻塞的情况大体分为5种
1.调用sleep()方法主动放弃占用的CPU资源
2.调用了阻塞式的I/O方法,在该方法返回前,线程被阻塞
3.线程尝试获取被其他线程持有的同步监视器(锁)
4.线程等待某个通知(notify)
5.程序调用了suspend()方法将线程挂起,此方法容易导致死锁,应尽量避免使用
run方法运行结束后进入销毁阶段,整个线程执行完毕
执行wait()方法后锁被立即释放,sleep()方法不释放锁,notify()方法不立即释放锁,需要等该方法所在的synchronized方法/块执行完毕后释放
当线程调用wait()方法后,再对该线程调用interrupt()方法会出现InterruptedException异常
唤醒线程的顺序依赖于JVM的实现
wait(long timeout),等待某段时间内(单位为毫秒)是否有线程对锁进行notify唤醒,超时自动唤醒,继续向下执行时需要再次持有锁,如果没有锁则一直等待,直到有锁为止
通知过早问题:先执行notify()后执行wait(),则wait()方法永远不会被通知,可以使用一个标记,如果已经先通知过wait(),那么wait()方法就没有执行的必要了
wait条件发生变化与使用while的必要性
条件发生改变时没有得到及时响应,多个呈waiting状态的线程被唤醒继续执行代码导致出现异常
生产者/消费者模式的实现
一生产者与一消费者:操作值
多个生产者与多个消费者:操作值(假死)
假死:全部线程都进入waiting状态,程序不再执行任务业务功能,整个项目呈停止状态
为何出现假死状态?因为无法保证notify()唤醒的都是“同类”,有可能出现生产者唤醒生产者,消费者唤醒消费者,这种情况积少成多就会使所有线程进入waiting状态
如何解决假死?发送通知时不只通知“同类”也通知“异类”,即将notify()换成notifyAll()
一生产者与一消费者:操作栈
生产者向堆栈List中存数据,消费者从堆栈List中取数据
一生产者与多消费者:操作栈
使用while解决wait条件改变问题,使用notifyAll()解决假死问题
多生产者与一消费者:操作栈
多生产者与多消费者:操作栈
结论:想要实现任意数量的几对几的生产和消费实例,可以使用while结合notifyAll()实现,这种组合具有通用性
允许连续生产多个:允许连续消费多个
通过管道进行线程间通信,Java提供了各种输入/输出流,其中管道流(pipe stream)是一种特殊的流,用于在不同的线程间直接传送数据,一个线程发送数据到输出管道,另一个线程从输入管道中读取数据
字节流
PipedInputStream
PipedOutputStream
字符流
PipedReader
PipedWriter
实现wait/notify的交叉备份
使用一个标志和while循环+notifyAll()实现两个线程交替执行备份
join()方法的使用
join()方法的作用是等待线程对象销毁,如果主线程创建并启动子线程,子线程执行耗时任务,主线程往往早于子线程结束,如果主线程需要等待子线程的执行结果时,就可以使用join()方法。
join()方法使所属的线程对象x正常执行run()方法,使当前线程z无限期阻塞,等待x对象销毁后继续执行z后面的代码
join()方法具有使线程排队运行的效果,类似同步,但join()内部使用wait(),而synchronized使用锁
join()方法遇到interrupt()方法,不论顺序,会使当前线程抛出InterruptedException异常
join(long millis),x.join(毫秒)等待指定的时间后,不管x线程是否执行完毕,当前线程获得锁后继续向后执行。如果没有获得锁,则一直尝试获取,直到获得锁为止
join(long millis)和sleep(long millis)的区别
join(long millis)方法内部使用wait(long timeout)实现,所以具有释放锁的特点
sleep(long millis)方法不释放锁
join()或join(long millis)方法执行后锁立即被释放
join(long millis)方法是synchronized方法,即在执行时需要获得锁,当join(long millis)在线程执行之前先获得锁并在内部执行wait(long timeout)时,可能会出现意外的打印顺序
join(long millis,int nanos),等待指定的时长为毫秒+纳秒,nanos<0或nanos>999999,则抛出IllegalArgumentException nanosecond timeout value out of range
ThreadLocal类的使用
变量值的共享可以使用public static变量的形式实现,此时所有的线程都使用同一个变量,ThreadLocal可以实现变量在不同线程的隔离性,也就是不同的线程拥有自己的值,不同线程的值是可以通过ThreadLocal类进行保存的
ThreadLocal的主要作用是将数据放入当前线程对象的Map中,这个Map是Thread类的实例变量。ThreadLocal自己不管理、不存储任何数据只是将数据放入Map中,流程:数据-->ThreadLocal-->currentThread()-->Map
每个线程中的Map只存储自己的数据并且其他线程不可访问,key为ThreadLocal对象,value为值,由于Map的key不可重复,所以一个ThreadLocal对象对应一个value
get()方法与null,如果从未在Thread中的Map中存储ThreadLocal对象对应的value,则get()方法返回null
ThreadLocal类存取数据流程分析
可以继承ThreadLocal类重写initialValue()方法指定get()方法返回值为null时的默认值,子线程和父线程各自拥有自己的默认值
InheritableThreadLocal类的使用
使用InheritableThreadLocal可使子线程继承父线程(线程A创建线程B,则A是B的父线程)的值
值继承特性在源代码中的执行流程
1.InheritableThreadLocal继承了ThreadLocal重写了三个方法
2.InheritableThreadLocal的set()方法是调用的父类的set()方法,但set()方法中的getMap(Thread t)和createMap(Thread t, T firstValue)是调用的本类重写的方法
3.getMap(Thread t)和createMap(Thread t,T firstValue)返回和创建的是ThreadLocal.ThreadLocalMap inheritableThreadLocals对象,由此可知set()方法是向inheritableThreadLocals中存入数据
4.在创建子线程时,在Thread()构造方法中调用init()方法,在init()方法中调用Thread.createInheritedMap()方法初始化子线程的inheritableThreadLocals
5.在ThreadLocalMap(parentMap)中将父线程的Entry[] table对象赋值给子线程的table对象。
上述过程是在创建子线程时完成的,也就是说当父线程设置新的值时,子线程使用的还是父线程的旧值,子线程设置新的值时,父线程仍是旧值
子线程从父线程继承可变对象数据类型时,子线程可以取到对象最新的数据(因为子线程从父线程复制数据时,复制的是对象的引用)
可以重写childValue()方法对继承的值进行加工,childValue()方法仅在子线程创建时调用一次
Lock对象的使用
ReentrantLock类的使用
JDK5新增的ReentrantLock类也可以实现线程间同步,并且在拓展功能上更强大
调用ReentrantLock对象的lock()方法获得锁,调用unlock()方法释放锁,这两个方法成对使用,将需要同步的代码放在lock()和unlock()之间即可
多个方法或代码块使用同一个ReentrantLock对象锁时,行为和synchronized关键字相同,哪个线程持有锁,哪个线程就执行业务,等待锁被释放时再次争抢锁,线程之间执行的顺序是随机的
ReentrantLock借助于Condition也可以实现wait/notify模式
一个Lock对象可以创建多个Condition实例,线程对象注册在指定的Condition中,可以实现有选择性的进行线程通知,在调度线程上更加灵活
Condition对象的作用是控制并处理线程的状态,可以使线程进入wait状态,也可以使线程继续运行
condition.await(),使当前线程在接到通知或被中断之前一直处于wait状态。相当于wait()
await()方法内部调用了Unsafe类的public native void park(boolean isAbsolute,long time)方法实现了暂停线程
await(long time,TimeUnit unit)相当于wait(long timeout)
condition.signal(),唤醒等待的线程。相当于notify()
signalAll()相当于notifyAll()
在调用condition.await()之前必须调用lock.lock()获得锁,否则抛出IllegalMonitorStateException
使用Condition唤醒部分指定的线程
使用多个Condition对象将线程分组,从而可以唤醒指定组的线程
生产者/消费者模式一对一交替输出
生产者/消费者模式多对多交替输出
使用while和signalAll()组合解决程序假死问题
公平锁与非公平锁
公平锁:先到先得策略,获取锁之前先检查队列中有没有排队等待的线程,没有则尝试获取锁,有则将当前线程加入队列中等待
new ReentrantLock(true)
非公平锁:“有机会插队策略”线程先尝试获取锁,如果获取成功,则说明线程虽然是后启动的,但是先获得了锁,如果获取锁没有成功则将自身加入到队列中等待
默认为非公平锁new ReentrantLock()或new ReentrantLock(false)
public int getHoldCount():返回”当前线程“保持此锁定的个数,即调用lock()方法的次数
public final int getQueueLength():返回正在等待此锁的线程估计数
public int getWaitQueueLength(Condition condition):返回等待与此锁相关的给定条件的Condition的线程估计数
public final boolean getQueuedThread(Thread thread):查询指定的线程是否在等待获取此锁,即判断参数中的线程是否在等待队列中
public final boolean hasQueuedThreads():查询是否有线程在等待此锁,即等待队列中是否有正在等待的线程
public boolean hasWaiters(Condition condition):查询是否有线程正在等待与此锁有关的Condition条件,即是否有线程执行了Condition中的await()而呈等待状态
public final boolean isFair():判断是不是公平锁
public boolean isHeldByCurrentThread():查询当前线程是否保持此锁
public boolean isLocked():查询此锁是否被任意线程保持,并且尚未释放
public void lockInterruptibly():当某个线程尝试获取锁并阻塞在lockInterruptibly()方法时,该线程可以被中断,即抛出InterruptException异常
public boolean tryLock():嗅探拿锁,如果线程发现锁被其他线程持有了,则返回false,程序继续向下执行,而不是阻塞等待锁
public boolean tryLock(long timeout,TimeUnit unit):嗅探拿锁,如果在指定的时间内获得锁,则返回true,超时则返回false
public boolean await(long time,TimeUnit unit):同wait(long timeout)方法,超时自动唤醒线程
public boolean awaitNanos(long nanosTimeout):同wait(long timeout),超时自动唤醒线程,时间单位为纳秒(ns)
public boolean awaitUntil(Date deadLine):在指定的Date结束等待,在等待时间到达前也可以被其他线程提前唤醒
public void awaitUninterruptibly():线程在等待的过程中,不允许被中断,调用此方法等待后,即使调用interrupt()也不会抛出InterruptedException
ReentrantReadWriteLock类的使用
ReentrantLock具有互斥排他的效果,同一时间只有一个线程在执行ReentrantLock.lock()方法后面的任务,这样做保证了线程安全性,但是效率较低
ReentrantLock的缺点是对于读操作也会同步,这样会耗费大量时间,降低运行效率
ReentrantReadWriteLock是一种读写锁,可以在读操作时不需要同步执行,提高运行效率
读写锁有两个锁
读操作相关的锁,也称共享锁
写操作相关的锁,也称排他锁
读操作是指读取实例变量的值,写操作是指向实例变量写入值
互斥与不互斥
读写互斥
写读互斥
写写互斥
读读异步
只要出现写锁,就会出现互斥同步的效果
readLock():读锁,允许多个线程异步执行lock()方法后面的代码
lock()
unlock()
writeLock():写锁,同一时间只允许一个线程执行lock()方法后面的代码
lock()
unlock()
拾遗增补
线程的状态
线程对象在不同的运行时期有不同的状态,状态信息存在于State枚举中
线程的几个状态
NEW:线程尚未启动
RUNNABLE:正在Java虚拟机中执行
BLOCKED:受阻塞并等待某个锁
WAITING:无限期的等待另一线程执行某个操作
TIMED_WAITING:等待另一个线程执行取决于等待指定时间的操作
TERMINATED:线程已退出
线程组
为了方便管理线程,可以将具有相同功能的线程归到一个线程组中,线程组中既可以有线程对象,也可以有线程组
线程对象关联线程组
一级关联:父对象关联子对象
多级关联:父对象关联子对象,子对象再关联子对象
线程组自动归属特性
创建一个线程组时,如果不指定所属线程组,则自动归到当前线程对象所属的线程组中
获取父线程组
在线程组中添加子线程组
使用线程组批量停止线程
递归与非递归取得组内的对象
public int enumerate(ThreadGroup list[], boolean recurse),recurse为true表示递归获取,false则不是递归获取
Thread.activeCount():返回当前线程的线程组中的活动线程的数量
Thread.enumerate(Thread[] tarray):将当前线程的线程组及子组中的每一个活动线程复制到数组中
单例SimpleDateFormat在多线程环境下会出现转换错误
解决方案1:创建多个SimpleDateFormat实例
解决方案2:使用ThreadLocal存储SimpleDateFormat实例
线程中出现异常的处理
当多个线程中出现异常时,需要在每一个线程的run()方法中使用catch语句处理,容易造成代码冗余,此时可以使用setDefaultUncaughtExceptionHandler()和setUncaughtExceptionHandler()方法集中处理线程的异常
public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh):给指定的线程设置默认的异常处理器
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh):对指定线程类的所有线程对象设置默认的异常处理器
UncaughtExceptionHandler接口的作用是当线程中出现异常而终止时,JVM会调用void uncaughtException(Thread t,Throwable e)方法来处理异常
如果线程的run()方法中使用catch捕捉了异常,则uncaughtException(Thread t,Throwable e)方法不会执行
线程组内处理异常
默认情况下,线程组中的一个线程出现异常不会影响其他线程的运行
如何实现线程组中一个线程出现异常后,该组中的其他线程也都停止?
线程异常处理的优先性
如果调用setUncaughtExceptionHandler()方法设置了异常处理器,则此异常处理器优先处理,其他异常处理器不再处理
单例模式与多线程
立即加载/饿汉模式
缺点:不能有其他实例变量,因为getInstance()方法没有同步,可能会出现非线程安全问题
延迟加载/懒汉模式
缺点:在多线程环境中可能出现创建出了多个实例的情况
解决方案
1.使用synchronized关键字修饰getInstance()方法可以保证多个线程获取的是相同的实例,但是此方式是同步运行,效率较低
2.使用同步代码块对整个方法体进行同步,效率较低
3.对重要的代码单独进行同步,但在多线程环境下还是会出现非线程安全问题
4.使用DCL(Double-Check Locking,双检查锁)机制
DCL使用volatile关键字的必要性
MyObject object = new MyObject(),这一步可能发生代码重排序
使用静态内部类实现单例模式,也可以解决非线程安全问题
序列化与反序列化的单例模式实现
当单例的对象序列化时,使用默认的反序列化行为取出的对象是多例的
protected Object readResolve()方法的作用是在反序列化时不创建新的MyObject对象,而是复用JVM内存中原有的MyObject单例对象
注意:该方法保证的是在进一个JVM中序列化和反序列化时MyObject对象的单例性
使用static代码块实现单例模式
使用enum枚举类型实现单例模式
定时器Timer
定时器Timer的使用
Timer类的主要作用是设置计划任务,即在指定的时间开始执行某一个任务
public void schedule(TimerTask task,Date time):在指定日期执行一次某一任务
public void schedule(TimerTask task,Date firstTime,long period),在指定的日期后按指定的间隔时间(毫秒)周期性的执行某一任务
public void schedule(TimerTask task,long delay):以当前时间为参考时间,延迟指定的毫秒数后执行一次Task任务
public void schedule(TimerTask task,long delay,long period):以当前时间为参考,延迟指定的毫秒数后按指定的间隔时间(毫秒)周期性的执行某一任务
public void scheduleAtFixedRate(TimerTask task,Date firstTime,long period)
public void scheduleAtFixedRate(TimerTask task,long delay,long period)
此方法具有追赶执行性,也就是说如果指定的时间是已经过去的时间,该方法会将之前没有执行的任务追加执行,即指定时间段内的运行次数必须完整
1.如果计划时间早于当前时间,则立即执行Task任务 2.TimerTask以队列的方式顺序执行,执行时间可能与预期的时间不一致,因为有可能前面的任务耗时较长,导致后面的任务执行时间被延后,这是因为一个Timer创建了一个TimerThread,一个TimerThread管理一个队列,按顺序执行Task 3.如果任务不延时则下次任务开始时间为上次任务的开始时间+period(或delay)如果任务延时则下次任务的开始时间为上次任务的结束时间(“不延时”是指任务的执行时间小于period间隔时间)
创建Timer对象时,Timer对象内部创建了一个非守护线程TimerThread用来执行计划任务,当计划任务执行完后TimerThread并未销毁
public void cancel():终止当前计时器,丢弃所有已安排的任务(清空任务队列中的所有任务),此操作不会影响正在执行的任务,此操作也会使它的执行线程终止,无法继续安排更多任务
注意:在此计时器(Timer)调用的计时器任务(TimerTask)的run()方法中调用cancel()可以确保当前任务是此计时器执行的最后一个任务。此方法可重复调用,但第二次和后续调用无效
cancel()有时并不会停止计划任务,即计划任务正常执行,因为cancel()有时可能没有抢到queue锁
间隔执行Task任务的算法
三个任务A、B、C,每次将最后一个Task任务放入队头,然后执行队头中Task任务的run()方法
TimerTask是一个抽象类,封装任务,将要执行计划任务的代码放入TimerTask的子类中
public boolean cancel():将自身从任务队列中清除