Java多线程编程核心技术
文章目录
线程概述
-
进程
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是操作系统进行资源分配与调度的基本单位。
可以把进程简单的理解为正在操作系统中运行的一个程序。
-
线程
线程(Thread)是进程的一个执行单元,是系统调度和执行的最小单位。
-
主线程和子线程
JVM启动时会创建一个主线程,该主线程负责执行main方法,主线程就是运行main方法的线程
线程的常用方法
currentThread(); //返回对当前正在执行的线程对象的引用
getName(); //返回此线程的名称
setName(String name); //将此线程的名称更改为等于参数name
isAlive(); //测试这个线程是否活着
sleep(long millis); //让当前线程休眠指定的毫秒数
getId(); //返回此线程的唯一标识
yield(); //使当前线程放弃当前CPU资源
setPriority(int newPriority); //更改此线程的优先级,默认优先级为5,优先级具有继承性
interrupt(); //中断线程(仅仅是在当前线程打一个停止标志,并不是真正的停止线程)
isInterrupted(); //判断线程的中断标志
setDaemon(); //将此线程标记为守护线程或用户线程
join(); //等待此线程终止
线程的生命周期
多线程编程的优势与存在的风险
-
多线程编程具有以下优势:
- 提高系统的吞吐率(Throughout)多线程编程可以使一个进程有多个并发(concurrent,即同时进行的)的操作
- 提高响应性(Responsiveness)Web服务器采用一些专门的线程负责用户的请求处理,缩短了用户的等待时间
- 充分利用多核(Multicore)处理器资源,通过多线程可以充分的利用CPU资源
-
多线程编程存在的问题与风险:
- 线程安全(Thread safe)问题,多线程共享数据时,如果没有采取正确的并发访问控制措施,就可能会产生数据一致性问题,如读取脏数据(过期数据),如丢失数据更新。
- 线程活性(thread liveness)问题,由于程序自身的缺陷或者由资源缺稀性导致线程一直处于非RUNNABLE状态,这就是线程活性问题,常见的活性故障有以下几种:死锁、锁死、活锁、饥饿
- 上下文切换(Context Switch)处理器从执行一个线程切换到执行另外一个线程
- 可靠性,可能会由一个线程导致JVM意外终止,其他的线程也无法执行
线程安全问题
非线程安全主要是指多个线程对同一个对象的实例变量进行操作时,会出现值被更改,值不同步的情况。
线程安全问题表现为三个方面:
-
原子性
-
原子(Atomic)就是不可分割的意思,原子操作的不可分割有两层含义:
- 访问(读,写)某个共享变量的操作从其他线程来看,该操作要么已经执行完毕,要么尚未发生,即其他线程看不到当前操作的中间结果
- 访问同一组共享变量的原子操作是不能够交错的
-
java有两种方式实现原子性:一种是使用锁,另一种利用处理器的CAS(Compare and Swap)指令:
- 锁具有排它性,保证共享变量在某一时刻只能被一个线程访问
- CAS指令直接在硬件(处理器和内存)层次上实现,看作是硬件锁
-
-
可见性
- 在多线程环境中,一个线程对某个共享变量进行更新之后,后续其他的线程可能无法立即读到这个更新的结果,这就是线程安全问题的另外一种形式:可见性(visibility)
- 如果一个线程对共享变量更新后,后续访问该变量的其他线程可以读到更新的结果,称这个线程对共享变量的更新对其他线程可见,否则称这个线程对共享变量的更新对其他线程不可见
- 多线程程序因为可见性问题可能会导致其他线程读取到了旧数据(脏数据)
-
有序性
- 有序性(Ordering)是指在什么情况下一个处理器上运行的一个线程所执行的内存访问操作在另外一个处理器运行的其他线程看来是乱序的。
- 乱序是指内存访问操作的顺序看起来发生了变化。
-
重排序
- 在多核处理器的环境下,编写的顺序结构,这种操作执行的顺序可能是没有保障的:
- 编译器可能会改变两个操作的先后顺序;
- 处理器也可能不会按照目标代码的顺序执行;
- 这种一个处理器上执行的多个操作,在其他处理器来看它的顺序与目标代码指定的顺序可能不一样,这种现象称为重排序;
- 重排序是对内存访问有关操作的一种优化,可以在不影响单线程程序正确的情况下提升程序的性能。但是,可能对多线程程序的正确性产生影响,即可能导致线程安全问题
- 重排序与可见性问题类似,不是必然出现的
- 与内存操作顺序有关的几个概念:
- 源代码顺序,就是源码中指定的内存访问顺序
- 程序顺序,处理器上运行的目标代码所指定的内存访问顺序
- 执行顺序,内存访问操作在处理器上的实际执行顺序
- 感知顺序,给定处理器所感知到的该处理器及其他处理器的内存访问操作的顺序
- 可以把重排序分为指令重排序与存储子系统重排序两种
- 指令重排序主要是由JIT编译器,处理器引起的,指程序顺序与执行顺序不一样
- 存储子系统重排序是由高速缓存,写缓冲器引起的,感知顺序与执行顺序不一致
-
指令重排序
- 在源码顺序与程序顺序不一致,或者程序顺序与执行顺序不一致的情况下,我们就说发生了指令重排序(Instruction Reorder)
- 指令重排是一种动作,确实对指令的顺序做了调整,重排序的对象指令。
- javac编译器一般不会执行指令重排序,而JIT编译器可能执行指令重排序。
- 处理器也可能执行指令重排序,使得执行顺序与程序顺序不一致。
- 指令重排不会对单线程程序的结果正确性产生影响,可能导致多线程出现非预期的结果
-
存储子系统重排序
- 存储子系统是指写缓冲器与高速缓存
- 高速缓存(Cache)是CPU中为了匹配与主内存处理速度不匹配而设计的一个高速缓存
- 写缓冲器(Store buffer,Write buffer)用来提高写高速缓存操作的效率
- 即使处理器严格按照程序顺序执行两个内存访问操作,在存储子系统的作用下,其他处理器对这两个操作的感知顺序与程序顺序不一致,即这两个操作的执行顺序看起来像是发生了变化,这种现象称为存储子系统重排序
- 存储子系统重排序并没有真正的对指令执行顺序进行调整,而是造成一种指令执行顺序被调整的假象
- 存储子系统重排序对象是内存操作的结果
- 从处理器角度来看,读内存就是从指定的RAM地址中加载数据到寄存器,称为Load操作,写内存就是把数据存储到指定的地址表示的RAM存储单元中,称为Store操作,内存重排序有以下四种可能:
- LoadLoad重排序,一个处理器先后执行两个读操作L1和L2,其他处理器对两个内存操作的感知顺序可能是L2>L1
- StoreStore重排序,一个处理器先后执行两个写操作W1和W2,其他处理器对两个内存操作的感知顺序可能是W2->W1
- LoadStore 重排序,一个处理器先执行读内存操作 L1 再执行写内存操作 W1, 其他处理器对两个内存操作的感知顺序可能是 W1->L1
- StoreLoad重排序,一个处理器先执行写内存操作W1再执行读内存操作 L1, 其他处理器对两个内存操作的感知顺序可能是 L1->W1
- 内存重排序与具体的处理器微架构有关,不同架构的处理器所允许的内存重排序不同
- 内存重排序可能会导致线程安全问题。
-
貌似串行语义
-
JIT编译器,处理器,存储子系统是按照一定的规则对指令,内存操作的结果进行重排序,给单线程程序造成一种假象—指令是按照源码的顺序执行的,这种假象称为貌似串行语义,并不能保证多线程环境程序的正确性。
-
为了保证貌似串行语义,有数据依赖关系的语句不会被重排序,只有不存在数据依赖关系的语句才会被重排序,如果两个操作(指令)访问同一个变量,且其中一个操作(指令)为写操作,那么这两个操作之间就存在数据依赖关系
//例如 x = 1; y = x+1; //后一条语句的操作树包含前一条语句的执行结果 y = x; x = 1; //先读取x变量,再更新x变量的值 x = 1; x = 2; //两条语句同时对一个变量进行写操作 //如果不存在数据依赖关系则可能重排序 double price = 11.1; int quantity = 10; double sum = price * quantity;
-
存在控制依赖关系的语句允许重排,一条语句(指令)的执行结果会决定另一条语句(指令)能否被执行,这两条语句(指令)存在控制依赖关系,如在if语句中允许重排,可能存在处理器先执行代码块,再判读if条件是否成立
-
-
保证内存访问的顺序性
- 可以使用volatile关键字,synchronized关键字实现有序性
java内存模型
- 每个线程都有独立的栈空间
- 每个线程都可以访问堆内存
- 计算机的CPU不直接从主内存中读取数据,CPU读取数据时,先把主内存的数据读到Cache缓存中,把Cache中的数据读到Register寄存器中。
- JVM中的共享的数据可能会被分配到Register寄存器中,每个CPU都有自己的Register寄存器,一个CPU不能读取其他CPU上寄存器中的内容,如果两个线程分别运行在不同处理器(CPU)上,而这个共享的数据被分配到寄存器上,会产生可见性问题。
- 即使JVM中的共享数据分配到主内存中,也不能保证数据的可见性,CPU不直接对主内存访问,而是通过Cache高速缓存进行的,一个处理器上运行的线程对数据的更新可能只是更新到处理器的写缓冲器(Store Buffer),还没有到达Cache缓存,更不用说主内存了,另外一个处理器不能读取到该处理器写缓冲器上的内容,会产生运行在另外一个处理器上的线程无法看到该处理器对共享数据的更新。
- 一个处理器的Cache不能直接读取另外一个处理器的Cache,但是一个处理器可以通过缓存一致性协议(Cache Coherence Protocol)来读取其他处理器缓存中的数据,并将读取的数据更新到该处理器的Cache中,这个过程称为缓存同步,缓存同步使得一个处理器上运行的线程可以读取到另一个处理器上运行的线程对共享数据所做的更新,即保障了可见性,为了保障可见性,必须使一个处理器对共享数据的更新最终被写入该处理器的Cache,这个过程称为冲刷处理器缓存。
线程同步
线程同步机制
- 线程同步机制是一套用于协调线程之间的数据访问的机制,该机制可以保障线程安全
- java平台提供的线程同步机制包括:锁,volatile关键字,final关键字,static关键字,以及相关的API,如Object.wait()/Object.notify()等
锁概述
- 线程安全问题的产生前提是多个线程并发访问共享数据
- 将多个线程对共享数据的并发访问转换为串行访问,即一个共享数据一次只能被一个线程访问,锁就是利用这种思路来保障线程安全的。
- 锁(Lock)可以理解为对共享数据进行保护的一个许可证,对于同一个许可证保护的共享数据来说,任何线程想要访问这些共享数据必须先持有该许可证,一个线程只有在持有许可证的情况下才能对这些共享数据进行访问;并且一个许可证一次只能被一个线程持有;许可证线程在结束对共享数据的访问后必须释放其持有的许可证。
- 一线程在访问共享数据前必须先获得锁;获得锁的线程称为锁的持有线程;一个锁一次只能被一个线程持有。锁的持有线程在获得锁之后和释放锁之前这段时间所执行的代码称为临界区。
- 锁具有排他性,即一个锁一次只能被一个线程持有,这种锁称为排它锁或互斥锁
- JVM把锁分为内部锁和显示锁两种,内部锁通过synchronized关键字实现;显示锁通过java.concurrent.locks.Lock接口的实现类实现。
锁的作用
- 锁可以实现对共享数据的安全访问,保障线程的原子性,可见性与有序性
- 锁是通过互斥保障原子性,一个锁只能被一个线程持有,这就保证临界区的代码一次只能被一个线程执行,使得临界区代码所执行的操作自然而然的具有不可分割的特性,即具备了原子性。
- 可见性的保障是通过写线程冲刷处理器的缓存和读线程刷新处理器缓存的动作,锁的释放隐含着冲刷处理器缓存的动作。
- 锁能够保障有序性,写线程在临界区所执行的在读线程所执行的临界区看来像是完全按照源码顺序执行的。
- 使用锁保障线程的安全性,必须满足以下条件:
- 这些线程在访问共享数据时必须使用同一个锁
- 即使是读取共享数据的线程也需要使用同步锁
锁相关概念
-
可重入性
可重入性描述这样一个问题:一个线程持有该锁的时候能再次(多次)申请该锁
void methodA(){ 申请a锁 methodB(); 释放a锁 } void methodB(){ 申请a锁 .... 释放a锁 }
如果一个线程持有一个锁的时候还能继续成功申请该锁,称该锁是可重入的,否则就称该锁为不可重入的
-
锁的争用与调度
java平台内部锁属于非公平锁,显示Lock锁既支持公平锁又支持非公平锁
-
锁的粒度
一个锁可以保护的共享数据的数量大小称为锁的粒度
锁保护共享数据量大,称该锁的粒度粗,否则就称该锁的粒度细。
锁的粒度过粗会导致线程在申请锁时会进行不必要的等待,锁的粒度过细会增加锁调度的开销。
内部锁:synchronized 关键字
-
java中的每个对象都有一个与之关联的内部锁(Intrinsic lock)这种锁也称为监视器(Monitor),这种内部锁是一种排他锁,可以保障原子性,可见性与有序性。
-
内部锁是通过synchronized关键字实现的,synchronized关键字修饰代码块,修饰方法
-
修饰代码块的语法:
synchronized(对象锁){ 同步代码块,可以在同步代码块种访问共享数据 }
-
修饰实例方法就称为同步实例方法
-
修饰静态方法就称为同步静态方法
synchronized 同步代码块 和 同步方法
-
this 锁对象
public void method(){ //使用this当前对象作为锁对象 //谁调用该方法 this 就表示谁 synchronized( this ){ System.out.println("铭鸽鸽"); } }
-
使用一个常量作为锁对象
public static final String STR = new String("铭鸽鸽"); //以下三个方法使用一个常量作为锁对象,可以实现同步 public void method(){ //使用一个常量作为锁对象,不同的对象调用也可以保证线程安全 synchronized( STR ){ System.out.println("铭鸽鸽"); } } public void method2(){ synchronized( STR ){ System.out.println("铭鸽鸽"); } } public static void method3(){ synchronized( STR ){ System.out.println("铭鸽鸽"); } }
-
synchronized修饰实例方法,把整个方法体作为同步代码块,默认的锁对象是this对象
//以下两个方法锁住的对象都是this,可以实现同步 public synchronized void method(){ System.out.println("铭鸽鸽"); } public void method2(){ synchronized( this ){ System.out.println("铭鸽鸽"); } }
-
synchronized修饰静态方法,把整个方法体作为同步代码块,默认的锁对象是当前类运行时类对象,也就是该类的".class"文件,也称为类锁
//以下两个方法使用的都是类锁,可以实现同步 public void m1(){ synchronized( 类名.class ){ System.out.println("铭鸽鸽"); } } public synchronized static void m2(){ System.out.println("铭鸽鸽"); }
-
同步方法与同步代码块如何选择:
- 同步方法锁的粒度粗,执行效率低
- 同步代码块锁的粒度细,执行效率高
脏读
- 出现脏读的原因:对共享数据的修改与对共享数据的读取不同步
- 解决方法:不仅对修改数据的代码块进行同步,还要对取数据的代码块同步
线程出现异常会自动释放锁对象
死锁
-
死锁的产生:
在多线程程序中,同步时可能需要使用多个锁,如果获得锁的顺序不一致,可能会导致死锁
-
如何避免死锁?
当需要获得多个锁时,所有线程获得锁的顺序保持一致即可
//以下代码可能会产生死锁
public class Deadlock{
public static void main(String[] args){
SubThread t1 = new SubThread();
t1.setName("a");
t1.start();
SubThread t2 = new SubThread();
t2.setName("b");
t2.start();
}
}
class SubThread extends Thread{
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public void run(){
if("a".equals(Thread.currentThread().getName())){
synchronized( lock1 ){
System.out.println("a线程获得了lock1锁");
synchronized( lock2 ){
System.out.println("a线程获得了lock2锁");
}
}
}
if("b".equals(Thread.currentThread().getName())){
synchronized( lock2 ){
System.out.println("b线程获得了lock2锁");
synchronized( lock1 ){
System.out.println("b线程获得了lock1锁");
}
}
}
}
}
轻量级同步机制:volatile 关键字
-
volatile 的作用
- volatile 可以强制线程从公共(主)内存中读取变量的值,而不是从线程的本地内存中读取
- volatile 可以使变量在多个线程之间可见
-
volatile 和 synchronized 比较
- volatile 关键字是线程同步的轻量级实现,所以volatile性能肯定比 synchronized 要好;volatile 只能修饰变量,而 synchronized可以修饰方法,代码块。随着JDK新版本的发布,synchronized的执行效率也有较大的提升,在开发中使用 sychronized 的比率还是很大的。
- 多线程访问volatile变量不会发生阻塞,而使用synchronized 可能会阻塞。
- volatile 能保证数据的可见性,但是不能保证原子性;而synchronized可以保证原子性,也可以保证可见性。
- 关键字 volatile 解决的是变量在多个线程之间的可见性;synchronized 关键字解决多个线程之间访问公共资源的同步性。
-
volatile 非原子特征
- volatile 关键字增加了实例变量在多个线程之间的可见性,但是不具备原子性
-
常用原子类进行自增自减操作
public class Test {
public static void main(String[] args) throws InterruptedException {
for(int i = 0; i < 1000; i++){
new MyThread().start();
}
Thread.sleep(1000);
System.out.println(MyThread.count.get());
}
static class MyThread extends Thread {
//使用AtomicInteger对象
private static AtomicInteger count = new AtomicInteger();
public static void addCount(){
for(int i = 0; i < 10000; i++){
//自增的后缀形式
count.getAndIncrement();
}
System.out.println(Thread.currentThread().getName() + "count=" + count.get());
}
public void run(){
addCount();
}
}
}
CAS
-
CAS(Compare And Swap)是由硬件实现的。
-
CAS 可以将 read-modify-write 这类的操作转换为原子操作
-
CAS原理:在把数据更新到主内存时,再次读取主内存变量的值,如果现在变量的值与期望的值(操作起始时读取的值)一样就更新。
-
使用CAS实现线程安全的计数器
public class CASTest { public static void main(String[] args){ CASCounter casCounter = new CASCounter(); for(int i = 0; i < 100000; i++){ new Thread(() -> System.out.println(casCounter.incrementAndGet())); } } } class CASCounter { private long value; public long getValue(){ return value; } //定义 comare and swap 方法 private boolean compareAndSwap(long expectedValue, long newValue){ //如果当前value的值与期望的expectedValue值一样,就把当前的Value字段替换为newValue值 synchronized( this ){ if(value == expectedValue){ value = newValue; return true; }else { return false; } } } //定义自增的方法 public long incrementAndGet(){ long oldValue; long newValue; do{ oldValue = value; newValue = oldValue + 1; }while( !compareAndSwap(oldValue,newValue)); return newValue; } }
-
CAS实现原子操作背后有一个假设:共享变量的当前值与当前线程提供的期望值相同,就认为这个变量没有被其他线程修改过。
-
实际上这种假设不一定总是成立,如有共享变量 count = 0
A 线程对 count 值修改为 10
B 线程对 count 值修改为 20
C 线程对 count 值修改为 0
当前线程看到count变量的值现在是0,现在是否认为count变量的值没有被其他线程更新呢?这种结果是否能够接受??
这就是CAS中的ABA问题,即共享变量经历了A->B->A的更新。
是否能够接受ABA问题跟实现的算法有关。
如果想要规避ABA问题,可以为共享变量引入一个修订号(时间戳),每次修改共享变量时,相应的修订号就会增加1,ABA变量更新过程变量:
【A,0】——> 【B,1】——> 【A,2】,每次对共享变量的修改都会导致修订号的增加,通过修订号依然可以准确判断变量是否被其他线程修改过,AtomicStampedReference类就是基于这种思想产生的。
-
原子变量类:
- 原子变量类基于CAS实现的,当对共享变量进行read-modify-write更新操作时,通过原子变量类可以保障操作的原子性与可见性,对变量的read-modify-write更新操作是指当前操作不是一个简单的赋值,而是变量的新值依赖变量的旧值,如自增操作i++,由于volatile只能保证可见性,无法保障原子性,原子变量类内部就是借助一个volatile变量,并且保障了该变量的read-modify-write操作的原子性,有时把原子变量类看作增强的volatile变量,原子变量类有12个
- 基础数据型:AtomicInteger, AtomicLong, AtomicBoolean
- 数组型:AtomicIntegerArray, AtomicLongArray,AtomicReferenceArray
- 字段更新器:AtomicIntegerFieldUpdater,AtomicLongFieldUpdater, AtomicReferenceFieldUpdater
- 引用型:AtomicReference, AtomicStampedReference, AtomicMarkableReference
-
使用原子变量类定义一个计数器,该计数器,在整个程序中都能使用,并且所有的地方都使用这一个计数器,这个计数器可以设计为单例
public class Indicator { //构造器私有化 private Indicator { } //定义一个私有的本类静态的对象 private static final Indicator INSTANCE = new Indicator(); //提供一个公共静态方法返回该类的唯一实例 public static Indicator getInstance(){ return INSTANCE; } //使用原子变量类保存请求总数,成功数,失败数 private final AtomicLong requestCount = new AtomicLong(0); private final AtomicLong seccessCount = new AtomicLong(0); private final AtomicLong fialureCount = new AtomicLong(0); //有新的请求 public void newRequestReceive(){ requestCount.incrementAndGet(); } //处理成功 public void requestProcessSuccess(){ successCount.incrementAndGet(); } //处理失败 public void requestProcessSuccess(){ fialureCount.incrementAndGet(); } //查看总数,成功数,失败数 public long getRequestCount(){ return requestCount.get(); } public long getSuccessCount(){ return successCount.get(); } public long getFailureCount(){ return fialureCount.get(); } } /* 模拟服务器的请求总数,处理成功数,处理失败数 */ public class Test { public static void main(String[] args){ //通过线程模拟请求,在实际应用中可以在ServletFilter中调用Indicator计数器的相关方法 for(int i = 0; i < 10000; i++){ new Thread(new Runnable(){ public void run(){ //每个线程就是一个请求,请求总数要加1 Indicator.getInstance().newRequestReceive(); int num = new Random().nextInt(); if(num % 2 == 0){ Indicator.getInstance().requestProcessSuccess(); }else { Indicator.getInstance().requestProcessFailure(); } } }).start(); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println( Indicator.getInstance().getRequestCount()); //总的请求数 System.out.println( Indicator.getInstance().getSuccessCount()); //成功数 System.out.println( Indicator.getInstance().getFailureCount()); //失败数 } }
-
AtomicIntegerArray的基本操作
public class Test { public static void main(String[] args) { //1)创建一个指定长度的原子数组 AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10); System.out.println( atomicIntegerArray ); //[0, 0, 0, 0, 0, 0, 0, 0, 0, 0] //2)返回指定位置的元素 System.out.println( atomicIntegerArray.get(0)); //0 System.out.println( atomicIntegerArray.get(1)); //0 //3)设置指定位置的元素 atomicIntegerArray.set(0, 10); //在设置数组元素的新值时, 同时返回数组元素的旧值 System.out.println( atomicIntegerArray.getAndSet(1, 11) ); //0 System.out.println( atomicIntegerArray ); //[10, 11, 0, 0, 0, 0, 0, 0, 0, 0] //4)修改数组元素的值,把数组元素加上某个值 System.out.println( atomicIntegerArray.addAndGet(0, 22) ); //32 System.out.println( atomicIntegerArray.getAndAdd(1, 33)); //11 System.out.println( atomicIntegerArray ); //[32, 44, 0, 0, 0, 0, 0, 0, 0, 0] //5)CAS 操作 //如果数组中索引值为 0 的元素的值是 32 , 就修改为 222 System.out.println( atomicIntegerArray.compareAndSet(0, 32, 222)); //true System.out.println( atomicIntegerArray ); //[222, 44, 0, 0, 0, 0, 0, 0, 0, 0] System.out.println( atomicIntegerArray.compareAndSet(1, 11, 333)); //false System.out.println(atomicIntegerArray); //6)自增/自减 System.out.println( atomicIntegerArray.incrementAndGet(0) ); //223, 相当于前缀 System.out.println( atomicIntegerArray.getAndIncrement(1)); //44, 相当于后缀 System.out.println( atomicIntegerArray ); //[223, 45, 0, 0, 0, 0, 0, 0, 0, 0] System.out.println( atomicIntegerArray.decrementAndGet(2)); //-1 System.out.println( atomicIntegerArray); //[223, 45, -1, 0, 0, 0, 0, 0, 0, 0] System.out.println( atomicIntegerArray.getAndDecrement(3)); //0 System.out.println( atomicIntegerArray ); //[223, 45, -1, -1, 0, 0, 0, 0, 0, 0] } }
//在多线程中使用AtomicIntegerArray原子数组 public class Test02 { //定义原子数组 static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10); public static void main(String[] args) { //定义线程数组 Thread[] threads = new Thread[10]; //给线程数组元素赋值 for (int i = 0; i < threads.length; i++) { threads[i] = new AddThread(); } //开启子线程 for (Thread thread : threads) { thread.start(); } //在主线程中查看自增完以后原子数组中的各个元素的值,在主线程中需要在所有子线程都执行完后再查看 //把所有的子线程合并到当前主线程中 for (Thread thread : threads) { try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println( atomicIntegerArray ); } //定义一个线程类,在线程类中修改原子数组 static class AddThread extends Thread{ @Override public void run() { //把原子数组的每个元素自增 1000 次 for (int j = 0; j < 100000; j++) { for (int i = 0; i < atomicIntegerArray.length(); i++) { atomicIntegerArray.getAndIncrement(i % atomicIntegerArray.length()); } } /* for (int i = 0; i < 10000; i++) { atomicIntegerArray.getAndIncrement(i % atomicIntegerArray.length()); }*/ } } }
-
AtomicIntegerFieldUpdater 可以对原子整数字段进行更新,要求:
- 字符必须使用volatile修饰,使线程之间可见
- 只能是实例变量,不能是静态变量,也不能使用final修饰
public class User { int id; volatile int age; public User(int id,int age){ this.id = id; this.age = age; } public String toString(){ return "User{" + "id=" + id +", age=" + age + '}'; } } public class SubThread extends Thread{ //要更新的User对象 private User user; //创建AtomicIntegerFieldUpdater更新器 private AtomicIntegerFieldUpdater<User> updater = AtomicIntegerFieldUpdater.newUpdater(User.class,"age"); public SubThread(User user){ this.user = user; } public void run(){ //在子线程中对user对象的age字段自增10次 for(int i = 0; i < 10; i++){ System.out.println(updater.getAndIncrement(user)); } } } public class Test { public static void main(String[] args){ User user = new User(1234,10); for(int i = 0; i < 10; i++){ new SubThread(user).start(); } try{ Thread.sleep(1000); } catch (InterruptedException e){ e.printStackTrace(); } System.out.println(user); } }
-
使用 AtomicReference 原子读写一个对象
public class Test{ //创建一个 AtomicReference 对象 static AtomicReference<String> atomicReference = new AtomicReference<>("abc"); public static void main(String[] args) throws InterruptedException { //创建 100 个线程修改字符串 for (int i = 0; i < 100; i++) { new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(new Random().nextInt(20)); } catch (InterruptedException e) { e.printStackTrace(); } if(atomicReference.compareAndSet("abc","def")){ System.out.println(Thread.currentThread().getName() + "把字符串abc 更改为 def"); } } }).start(); } //再创建 100 个线程 for (int i = 0; i < 100; i++) { new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(new Random().nextInt(20)); } catch (InterruptedException e) { e.printStackTrace(); } if(atomicReference.compareAndSet("def","abc")){ System.out.println(Thread.currentThread().getName() + "把字符串还原为 abc"); } } }).start(); } Thread.sleep(1000); System.out.println(atomicReference.get()); } }
-
演示AtomicReference可能会出现CAS的ABA问题
public class Test { private static AtomicReference<String> atomicReference = new AtomicReference<>("abc"); public static void main(String[] args) throws InterruptedException { //创建第一个线程,先把 abc 字符串改为"def",再把字符串还原为 abc Thread t1 = new Thread(new Runnable() { @Override public void run() { atomicReference.compareAndSet("abc", "def"); System.out.println(Thread.currentThread().getName() + "--" + atomicReference.get()); atomicReference.compareAndSet("def", "abc"); } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println( atomicReference.compareAndSet("abc", "ghg")); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println( atomicReference.get()); } }
-
AtomicStampedReference原子类可以解决 CAS 中的 ABA 问题
//在 AtomicStampedReference 原子类中有一个整数标记值 stamp, 每次执行 CAS 操作时,需要对比它的版本,即比较 stamp 的值 public class Test { private static AtomicStampedReference<String> stampedReference = new AtomicStampedReference<>("abc", 0); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new Runnable() { @Override public void run() { stampedReference.compareAndSet("abc", "def",stampedReference.getStamp(), stampedReference.getStamp()+1); System.out.println(Thread.currentThread().getName() + "--"+stampedReference.getReference()); stampedReference.compareAndSet("def", "abc",stampedReference.getStamp(), stampedReference.getStamp()+1); } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { int stamp = stampedReference.getStamp(); //获得版本号 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println( stampedReference.compareAndSet("abc", "ggg", stamp,stamp+1)); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println( stampedReference.getReference() ); } }
线程间的通信
等待/通知机制
-
在单线程编程中,要执行的操作需要满足一定的条件才能执行,可以把这个操作放在if语句块中。
-
在多线程编程中,可能A线程的条件没有满足只是暂时的,稍后其他的线程B可能会更新条件使得A线程的条件得到满足,可以将A线程暂停,直到它的条件得到满足后再将A线程唤醒,伪代码如下:
atomics { //原子操作 while(条件不成立){ 等待 } 当前线程被唤醒条件满足后,继续执行下面的操作 }
等待/通知机制的实现
-
Object 类中的wait()方法可以使执行当前代码的线程等待,暂停执行,直到接到通知或被中断为止
-
注意:wait()方法只能在同步代码块中由锁对象调用,调用wait()方法,当前线程会释放锁
//伪代码 //在调用wait()方法前获得对象的内部锁 synchronized(锁对象){ while(条件不成立){ //通过锁对象调用wait()方法暂停线程,会释放锁对象 锁对象.wait(); } //线程的条件满足了继续向下执行 }
-
Object类的notify()可以唤醒线程,该方法也必须在同步代码块中由锁对象调用,没有使用锁对象调用wait()/notify()会抛出IlegalMonitorStateExeption异常,如果有多个等待的线程,notify()方法只能唤醒其中的一个,在同步代码块中调用notify()方法后,并不会立即释放锁对象,需要等当前同步代码块执行完后才会释放锁对象,一般将notify()方法放在同步代码块的最后,其伪代码如下:
synchronized(锁对象){ //执行修改保护条件的代码 //唤醒其他线程 锁对象.notify(); }
-
需要通过notify()唤醒等待的线程,notify()方法不会释放锁对象
public class Test { public static void main(String[] args){ String lock = "wym"; new Thread(() -> { synchronized(lock){ try { System.out.println("开始等待") lock.wait(); System.out.println("被唤醒") }catch (InterruptedException e) { e.printStackTrace(); } } }).stack(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { synchronized(lock){ System.out.println("唤醒正在等待的线程"); lock.notify(); //notify()方法不会释放锁对象 System.out.println("继续执行我的代码"); } }).stack(); } } /* 执行结果: 开始等待 唤醒正在等待的线程 继续执行我的代码 被唤醒 */
interrupt()方法会中断wait()
- 当线程处于wait()等待状态时,调用线程对象的interrupt()方法会中断线程的等待,会产生InterruptedException 异常
notify()与notifyAll()
- notify()一次只能唤醒一个线程,如果有多个等待的线程,只能随机唤醒其中的某一个;想要唤醒所有等待线程,需要调用notifyAll()。
wait(long)的使用
- wait(long)带有long类型参数的wait()等待,如果在参数指定的时间内没有被唤醒,超时后会自动唤醒。
通知过早
- 线程wait()等待后,可以调用notify()唤醒线程,如果notify()唤醒的过早,在等待之前就调用了notify()可能会打乱程序正常的运行逻辑
wait等待条件发生了变化
-
在使用wait/notify模式时,注意wait条件发生了变化,也可能会造成逻辑的混乱
/* 定义一个集合 定义一个线程向集合中添加数据,添加完数据后通知另外的线程从集合中取数据 定义一个线程从集合中取数据,如果集合中没有数据就等待 */ public class Test { public static void main(String[] args){ //定义添加数据的线程对象 ThreadAdd threadAdd = new ThreadAdd(); //定义取数据的线程对象 ThreadSubtract threadSubtract = new ThreadSubtract(); threadSubtract.setName("subtract 1"); //测试一:先开启添加数据的线程,再开启一个取数据的线程,大多数情况下会正常取数据 /*threadAdd.start(); threadSubtract.start();*/ //测试二:先开启取数据的线程,再开启添加数据的线程,取数据的线程会先等待,等到添加数据之后,再取数据 /*threadSubtract.start(); threadAdd.start();*/ //测试三:开启两个取数据的线程,再开启添加数据的线程 ThreadSubtract threadSubtract2 = new ThreadSubtract(); threadSubtract2.setName("subtract 2 "); threadSubtract.start(); threadSubtract2.start(); threadAdd.start(); /* 某一次执行结果: subtract 1 begin wait... subtract 2 从集合中取了data后,集合中数据的数量:0 subtract 1 end wait... Exception in thread "subtract 1 " java.lang.IndexOutOfBoundsException: 分析可能的执行顺序: threadSubtract 线程先启动, 取数据时,集合中没有数据,wait()等待 threadAdd 线程获得 CPU 执行权, 添加数据 , 把 threadSubtract 线程唤醒, threadSubtract2 线程开启后获得 CPU 执行权, 正常取数据 threadSubtract 线程获得 CPU 执行权, 打印 end wait..., 然后再执行 list.remove(0) 取 数 据 时 , 现 在 list 集 合 中 已 经 没 有 数 据 了 , 这 时 会 产 生 java.lang.IndexOutOfBoundsException 异常 出现异常的原因是: 向 list 集合中添加了一个数据,remove()了两次 如何解决? 当等待的线程被唤醒后, 再判断一次集合中是否有数据可取. 即需要把 sutract()方法中的 if 判断改为 while */ } //定义List集合 static List list = new ArrayList<>(); //定义方法从集合中取数据 public static void subtract(){ synchronized (list) { //if (list.size() == 0) { while (list.size() == 0) { try { System.out.println(Thread.currentThread().getName() + " begin wait...."); list.wait(); //等待 System.out.println(Thread.currentThread().getName() + " end wait.."); }catch (InterruptedException e) { e.printStackTrace(); } } Object data = list.remove(0); //从集合中取出一个数据 System.out.println( Thread.currentThread().getName() + "从集合中取了" + data +"后,集合中数据的数量:" + list.size()); } } //定义方法向集合中添加数据后,通知等待的线程取数据 public static void add(){ synchronized (list){ list.add("data"); System.out.println( Thread.currentThread().getName() + "存储了一个数据"); list.notifyAll(); } } //定义线程类调用 add()取数据的方法 static class ThreadAdd extends Thread{ @Override public void run() { add(); } } //定义线程类调用 subtract()方法 static class ThreadSubtract extends Thread{ @Override public void run() { subtract(); } } }
生产者消费者模式
-
在java中,负责产生数据的模块是生产者,负责使用数据的模块是消费者,生产者消费者解决数据的平衡问题,即先有数据然后才能使用,没有数据时,消费者需要等待
//多生产者,多消费者 public class Test { public static void main(String[] args) { Need need = new Need(); ProducerNeed p = new ProducerNeed(need); ProducerNeed p2 = new ProducerNeed(need); p.setName("生产者1"); p2.setName("生产者2"); ConsumerNeed c = new ConsumerNeed(need); ConsumerNeed c2 = new ConsumerNeed(need); ConsumerNeed c3 = new ConsumerNeed(need); c.setName("消费者1"); c2.setName("消费者2"); c3.setName("消费者3"); c.start(); c2.start(); c3.start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } p2.start(); p.start(); } } class Need { List<Integer> listNeed = new ArrayList<>(10); public synchronized void setListNeed(){ while(listNeed.size() != 0){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } for(int i = 0; i < 10; i++){ listNeed.add(i); } System.out.println(Thread.currentThread().getName() + "需求生产完毕"); this.notifyAll(); } public synchronized void getListNeed(){ while(listNeed.size() == 0){ try { System.out.println(Thread.currentThread().getName() + "在等待需求"); this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + ":" + listNeed.remove(0)); this.notifyAll(); } } class ProducerNeed extends Thread { private Need need; public ProducerNeed(Need need){ this.need = need; } public void run(){ while(true){ need.setListNeed(); } } } class ConsumerNeed extends Thread { private Need need; public ConsumerNeed(Need need){ this.need = need; } public void run(){ while(true){ need.getListNeed(); } } }
-
通过管道实现线程间的通信
- 在java.io包中的PipeStream管道流用于在线程之间传送数据。一个线程发送数据到输出管道,另外一个线程从输入管道中读取数据。相关的类包括:PipedInputStream和PipedOutputStream,PipedReader和PipedWriter。
public class Test { public static void main(String[] args) throws IOException { //定义管道字节流 PipedInputStream inputStream = new PipedInputStream(); PipedOutputStream outputStream = new PipedOutputStream(); //在输入管道流与输出管道流之间建立连接 inputStream.connect(outputStream); //创建线程向管道流中写入数据 new Thread(() -> writeData(outputStream)).start(); //定义线程从管道流读取数据 new Thread(() -> readData(inputStream)).start(); } //定义方法向管道流中写入数据 public static void writeData(PipedOutputStream out){ try { //分别把0~100之间的数写入管道 for(int i = 0; i < 100; i++){ String data = "" + i; out.write(data.getBytes()); //把字节数组写入到输出管道流中 } out.close(); //关闭管道流 } catch (IOException e){ e.printStackTrace(); } } //定义方法从管道流中读取数据 public static void readData(PipedInputStream in){ byte[] bytes = new byte[1024]; try{ //从管道输入字节流中读取字节保存到字节数组中 int len = in.read(bytes); //返回读到的字节数,如果没有读到任何数据返回-1 while(len != -1){ System.out.println(new String(bytes,0,len)); len = in.read(bytes); } in.close(); } catch (IOException e){ e.printStackTrace(); } } }
ThreadLocal的使用
-
除了控制资源的访问外,还可以通过增加资源来保证线程安全,ThreadLocal主要解决为每个线程绑定自己的值。
package com.wym.threadlocal; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; /** * @author wym * @description 在多线程环境中,把字符串转换为日期对象,多个线程使用同一个SimpleDateFormat对象可能会产生线程安全问题 * @date 2021年12月25 15:34 */ public class Test { //private static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); private static ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); static class ParseDate implements Runnable{ private int i; public ParseDate(int i) { this.i = i; } @Override public void run() { try { String text = "2021-12-25 16:56:" + i % 60; Date date = threadLocal.get().parse(text); System.out.println(i + "-----" + date); } catch (ParseException e) { e.printStackTrace(); } } } public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 100; i++) { new Thread(new ParseDate(i)).start(); } } }
Lock 显示锁
在JDK5中增加了Lock锁接口,有ReentrantLock实现类,ReentranLock锁称为可重入锁,它功能比synchronized多。
锁的可重入性及ReentrantLock的基本使用
-
锁的可重入是指,当一个线程获得一个对象锁后,再次请求该对象锁时是可以获得该对象的锁的。
-
ReentrantLock的基本使用,调用lock()方法获得锁,调用unlock()释放锁
public class Test { //定义显示锁 static Lock lock = new ReentrantLock(); //定义方法 public static void sm(){ //先获得锁 lock.lock(); //for循环就是同步代码块 for(int i = 0; i < 100; i++){ System.out.println(Thread.currentThread().getName() + "--" + i); } //释放锁 lock.unlock(); } public static void main(String[] args){ Runnable r = new Runnable(){ public void run(){ sm(); } }; //启动三个线程 new Thread(r).start(); new Thread(r).start(); new Thread(r).start(); } }
package com.wym.lock; import java.util.Random; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @author wym * @description 使用Lock锁同步不同方法中的同步代码块 * @date 2021年12月26 13:39 */ public class Test { //定义锁对象 static Lock lock = new ReentrantLock(); public static void sm1(){ //经常在try代码块中获得Lock锁,在finally子句中释放锁 try { //获得锁 lock.lock(); System.out.println(Thread.currentThread().getName() + "-- method 1 -- " + System.currentTimeMillis() ); Thread.sleep(new Random().nextInt(1000)); System.out.println(Thread.currentThread().getName() + "-- method 1 -- " + System.currentTimeMillis() ); }catch (InterruptedException e) { e.printStackTrace(); } finally { //释放锁 lock.unlock(); } } public static void sm2(){ //经常在try代码块中获得Lock锁,在finally子句中释放锁 try { //获得锁 lock.lock(); System.out.println(Thread.currentThread().getName() + "-- method 2 -- " + System.currentTimeMillis() ); Thread.sleep(new Random().nextInt(1000)); System.out.println(Thread.currentThread().getName() + "-- method 2 -- " + System.currentTimeMillis() ); }catch (InterruptedException e) { e.printStackTrace(); } finally { //释放锁 lock.unlock(); } } public static void main(String[] args){ Runnable r1 = (Test::sm1); Runnable r2 = (Test::sm2); new Thread(r1).start(); new Thread(r1).start(); new Thread(r1).start(); new Thread(r2).start(); new Thread(r2).start(); new Thread(r2).start(); } }
-
ReentrantLock锁的可重入性
/* ReentrantLock锁的可重入性 */ public class Test { static class SubThread extends Thread{ //定义锁对象 private static Lock lock = new ReentrantLock(); //定义变量 public static int num = 0; public void run(){ for(int i = 0; i < 10000; i++){ try { //可重入锁指可以反复获得该锁 lock.lock(); lock.lock(); num++; }finally { //获得多少次就要释放多少次 lock.unlock(); lock.unlock(); } } } } public static void main(String[] args) throws InterruptedException { SubThread t1 = new SubThread(); SubThread t2 = new SubThread(); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(SubThread.num); } }
-
lockInterruptibly()方法的使用
-
lockInterruptibly()方法的作用:如果当前线程未被中断则获得锁,如果当前线程被中断则出现异常
public class Test { static class Server { //定义锁对象 private ReentrantLock lock = new ReentrantLock(); public void serviceMethod(){ try { //lock.lock(); lock.lockInterruptibly(); System.out.println(Thread.currentThread().getName() + "--获得lock,执行耗时操作"); for(int i = 0; i < Integer.MAX_VALUE; i++){ new StringBuilder(); } System.out.println(Thread.currentThread().getName() + "--耗时操作执行完毕"); } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName() + "被中断了"); } finally { //释放锁 if (lock.isHeldByCurrentThread()) { lock.unlock(); System.out.println(Thread.currentThread().getName() + "--释放锁"); } } } } public static void main(String[] args) throws InterruptedException { Server server = new Server(); Runnable r = (server::serviceMethod); Thread t1 = new Thread(r); t1.setName("线程1"); t1.start(); Thread.sleep(50); Thread t2 = new Thread(r); t2.setName("线程2"); t2.start(); Thread.sleep(50); //中断t2线程 t2.interrupt(); } }
-
对于synchronized内部锁来说,如果一个线程在等待锁,只有两个结果:要么该线程获得锁继续执行,要么就保持等待。
-
对于ReentrantLock可重入锁来说,提供另外一种可能,在等待锁的过程中,程序可以根据需要取消对锁的请求。
public class Test { static class IntLock implements Runnable { //创建两个ReentrantLock锁对象 public static ReentrantLock lock1 = new ReentrantLock(); public static ReentrantLock lock2 = new ReentrantLock(); //定义整数变量,决定使用哪个锁 int lockNum; public IntLock(int lockNum){ this.lockNum = lockNum; } public void run(){ try{ if(lockNum % 2 == 1){ //如果是奇数,先锁1,再锁2 lock1.lockInterruptibly(); System.out.println(Thread.currentThread().getName() + "获得锁1,还需获得锁2"); Thread.sleep(new Random().nextInt(500)); lock2.lockInterruptibly(); System.out.println(Thread.currentThread().getName() + "同时获得了锁1与锁2"); }else { //如果是偶数,先锁2,再锁1 lock2.lockInterruptibly(); System.out.println(Thread.currentThread().getName() + "获得锁2,还需获得锁1"); Thread.sleep(new Random().nextInt(500)); lock1.lockInterruptibly(); System.out.println(Thread.currentThread().getName() + "同时获得了锁1与锁2"); } }catch(InterruptedException e){ e.printStackTrace(); }finally{ if ( lock1.isHeldByCurrentThread()) //判断当前线程是否持有该锁 lock1.unlock(); if (lock2.isHeldByCurrentThread()) lock2.unlock(); System.out.println( Thread.currentThread().getName() + "线程退出"); } } } public static void main(String[] args){ IntLock intLock1 = new IntLock(11); IntLock intLock2 = new IntLock(22); Thread t1 = new Thread(intLock1); Thread t2 = new Thread(intLock2); t1.start(); t2.start(); //在main线程中等待2秒,如果还有线程没有结束就中断该线程 Thread.sleep(2000); //可以中断热河一个线程来解决死锁 if(t2.isAlive()){ t2.interrupt(); } } }
-
tryLock(long time,TimeUnit unit);的作用在给定等待时长内锁没有被另外的线程持有,并且当前线程也没有被中断,则获得该锁,通过该方法可以实现锁对象的限时等待。
/* tryLock(long time,TimeUnit unit)的基本使用 */ public class Test { static class TimeLock implements Runnable { //定义锁对象 private static ReentrantLock lock = new ReentrantLock(); public void run(){ try{ if(lock.tryLock(3,TimeUnit.SECONDS)){ System.out.println(Thread.currentThread().getName() + "获得锁,执行耗时操作"); /* 假设Thread-0线程先持有锁,完成任务需要4秒钟,Thread-1线程尝试获得锁,Thread-1线程在3秒内还没有获得锁的 话,Thread-1线程会放弃 */ //Thread.sleep(4000); /* 假设Thread-0线程先持有锁,完成任务需要2秒钟,Thread-1线程尝试获得锁,Thread-1线程会一直尝试,在它约定尝 试的3秒内可以获得锁对象 */ Thread.sleep(2000); }else { System.out.println(Thread.currentThread().getName() + "没有获得锁"); } }catch (InterruptedException e){ e.printStackTrace(); }finally { if (lock.isHeldByCurrentThread()){ lock.unlock(); } } } } public static void main(String[] args){ TimeLock timeLock = new TimeLock(); Thread t1 = new Thread(timeLock); Thread t2 = new Thread(timeLock); t1.start(); t2.start(); } }
-
tryLock()仅在调用时锁定未被其他线程持有的锁,如果调用方法时,锁对象对其他线程持有,则放弃,调用方法尝试获得锁,如果该锁没有被其他线程占用则返回true表示锁定成功;如果锁被其他线程占用则返回false,不等待。
/* tryLock()当锁对象没有被其他线程持有的情况下才会获得该锁定 */ public class Test { static class Service { private ReentrantLock lock = new ReetrantLock(); public void serviceMethod(){ try { if(lock.tryLock()){ System.out.println(Thread.currentThread().getName() + "获得锁定"); Thread.sleep(3000); //模拟执行任务时长 }else { System.out.println(Thread.currentThread() + "没有获得锁定"); } }catch (InterruptedException e){ e.printStackTrace(); }finally { if (lock.isHeldByCurrentThread()){ lock.unlock(); } } } } public static void mian(String[] args) throws InterruptedException{ Service service = new Service(); Runnable r = new Runable(){ public void run(){ service.serviceMethod(); } }; Thread t1 = new Thread(r); t1.start(); Thread.sleep(50); //睡眠50毫秒,确保t1线程锁定 Thread t2 = new Thread(r); t2.start(); } }
-
使用tryLock()可以避免死锁
public class Test { static class IntLock implements Runnable { private static ReentrantLock lock1 = new ReentrantLock(); private static ReentrantLock lock2 = new ReentrantLock(); //用于控制锁的顺序 private int lockNum; public IntLock(int lockNum){ this.lockNum = lockNum; } public void run(){ if(lockNum % 2 == 0){ //偶数先锁1,再锁2 while(true){ try{ if(lock1.tryLock()){ System.out.println(Thread.currentThread().getName() + "获得锁1,还想获得锁2"); //Thread.sleep(new Random().nextInt(100)); } try{ if(lock2.tryLock()){ System.out.println(Thread.currentThread().getName() + "同时获得锁1与锁2--完成任务了"); return; } }finally { if(lock2.isHeldByCurrentThread()){ lock2.unlock(); } } }finally { if(lock1.isHeldByCurrentThread()){ lock1.unlock(); } } } }else { while(true){ try{ if(lock2.tryLock()){ System.out.println(Thread.currentThread().getName() + "获得锁2,还想获得锁1"); //Thread.sleep(new Random().nextInt(100)); } try{ if(lock1.tryLock()){ System.out.println(Thread.currentThread().getName() + "同时获得锁1与锁2--完成任务了"); return; } }finally { if(lock1.isHeldByCurrentThread()){ lock1.unlock(); } } }finally { if(lock2.isHeldByCurrentThread()){ lock2.unlock(); } } } } } } public static void main(String[] args){ IntLock intLock1 = new IntLock(11); IntLock intLock2 = new IntLock(22); Thread t1 = new Thread(intLock1); Thread t2 = new Thread(intLock2); t1.start(); t2.start(); //运行后,使用tryLock()尝试获得锁,不会傻傻的等待,通过循环不停的再次尝试,如果等待的时间足够长,线程总是会获得想要的资源 } }
-
newCondition()方法
-
关键字synchronized与wait()/notify()这两个方法一起使用可以实现等待/通知模式,Lock锁的newContition方法返回Condition对象,Condition类也可以实现等待/通知模式。
-
使用notify()通知时,JVM会随机唤醒某个等待的线程,使用Condition类可以进行选择性通知,Condition比较常用的两个方法:
- await()会使当前线程等待,同时会释放锁,当其他线程调用signal()时,线程会重新获得锁并继续执行。
- signal()用于唤醒一个等待的线程
- 注意:在调用Condition的await()/signal()方法前,也需要线程持有相关的Lock锁,调用await()后线程会释放这个锁,在singal()调用后会从当前Condition对象的等待队列中,唤醒一个线程,唤醒的线程尝试获得锁,一旦获得锁成功就继续执行。
/* Condition等待与通知 */ public class Test { //定义锁 static Lock lock = new ReentrantLock(); //获得Condition对象 static Condition condition = lock.newCondition(); //定义线程子类 static class SubThread extends Thread{ public void run(){ try{ //在调用await()前必须先获得锁 lock.lock(); System.out.println("method lock"); condition.await(); System.out.println("method await"); }catch(InterruptedException e){ e.printStackTrace(); }finally{ lock.unlock(); System.out.println("method unlock"); } } } public static void main(String[] args) throws InterruptedException { SubThread t = new SubThread(); t.start(); //子线程启动后,会转入等待状态 Thread.sleep(3000); //主线程在睡眠3秒后,会转入等待状态 try{ lock.lock(); condition.signal(); }finally{ lock.unlock(); } } }
/* 多个Condition实现通知部分线程,使用更灵活 */ public class Test { static class Service { //定义锁对象 private ReentrantLock lock = ReentrantLock(); //定义两个Condition对象 private Condition C1 = lock.newCondition(); private Condition c2 = lock.newCondition(); //定义方法,使c1等待 public waitFun1(){ try{ lock.lock(); System.out.println(Thread.currentThread().getName() + "begin wait:" + System.currentTimeMillis()); c1.await(); System.out.println(Thread.currentThread().getName() + "end wait:" + System.currentTimeMillis()); }catch(InterruptedException e){ e.printStackTrace(); }finally{ lock.unlock(); } } //定义方法,使c2等待 public waitFun2(){ try{ lock.lock(); System.out.println(Thread.currentThread().getName() + "begin wait:" + System.currentTimeMillis()); c2.await(); System.out.println(Thread.currentThread().getName() + "end wait:" + System.currentTimeMillis()); }catch(InterruptedException e){ e.printStackTrace(); }finally{ lock.unlock(); } } //定义方法,唤醒c1的等待 public signal1(){ try{ lock.lock(); System.out.println(Thread.currentThread().getName()+"sigal A time"+System.currentTimeMillis()); c1.signal(); System.out.println(Thread.currentThread().getName()+"sigal A time "+System.currentTimeMillis()); }catch(InterruptedException e){ e.printStackTrace(); }finally{ lock.unlock(); } } //定义方法,唤醒c2的等待 public signal2(){ try{ lock.lock(); System.out.println(Thread.currentThread().getName()+"sigal A time"+System.currentTimeMillis()); c2.signal(); System.out.println(Thread.currentThread().getName()+"sigal A time "+System.currentTimeMillis()); }catch(InterruptedException e){ e.printStackTrace(); }finally{ lock.unlock(); } } } public static void main(String[] args) throws InterruptedException { Service service = new Service(); //开启两个线程,分别调用waitFun1(),waitFun2()方法 new Thread(new Runnable(){ public void run(){ service.waitFun1(); } }).start(); new Thread(new Runnable(){ public void run(){ service.waitFun2(); } }).start(); Thread.sleep(3000); //main 线程睡眠 3 秒 service.signalB(); } }
-
-
使用Condition实现生产者/消费者设计模式,两个线程交替打印
package com.wym.lock; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /* * @author wym * @description 使用Condition实现生产者消费者设计模式,两个线程交替打印 * @date 2021年12月26 13:39 */ public class Test { static class MyService { //创建锁对象 private final Lock lock = new ReentrantLock(); //创建Condition对象 private final Condition condition = lock.newCondition(); //定义交替打印标志 private boolean flag = true; private int count = 1; //打印单数方法 public void printOne(){ try { lock.lock(); while (flag){ condition.await(); } System.out.println(Thread.currentThread().getName() + "--" + count++); TimeUnit.SECONDS.sleep(1); flag = true; condition.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } //打印双数方法 public void printTwo(){ try { lock.lock(); while (!flag){ condition.await(); } System.out.println(Thread.currentThread().getName() + "--" + count++); TimeUnit.SECONDS.sleep(1); flag = false; condition.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } public static void main(String[] args) { MyService myService = new MyService(); new Thread(() -> { while (true) { myService.printOne(); } }).start(); new Thread(() -> { while (true) { myService.printTwo(); } }).start(); } }
// 使用Condition,实现多生产者消费者问题 package com.wym.lock; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @author wym * @description 使用Condition实现多生产者多消费者设计模式,注意:要使用signalAll()方法唤醒全部,否则会出现假死现象 * @date 2021年12月26 13:39 */ public class Test { static class MyService { //创建锁对象 private final Lock lock = new ReentrantLock(); //创建Condition对象 private final Condition condition = lock.newCondition(); //定义交替打印标志 private boolean flag = true; private int count = 1; //打印单数方法 public void printOne(){ try { lock.lock(); while (flag){ condition.await(); } System.out.println(Thread.currentThread().getName() + "--" + count++); TimeUnit.SECONDS.sleep(1); flag = true; condition.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } //打印双数方法 public void printTwo(){ try { lock.lock(); while (!flag){ condition.await(); } System.out.println(Thread.currentThread().getName() + "--" + count++); TimeUnit.SECONDS.sleep(1); flag = false; condition.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } public static void main(String[] args) { MyService myService = new MyService(); new Thread(() -> { while (true) { myService.printOne(); } }).start(); new Thread(() -> { while (true) { myService.printTwo(); } }).start(); } }
-
-
公平锁与非公平锁
-
大多数情况下,锁的申请都是非公平的。如果线程1与线程2都在请求锁A,当锁A可用时,系统只是会从阻塞队列中随机的选择一个线程,不能保证其公平性。
-
公平的锁会按照时间先后顺序,保证先到先得,公平锁的这一特点不会出现线程饥饿现象
-
synchronized内部锁是非公平的,ReentrantLock重入锁提供了一个构造方法:ReentrantLock(boolean fair),当在创建锁对象时实参传递true可以把锁设置为公平锁,公平锁看起来很公平,但是要实现公平锁必须要系统维护一个有序队列,公平锁的实现成本较高,性能也低,因此默认情况下锁是非公平的。不是特别的需求,一般不适用公平锁。
public class Test { static ReentrantLock lock = new ReentrantLock(true); public static void main(String[] args){ Runnable runable = new Runnable(){ public void run(){ while(true){ try{ lock.lock(); System.out.println(Thread.currentThread().getName() + "获得了锁对象"); }finally{ lock.unlock(); } } } }; for(int i = 0; i < 5; i++){ new Thread(runable).start(); } } } /* 如果是公平锁, 多个线程不会发生同一个线程连续多次获得锁的可能,保证了锁的公平性 运行结果: Thread-3获得了锁对象 Thread-0获得了锁对象 Thread-4获得了锁对象 Thread-2获得了锁对象 Thread-1获得了锁对象 */
-
-
ReentrantLock常用的几个方法
int getHoldCount() //返回当前线程调用 lock()方法的次数 int getQueueLength() //返回正等待获得锁的线程预估数 int getWaitQueueLength(Condition condition) //返回与 Condition 条件相关的等待的线程预估数 boolean hasQueuedThread(Thread thread) //查询参数指定的线程是否在等待获得锁 boolean hasQueuedThreads() //查询是否还有线程在等待获得该锁 boolean hasWaiters(Condition condition) //查询是否有线程正在等待指定的 Condition 条件 boolean isFair() //判断是否为公平锁 boolean isHeldByCurrentThread() //判断当前线程是否持有该锁 boolean isLocked() //查询当前锁是否被线程持有
ReentrantReadWriteLock读写锁
-
synchronized内部锁与ReentrantLock锁都是独占锁(排它锁),同一时间只允许一个线程执行同步代码块,可以保证线程的安全性,但是执行效率低
-
ReentrantReadWriteLock读写锁是一种改进的排他锁,也可以称作共享/排他锁,允许多个线程同时读取共享数据,但是一次只允许一个线程对共享数据进行更新。
-
读写锁通过读锁与写锁来完成读写操作,线程在读取共享数据前必须先持有读锁,该读锁可以同时被多个线程持有,即它是共享的,线程在修改共享数据前必须先持有写锁,写锁是排它的,一个线程持有写锁时其他线程无法获得相应的锁
-
读锁只是在读线程之间共享,任何一个线程持有读锁时,其他线程都无法获得写锁,保证线程在读取数据期间没有其他线程对数据进行更新,使得读线程能够读到数据的最新值,保证在读数据期间共享变量不被修改
-
读锁:
- 获得条件:写锁未被任意线程持有
- 排他性:对读线程是共享的,对写线程是排他的
- 作用:允许多个读线程可以同时读取共享数据,保证在读共享数据时,没有其他线程对共享数据进行修改
-
写锁:
- 获得条件:该写锁未被其他线程持有,并且相应的读锁也未被其他线程持有
- 排他性:对读线程或者写线程都是排他的
- 作用:保证写线程以独占的方式修改共享数据
-
读写锁允许读读共享,读写互斥,写写互斥
-
在java.util.concurrent.locks包中定义了ReadWriteLock接口,该接口中定义了readLock()返回读锁,定义writeLock()方法写锁,该接口的实现类是ReentrantReadWriteLock。
-
注意readLock()与writeLock()方法返回的锁对象是同一个锁的两个不同的角色,不是分别获得两个不同的锁,ReadWriteLock接口实例可以充当两个角色。
-
读写锁的基本使用方法:
//定义读写锁 ReadWriteLock rwLock = new ReentrantReadWriteLock(); //获得读锁 Lock readLock = rwlock.readLock(); //获得写锁 Lock writeLock = rwlock.writeLock(); //读数据 readLock.lock(); //申请读锁 try{ //读取共享数据 }finally { //总是在finally子句中释放锁 readLock.unlock(); } //写数据 writeLock.lock(); //申请写锁 try{ //更新修改共享数据 }finally { writeLock.unlock(); //总是在finally子句中释放锁 }
读读共享
-
ReadWriteLock读写锁可以实现多个线程同时读取共享数据,即读读共享,可以提供程序读取数据的效率
/* ReadWriteLock 读写锁可以实现读读共享,允许多个线程同时获得读锁 */ public class Test { static class Service { //定义读写锁 ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); //定义方法读取数据 public void read(){ try{ //获得读锁 readWriteLock.readLock().lock(); System.out.println(Thread.currentThread().getName()+"获得读锁,开始读取数据的时间:"+System.currentTimeMillis()); //模式读取数据用时 TimeUnit.SECONDS.sleep(3); }catch(InterruptedException e) { e.printStackTrace(); }finally{ //释放读锁 readWriteLock.readLock().unlock(); } } } public static void main(String[] args){ Service service = new Service(); for(int i = 0; i < 5; i++){ new Thread(service::read).start(); } } }
写写互斥
-
通过ReadWriteLock读写锁中的写锁,只允许有一个线程执行lock()后面的代码
/* 演示 ReadWriteLock 的 writeLock()写锁是互斥的,只允许有一个线程持有 */ public class Test { static class Service { //定义读写锁 ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); //定义方法写数据 public void write(){ try{ //获得写锁 readWriteLock.writeLock().lock(); System.out.println(Thread.currentThread().getName()+"获得读锁,开始读取数据的时间:"+System.currentTimeMillis()); //模式读取数据用时 TimeUnit.SECONDS.sleep(3); }catch(InterruptedException e) { e.printStackTrace(); }finally{ //释放写锁 readWriteLock.writeLock().unlock(); } } } public static void main(String[] args){ Service service = new Service(); for(int i = 0; i < 5; i++){ new Thread(service::write).start(); } } }
读写互斥
-
写锁是独占锁,是排他锁,读线程与写线程也是互斥的
/* 演示 ReadWriteLock 的读写互斥 一个线程获得读锁时,写线程等待; 一个线程获得写锁时,其他线程等待 */ public class Test { static class Service { //定义读写锁 ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); Lock readLock = readWriteLock.readLock(); //获得读锁 Lock writeLock = readWriteLock.writeLock(); //获得写锁 //定义方法读数据 public void read(){ try{ //获得读锁 readLock.lock(); System.out.println(Thread.currentThread().getName()+"获得读锁,开始读取数据的时间:"+System.currentTimeMillis()); //模式读取数据用时 TimeUnit.SECONDS.sleep(3); }catch(InterruptedException e) { e.printStackTrace(); }finally{ //释放读锁 readLock.unlock(); } } //定义方法写数据 public void write(){ try{ //获得写锁 writeLock.lock(); System.out.println(Thread.currentThread().getName()+"获得读锁,开始读取数据的时间:"+System.currentTimeMillis()); //模式读取数据用时 TimeUnit.SECONDS.sleep(3); }catch(InterruptedException e) { e.printStackTrace(); }finally{ //释放写锁 writeLock.unlock(); } } } public static void main(String[] args){ Service service = new Service(); new Thread(service::read).start(); new Thread(service::write).start(); } }
线程管理
线程组
- 线程组概述
- 类似与在计算机中使用文件夹管理文件,也可以使用线程组来管理线程,在线程组中定义一组相似(相关)的线程,在线程组中也可以定义子线程组
- Thread类中有几个构造方法允许在创建线程时指定线程组,如果在创建线程时没有指定线程组则该线程就属于父线程所在的线程组,JVM在创建main线程时会为它指定一个线程组,因此每个java线程都有一个线程组与之关联,可以调用线程的getThreadGroup()方法返回线程组。
- 线程组开始是出于安全的考虑设计用来区分不同的Applet,然而ThreadGroup并未实现这一目标,在新开发的系统中,已经不常用线程组,现在一般会将一组相关的线程存入一个数组或一个集合中,如果仅仅是用来区分线程时,可以使用线程名称来区分,多数情况下,可以忽略线程组。
-
创建线程组
public class Test { public static void main(String[] args){ //1) 返回当前 main 线程的线程组 ThreadGroup mainGroup = Thread.currentThread().getThreadGroup(); System.out.println(mainGroup); //2) 定义线程组,如果不指定所属线程组,则自动归属当前线程所属的线程组中 ThreadGroup group1 = new ThreadGroup("group1"); System.out.println(group1); //3)定义线程组, 同时指定父线程组 ThreadGroup group2 = new ThreadGroup(mainGroup,"group2"); //现在 group1 与 group2 都是 maingroup 线程组中的子线程组, 调用线程组的 getParent()方法返回父线程组 System.out.println( group1.getParent() == mainGroup); System.out.println( group2.getParent() == mainGroup); //4) 在创建线程时指定所属线程组 Runnable r = new Runnable() { @Override public void run() { System.out.println(Thread.currentThread()); } }; //在创建线程时,如果没有指定线程组,则默认线程归属到父线程的线程组中 //在 main 线程中创建了 t1 线程,称 main 线程为父线程,t1 线程为子线程, t1 没有指定线程组则 t1 线程就归属到父线程 main 线程的线程组中 //创建线程时,可以指定线程所属线程组 Thread t2 = new Thread(group1, r, "t2"); Thread t3 = new Thread(group2, r, "t3"); System.out.println(t2); System.out.println(t3) } }
-
线程组的基本操作
activeCount() //返回当前线程组及子线程组中活动线程的数量(近似值) activeGroupCount() //返回当前线程组及子线程组中活动线程组的数量(近似值) int enumerate(Thread[] list) //将当前线程组中的活动线程复制到参数数组中 enumerate(ThreadGroup[] list) //将当前线程组中的活动线程组复制到参数数组中 getMaxPriority() //返回线程组的最大优先级,默认是 10 getName() //返回线程组的名称 getParent() //返回父线程组 interrupt() //中断线程组中所有的线程 isDaemon() //判断当前线程组是否为守护线程组 list() //将当前线程组中的活动线程打印出来 parentOf(ThreadGroup g) //判断当前线程组是否为参数线程组的父线程组 setDaemon(boolean daemon) //设置线程组为守护线程组
-
复制线程组中的线程及子线程组
enumerate(Thread[] list) //把当前线程组和子线程组中所有的线程复制到参数数组中 enumerate(Thread[] list, boolean recursive) //如果第二个参数设置为 false,则只复制当前线程组中所有的线程,不复制子线程组中的线程 enumerate(ThreadGroup[] list) //把当前线程组和子线程组中所有的线程组复制到参数数组中 enumerate(ThreadGroup[] list, boolean recurse) //第二个参数设置false,则只复制当前线程组的子线程组
-
线程组的批量中断
- 线程组的 interrupt() 可以给该线程组中所有的活动线程添加中断标志
-
设置守护线程组
- 守护线程是为了其他线程提供服务的,当JVM中只有守护线程时,守护线程会自动晓辉,JVM会退出
- 调用线程组的setDaemon(true)可以把线程组设置为守护线程组,当守护线程组中没有任何活动线程时,守护线程组会自动销毁
- 注意线程组的守护属性,不影响线程组中线程的守护属性,或者说守护线程组中的线程可以是非守护线程
捕获线程的执行异常
-
捕获线程的执行异常
-
在线程的run()方法中,如果有受检异常必须进行捕获处理,如果想要获得run()方法中出现的运行时异常,可以通过回调UncaughtExceptionHandler 接口获得哪个线程出现了运行时异常,在Thread类中有关处理运行异常的方法有:
- getDefaultUncaughtExceptionHandler() 获得全局的(默认的)UncaughtExceptionHandler
- getUncaughtExceptionHandler() 获得当前线程的 UncaughtExceptionHandler
- setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh ) 设置全局的 UncaughtExceptionHandler
- setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh) 设置当前线程的 UncaughtExceptionHandler
-
当线程运行过程中出现异常,JVM会调用Thread类的dispatchUncaughtException(Throwable e) 方法,该方法会调用getUncaughtExceptionHandler().uncaughtException(this, e); 如果想要线程中出现异常的信息,就需要设置线程的UncaughtExceptionHandler
/* 演示设置线程的 UnCaughtExceptionHandler 回调接口 */ public class Test { public static void main(String[] args) { Thread.setDefaultUncaughtExceptionHandler((t,e) -> System.out .println(t.getName() + "线程产生了异常:" + e.getMessage())); new Thread(() -> { try { System.out.println(Thread.currentThread().getName() + "开始运行"); TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(1 / 0); }).start(); new Thread(() -> { String text = null; System.out.println(text.length()); }).start(); /* 在实际开发中,这种设计异常处理的方式还是比较常用的,尤其是异常执行的方法 如果线程产生了异常,JVM会调用dispatchUncaughtException()方法,在该方法中调用了 getUncaughtExceptionHandler().uncaughtException(this, e);如果当前线程设置 了UncaughtExceptionHandler回调接口就直接调用它自己的uncaughtException方法,如果 没有设置则调用当前线程所在线程组UncaughtExceptionHandler回调接口的uncaughtException 方法,如果线程组也没有设置回调接口,则直接把异常的栈信息定向到System.err中 */ } }
-
-
注入Hook钩子线程
- 现在很多软件包括Mysql,Zookeeper,kafka等都存在Hook线程的校验机制,目的是校验进程是否已启动,防止重复启动程序
- Hook线程也称为钩子线程,当JVM退出的时候会执行Hook线程,经常在程序启动时创建一个.lock文件,用.lock文件校验程序是否启动,在程序退出(JVM退出)时删除该.lcok文件,在Hook线程中除了防止重新启动进程外,还可以做资源释放,尽量避免在Hook线程中进行复杂的操作
/* 通过HooK线程防止程序重复启动 */ public class Test { public static void main(String[] args) { //1)注入 Hook 线程,在程序退出时删除.lock 文件 Runtime.getRuntime().addShutdownHook(new Thread(){ @Override public void run() { System.out.println("JVM退出,会启动当前Hook线程,在Hook线程中删除.lock文件"); getLockFile().toFile().delete(); } }); //2)程序运行时,检查 lock 文件是否存在,如果 lock 文件存在,则抛出异常 if (getLockFile().toFile().exists()) { throw new RuntimeException("程序已启动"); }else { try { getLockFile().toFile().createNewFile(); System.out.println("程序在启动时创建了lock文件"); } catch (IOException e) { e.printStackTrace(); } } //模拟程序运行 for (int i = 0; i < 10; i++) { System.out.println("程序正在运行"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } } private static Path getLockFile() { return Paths.get("","temp.lock"); } }
线程池
-
什么是线程池?
-
线程的创建方式:可以通过 new Thread().start();这种形式开启一个线程,当run()方法运行结束,线程对象会被GC释放
-
真实的生产环境中,可能需要很多线程来支撑整个应用,当线程数量非常多时,反而会耗尽CPU资源,如果不对线程进行控制与管理,反而会影响程序的性能,线程开销主要包括:创建与启动线程的开销;线程销毁开销;线程调度开销;线程数量受限CPU处理器数量。
-
线程池就是有效使用线程的一种常用方式,线程池内部可以预先创建一定数量的工作线程,客户端代码直接将任务作为一个对象提交给线程池,线程池将这些任务缓存工作队列中,线程池的工作线程不断地从队列中取出任务执行。
-
-
JDK对线程池的支持
- JDK提供了一套Executor框架,可以帮助开发人员有效的使用线程池
-
线程池的基本使用
/* 线程池的基本使用 */ public class Test { public static void main(String[] args){ //创建线程池,线程数量为5 ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5); //向线程池中提交18个任务,这18个任务存储到线程池的阻塞队列中,线程池中这5个线程就从阻塞队列中取任务执行 for(int i = 0; i < 18; i++){ fixedThreadPool.execute(() -> { System.out.println(Thread.currentThread().getId() + " 编号正在执行任务"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } }); } } }
/* 线程池的计划任务 */ public class Test { public static void main(String[] args){ //创建一个有调度功能的线程池 ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10); //在延迟2秒后执行任务,schedule(Runnable任务,延迟时长,时间单位) scheduledExecutorService.schedule(() -> System.out.println(Thread.currentThread().getId() + "---" + System.currentTimeMillis()), 2,TimeUnit.SECONDS); //以固定的频率执行任务,开启任务的时间是固定的,在3秒后执行任务,以后每隔2秒重新执行一次 scheduledExecutorService.scheduleAtFixedRate(() -> { System.out.println(Thread.currentThread().getId() + "---" + System.currentTimeMillis()); try { //如果执行任务时长超过了时间间隔,则任务完成后立即开启下个任务 TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } },3,2,TimeUnit.SECONDS); //不管执行任务耗时多长,总是在任务结束后的2秒再次开启新的任务 scheduledExecutorService.scheduleWithFixedDelay(() -> { System.out.println(Thread.currentThread().getId() + "---" + System.currentTimeMillis()); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } },3,2,TimeUnit.SECONDS); } }
-
核心线程池的底层实现
-
查看Executors工具类中newCachedThreadPool(),newSingleThreadExcecutor(), newFixedThreadPool()源码:
-
newCachedThreadPool
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); } //该线程池在极端情况下,每次提交新的任务都会创建新的线程执行,适合用来执行大量耗时短并且提交频繁的任务
-
newSingleThreadExcecutor
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
-
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } //可以用来实现单一的生产者消费者模式
-
-
Excutors工具类中返回线程池的方法底层都使用了ThreadPoolExecutor线程池,这些方法都是ThreadPoolExecutor线程池的封装。
-
ThreadPoolExecutor的构造方法
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) 各个参数的含义: corePoolSize:指定线程池中核心线程的数量 maximumPoolSize:指定线程池中最大线程数量 keepAliveTime:当线程池中的线程数量超过corePoolSize时,多余的空闲线程存活时长,即空闲线程在多长时间内销毁 unit:是keepAliveTime的时长单位 workQueue:任务队列,把任务提交到该任务队列中等待执行 threadFactory:线程工厂,用于创建线程 handler:拒绝策略,当任务太多来不及处理时,如何拒绝
-
任务队列说明:workQueue工作队列是指提交未执行的任务队列,它是BlockingQueue接口的对象,仅用于存储Runnable任务,根据队列功能分类,在ThreadPoolExecutor构造方法中可以使用以下几种阻塞队列:
- 直接提交队列,由SynchronousQueue对象提供,该队列没有容量,提交给线程池的任务不会被真实保存,总是将新的任务提交给线程执行,如果没有空闲线程,则尝试创建新的线程,如果线程数量已经达到maxinumPoolSize规定的最大值则执行拒绝策略。
- 有界任务队列,由ArrayBlockingQueue实现,在创建ArrayBlockingQueue对象时,可以指定一个容量,当有任务需要执行时,如果线程池中线程数小于corePoolSize 核心线程数则创建新的线程;如果大于corePoolSize核心线程数则加入等待队列,如果队列已满则无法加入,在线程数小于maxinumPoolSize指定的最大线程数前提下会创建新的线程来执行,如果线程数大于maxinumPoolSize最大线程数则执行拒绝策略。
- 无界任务队列,由LinkedBlockingQueue对象实现,与有界队列相比,除非系统资源耗尽,否则无界队列不存在任务入队失败的情况,当有新的任务时,在系统线程数小于corePoolSize核心线程数则创建新的线程来执行任务,当线程池中线程数量大于corePoolSize核心线程数则把任务加入阻塞队列。
- 优先任务队列是通过PriorityBlockingQueue实现的,是带有任务优先级的队列,是一个特殊的无界队列,不管是ArrayBlockingQueue队列还是LinkedBlockingQueue队列都是按照先进先出算法处理任务的,在PriorityBlockingQueue队列中可以根据任务优先级先后执行。
-
-
拒绝策略
-
ThreadPoolExecutor构造方法的最后一个参数指定了拒绝策略,当提交给线程池的任务量超过实际承载能力时,如何处理?即线程池中的线程已经用完了,等待队列也满了,无法为新提交的任务服务,可以通过拒绝策略来处理这个问题,JDK提供了四种拒绝策略:
- AbortPolicy策略,会抛出异常
- CallerRunsPolicy策略,只要线程池没关闭,会在调用者线程中运行当前被丢弃的任务
- DiscardOldestPolicy将任务队列中最老的任务丢弃,尝试再次提交新任务
- DiscardPolicy直接丢弃这个无法处理的任务
-
Executors工具类提供的静态方法返回的线程池默认的拒绝策略是AbortPolicy抛出异常,如果内置的拒绝策略无法满足实际需求,可以扩展RejectedExecutionHandler接口
/* 自定义拒绝策略 */ public class Test { public static void main(String[] args){ //定义任务 Runnable r = new Runnable(){ public void run(){ int num = new Random().nextInt(5); System.out.println(Thread.currentThread().getId() + "---" + System.currentTimeMillis() + "开始睡眠" + num + "秒"); try { TimeUnit.SECONDS.sleep(num); }catch(InterruptedException e){ e.printStackTrace(); } } } //创建线程池,自定义拒绝策略 ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10), Executors.defaultThreadFactory(), (r,e) -> System.out.println(r + "is discarding...")); //向线程池提交若干任务 for(int i = 0; i < Integer.MAX_VALUE; i++){ threadPoolExecutor.submit(r); } } }
-
-
ThreadFactory线程工厂
-
线程池中的线程从哪儿来的?答案就是ThreadFactory。
-
ThreadFactory是一个接口,只有一个用来创建线程的方法:Thread newThread(Runnable r);当线程池中需要创建线程时就会调用该方法。
public class Test { //定义任务 Runnable r = new Runnable(){ public void run(){ int num = new Random().nextInt(10); System.out.println(Thread.currentThread().getId() + "---" + System.currentTimeMillis() + "开始睡眠:" + num + "秒"); try { TimeUnit.SECONDS.sleep(num); }catch(InterruptedException e){ e.printStackTrace(); } } }; //创建线程池,使用自定义线程工厂,采用默认的拒绝策略是抛出异常 ExecutorService executorService = new ThreadPoolExecutor(5,5,0,TimeUnit.SECONDS,new SynchronousQueue<>(), new ThreadFactory(){ public Thread newThread(Runnable r){ //根据参数r接收的任务,创建一个线程 Thread t = new Thread(r); //设置为守护线程,当主线程运行结束,线程池中的线程会自动退出 t.setDaemon(true); System.out.println("创建了线程:" + t); return r; } }); //提交了5个任务,当给当前线程池提交的任务超过5个时,线程池默认抛出异常 for(int i = 0; i < 5; i++){ executorService.submit(r); } //主线程睡眠 Thread.sleep(10000); //主线程睡眠超时,主线程结束,线程池中的线程会自动退出 }
-
-
监控线程池
-
ThreadPoolExecutor提供了一组方法用于监控线程池
int getActiveCount() //获得线程池中当前活动线程的数量 long getCompletedTaskCount() //返回线程池完成任务的数量 int getCorePoolSize() //线程池中核心线程的数量 int getLargestPoolSize() //返回线程池曾经达到的线程的最大数 int getMaximumPoolSize() //返回线程池的最大容量 int getPoolSize() //当前线程池的大小 BlockingQueue<Runnable> getQueue() //返回阻塞队列 long getTaskCount() //返回线程池收到的任务总数
/* 监控线程池 */ public static void main(String[] args) throws InterruptedException { //定义任务 Runnable r = () -> { System.out.println(Thread.currentThread().getId() + " 编号 的线程开始执行:" + System.currentTimeMillis()); try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } }; //定义线程池 ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(2,5,0,TimeUnit.SECONDS, new ArrayBlockingQueue<>(5),Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardPolicy()); //向线程池提交30个任务 for (int i = 0; i < 30; i++) { poolExecutor.submit(r); System.out.println("当前线程池核心线程数量:" + poolExecutor.getCorePoolSize() + ",最大线程数:" + poolExecutor.getMaximumPoolSize() + ",当前线程大小:" + poolExecutor.getPoolSize() + ",活动线程数量:" + poolExecutor.getActiveCount() + ",收到任务数量:" + poolExecutor.getTaskCount() + ",完成任务数:" + poolExecutor.getCompletedTaskCount() + ",等待任务数:" + poolExecutor.getQueue().size()); TimeUnit.MILLISECONDS.sleep(500); } System.out.println("--------------------------------------------"); while (poolExecutor.getActiveCount() > 0) { System.out.println("当前线程池核心线程数量:" + poolExecutor.getCorePoolSize() + ",最大线程数:" + poolExecutor.getMaximumPoolSize() + ",当前线程大小:" + poolExecutor.getPoolSize() + ",活动线程数量:" + poolExecutor.getActiveCount() + ",收到任务数量:" + poolExecutor.getTaskCount() + ",完成任务数:" + poolExecutor.getCompletedTaskCount() + ",等待任务数:" + poolExecutor.getQueue().size()); TimeUnit.SECONDS.sleep(1); } }
-
-
扩展线程池
-
有时需要对线程池进行扩展,如在监控每个任务的开始和结束时间,或者自定义一些其他增强的功能
-
ThreadPoolExecutor线程池提供了两个方法:
protected void afterExecute(Runnable r, Throwable t)
protected void beforeExecute(Thread t, Runnable r)
-
在线程池执行某个任务前会调用beforeExecute()方法,在任务结束后(任务异常退出)会执行afterExecute()方法
-
查看ThreadPoolExecutor源码,在该类中定义了一个内部类Worker,ThreadPoolExecutor线程池中的工作线程就是Worker类的实例,Worker实例在执行时会调用beforeExecute()与afterExecute()方法
/* 扩展线程池 */ public class Test { private static class MyTask implements Runnable { String name; public MyTask(String name){ this.name = name; } public void run(){ System.out.println(name + "任务正在被线程" + Thread.currentThread().getId() + "执行"); try { TimeUnit.SECONDS.sleep(1) }catch(InterruptedException e){ e.printStackTrace(); } } } public static void main(String[] args){ //定义扩展线程池,可以定义线程池类继承ThreadPoolExecutor,在子类中重写beforeExecute()/afterExecute()方法 //也可以直接使用ThreadPoolExecutor的内部类 ExecutorService executorService = new ThreadPoolExecutor(5,5,0,TimeUnit.SECONDS,new LinkedBlockingQueue<>()){ //在内部类中重写任务开始方法 protected void beforeExecute(Thread r,Runnable r){ System.out.println(t.getId() + "线程准备执行任务:" + ((MyTask)r).name); } protected void afterExecute(Runnable r,Throwable t){ System.out.println(((MyTask)r).name + "任务执行完毕"); } protected void terminated(){ System.out.println("线程池退出"); } }; //向线程池中添加任务 for(int i = 0; i < 5; i++){ MyTask task = new MyTask("task-" + i); executorService.execute(task); } //关闭线程池仅仅是说线程池不再接收新的任务,线程池中已接收的任务正常执行完毕 executorService.shutdown(); } }
-
-
优化线程池大小
- 线程池大小对系统性能是有一定影响的,过大或者过小都会无法发挥最优的系统性能,线程池大小不需要非常精确,只要避免极大或者极小的情况即可,一般来说,线程池大小需要考虑CPU数量,内存大小等因素,在《Java Concurrency in Practice》书中给出一个估算线程池大小的公式:
- 线程池大小 = CPU的数量 * 目标CPU的使用率 * (1 + 等待时间与计算时间的比)
- 线程池大小对系统性能是有一定影响的,过大或者过小都会无法发挥最优的系统性能,线程池大小不需要非常精确,只要避免极大或者极小的情况即可,一般来说,线程池大小需要考虑CPU数量,内存大小等因素,在《Java Concurrency in Practice》书中给出一个估算线程池大小的公式:
-
线程池死锁
- 如果在线程池中执行的任务A在执行过程中又向线程池提交了任务B,任务B添加到了线程池的等待队列中,如果任务A的结束需要等待任务B的执行结果,就有可能会出现这种情况:线程池中所有的工作线程都处于等待任务处理结果,而这些任务在阻塞队列中等待执行,线程池中没有可以对阻塞队列中的任务进行处理的线程,这种等待会一直持续下去,从而造成死锁。
- 适合给线程池提交相互独立的任务,而不是彼此依赖的任务,对于彼此依赖的任务,可以考虑分别提交给不同的线程池来执行。
-
线程池中的异常处理
-
在使用ThreadPoolExecutor进行submit提交任务时,有的任务抛出了异常,但是线程池并没有进行提示,即线程池把任务中的异常给吃掉了,可以把submit提交改为execute执行,也可以对ThreadPoolExecutor线程池进行扩展,对提交的任务进行包装:
/* 自定义线程池类,对ThreadPoolExecutor进行扩展 */ public class Test { //自定义线程池类 private static class TraceThreadPoolExecutor extends ThreadPoolExecutor{ public TraceThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); } //定义方法,对执行的任务进行包装,接收两个参数,第一个参数接收要执行的任务,第二个参数是一个Exception异常 public Runnable wrap(Runnable task,Exception exception){ return () -> { try { task.run(); } catch (Exception e) { exception.printStackTrace(); throw e; } }; } @Override public Future<?> submit(Runnable task) { return super.submit(wrap(task,new Exception("客户跟踪异常"))); } @Override public void execute(Runnable command) { super.execute(wrap(command,new Exception("客户跟踪异常"))); } } //定义类实现Runnable接口,用于计算两个数相除 private static class DivideTask implements Runnable{ private int x; private int y; public DivideTask(int x, int y) { this.x = x; this.y = y; } @Override public void run() { System.out.println(Thread.currentThread().getId() + "号线程计算:" + x + "/" + y + "=" + (x/y)); } } public static void main(String[] args) { //创建线程池 ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 0, TimeUnit.SECONDS, new SynchronousQueue<>()); //自定义线程池 ThreadPoolExecutor trace = new TraceThreadPoolExecutor(0, Integer.MAX_VALUE, 0, TimeUnit.SECONDS, new SynchronousQueue<>()); for (int i = 0; i < 5; i++) { /* 在没有使用自定义线程池之前,submit()方法会吃掉异常,解决方法是使用execute()方法提交任务 poolExecutor.submit(new DivideTask(10,i)); poolExecutor.execute(new DivideTask(10,i)); */ trace.submit(new DivideTask(10,i)); } } }
-
-
ForkJoinPool线程池
-
“分而治之”是一个有效的处理大数据的方法,著名的MapReduce就是采用这种分而治之的思路,简单点说,如果要处理的1000个数据,但是我们不具备处理1000个数据的能力,可以只处理10个数据,可以把这1000个数据分阶段处理100次,每次处理10个数据,可以把这1000个数据分阶段处理100次,把100次的处理结果进行合成,形成最后这1000个数据的处理结果
-
把一个大任务调用fork()方法分解为若干小的任务,把小任务的处理结果进行join()合并为大任务的结果
-
系统对ForkJoinPool线程池进行了优化,提交的任务数量与线程的数量不一定是一对一关系,在多数情况下,一个物理线程实际上需要处理多个逻辑任务
-
ForkJoinPool线程池中最常用的方法是: ForkJoinTask submit(ForkJoinTask task) 向线程池提交一个ForkJoinTask任务,ForkJoinTask任务支持fork()分解与join()等待的任务,ForkJoinTask有两个重要的子类:RecursiveAction 和 RecursiveTask,它们的区别在于RecursiveAction任务没有返回值,RecursiveTask任务可以带有返回值。
/* 演示ForkJoinPool线程池的使用 使用该线程池模拟数列求和 */ public class Test { //计算数列的和,需要返回结果,可以定义任务继承RecursiveTask private static class CountTask extends RecursiveTask<Long> { //定义数据规模的阈值,超过该数就要分解 private static final int THRESHOLD = 10000; //定义每次把大任务分解为100个小任务 private static final int TASK_NUM = 100; //计算数列的起始值 private long start = 0; //计算数列的结束值 private long end = 0; public CountTask(long start, long end) { this.start = start; this.end = end; } //重写RecursiveTask类的compute()方法,计算数列的结果 @Override protected Long compute() { //保存计算的结果 long sum = 0; //判断任务是否需要分解 if ((end - start < THRESHOLD)) { //小于阈值可以直接计算 for (long i = start; i <= end; i++) { sum += i; } }else { //需要分解任务 long step = (start + end) / TASK_NUM; /* 注意:如果任务划分的层次很深,即阈值太小,每个任务的计算量很小,层次划分就会很深,导致以下情况 1.系统内的线程数量会越积越多,导致性能下降严重 2.分解次数过多,方法调用过多可能会导致栈溢出 */ //创建一个存储任务的集合 List<CountTask> subTaskList = new ArrayList<>(); //每个任务的起始位置 long pos = start; for (int i = 0; i < TASK_NUM; i++) { //每个任务的结束位置 long lastOne = pos + step; //调整最后一个任务的结束位置 if (lastOne > end) { lastOne = end; } //创建子任务 CountTask task = new CountTask(pos,lastOne); //把任务添加到集合中 subTaskList.add(task); //调用fork()提交子任务 task.fork(); //调整下个任务的起始位置 pos += step + 1; } //等待所有的子任务结束后,合并计算结果 for (CountTask task : subTaskList) { //join()会一直等待子任务执行完毕返回执行结果 sum += task.join(); } } return sum; } } public static void main(String[] args) { //创建ForkJoinPool线程池 ForkJoinPool forkJoinPool = new ForkJoinPool(); CountTask task = new CountTask(0,200000); ForkJoinTask<Long> result = forkJoinPool.submit(task); try { Long res = result.get(); System.out.println("多任务计算结果:" + res); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } long sum = 0; for (int i = 0; i <= 200000; i++) { sum += i; } System.out.println("验证:" + sum); } }
-
保障线程安全的设计技术
- 从面向对象设计的角度出发介绍几种保障线程安全的设计技术,这些技术可以使得我们在不必借助锁得情况下保障线程安全,避免锁可能导致的问题及开销。
Java运行时存储空间
- java运行时(Java Runtime)空间可以分为栈区,堆区与方法区(非堆空间)
- 栈空间(Stack Space)为线程的执行准备一段固定大小的存储空间,每个线程都有独立的线程栈空间,创建线程时就为线程分配栈空间,在线程栈中每调用一个方法就给方法分配一个栈帧,栈帧用于存储方法的局部变量,返回值等私有数据,即局部变量存储在栈空间中,基本类型变量也是存储在栈空间中,引用类型变量值也是存储在栈空间中,引用的对象存储在堆中。由于线程是相互独立的,一个线程不能访问另外一个线程的栈空间,因此线程对局部变量以及只能通过当前线程的局部变量才能访问的对象进行的操作具有固定的线程安全性。
- 堆空间(Heap Space)用于存储对象,是在JVM启动时分配的一段可以动态扩容的内存空间,创建对象时,在堆空间中给对象分配存储空间,实例变量就是存储在堆空间中的,堆空间是多个线程之间可以共享的空间,因此实例变量可以被多个线程共享,多个线程同时操作实例变量可能存在线程安全问题。
- 非堆空间(Non-Heap Space)用于存储常量,类的元数据等,非堆空间也是在JVM启动时分配的一段可以动态扩容的存储空间,类的元数据包括静态变量,类有哪些方法及这些方法的元数据(方法名,参数,返回值等),非堆空间也是多个线程共享的,因此访问非堆空间中的静态变量也可能存在线程安全问题。
- 堆空间和非堆空间是线程可以共享的空间,即实例变量与静态变量是线程可以共享的,可能存在线程安全问题,栈空间是线程私有的存储空间,局部变量存储在栈空间中,局部变量具有固定的线程安全性。
无状态对象
- 对象就是数据及对数据操作的封装,对象所包含的数据称为对象的状态(State),实例变量与静态变量称为状态变量
- 如果一个类的同一个实例被多个线程共享并不会使这些线程存储共享的状态,那么该类的实例就称为无状态对象(Stateless Object),反之如果一个类的实例被多个线程共享会使这些线程存在共享状态,那么该类的实例称为有状态对象。实际上无状态对象就是不包含任何实例变量也不包含任何静态变量的对象。
- 线程安全问题的前提是多个线程存在共享的数据,实现线程安全的一种办法就是避免在多个线程之间共享数据,使用无状态对象就是这种方法。
不可变对象
- 不可变对象是指一经创建它的状态就保持不变的对象,不可变对象具有固定的线程安全性,当不可变对象现实实体的状态发送变化时,系统会创建一个新的不可变对象,就如String字符串对象,一个不可变对象需要满足以下条件:
- 类本身使用final修饰,防止通过创建子类来改变它的定义
- 所有字段都是final修饰的,final字段在创建时必须显示初始化,不能被修改
- 如果字段引用了其他状态可变的对象(集合,数组),则这些字段必须时private私有的
- 不可变对象的主要应用场景:
- 被建模对象的状态变化不频繁
- 同时对一组相关数据进行写操作,可以应用不可变对象,既可以保障原子性也可以避免锁的使用
- 使用不可变对象作为安全可靠的Map键,HashMap键值对的存储位置与键的hashCode()有关,如果键的内部状态发生了变化会导致键的哈希码不同,可能会影响键值对的存储位置。如果HashMap的键是一个不可变对象,则hashCode()方法的返回值恒定,存储位置是固定的。
线程特有对象
- 我们可以选择不共享非线程安全的对象,对于非线程安全的对象,每个线程都创建一个该对象的实例,各个线程访问各自创建的实例,一个线程不能访问另外一个线程创建的实例,这种各个线程创建各自的实例,一个实例只能被一个线程访问的对象就称为线程特有对象,线程特有对象既保障了对非线程安全对象的访问的线程安全,又避免了锁的开销,线程特有对象也具有固定的线程安全性。
- ThreadLocal 类相当于线程访问其特有对象的代理,即各个线程通过ThreadLocal对象可以创建并访问各自的线程特有对象,泛型T指定了线程特有对象的类型,一个线程可以使用不同的ThreadLocal实例来创建并访问不同的线程特有对象
- ThreadLocal实例为每个访问它的线程都关联了一个该线程特有的对象,ThreadLocal实例都有当前线程与特有实例之间的一个关联。
修饰器模式
- 装饰器模式可以用来实现线程安全,基本思想是为非线程安全的对象创建一个相应的线程安全的外包装对象,客户端代码不直接访问非线程安全的对象而是访问它的外包装对象,外包装对象与非线程安全的对象具有相同的接口,即外包装对象的使用方式与非线程安全对象的使用方式相同,而外包装对象内部通常会借助锁,以线程安全的方式调用相应的非线程安全对象的方法。
- 在java.util.Collections工具类中提供了一组synchronizedXXX(xxx) 可以把不是线程安全的xxx集合转换为线程安全的集合,它就是采用了这种装饰者模式,这个方法返回值就是指定集合的外包装对象,这类集合又称为同步集合。
- 使用装饰者模式的一个好处就是实现关注点分离,在这种设计中,实现同一组功能的对象的两个版本:非线程安全的对象与线程安全的对象,对于非线程安全的在设计时只关注要实现的功能,对于线程安全的版本只关注线程安全性
锁的优化及注意事项
有助于提供锁性能的几点建议
-
减少锁持有时间
-
对于使用锁进行并发控制的应用程序来说,如果单个线程特有锁的时间过长,会导致锁的竞争更加激烈,会影响系统的性能,在程序中需要尽可能减少线程对锁的持有时间,如下面代码:
public synchronized void syncMethod(){ othercode1(); mutexMethod(); othercode2(); }
-
在syncMethod同步方法中,假设只有mutexMethod()方法是需要同步的,othercode1()方法与othercode2()方法不需要进行同步,如果othercode1与othercode2这两个方法需要花费较长的CPU时间,在并发量较大的情况下这种同步方案会导致等待线程的大量增加,一个较好的优化方案是,只在必要时进行同步,可以减少锁的持有时间,提高系统的吞吐量,如把上面的代码改为:
public void syncMethod(){ othercode1(); synchronized(this){ mutexMethod(); } othercode2(); }
-
只对mutexMethod方法进行同步,这种减少锁持有时间有助于降低锁冲突的可能性,提升系统的并发能力。
-
-
减小锁的粒度
- 一个锁保护的共享数据的数量大小称为锁的粒度,如果一个锁保护的共享数据的数量大就称该锁的粒度粗,否则称该锁的粒度细,锁的粒度过粗会导致线程在申请锁时需要进行不必要的等待,减少锁粒度是一种削弱多线程锁竞争的一种手段,可以提高系统的并发性。
- 在JDK7前,java.util.concurrent.ConcurrentHashMap类采用分段锁协议,可以提高程序的并发性
-
使用读写分离锁代替独占锁
- 使用ReadWriteLock读写分离锁可以提高系统性能,使用读写分离锁也是减小锁粒度的一种特殊情况,第二条建议是能分割数据结构实现减小锁的粒度,那么读写锁是对系统功能点的分割。
- 在多数情况下都允许多个线程同时读,在写的使用采用独占锁,在读多写少的情况下,使用读写锁可以大大提高系统的并发能力。
-
锁分离
- 将读写锁的思想进一步延伸就是锁分离,读写锁是根据读写操作功能上的不同进行了锁分离,根据应用程序功能的特点,也可以对独占锁进行分离,如java.util.concurrent.LinkedBlockingQueue类中take()与put()方法分别从队头取数据,把数据添加到队尾,虽然这两个方法都是对队列进行修改操作,由于操作的主题是链表,take()操作的是链表的头部,put()操作的是链表的尾部,两者并不冲突,如果采用独占锁的话,这两个操作不能同时并发,在该类中就采用锁分离,take()取数据时有取锁,put()添加数据时有自己的添加锁,这样take()与put()就相互独立的实现了并发。
-
锁粗化
-
为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,但是凡事都有一个度,如果对同一个锁不断的进行请求,同步和释放,也会消耗系统资源,如:
public void method1(){ synchronized( lock ){ 同步代码块 1 } synchronized( lock ){ 同步代码块 2 } }
-
JVM在遇到一连串不断对同一个锁进行请求和释放操作时,会把所有的锁整合成对锁的一次请求,从而减少对锁的请求次数,这个操作叫做锁的粗化,如上一段代码会整合为:
public void method1(){ synchronized( lock ){ 同步代码块 1 同步代码块 2 } }
-
在开发过程中,也应该有意识的在合理的场合进行锁的粗化,尤其在循环体内请求锁时,如:
for(int i = 0 ; i< 100; i++){ synchronized(lock){} }
-
这种情况下,意味着每次循环都需要申请锁和释放锁,所以一种更合理的做法就是在循环体外请求一次锁,如:
synchronized( lock ){ for(int i = 0 ; i< 100; i++){} }
-
JVM对锁的优化
- 锁偏向
- 锁偏向是一种针对加锁操作的优化,如果一个线程获得了锁,那么锁就进入偏向模式,当这个线程再次请求锁时,无须再做任何同步操作,这样可以节省有关锁申请的时间,提高了程序的性能。
- 锁偏向在没有锁竞争的场合可以有较好的优化效果,对于锁竞争比较激烈的场景,效果不佳,锁竞争激烈的情况下可能是每次都是不同线程来请求锁,这时偏向模式失效。
- 轻量级锁
- 如果锁偏向失败,JVM不会立即挂起线程,还会使用一种称为轻量级锁的优化手段,会将对象的头部作为指针,指向持有锁的线程堆栈内部,来判断一个线程是否持有对象锁,如果线程获得轻量级锁成功,就进入临界区,如果获得轻量级锁失败,表示其他线程抢到了锁,那么当前线程的锁的请求就膨胀为重量级锁,当前线程就转到阻塞队列中变为阻塞状态。
- 偏向锁,轻量级锁都是乐观锁,重量级锁是悲观锁
- 一个对象刚开始实例化时,没有任何线程访问它,它是可偏向的,即它认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,偏向第一个线程,这个线程在修改对象头成为偏向锁时使用CAS操作,将对象头中ThreadId改成自己的ID,之后再访问这个对象,只需要对比ID即可,一旦有第二个线程访问该对象,因为偏向锁不会主动释放,所以第二个线程可以查看对象的偏向状态,当第二个线程访问对象时,表示在这个对象上已经存在竞争了,检查原来持有对象锁的线程是否存活,如果挂了则将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行原来线程的栈,检查该对象的使用情况,如果任然需要偏向锁,则偏向锁升级为轻量级锁。
- 轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对同一个锁的操作会错开,或者稍微等待一下(自旋)另外一个线程就会释放锁,当自旋锁超过一定次数,或者一个线程持有锁,一个线程在自旋,又来第三个线程访问时,轻量级锁会膨胀为重量级锁,重量级锁除了持有锁的线程外,其他的线程都阻塞
对于使用锁进行并发控制的应用程序来说,如果单个线程特有锁的时间过长,会导致锁的竞争更加激烈,会影响系统的性能,在程序中需要尽可能减少线程对锁的持有时间,如下面代码:
public synchronized void syncMethod(){
othercode1();
mutexMethod();
othercode2();
}
-
在syncMethod同步方法中,假设只有mutexMethod()方法是需要同步的,othercode1()方法与othercode2()方法不需要进行同步,如果othercode1与othercode2这两个方法需要花费较长的CPU时间,在并发量较大的情况下这种同步方案会导致等待线程的大量增加,一个较好的优化方案是,只在必要时进行同步,可以减少锁的持有时间,提高系统的吞吐量,如把上面的代码改为:
public void syncMethod(){ othercode1(); synchronized(this){ mutexMethod(); } othercode2(); }
-
只对mutexMethod方法进行同步,这种减少锁持有时间有助于降低锁冲突的可能性,提升系统的并发能力。
-
减小锁的粒度
- 一个锁保护的共享数据的数量大小称为锁的粒度,如果一个锁保护的共享数据的数量大就称该锁的粒度粗,否则称该锁的粒度细,锁的粒度过粗会导致线程在申请锁时需要进行不必要的等待,减少锁粒度是一种削弱多线程锁竞争的一种手段,可以提高系统的并发性。
- 在JDK7前,java.util.concurrent.ConcurrentHashMap类采用分段锁协议,可以提高程序的并发性
-
使用读写分离锁代替独占锁
- 使用ReadWriteLock读写分离锁可以提高系统性能,使用读写分离锁也是减小锁粒度的一种特殊情况,第二条建议是能分割数据结构实现减小锁的粒度,那么读写锁是对系统功能点的分割。
- 在多数情况下都允许多个线程同时读,在写的使用采用独占锁,在读多写少的情况下,使用读写锁可以大大提高系统的并发能力。
-
锁分离
- 将读写锁的思想进一步延伸就是锁分离,读写锁是根据读写操作功能上的不同进行了锁分离,根据应用程序功能的特点,也可以对独占锁进行分离,如java.util.concurrent.LinkedBlockingQueue类中take()与put()方法分别从队头取数据,把数据添加到队尾,虽然这两个方法都是对队列进行修改操作,由于操作的主题是链表,take()操作的是链表的头部,put()操作的是链表的尾部,两者并不冲突,如果采用独占锁的话,这两个操作不能同时并发,在该类中就采用锁分离,take()取数据时有取锁,put()添加数据时有自己的添加锁,这样take()与put()就相互独立的实现了并发。
-
锁粗化
-
为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,但是凡事都有一个度,如果对同一个锁不断的进行请求,同步和释放,也会消耗系统资源,如:
public void method1(){ synchronized( lock ){ 同步代码块 1 } synchronized( lock ){ 同步代码块 2 } }
-
JVM在遇到一连串不断对同一个锁进行请求和释放操作时,会把所有的锁整合成对锁的一次请求,从而减少对锁的请求次数,这个操作叫做锁的粗化,如上一段代码会整合为:
public void method1(){ synchronized( lock ){ 同步代码块 1 同步代码块 2 } }
-
在开发过程中,也应该有意识的在合理的场合进行锁的粗化,尤其在循环体内请求锁时,如:
for(int i = 0 ; i< 100; i++){ synchronized(lock){} }
-
这种情况下,意味着每次循环都需要申请锁和释放锁,所以一种更合理的做法就是在循环体外请求一次锁,如:
synchronized( lock ){ for(int i = 0 ; i< 100; i++){} }
-
JVM对锁的优化
- 锁偏向
- 锁偏向是一种针对加锁操作的优化,如果一个线程获得了锁,那么锁就进入偏向模式,当这个线程再次请求锁时,无须再做任何同步操作,这样可以节省有关锁申请的时间,提高了程序的性能。
- 锁偏向在没有锁竞争的场合可以有较好的优化效果,对于锁竞争比较激烈的场景,效果不佳,锁竞争激烈的情况下可能是每次都是不同线程来请求锁,这时偏向模式失效。
- 轻量级锁
- 如果锁偏向失败,JVM不会立即挂起线程,还会使用一种称为轻量级锁的优化手段,会将对象的头部作为指针,指向持有锁的线程堆栈内部,来判断一个线程是否持有对象锁,如果线程获得轻量级锁成功,就进入临界区,如果获得轻量级锁失败,表示其他线程抢到了锁,那么当前线程的锁的请求就膨胀为重量级锁,当前线程就转到阻塞队列中变为阻塞状态。
- 偏向锁,轻量级锁都是乐观锁,重量级锁是悲观锁
- 一个对象刚开始实例化时,没有任何线程访问它,它是可偏向的,即它认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,偏向第一个线程,这个线程在修改对象头成为偏向锁时使用CAS操作,将对象头中ThreadId改成自己的ID,之后再访问这个对象,只需要对比ID即可,一旦有第二个线程访问该对象,因为偏向锁不会主动释放,所以第二个线程可以查看对象的偏向状态,当第二个线程访问对象时,表示在这个对象上已经存在竞争了,检查原来持有对象锁的线程是否存活,如果挂了则将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行原来线程的栈,检查该对象的使用情况,如果任然需要偏向锁,则偏向锁升级为轻量级锁。
- 轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对同一个锁的操作会错开,或者稍微等待一下(自旋)另外一个线程就会释放锁,当自旋锁超过一定次数,或者一个线程持有锁,一个线程在自旋,又来第三个线程访问时,轻量级锁会膨胀为重量级锁,重量级锁除了持有锁的线程外,其他的线程都阻塞