1.线程概述
进程:
进程是计算机中的程序关于某数据集合上的一次运行活动,
是操作系统进行资源分配与调度的基本单位。
可以把进程简单的理解为正在操作系统中运行的一个程序。
线程:
线程是进程的一个执行单元,
一个线程就是进程中一个单一顺序的控制流,
线程是进程的一个执行分支。
进程是线程的容器,一个进程至少有一个进程。
一个进程中也可以有多个线程。
在操作系统中是以进程为单位分配资源,如虚拟空间,文件描述符等。
每个线程都有各自的线程栈、自己的寄存器环境、自己的线程本地存储。
主线程与子线程:
JVM启动时会创建一个主线程,该线程主要负责执行main方法,
主线程就是运行main方法的线程
Java中的线程不是孤立的,线程之间存在一些联系。
如果在A线程中创建了B线程,称B线程是A线程的子线程,A线程是B线程的父线程。
2.串行、并行和并发
并发可以提高事务的处理效率
并行是一种更为严格更为理想的并发
从硬件角度来说,如果单核CPU,一个处理器一次只能执行一个线程的情况下,
处理器可以使用时间片轮转技术,可以让CPU快速的在各个线程之间进行切换,
对于用户来说,感觉就像三个线程同时执行一样。
如果是多核心CPU,可以为不同的线程分配不同的CPU内核。
创建线程的两种方式:
1.定义Thread类的子类
2.定义一个Runnable接口的实现类
注意:这两种创建线程的方式没有本质的区别
package test.Thread;
/*
定义一个类继承Thread
*/
public class MyThread extends Thread {
/*
重写父类中的run()方法
run()方法体中的代码就是子线程要执行的任务
*/
@Override
public void run(){
super.run();
System.out.println("这是子线程打印的内容");
}
}
public class test {
public static void main(String[] args) {
System.out.println("JVM启动main线程,main线程执行main方法");
//创建子线程对象
MyThread thread=new MyThread();
//启动线程
thread.start();
/*
调用线程的start()方法来启动线程,启动线程的实质就是请求JVM运行相应的线程,
这个线程具体是什么时候运行,由线程调度器Scheduler决定
注意:start()方法调用结束并不意味着子线程开始运行
新开启的线程会执行run()方法
如果开启了多个线程,start()调用的顺序并不一定就是线程启动的顺序
多线程运行结果与代码调用顺序无关
*/
System.out.println("main线程后边其他的代码....");
}
}
验证多线程运行结果的随机性
package test.Thread.p2;
public class MyThread2 extends Thread{
@Override
public void run(){
try {
for(int i=0;i<=10;i++){
System.out.println("sub thread:"+i);
int time= (int) (Math.random()*1000);
Thread.sleep(time); //线程休眠,单位是ms
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
package test.Thread.p2;
/*
演示运行结果的随机性
*/
public class test {
public static void main(String[] args) {
MyThread2 thread2=new MyThread2();
thread2.start();//开启子线程
//当前是main线程
try {
for(int i=0;i<=10;i++){
System.out.println("main---:"+i);
int time= (int) (Math.random()*1000);
Thread.sleep(time); //线程休眠,单位是ms
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
实现Runnable接口的形式创建线程
package test.Thread.p3;
/*
当线程类已经有父类了,就不能用继承Thread类的形式创建线程,可以使用实现Runnable接口的形式
1.定义实现Runnable接口
*/
public class MyRunnable implements Runnable{
//2.重写Runnable接口中的run方法,run方法就是子线程要执行的代码
@Override
public void run() {
for(int i=1;i<=100;i++){
System.out.println("sub thread --> "+i);
}
}
}
package test.Thread.p3;
public class test {
public static void main(String[] args) {
//3.创建Runnable接口的实现类对象
MyRunnable runnable=new MyRunnable();
//创建线程对象
Thread thread=new Thread(runnable);
//开启线程
thread.start();
//当前是main线程
for (int i=1;i<=100;i++){
System.out.println("main -->"+i);
}
//有时调用Thread(Runnable)构造方法时,实参也会传递匿名内部类对象
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
for (int i=1;i<=100;i++){
System.out.println("sub--->"+i);
}
}
});
thread2.start();
}
}
同一段代码可能被不同的线程执行,因此当前线程是相对的
获得当前线程的currentThread方法的返回值是在代码运行时候的线程对象
注意:当前线程就是调用这一段代码的线程
package test.Thread.p4;
/*
定义线程类
分别在构造方法和run()方法中打印当前线程名称
*/
public class SubThread1 extends Thread{
public SubThread1(){
System.out.println("构造方法打印当前线程的名称:"+Thread.currentThread().getName());
}
@Override
public void run(){
System.out.println("run方法打印当前线程的名称:"+Thread.currentThread().getName());
}
}
package test.Thread.p4;
/*
注意一点就行了:当前线程就是调用这一段代码的线程
*/
public class test01CurrentThread {
public static void main(String[] args) {
System.out.println("main方法中打印当前线程"+Thread.currentThread().getName());
//创建子线程,调用SubThread1构造方法,
// 在main线程中调用构造方法,所以构造方法中输出的当前线程就是main线程
SubThread1 s1=new SubThread1();
s1.start();//启动子线程,子线程会调用run()方法,所以在run方法中输出的当前线程就是Thread-0(子线程)
s1.run();//在main方法中直接调用run方法,没有开启新的线程,所以在run方法中输出的当前线程就是main线程
}
}
currentThread的一个复杂案例
package test.Thread.p5;
public class SubThread2 extends Thread{
public SubThread2(){
System.out.println("构造方法中,Thread.currentThread().getName() :"+Thread.currentThread().getName());
System.out.println("构造方法,this.getName() :"+this.getName());
}
@Override
public void run(){
System.out.println("run方法中,Thread.currentThread().getName() :"+Thread.currentThread().getName());
System.out.println("run方法,this.getName() :"+this.getName());
}
}
package test.Thread.p5;
public class test02currentThread {
public static void main(String[] args) throws InterruptedException {
//创建子线程对象
SubThread2 t2 = new SubThread2();
t2.setName("t2");//设置线程名称
t2.start();
Thread.sleep(500);
//Thread(Runnable)构造方法形参是Runnable接口,调用时传递的实参是接口的实现类对象
// Thread类是Runnable接口的实现类
Thread t3=new Thread(t2);//这里实际上是传的Runnable接口实现类对象
// t2是t3的父类,创建的时候不会执行父类的构造方法
t3.start();//线程开启后,run方法中的this是我们传的t2实现类对象,为什么呢?
//看Thread类的源码得知,Thread类的run()方法有个判断,如果实现类时候传了target参数且不为空
//则执行target的run方法,所以,这里我们看到的this是t2
System.out.println("t3 :"+t3.getName());//t3的名字是Thread-1而不是t2
}
}
运行结果:
构造方法中,Thread.currentThread().getName() :main
构造方法,this.getName() :Thread-0
run方法中,Thread.currentThread().getName() :t2
run方法,this.getName() :t2
t3 :Thread-1
run方法中,Thread.currentThread().getName() :Thread-1
run方法,this.getName() :t2
public
class Thread implements Runnable {
@Override
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
@Override
public void run() {
if (target != null) {
target.run();
}
}
}
Thread的setName( )/getName( )
thread.setName(线程名称)//设置线程名称
thread.setName()//返回线程名称
通过设置线程名称,有助于程序调试,提高程序的可读性,建议为每一个线程设置一个能够体现线程功能的名称
isAlive方法测试线程活动状态
thread. isAlive()//判断当前线程是否处于活动状态
活动状态就是线程已启动并尚未终止
sleep方法
Thread.sleep(毫秒数);//让当前线程休眠指定的毫秒数
当前线程指的是Thread.currentThread()返回的线程
//在子线程的run方法中,如果没有受检异常(编译时异常)需要处理,只有选择捕获处理,不能抛出处理
//因为我们继承的Thread重写的是Thread类的run方法,Thread类是捕获,我们也只能选择捕获处理
简单计时器
package test.Thread.sleep;
/*
完成一个简易的计时器
*/
public class SimpleTimer {
public static void main(String[] args) throws InterruptedException {
int remaining = 10; //从60秒开始计时
// //读取main方法参数
// if (args.length==1){
// remaining = Integer.parseInt(args[0]);
// }//可以在run的editConfigration的Argument中设置参数
while(true){
System.out.println("还有:"+remaining+"秒");
if(remaining<0){
break;
}
remaining--;
Thread.sleep(1000);//线程休眠
}
System.out.println("计时完成!!");
}
}
getId( )方法
thread.getId( )可以获得线程的唯一标识
注意:某个编号的线程运行结束后,该编号可能被后续创建的线程使用
重启JVM后,同一个线程的编号可能不一样
yieId( )方法
thread.yieId( ) // 放弃当前的CPU资源
setPriority( )方法
thread.setPriority( )//设置线程的优先级
java线程的优先级的取值范围是1~10,如果超出这个范围会抛异常 IllegalArgumentException.
在操作系统中,优先级越高的线程获得CPU的资源的概率越大
线程优先级本质上只是给线程调度器一个提示信息,以便于调度器决定先调度哪些线程,
注意:不能保证优先级搞的线程先运行
Java优先级设置不当或者滥用可能会导致某些线程永远无法得到运行,即产生了线程饥饿
线程的优先级并不是设置的越高越好,一般情况下使用普通的优先级(默认值是5)即可,即在开发时不必设置线程的优先级
线程的优先级具有继承性,B线程继承了A线程,则B线程的优先级和A线程一样
interrupt方法( )
//可以中断线程,仅仅是给线程标记中断,线程并没有真正的中断
//注意:调用interrupt()方法仅仅是在当前线程打印了一个停止标志,并不是真正的停止线程
//线程有个isinterrupted()方法,该方法返回线程中断标志
@override
public void run(){
for(int i=1;i<=10000;i++){
if(this.isinterrupted()){
sout("当前线程中断的标志为true,我要退出了")
//break; //中断循环,run()方法执行完毕,子线程运行完毕
return; //直接结束当前run()方法的执行
}
}
}
setDaemon()方法
/*
作用:设置线程为守护线程
Java中的线程分为 用户线程 与 守护线程
守护线程是为其他线程提供服务的线程,如垃圾回收(GC)就是一个典型的守护线程
守护线程不能单独运行,当JVM没有其他用户线程,只有守护线程时,守护线程会自动销毁,JVM会退出
注意:销毁守护线程需要一点时间,在完全销毁之前,守护线程还是会运行
设置守护线程的代码,应该在线程启动前
*/
线程的生命周期
线程的生命周期就是线程的 生老病死,即线程的状态
线程生命周期可以通过getState()方法来获得
线程的状态是Thread.State枚举类型定义的,有以下几种:
//NEW状态,
新建状态,创建了线程对象,在调用start()启动之前的状态
//RUNNABLE状态,
可运行状态,它是一个复合状态,包含:READY和RUNNING两个状态。
//READY状态,
该线程可以被线程调度器进行调度使它处于RUNNING状态。
//RUNNING状态,
表示线程正在执行。
Tread.yieId()方法可以把线程由RUNNING状态转换为READY状态
//BLOCKED 阻塞状态,
线程发起一个阻塞的I/O操作,或者申请用其他线程占用的独占资源,线程会转为BLOCKED阻塞状态
处于阻塞状态的线程不会占用CPU资源,当阻塞I/O操作执行完,或者线程获得了其申请的资源,线程可以转为RUNNABLE。
//WAITING等待状态,
线程执行了object.wait(),thread.join()方法会把线程转换为WAITING等待状态,
执行object.notify()方法,或者加入的线程执行完毕,当前线程会转换为RUNNABLE状态
//TIME_WAITING状态,
与WAITING状态类似,都是等待状态,区别在于处于该状态的线程不会无限期等待,
如果线程没有在指定时间范围内完成期望的操作,该线程会自动转换为RUNNABLE。
//TERMINATED 终止状态
线程处于终止状态
线程状态图
多线程的优势与存在的风险
优势:
1.提高系统的吞吐率(Throught), 多线程编程可以使一个进程有多个并发(concurrent,即同时进行)的操作
2.提高响应性(Responsiveness), Web服务器会采用一些专门的线程负责用户的请求处理,
缩短了用户的等待时间。
3.充分利用多核(Multicore)处理器资源, 通过多线程可以充分的利用CPU资源,可以避免资源浪费
风险:
1.线程安全(Thread safe)问题, 多线程共享数据时,如果没有采取正确的并发访问控制措施,
就可能会产生数据一致性问题,如读取脏数据(过期的数据)问题,如丢失数据更新.
2.线程活性问题, 由于程序自身的缺陷或者由资源稀缺导致线程一直处于非RUNNABLE状态,这就是线程活性问题,
常见的活性故障有如下几种:
1.死锁(Deadlock), 类似鹬蚌相争
2.锁死(Lockout), 类似于睡美人故事中,如果王子挂掉了,睡美人一直处于沉睡状态
3.活锁(LiveLock), 类似于小猫一直转圈咬自己尾巴
4.饥饿(Starvation),类似于健壮的雏鸟总是从母鸟的嘴中抢到食物,弱小的雏鸟抢不到食物饿死了
3.上下文切换(Context Switch), 处理器从执行一个线程切换到执行另外一个线程
4.可靠性, 可能会有一个线程导致JVM意外终止,其他的线程也无法执行。
线程安全问题
非线程安全:主要是指多个线程对同一个对象的实例变量进行操作时,会出现值被更改,值不同步的情况
线程安全问题主要表现为三个方面:原子性、可见性、有序性
1.原子性
原子(Atomic)就是不可分割的意思,原子操作的不可分割有两层含义:
1.访问(读,写)某个共享变量的操作,从其他线程来看,该操作要么执行完毕,要么尚未发生,
即其他线程看不到当前操作的中间结果。
2.访问同一组变量的原子操作是不能够交错的
如:现实生活中从ATM机取款,对于用户来说,要么操作成功,用户余额减少了,增加一条交易记录;
要么没拿到钱,相当于取款操作没有发生。
Java有两种方式来实现原子性:1.使用锁
2.利用处理器的CAS(Compare and Swap)指令
锁具有排它性,保证共享变量在某一时刻只能被一个线程访问。
CAS指令直接在硬件(处理器和内存)层次上实现原子操作,看作是硬件锁。
//在Java中提供了一个线程安全的AtomicInteger类,保证了操作的原子性
static class MyInt{
//int num;
AtomicInteger num=new AtomicInteger();
public int getNum(){
return num.getAndIncrement();//先返回再加一
//return num++;
/*
自增操作实现步骤:
读取num值
num自增
把自增后的值赋值给num变量
*/
}
}
2.可见性
在多线程环境中,一个线程对某个共享变量进行更新之后,后续的其他线程可能无法立即读取到更新后的结果,
这就是线程安全问题的另外一种形式:可见性(visibility).
如果一个线程对共享变量更新后,后续访问该变量的其他线程可以读取到更新的结果,
称这个线程对共享变量的更新对其他线程可见,否则称这个线程对共享变量的更新对其他线程不可见。
多线程程序因为可见性问题,可能会导致其他线程读取到旧数据(脏数据)。
package test.Thread.threadsafe;
import java.util.Random;
/*
测试线程的可见性
*/
public class Test02 {
public static void main(String[] args) throws InterruptedException {
MyTask task = new MyTask();
new Thread(task).start();
Thread.sleep(3000);
//主线程3秒之后取消子线程
task.cancel();
/*
可能会出现以下情况:
在main线程当中,调用了task.cancel()方法,把task对象的toCancel变量修改为true
可能存在子线程看不到main线程对toCancel做的修改,在子线程中toCancel变量一直为false
导致子线程看不到main线程对toCancel变量更新的原因,可能:
1.JIT即时编译器可能会对run方法中的while循环进行优化,优化为:
if(!toCancel){
while(true){
if(doSometing()){
......
}
}
}
可能会导致死循环的情况
2.可能计算机的存储系统有关,假设分别有两个cpu内核运行main线程和子线程,
运行子线程的cpu,可能无法立即读取main线程cpu的数据
*/
}
static class MyTask implements Runnable{
private boolean toCancel=false;
@Override
public void run() {
while(!toCancel){
if (doSometing()){
}
if (toCancel){
System.out.println("任务被取消");
}
else {
System.out.println("任务正常结束");
}
}
}
private boolean doSometing(){
System.out.println("执行某个任务");
try {
Thread.sleep(new Random().nextInt(1000));//模拟执行任务的时间
} catch (InterruptedException e) {
e.printStackTrace();
}
return true;
}
public void cancel(){
toCancel=true;
System.out.println("收到 取消线程的消息");
}
}
}
3.有序性
有序性(Ordering)是指在什么情况下,一个处理器上运行的一个线程所执行的内存访问操作
在另外一个处理器运行的其他线程看来是乱序的(Out of Order).
乱序是指内存访问操作的顺序看起来发生了变化
重排序的概念:
在多核处理器的环境下,编写的顺序结构,这种操作执行的顺序可能是没有保障的:
1.编译器可能会改变两个操作的顺序;
2.处理器也可能不会按照目标代码的顺序执行;
这种一个处理器上执行的多个操作,在其他处理器来看它的顺序,
与目标代码指定的顺序可能不一样,这种现象称为 重排序 。
重排序是对内存访问有序操作的一种优化,可以在不影响单线程程序正确的情况下提升程序的性能。
但是,可能对多线程程序的正确性产生影响,即可能导致线程安全问题。
重排序与可见性问题类似,不是必然出现的。
与内存操作顺序有关的几个概念:
源代码顺序:就是源码中指定的内存访问顺序。
程序顺序:处理器上运行的目标代码所指定的内存访问顺序
执行顺序:内存访问操作在处理器上实际执行顺序
感知顺序:给定处理器所感知到的该处理器及其他处理器的内存访问操作的顺序
可以把重排序分为 指令重排序 与 存储子系统重排序 两种。
指令重排序主要是由JIT编译器,处理器引起的,指程序顺序与执行顺序不一样
存储子系统重排序是由高速缓存,写缓冲器引起的,感知顺序 与 执行顺序 不一致
---------------------------------------------------------------------------------------------------------------
4.指令重排序
在 源码顺序 与 程序顺序 不一致,或者 程序顺序 与 执行顺序 不一致的情况下,
我们就说发生了 指令重排序(Instruction Reorder)。
指令重排是一种动作,确实对指令的顺序做了调整,重排序的对象是 指令。
1)Javac编辑器 一般不会执行指令重排序,而 JIT编辑器 可能执行指令重排序。
2)处理器也可能执行指令重排序,使得执行顺序与程序顺序不一致。
//指令重排不会对单线程程序的结果的正确性产生影响,但是可能会导致多线程出现非预期的结果
5.存储子系统重排序
存储子系统是指写缓冲器与高速缓存。
高速缓存(Cache)是CPU中为了匹配与主内存处理速度不匹配而设计的一种高速缓存。
写缓冲器(Store buffer,Writer buffer)用来提高写 高速缓存 操作的效率
即使处理器严格按照程序的顺序执行两个内存访问操作,在存储子系统的作用下,
其他处理器对这两个操作的 感知顺序 与 程序顺序 不一致,即这两个操作的顺序看起来
像是发生了变化(错觉),这种现象称为存储子系统重排序。
//存储子系统重排序并没有真正的对指令执行顺序进行调整,而是造成一种指令执行顺序被调整的假象。
存储子系统重排序的 对象 是 内存操作的结果
从处理器角度来看,读内存 就是从指定的RAM地址中加载数据到寄存器,称为Load操作;
写内存 就是把数据存储到指定的地址表示的RAM存储单元中,称为Store操作;
内存重排序有以下四种可能:
1.LoadLoad重排序,一个处理器,先后执行两个读操作L1和L2,
其他处理器对这两个内存操作的感知顺序可能是 L2->L1
2.StoreStore重排序,一个处理器先后执行两个写操作W1和W2,
其他处理器对两个内存操作的感知顺序可能是 W2->W1
3.LoadStore重排序,一个处理器先执行读内存操作L1,再执行写内存操作W1,
其他处理器对这两个内存操作的感知顺序可能是 W1->L1
4.StoreLoad重排序,一个处理器先执行写内存操作W1,再执行读内存操作L1,
其他处理器对这两个内存操作的感知顺序可能是 L1->W1
内存重排序与具体的处理器微架构有关,不同架构的处理器所允许的内存重排序不同
内存重排序可能会导致线程安全问题,假设有两个共享变量
int data=0; boolean ready=false;
---------------------------------------------------------
处理器1 || 处理器2
---------------------------------------------------------
data=1; //s1 ||
ready=true; //s2 ||
----------------------------------------------------------
|| while(!ready){} //L3操作
|| sout(data); //L4操作
----------------------------------------------------------
貌似串行语义
JIT编译器,处理器,存储子系统是按照一定的规则对指令,
内存操作的结果进行重排序,单线程程序造成一种假象,
指令是按照源码的顺序执行的,这种假象我们称为貌似串行语义。
并不能保证多线程环境下程序的正确性。
/*
为了保证貌似串行语义,有数据依赖关系的语句不会被重排序,
只有不存在数据依赖关系的语句才会被重排序。
如果两个操作(指令)访问同一个变量,且其中一个操作(指令)为写操作,
那么这两个操作之间就存在数据依赖关系(Data dependency). 如: x=1; y=x+1; 后一条语句的操作数,
包含前一个语句的执行结果; y=x; x=1;
先读取x变量,再更新x变量的值。 x=1;x=2;
两条语句同时对一个变量进行写操作
如果不存在数据依赖关系,则可能重排序,如: double price = 45.8; int quantity = 10; 前两条可能进行重排
double sum=price*quentity; 但是存在控制依赖关系的语句允许重排:
一条语句(指令)的执行结果会决定另一条语句(指令)能否被执行,
这两条语句(指令)存在控制依赖关系(Control Dependency).
如在if语句中,允许重排,可能存在处理器先执行if代码块,
再判断if条件是否成立
可理解为:单线程,先把两部分的结果都得到(布尔值和真实结果),再判断真实结果是否展示
保证内存访问的顺序性
可以使用Volatile关键字
synchronized关键字实现有序性即实现感知顺序和源码顺序一致
Java内存模型
1.每个线程都有独立的栈空间
2.每个线程都可以访问堆内存
3.计算机的CPU不直接从主内存中读取数据,CPU读取数据时,
先把主内存的数据读到Cache缓存中,再把Cache中的数据读到Register寄存器中
4.JVM中的共享的数据可能会被分配到 Register寄存器 中,
每个CPU都有自己的Register寄存器
一个CPU不能读取其他CPU 寄存器中的内容,
如果两个线程分别运行在不同的CPU上,
而这个共享数据被分配到寄存器上,会产生可见性问题。
5.即使JVM中的共享数据分配到主内存中,也不能保证数据的可见性,
CPU不直接对主内存访问, 而是通过Cache高速缓存进行的,
一个处理器上运行的线程对数据的更新可能
只是更新到处理器的 写缓冲器(Store Buffer),还没有到达 Cache缓存,
更不用说主内存了。
另外一个处理器不能读取到该处理器的写缓冲器上的内容,
会产生运行在另外一个处理器上的线程无法看到该处理器对共享数据的更新。
6.一个处理器的 Cache(高速缓存)不能读取另外一个处理器的 Cache,
但是一个处理器可以通过 缓存一致性协议(Cache Cocherence Protocol)
来读取其他处理器缓存中的数据,并将读取到的数据更新到该处理器的Cache中。
这个过程称为 缓存同步 。
缓存同步使得 一个处理器上运行的线程 可以读取到
另外一个处理器运行的线程 对 共享数据 所做的更新,保障了可见性。
为了保障可见性,必须使一个处理器对共享数据的更新最终被写入该处理器的Cache, 这个过程我们称为 冲刷处理器缓存 */
可以把Java内存模型抽象为:
规定:
每个线程之间的共享数据都存储在主内存中
每个线程都有一个私有的本地内存(工作内存),线程的工作内存是抽象的概念,
不是真实存在的,它涵盖 缓冲器,寄存器,其他硬件的优化。
每个线程从主内存中吧数据读取到本地工作内存中,保存共享数据的副本
线程在自己的工作内存中处理数据,仅对当前线程可见,对其他线程是不可见的。
线程同步
线程同步机制简介
线程同步机制,是一套用于协调线程之间的数据访问的机制,该机制可以保障线程安全。Java平台提供的线程同步机制包括:锁,Volatile关键字,final关键字,static关键字以及相关的API,如Object.wait()/Object.notify()等
3.锁概述
线程安全问题的产生前提是多个线程并发访问共享数据。
将多个线程对共享数据的并发访问 转换 为 串行访问,
即一个共享数据只能被一个线程访问,
锁就是复用这种思路来保证线程安全的锁可以理解为对共享数据进项保护的一个许可证,
对于同一个许可证保护的共享数据来说,任何线程想要访问这些共享数据,
必须先持有该许可证,一个线程只有在持有许可证的情况下,
才能对这些共享数据进行访问,并且一个许可证只能被一个线程持有;
许可证线程在结束对共享数据访问后 必须释放其持有的许可证。
一个线程在访问共享数据前必须先获得锁,
获得锁的线程 称为 锁的持有线程一个锁一次只能被一个线程持有,
锁的持有线程在 获得锁之后 和 释放锁之前,
这段时间锁执行的代码称为临界区(Critical Section)锁具有排他性(Exclusive),
一个锁一次只能被一个线程持有,这个锁称为 排它锁 或 互斥锁(Mutex).
JVM 把锁分为 内部锁 和 显示锁 两种,内部通过 sychronized关键字实现;
显示锁 通过 java.concurrent.locks.Lock 接口的实现类来实现的。
锁的作用
锁可以实现对共享数据的安全访问,保障线程的原子性,可见性和有序性。
通过互斥来保障原子性,一个锁只能被一个线程持有,
这就保证了临界区的代码一次只能被一个线程执行,
使得临界区代码所执行的操作自然而然的具有不可分割的特性,
即具备了原子性。
可见性的保障是通过 写线程 冲刷处理器的缓存
和 读线程 刷新处理器的缓存 这两个动作实现的,
在java平台中,锁的获得隐含着刷新处理器缓存的动作,
锁的释放隐含着冲刷处理器缓存的动作。
锁能够保障有序性,写线程在临界区所执行的操作,
在读线程执行的临界区看来像是完全按照源码顺序执行的。
注意:使用锁保障线程的安全性,必须满足以下条件:
这些线程在访问共享数据时必须使用同一个锁。
即使是读取共享数据的线程也需要使用同步锁,
锁相关的概念
1.可重入性可重入性(Reentrancy)描述这样一个问题:
一个线程持有该锁的时候能再次(多次)申请该锁
如果一个线程持有一个锁的时候还能够继续成功申请该锁,
称该锁是可重入的,否则就称该锁为不可重入的。
2.锁的争用与调度
Java平台中内部锁属于非公平锁,显示Lock锁既支持公平锁又支持非公平锁。3.锁的粒度一个锁可以保护的共享数据的数量大小称为锁的粒度。
锁保护共享数据量大,称该锁的粒度粗,否则称该锁的粒度细。
锁的粒度过粗,会导致线程在申请锁的时候会进行不必要的等待。
锁的粒度过细,会增加锁的粒度的开销。
内部锁:synchronized关键字
Java中的每个对象都有一个与之关联的内部锁(Intrinsic lock).
这种锁也称为监视器(Monitor),这种锁是一种排它锁,
他可以保障 原子性,可见性和有序性。内部锁是通过 synchronized关键字
实现的.synchronized关键字修饰的代码块,修饰该方法。
修饰代码块的语法:
synchronized( 对象锁 ){
同步代码块,可以在同步代码块中共享数据
}
修饰实例方法就称为 同步实例方法修饰静态方法就称为 同步静态方法
synchronized同步代码块
如果线程的锁对象不同,不能实现同步,想要同步必须使用同一个锁对象可以
使用一个常量作为锁对象,如果锁对象是一个常量,
跟你使用哪一个对象来调用就没有关系了。
不管是实例方法还是静态方法,只要是同一个锁对象,就可以同步。
注意:同步就是串行,
同步是使执行临界区代码的线程在临界区部分相对其他线程临界区执行的串行
使用synchronized修饰实例方法,同步实例方法,默认this作为锁对象。
//使用一个常量对象作为锁对象,不同方法中的同步代码块也可以同步
synchronized同步实例方法
//把整个方法体作为同步代码块
//默认的对象是this对象
public void m(){
synchronized(this){.....}
}
public synchronized void m2(){....}//这两个是等价的
synchronized同步静态方法
//把整个方法体作为同步代码块
//默认的锁对象是当前类的运行时类对象,Test06.class,
有人称它为 类锁
public void m1(){
//使用当前类的运行时类对象作为锁对象,可
//以简单的理解为把Test06类的字节码文件作为锁对象
synchronized(Test06.class){...}
}
//使用synchronized修饰静态方法,
//默认运行时Test06.class作为锁对象
public synchronized static void sm2(){ ....}
同步方法与同步代码块如何选择?
同步代码块
同步方法
同步方法的锁的粒度粗,执行效率低同步代码块得锁的粒度细,执行效率高
脏读
出现读取属性值出现了一些意外,读取的是中间值,而不是修改之后的值
出现的脏读原因:对共享数据的修改 与 对共享数据的读取 不同步
解决方法:不仅对修改数据的代码块进行同步,还要对读取数据的代码块进行同步。
线程出现异常会自动释放锁
死锁
在多线程程序中,同步时可能需要使用多个锁,
如果获得锁的顺序不一致,可能会导致死锁
举例:鹬蚌相争
如何避免死锁:当需要获得多个锁时,所有线程获得锁的顺序保持一致即可。
轻量级同步机制:volatile关键字
volatile的作用
使变量在多个线程之间可见.
可以强制线程从公共内存(主内存)中读取变量的值,而不是从工作内存中读取
volatile与synchronized的比较
1) volatile 关键字是线程同步的轻量级实现,所以volatile性能肯定比
synchronized要好,volatile只能修饰变量,
而synchronized可以修饰方法,代码块。
随着JDK新版本的发布,synchronized的执行效率也有较大的提升。
在开发中使用synchronized的比率还是很大的。
2)多线程访问volatile变量不会发生阻塞,而synchronized可能会阻塞
3)volatile能保证数据的可见性,但是不能保证原子性;
synchronized可以保证原子性,也可以保证可见性。
(volatile就是只有对内存进行冲刷的作用,而synchronized不仅有
内存冲刷,还加上了锁来进行串行)
4)关键字volatile解决的是变量在多个线程之间的可见性, synchronized关键字解决多个线程之间访问公共资源同步性。
volatile的非原子性
volatile 关键字增加了实例变量在多个线程之间的可见性,
但是不具备原子性
常用的原子类进行自增自减操作
我们知道i++操作不是原子操作,除了使用synchronized进行同步外,
也可以使用AtomicInteger/AtomicLong原子类进行实现。
CAS简介
CAS(Compare And Swap)是由硬件实现的.
CAS可以将read-modify-write 这类的操作转换为原子操作.
i++自增操作包括三个子操作:
读取i变量值对i 的值加1再把加1后的值 保存到主内存
CAS原理:在把数据更新到主内存时,再次读取主内存变量的值,
如果现在变量的值与期望的值 (操作起始时读取的值)一样就更新。
使用CAS实现一个线程安全计数器
public class CASTest{
public void main(String args[]){
CAScounter cascounter=new CAScounter();
for(int i=0;i<1000;i++){
new Thread(new Runnable()){
@overwrite
public void run(){
sout(casCounter.incrementAndGet());
}
}.start();
}
}
}
class CAScounter{
//使用volatile修饰value值,使线程可见
volatile private long value;
public long getvalue(){
return value;
}
//定义一个compare and swap 方法
private boolean compareAndSwap(long expectedValue,long newValue){
//如果当前value值与期望的值一样,就把当前的value字段替换为newValue值
synchronized(this){
if(value == expectValue){
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 value;
}
}
CAS的ABA问题
CAS实现原子操作背后有一个假设:共享变量的当前值与当前线程提供的期望值相同,就认为这个
变量没有其他线程修改过。
实际上这个假设不一定总是成立,如有共享变量 count=0;
A线程对count值修改为10
B线程对count值修改为20
C线程对count值修改为30
当前线程看到count变量的值现在是0,现在是否认为count变量的值没有被其他线程更新呢?
这种结果是否能够接受?
这就是CAS中的ABA问题,即共享变量经历了A->B->A的更新,是否能够接受ABA问题
跟实现的算法有关。
/*
如果想要规避ABA问题,也不难,我们可以为共享变量引入一个修订号(时间戳),
每次修改共享变量时,相应的修订号就会增加1.
ABA变量更新过程变为:[A,0]->[B,1]->[A,2],每次对共享变量的修改都会导致修订号的增加,
通过修订号依然能够准确判断变量是否被其他线程修改过。
*/
原子变量类
CAS实现原子操作背后有一个假设:共享变量的当前值与当前线程提供的期望值相同,就认为这个
变量没有其他线程修改过。
实际上这个假设不一定总是成立,如有共享变量 count=0;
A线程对count值修改为10
B线程对count值修改为20
C线程对count值修改为30
当前线程看到count变量的值现在是0,现在是否认为count变量的值没有被其他线程更新呢?
这种结果是否能够接受?
这就是CAS中的ABA问题,即共享变量经历了A->B->A的更新,是否能够接受ABA问题
跟实现的算法有关。
/*
如果想要规避ABA问题,也不难,我们可以为共享变量引入一个修订号(时间戳),
每次修改共享变量时,相应的修订号就会增加1.
ABA变量更新过程变为:[A,0]->[B,1]->[A,2],每次对共享变量的修改都会导致修订号的增加,
通过修订号依然能够准确判断变量是否被其他线程修改过。
*/
原子变量类有12个,如:
分组 | 原子变量类 |
---|---|
基础数据型 | AtomicInteger,AtomicLong,AtomicBoolean |
数组型 | AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray |
字段更新器 | AtomicIntegerFieldUpdater,AtomicLongFieldupdater,AtomicReferenceFieldUpdater |
引用型 | AtomicReference,AtomicStampedReference,AtomicMarkableReference |
AtomicLong
/*
使用原子变量类定义一个计数器
该计数器,在整个程序中都能使用,并且所有的地方都使用这一个计数器,
这个计数器可以设计为单例
*/
public class Indicator {
//构造方法私有化
private Indicator(){}//定义一个私有的本类静态的对象
private static final Indicator INSTANCE = new Indicator();
//3)提供一个公共静态方法返回该类唯一实例
public static Indicator getInstance(){
return INSTANCE;
}
//使用原子变量类保存请求总数,成功数,失败数
private final AtomicLong requestCount = new AtomicLong(0);//记录请求总数
private final AtomicLong successCount = new AtomicLong0);//处理成功总数
private final AtomicLong fialureCount = new AtomicLong(0);//处理失败总数
//有新的请求
public void newRequestReceive(){
requestCount.incrementAndGet();
}
//处理成功
public void requestProcessSuccess(){
successCount.incrementAndGet();
}
//处理失败
public void requestProcessFailure(){
fialureCount.incrementAndGet();
}
//查看总数,成功数,失败数
public long getRequestCount(){
return requestCount.get();
}
public long getSuccessCount(){
return successCount.get();
}
public long getFailureCount(){
return fialureCount.get():
}
AtomicIntegerArray的基本操作
//创建一个指定长度的原子数组
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10);
//[0,0,0,0,0,0,0,0,0,0]
get(指定元素下标) //返回指定位置元素
set(指定元素下标) //设置指定位置元素
getAndSet(指定元素下标,要设置的值)
//设置数组元素新值时,同时返回元素的旧值
addAndGet(指定元素下标,要加上的值) //先修改后返回
//修改数组元素的值,把数组元素加上某个值
getAndAdd(指定元素下标,要加上的值) //先返回后修改
compareAndSet(指定元素下标,期望值,修改后的值)
//CAS操作,如果指定元素和期望值相等,则修改,反之,不修改
incrementAndGet(指定元素下标) //先加一再返回
getAndIncrement(指定元素下标) //先返回后加一
decrementAndGet(指定元素下标)//先减一再返回
getAndDecrement(指定元素下标)//先返回后减一
在多线程中使用AtomicIntegerArray原子数组
//在多线程中使用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++){
thread[i]=new AddThread();
}
//开启子线程
for(Thread thread:threads){
thread.start();
}
//在主线程中查看自增完以后线程数组中的各个元素的值,
//在主线程中需要在所有子线程都执行完之后再查看
//把所有的子线程合并到当前主线程中
for(Thread thread:threads){
try{
thread.join();
}catch(InterruptedException e){
e.printStackTrace();
}
}
sout(atomicIntegerArray);
}
//定义一个线程类,在线程类中修改原子数组
static class AddThread extends Thread{
@Override
public void run() {
//把原子数组的每个元素自增1000次
for (int i =0; i <1000; i++){
for (int i = 0; i< atomicIntegerArray.length(); i++) {
atomicIntegerArray.getAndIncrement(i % atomicIntegerArray.length());
} //使数组的每个元素都加一
}
/* for (int i = e; i < 10000; i++) {
atomicIntegerArray.getAndIncrement(i % atomicIntegerArray.length());
}
*/
}
}
AtomicIntegerFieldUpdater
AtomicIntegerFieldUpdater可以对原子整数字段进行更新,
要求:
1)字符必须使用volatile修饰,使线程之间可见
2)只能是实例变量,不能是静态变量,也不能使用final修饰
AtomicReference
可以原子读写一个对象
AtomicStampedReference
AtomicStampedReference原子类可以解决CAS中的ABA问题
在AtomicStampedReference原子类中有一个整数标记值stamp
每次执行CAS操作时,都需要比较stamp的值(版本)
线程间的通信
等待/通知机制
在单线程编程中, 要执行的操作需要满足一定的条件才能执行,可以把这个操作放在if语句块中,
在多线程编程中,可能A线程的条件没有满足只是暂时的,稍后其他的线程B可能会更新条件使得A线程的
条件满足,可以将A线程暂停,直到它的条件满足后,我们再将A线程唤醒。
伪代码:
atomics{
//原子操作
while(条件不成立){
等待
}
当前线程被唤醒条件满足后,继续执行下边的操作
}
等待/通知机制的实现
//Object类中的wait()方法可以执行当前代码的线程等待,暂停执行
//直到接到通知或被中断为止。
调用wait()方法后,当前线程就会等待,释放锁对象,当前线程进入阻塞(等待)状态,需要被唤醒,
如果没有被唤醒,就会一直等待。
注意:
1) wait()方法只能在同步代码块中由锁对象调用
2) 调用wait()方法,当前线程会释放锁
其伪代码如下:
//在调用wait()方法前获得对象的内部锁
synchronized(锁对象){
while(条件不成立){
//通过锁对象调用wait()方法,暂停线程,会释放锁对象
锁对象.wait();
}
//线程的条件满足了继续向下执行。
}
------------------------------------------------------------------------------------
//Object类的notify()可以唤醒线程,该方法也必须在同步代码块中由锁对象调用,
//没有使用锁对象调用wait()/notify()会抛出IlegalMonitorStateException异常。
如果有多个等待的线程,notify()方法只能唤醒其中的一个
在同步代码块中调用notify()方法,并不会立即释放锁对象,
需要等待当前同步代码块执行完之后才会释放锁对象,
一般将notify()放在同步代码块的最后。
它的伪代码如下:
synchronized(锁对象){
//执行修改保护条件的代码
//唤醒其他线程
锁对象.notify();
}
------------------------------------------------------------------------------------
//Interrupt()方法会中断wait()
当线程处于wait()等待状态时,调用线程对象的interrupt()方法
会中断线程的等待状态,会产生interruptedException异常。
------------------------------------------------------------------------------------
//notify()与notifyAll()
notify() 只能随机唤醒一个线程
notifyAll() 唤醒所有等待的线程
调用一次notify()只能唤醒其中一个线程,其他等待的线程依然处于等待状态,
对于其他线程来说,错过了通知信号,这种现象也称为 信号丢失。
------------------------------------------------------------------------------------
//wait(long)的使用
wait(long)带有long类型参数的wait()等待,
如果在参数指定时间内没有被唤醒,
超时后会自动唤醒。
通知过早
线程wait()等待后,可以调用notify唤醒线程,如果notify唤醒的过早,
在等待之前就调用了notify,可能会打乱程序正常的执行逻辑。
实际上调用start就是告诉线程调度器,当前线程准备就绪,
线程调度器在什么时候开启这个线程不一定,
即调用start开启线程的顺序并不一定就是线程实际开启的顺序。
大多数情况下,t1先等待,t2再把t1唤醒。
如果t2先把t1线程唤醒,然后t1线程再等待,会出现t1等待没有收到通知的情况。
如何解决:t1等待后,让t2线程唤醒,如果t2线程先唤醒了t1线程,就不让t1线程等待了。
定义一个静态变量,作为是否第一个运行的线程标志,如果当前线程是第一个开启的线程,就等待。
如果t2先唤醒了t1,就把第一个线程的标志的值改为false。
wait等待条件发生了变化
在使用wait/notify模式时,注意wait等待条件发生了变化,也可能造成逻辑混乱。
生产者消费者设计模式
生产者:负责产生数据的模块
消费者:负责使用数据的模块
生产者消费者解决数据的平衡问题,即先有数据然后才能使用
没有数据时,消费者需要等待。
1.生产者消费者 操作值
单生产者单消费者
//生产者和消费者交替运行
多生产者多消费者
生产者生产数据后唤醒消费者取数据,
消费者取不到数据就等待。
假设有多个消费者线程取数据,没有数据就等待,
//为了避免多生产多消费者的情况下消费者取到空数据,
// 唤醒了消费者线程后,不能直接取,要再次回来判断一下是否为空
//我们需要把之前那个消费者取数据的判定方法由if改为while
//假死:出现消费者取数据后唤醒的是消费者,而生产者等待消费者取数据,但是消费者没有数据可取的情况,消费者和生产者互相等待。
//如何解决假死问题:把生产者生产数据后的唤醒notify改为notifyAll,
// 把消费者取数据后的唤醒notify也改为notifyAll
2.生产者消费者 操作栈
使生产者把数据存储到:List集合中,消费者从List集合取数据。
使用List集合模拟栈。
单生产单消费
//生产者和消费者交替运行
多生产多消费
//为了避免多生产多消费者的情况下消费者取到空数据,
// 唤醒了消费者线程后,不能直接取,要再次回来判断一下是否为空
//我们需要把之前那个消费者取数据的判定方法由if改为while
//如何解决假死问题:把生产者生产数据后的唤醒notify改为notifyAll,
// 把消费者取数据后的唤醒notify也改为notifyAll
通过管道实现线程间的通信
在java.io包中PipeStream管道流用于线程间传送数据,
一个线程发送数据到输出管道,另一个线程从输入管道中读取数据。
相关的类:PipedInputStream 和 PipedOutputStream,
PipedReader 和 PipedWriter
PipedInputStream inputStream=new PipedInputStream();
PipedOutputStream outputStream=new PipedOutputStream();
//在输入管道和输出管道之间建立连接
inputStream.connect(outputStream);
ThreadLocal的使用
除了控制资源的访问外,还可以通过增加资源来保证线程安全,
//ThreadLocal主要解决为每个线程绑定自己的值。
ThreadLocal的应用
在多线程环境中,把字符串转换为日期对象
多个线程使用同一个SimpleDateFormat对象可能会产生线程安全问题,有异常
为每个线程指定自己的SimpleDateFormat对象,使用ThreadLocal
ThreadLocal初始值,定义ThreadLocal类的子类,在子类中重写initialValue()方法指定初始值,
再一次调用get()方法不会返回null。
Lock显示锁
ReentrantLock锁称为 可重入锁 ,它的功能比synchronized多
锁的可重入性:当一个线程获得一个对象锁后,再次请求该对象锁时是可以获得该对象的锁的。
可重入锁:指可以反复获得该锁
假设锁不可重入的话,会造成死锁
ReentrantLock
lock()方法和unlock()方法
//定义显示锁
static Lock lock = new ReentrantLock();//一般定义成静态的,可以共享一个锁对象
//获得锁
lock.lock();
//释放锁
lock.unlock();
//经常在try代码块中获得Lock锁,在finally子句中释放锁
try{
lock.lock();//获得锁
.....
}catch(InterruptedException e){
e.printStackTrace()
}finally{
lock.unlock();//释放锁
}
lockInterruptibly() 方法
作用:如果当前线程未被中断则获得锁,如果当前线程被中断则出现异常。
lock.lock();//获得锁,即使使用了线程的interrupt()方法,也没有真正的中断线程。
//lock.lock();
lock.lockInterruptibly();//如果线程被中断了,不会获得锁,会产生异常
/*
对于synchronized内部锁来说,如果一个线程在等待锁,只有两个结果
要么获得锁继续执行,要么保持等待。
对于ReentrantLock可重入锁来说,提供另外一种可能,
在等待锁的过程中,程序可以根据需要取消对锁的请求。
*/
/*
解决死锁问题:
在main线程,等待3秒,如果线程没有结束就中断该线程
(这里的lock改成lockInterruptibly)
通过ReentrantLock锁的lockInterruptibly()方法可以避免死锁的产生
*/
trylock()方法
/*
tryLock(long time,TimeUnit unit)的作用:
在给定等待时长内 锁没有被另外的线程持有,
并且当前线程也没有被中断则获得该锁。
通过该方法可以实现对象的限时等待。
tryLock() 仅在调用时锁定未被其他线程持有的锁,
如果调用方法时,锁对象被其他线程持有,则放弃。
调用方法尝试获得锁,
如果该锁没有被其他线程占用,则返回true,表示锁定成功,
如果锁被其他线程占用,则返回false,不等待。
tryLock()可以避免死锁
*/
newCondition()方法
/*
关键字synchronized与wait()/notify()这两个方法一起使用可以实现等待/通知模式。
Lock锁的newCondition()方法返回Condition对象,Condition类也可以实现等待/通知模式。
使用notify()通知时,JVM会随机唤醒某个等待的线程,
使用Condition类可以进行选择性通知。
Condition比较常用的两个方法:
await()会使当前线程等待,同时会释放锁,
当其他线程调用signal()时,线程会重新获得锁并继续执行。
signal()用于唤醒一个等待的线程。
注意:在调用Condition的await()/signal()方法前,也需要线程持有相关的Lock锁。
调用await()后线程会释放这个锁,在signal()调用后会从当前Condition对象的等待队列中
唤醒一个线程,唤醒的线程会尝试获得锁,一旦获得锁成功就继续执行。
*/
/*
注意:凡是多对多的,为了解决不必要的麻烦,都把唤醒的那个改成All,
例如nofity-->notifyAll signal-->signalAll
*/
公平锁与非公平锁
大多数情况下,锁的申请都是非公平的,
如果线程1与线程2都在请求锁A,当锁A可用时,
系统只是会从阻塞队列中随机选择一个线程,不能保证其公平性。
公平锁会按照时间先后顺序,保证先到先得,
公平锁这一特点不会出现线程饥饿现象。
synchronized内部锁就是非公平的,
ReentrantLock锁默认是非公平的,
但是ReentrantLock重入锁提供了一个构造方法:ReentrantLock(boolean fair),
当在创建锁对象时,实参传递true,可以把该锁设置为公平锁。
/*
公平锁看起来很公平,但是要实现公平锁,要求系统维护一个有序队列,
公平锁的实现成本高,性能低,因此默认情况下选择非公平锁,
不是特别的需求,一般不使用公平锁。
如果是非公平锁,系统会倾向于让一个线程再次获得已经持有的锁,
这种分配策略是高效的,非公平的。
如果是公平锁,多个线程不会发生同一个线程连续获得锁的可能,保证锁的公平性
*/
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读写锁
ReentrantReadWriteLock读写锁是一种改进的排他锁,也可以称为 共享/排他锁。
允许多个线程同时读取共享数据,但是一次只允许一个线程对共享数据进行更新。
读写锁通过读锁与写锁来完成读写操作,线程在读取共享数据前必须先获得读锁,
读锁可以被多个线程持有,即它是共享的,
线程在修改共享数据前必须先持有写锁,写锁是排他的,
一个线程持有写锁时其他线程无法获得相应的锁。
/*
读锁只是在读线程之间共享,任何一个线程持有读锁时,其他线程都无法获得写锁,
保证线程在读取数据期间没有其他线程对数据进行更新,使得读线程能够读到数据的最新值,
保证在读数据期间共享变量不被修改。
*/
获得条件 | 排他性 | 作用 | |
---|---|---|---|
读锁 | 写锁未被任意线程持有 | 对读线程是共享的,对写线程是排他的 | 允许多个读线程可以同时读取共享数据,保证在读取共享数据时,没有其他线程对共享数据进行修改 |
写锁 | 该写锁未被其他线程持有,并且相应的读锁也未被其他线程持有 | 对读线程或者写线程都是排他的 | 保证写线程以独占的方式修改共享数据 |
读写锁允许读读共享,读写互斥,写写互斥。
java.util.concurrent.locks包中的ReadWriteLock接口
readLock() //返回读锁
writeLock() //返回写锁
实现类是ReentrantReadWriteLock
/*
注意:readLock()和writeLock()方法返回的 锁对象
是同一个锁的两个不同角色,
不是分别获得两个不同的锁。
ReadWriteLock接口实例可以充当两个角色,
*/
读写锁的基本使用方法
//定义读写锁
ReadWriteLock rwlock = new ReentrantReadwriteLock();
//获得读锁
Lock readLock =new rwLock.readLock();
//获得写锁
Lock writeLock =new rwLock.writeLock();
//读数据
readLock.lock();//申请读锁
try{
读取共享数据
}finally{
readLock.unlock();//总是在finally子句中释放锁
}
//写数据
writeLock.lock(); //申请写锁
try{
更新修改共享数据
}finally{
writeLock.unlock();//总是在finally子句中释放锁
}
读读共享
ReadwriteLock读写锁可以实现多个线程同时读取共享数据,
即读读共享可以提高程序的读取效率。
写写互斥
通过ReadWriteLock读写锁中的写锁,只允许一个线程执行lock()后边的代码。
读写互斥
写锁是独占锁,是排他锁,读线程与写线程也是互斥的。
线程管理
线程组(简要了解)
类似于在计算机中使用文件夹管理文件,也可以使用线程组来管理线程。
在线程组中定义一组相似(相关)的线程,在线程组中也可以定义子线程组。
Thread类有几个构造方法允许在创建线程时,指定线程组,
如果在创建线程时没有指定线程组,则该线程就属于父线程所在的线程组。
JVM在创建main线程时会为它指定一个线程组,
因此每个java线程都有一个线程组与它关联,
可以调用线程的getThreadGroup()方法返回线程组。
/*
线程组开始是出于安全考虑设计 用来区分 不同的Applet,
然而ThreadGroup并未实现这一目标,在新开发的系统中,已经不常用线程组。
现在一般会将一组相关的线程存入一个数组或一个集合中,
如果仅仅是用来区分线程时,可以使用线程名称来区分,
多数情况下可以忽略线程组。
*/
/*
定义线程组,如果不指定所属线程组,则自动归属当前线程所属的线程组中。
在创建线程时没有指定线程组,则默认线程归属到父线程所在的线程组中。
*/
线程组的基本操作
int activeCount() //返回当前线程组及子线程组中活动线程的数量(近似值)int activeGroupCount() // 返回当前线程组及子线程组中活动线程组的数量(近似值)int enumerate(Thread[] list) //将当前线程组中的活动线程复制到参数数组中int enumerate(ThreadGroup[] list) //将当前线程组中的活动线程组复制到参数数组中 int getMaxPriority() //返回线程组的最大优先级 ,默认是10String getName() //返回线程组的名称ThreadGroup getParent() //返回父线程组 void interrupt() //中断线程组中所有的线程 boolean isDaemon()//判断当前线程组是否为守护线程组 void list()//将当前线程组中的活动线程打印出来 boolean parentOf(ThreadGroup g) //判断当前线程组是否为参数线程组的父线程组 void setDaemon(boolean daemon)//设置线程组为守护线程组
/*注意:
1.main线程组的父线程组是system
2.线程组也是它自己的父线程组
*/
复制线程组中的线程及子线程组
enumerate(Thread[] list) //把当前线程组和子线程组中所有的线程复制到参数数组中
enumerate(Thread[] list,boolean recursive) //如果第二个参数设置为false,则只复制当前线程组中所有的线程,不复制子线程组中的线程
enumerate(ThreadGroup[] list)//把当前线程组和子线程组中所有的线程组复制到参数数组中
enumerate(ThreadGroup[] list,boolean recurse) //如果第二个参数设置为false,则只复制当前线程组的子线程组
线程组的批量中断
线程组的interrupt() 可以给该线程组中所有的活动线程添加中断标志。
//如果中断睡眠中的线程,会产生中断异常,同时会清除中断标志
设置守护线程组
守护线程是为其他线程提供服务的,
当JVM中只有守护线程时,守护线程会自动销毁,JVM会退出。
调用线程组的setDameon(true)可以把线程组设置为守护线程组,
当守护线程组中没有任何活动线程时,守护线程组会自动销毁。
注意:
线程组的守护属性,不影响线程组中线程的守护属性,
或者说守护线程组中的线程可以是非守护线程。
捕获线程的执行异常
在线程的run方法中,如果有受检异常,必须进行捕获处理,
如果想要获得run()方法中出现的运行时异常信息,
可以通过回调 UncaughtExceptionHandler 接口获得哪个线程出现了运行时异常。
在Thread类中有关处理运行异常的方法有:
static Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler()
//可以获得全局的(默认的)未捕获异常处理器
Thread.UncaughtExceptionHandler getUncaughtExceptionHandler()
//获得当前线程的未捕获异常处理器
static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
//设置全局的未捕获异常处理器
void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
//设置当前线程的未捕获异常处理器
当线程运行过程中出现异常,JVM会调用Thread类的dispatchUncaughtExceptionHandler(Throwable e)方法,
该方法会调用 getUncaughtExceptionHandler().uncaughtException(this,e);
如果想要获得线程中出现异常的信息,就需要设置线程的UncaughtExceptionHandler回调接口
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler(){
@override
public void uncaughtException(Thread t,Throwable e){
//t参数接收异常发生的线程,e就是该线程中产生的异常
System.out.println(t.getName()+"线程产生了异常"+e.getMessage());
}
});
在实际开发中,这种设计异常处理的方式还是比较常用的,尤其是异步执行的方法。
/*
如果产生了异常,,JVM会调用Thread类的dispatchUncaughtExceptionHandler()方法,
在该方法中调用了 getUncaughtExceptionHandler().uncaughtException(this,e);
如果当前线程设置了UncaughtExceptionHandler回调接口,就直接使用它自己的uncaughtException方法,
如果没有设置,则调用当前线程所在的线程组UncaughtExceptionHandler回调接口的
uncaughtException方法,
如果线程组也没有设置回调接口,则直接把异常的栈信息定向到System.error中
*/
注入Hook钩子线程
现在很多软件Mysql,Zookeeper,kafka都存在Hook线程的校验机制,
目的是校验进程是否已启动,防止重复启动程序。
Hook线程也称为钩子线程,当JVM退出的时候,会执行Hook线程。
经常在程序启动的时候创建一个.lock文件,用.lock文件校验程序是否启动,
在程序退出(JVM退出)时删除.lock文件,
在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()){
throw new RuntimeException("程序已启动");
}else{
try{
//文件不存在,说明程序是第一次启动,创建一个.lock文件
getLockFile().toFile().createNewFile();
}catch(IOException e){
e.printStackTrace();
}
}
//模拟程序运行
for(int i=0;i<100;i++){
sout("程序正在运行");
try{
TimeUnit.SECONDS.sleep(1);
}catch(InterruptedException e){
e.printStackTrace();
}
}
private static Path getLockFile(){
return Path.get("","tmp.lock");
}
}
}
线程池
什么是线程池
可以以 new Thread(()->{线程执行的任务}).start();这种形式开启一个线程,
当run方法运行结束,线程对象会被GC(垃圾回收器)释放。
在真实的生产环境中,可能需要多个线程来支撑整个应用,
当线程数量非常多时,反而会耗尽CPU资源,
如果不对线程进行控制与管理,反而会影响程序的性能。
线程开销主要包括:
1)创建与启动线程的开销;
2)线程销毁的开销;
3)线程调度的开销;
4)线程数量
5)受限CPU处理器数量
线程池就是有效使用线程的一种常用方式,
线程池内部可以预先创建一定数量的工作线程,
客户端代码直接将任务作为一个对象提交给线程池,
线程池将这些任务缓存在工作队列中,
线程池的工作线程不断地从队列中取出任务并执行。
JDK对线程池的支持
JDK提供了一套Executor框架,可以帮助开发人员有效地使用线程池。
线程池的基本使用
public class test{
public static void main(String args){
//创建有5个线程大小的线程池
ExecutorService fixedThreadPool = Excutors.newFixedThreadPool(5);
//向线程池提交18个任务,这18个任务存储到线程池的阻塞队列中,线程池中这5个线程就从阻塞队列中取任务执行
for(i=0;i<18;i++){
fixedThreadPool.execute(new Runnable(){
@override
public void run(){
sout(Thread.currentThread().getId()+"编号的线程在执行任务,开始时间:"+System.currentTimeMillis());
}
});
try{
Thread.sleep(3000); //模拟任务执行时长
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
线程池的计划任务
//创建一个有调度功能的线程池
ScheduledExecutorService scheduledExecutorService = Excutors.newScheduledThreadPool(5);
//常用的三个方法:
//(1)在延迟2秒后执行任务,schedule(Runnable 任务,延迟时长,时间单位)
scheduledExecutorService.schedule(new Runnable(){
@Override
public void run(){
sout(Thread.currentThread.getId()+"----------"+System.currentTimeMills());
}
},2,TimeUnit.SECONDS);
//(2)以固定的频率执行任务,开启任务的时间是固定的,在3秒后执行任务,以后每隔2s重新执行一次
scheduledExecutorService.scheduleAtFixedRate(new Runnable(){
@Override
public void run(){
sout(Thread.currentThread.getId()+"----固定频率开启任务-----"+System.currentTimeMills());
try{
TimeUnit.SECONDS.sleep(3);//睡眠模拟任务执行时间,如果任务执行时长超过了时间间隔,则任务完成立即开启下一个任务
}catch{
e.printStackTrace();
}
}
},3,2,TimeUnit.SECONDS);
//(3)在上次任务结束后,在固定延迟后,再次执行该任务, 不管执行任务耗时多长,总是在任务结束后的2s再次开启新的任务
scheduledExecutorService.scheduleWithFixedDelay(new Runnable(){
@Override
public void run(){
sout(Thread.currentThread.getId()+"----固定频率开启任务-----"+System.currentTimeMills());
try{
TimeUnit.SECONDS.sleep(3);
}catch{
e.printStackTrace();
}
}
}3,2,TimeUnit.SECONDS);
核心线程池的底层实现
查看Executor工具类中newCachedThreadPool(),newSingleThreadExecutor(),newFixedThreadPool()源码:
//(1) newCachedThreadPool()
/*
该线程在极端情况下,每次提交新的任务都会创建新的线程执行,适合用来执行耗时短提交频繁的任务
*/
public static ExecutorService newCachedThreadPool(){
return new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
}
------------------------------------------------------------------------------------------------------------------------------------
//(2) newFixedThreadPool()
/*
核心线程数等于最大线程数,线程池中的数量指定多少就是多少,不会增长,超过的会放入无界队列
*/
public static ExecutorService newFixedThreadPool(int nThreads){
return new ThreadPoolExecutor(nThreads,nThreads,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}
--------------------------------------------------------------------------------------------------------------------------------------
//(3) newSingleThreadExecutor()
/*
单一的生产者消费者模式中可以使用
*/
public static ExecutorService newSingleThreadExecutor(){
return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}
---------------------------------------------------------------------------------------------------------------------------------------
/*
解析:Executor工具类中返回线程池的方法底层都使用了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构造方法中
可以使用以下几种阻塞队列:
1.直接提交任务队列
由SynchronousQueue对象提供,该队列没有容量,提交给线程时的任务不会被真实的保存,
总是将新的任务提交给线程执行,如果没有空闲线程则尝试创建新的线程,
如果线程数量达到规定的最大值,则执行拒绝策略。
2.有界任务队列
由ArrayBlockingQueue实现,在创建ArrayBlockingQueue对象时,可以指定一个容量,
当有任务需要执行时,如果线程池中线程数小于corePoolSize核心线程数,
则创建新的线程,如果大于核心线程数,则加入等待队列,如果队列已满,则无法加入,
此时如果线程数小于指定的最大线程数,会创建新的线程来执行,
如果线程数大于指定的最大线程数,则执行拒绝策略。
3.无界任务队列
由LinkedBlockingQueue对象实现,与有界队列相比,
除非系统资源耗尽,否则无界队列不存在任务入队失败的情况,
当有新任务时,在系统线程数小于核心线程数,则创建新的线程来执行任务;
当线程池中线程数量大于核心线程数,则把任务加入阻塞队列
4.优先任务队列
是通过PriorityBlockingQueue实现的,是带有任务优先级的队列,是一种特殊的无界队列。
不管是ArrayBlockingQueue还是LinkedBlockingQueue,都是按照先进先出算法处理任务,
在PriorityBlockingQueue队列中可以根据任务优先级顺序先后执行。
线程池的拒绝策略
ThreadPoolExecutor的构造方法的最后一个参数指定了拒绝策略,
当提交给线程池的任务量超过实际承载能力时,如何处理?
即线程池中的线程已经用完,等待队列也满了,无法为新提交的任务服务,可以通过拒绝策略来处理这个问题。
//四种拒绝策略:
1.AbortPolicy策略:会抛出异常
2.CallerRunPolicy策略:只要线程池没关闭,会在调用者线程中运行当前被丢弃的任务
3.DiscardOldestPolicy策略:任务队列中最老的任务丢弃,尝试再次提交新任务
4.DiscardPolicy策略:直接丢弃无法处理的任务
// Executor工具类提供的静态方法返回的线程池默认的拒绝策略是AbortPolicy策略,抛出异常
// 如果内置的策略无法满足实际需求,可以扩展RejectedExecutionHandler接口
ThreadFactory
线程池中的线程从ThreadFactory生产
Thread是一个接口,只有一个用来创建线程的方法:Thread newThread(Runnable r);
当线程池中需要创建线程时就会调用该方法。
监控线程池
//ThreadPoolExecutor提供了几组方法用于监控线程池
int getActiveCount() //获得线程池中当前活动线程的数量
long getCompletedTaskCount() //返回线程池完成任务的数量
int getCorePoolSize() //线程池中核心线程的数量
int getLargestPoolSize() //返回线程池曾经达到的线程的最大数
int getMaximumPoolSize() //返回线程池的最大容量
int getPoolSize() //当前线程池的大小
BlockingQueue<Runnable> getQueue() //返回阻塞队列
long getTaskCount() //返回线程池收到的任务总数
扩展线程池
有时需要对线程池进行扩展
如在监控每个任务开始和结束时间,或者自定义一些其他增强的功能。
//ThreadPoolExecutor线程池提供了两个方法:
protected void afterExecute(Runnable r,Throwable t)
protected void beforeExecute(Thread t, Runnable r)
在线程池执行某个任务前会调用beforeExecute()方法,
在任务结束后(或任务异常退出)会执行afterExecute()方法
查看ThreadPoolExecutor源码,在该类中定义了一个内部类Worker,
ThreadPoolExecutor线程池中的工作线程就是Worker类的实例,
Worker实例在执行时会调用beforeExecute()与afterExecute()方法
//定义任务类
private static class MyTask implements Runnable{
private string name;
public MyTask(String name) {
this.name F name;
}
@Override
public void run() {
System.out . println(name +"任务正在被线程“+Thread.currentThread( ). getId() +”执行");
try {
Thread.sLeep(100e);//模拟任务执行时长
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public static void main(String[] args) {
//定义扩展线程池,可以定义线程池类继承ThreadPoolExecutor,在子类中重写beforeExecute()/afterExecute()方法
//也可以直接使用ThreadPoolExecutor的内部类
ExecutorService executorService = new ThreadPoolExecutor(5,5,0,TimeUnit.SECONDS,newLinkedBlockingQueue<>())
{
//在内部类中重写任务开始方法
@override
protected void beforeExecute( Thread t,Runnable r){
System.out . println(t.getId() +“线程准备执行任务:" +((MyTask)r).name);
}
@Override
protected void afterExecute(Runnable r,Throwable t){
System.out.println(((MyTask)r).name +“任务执行完毕");
}
@Overrideprotected void terminated() i
System.out.println("线程池退出");
};
//向线程池中添加任务
for(int i=0;i<5;i++){
MyTask task = new MyTask("task-" +i);
executorService.execute(task);
}
//关闭线程池
executorService.shutdown();//关闭线程池仅仅是说线程池不再接收新的任务,线程池中已接收的任务正常执行完毕
优化线程池数量
```java
线程池大小对系统性能有一定影响,过大或过小都无法发挥最优的系统性能。
线程池的大小不需要做的非常精确,只要避免出现极大或极小的情况即可,
一般来说,线程大小需要考虑CPU数量,内存大小等因素。
在《Java Concurrency Practice》书中
给出了一个估算线程池的大小的公式:
" 线程池大小 = CPU的数量 * 目标CPU的使用率 *(1 + 等待时间与计算时间的比) "
线程池死锁
### 线程池死锁
```java
如果在线程池中执行的 任务A 在执行过程中又向线程池中提交了 任务B,
任务B 添加到了线程池的等待队列中,如果 任务A 的结束需要等待 任务B 的执行结果,
就有可能出现这种情况:
/* 线程池中所有的工作线程都处于等待任务处理结果,
而这些任务在阻塞队列中等待执行
线程池中没有可以对阻塞队列中的任务进行处理的线程,
这种等待会一直持续下去,从而造成死锁 */
解决方法:
" 给线程池提交相互独立的任务,而不是彼此依赖的任务 对于彼此依赖的任务,可以考虑分别提交给不同的线程池来执行 "
线程池中的异常处理
在使用ThreadPoolExecutor进行submit提交任务时,有的任务抛出了异常,但是线程池并没有提示,
即线程池把任务中的异常给吃掉了。
线程池可能会吃掉程序中的异常,例如算术异常
/*几种解决方法:
1.把submit()提交方法改为execute();
2.对ThreadPoolExecutor线程池进行扩展,对submit()方法进行包装;
*/
-----------------------------------------------------------------------------------------------------------------------------------------
//自定义线程池类
private static class TraceThreadPollExecutor extends ThreadPoolExecutor{
public TraceThreadPollExecutor(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 new Runnable(){
@override
public void run(){
try {
task.run();
}catch (Exception e ){
exception.printstackTrace();
throw e;
}
}
}
//重写submit方法@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("客户跟踪异常")));
}
}
ForkJoinPool线程池
“分而治之” 是一个有效的处理大数据的方法,著名的 MapReduce就是采用这种分而治之的思路.
简单点说,如果要处理的1000个数据,但是我们不具备处理1000个数据的能力,只能一次处理10个数据,
可以把这1000个数据分阶段处理100次,每次处理10个,把100次的处理结果进行合成,形成最后这1000个数据的处理结果.
把一个大任务调用fork()方法分解为若干小的任务,把小任务处理结果进行join()合并大任务的结果。
系统对ForkJoinPool线程池进行了优化,提交的任务数量与线程的数量不一定是一对一关系,
在多数情况下,一个物理线程实际上需要处理多个逻辑任务。
ForkJoinPool线程池中最常用的方法是:
<T>ForkJoinTask<T>submit(ForkJoinTask<T>task) //向线程池提交一个ForkJoinTask任务,ForkJoinTask任务支持fork()分解与join()等待的任务
ForkJoinTask有两个重要的子类:RecursiveAction,RecursiveTask
它们的区别在于RecursiveAction任务没有返回值,RecursiveTask任务可以带有返回值。
//计算数列的和,需要返回结果,可以定义任务继承RecursiveTask
private static class CountTask extends RecursiveTask<Long>{
private static final int THRESHOLD = 10000;//定义数据规模的阈值,允许计算100oe个数内的和,超过该阈值的数列就需要分解
private static final int TASKNUM= 100; //每次把大任务分解为100个小任务
private long start;//计算数列的起始值
private long end;
//计算数列的结束值
public CountTask( long start,long end){
this.start = start;
this.end = end;
}
//重写RecursiveTask类的compute()方法,计算数列的结果
@override
protected Long compute() {
long sum=e ;//保存计算的结果
//判断任务是否需要继续分解,如果当前数列end与start范围的数超过阈值THRESHOLD,就需要继续分解
if ( end - start < THRESHOLD){
//小于阈值可以直接计算
for (long i = start ; i<= end; i++){
sum += i;
}else {
//数列范围超过阈值,需要继续分解
//约定每次分解成1ee个小任务,计算每个任务的计算量
long step = (start + end ) / 100;
//start = 0 , end = 200000,step = 2000,如果计算[ 0,200800]范围内数列的和,把该范围的数列分解为100个小任务,每个任务计算2000个数即可
//注意,如果任务划分的层次很深,即THRESHOLD阈值太小,每个任务的计算量很小,层次划分就会很深,
//可能出现两种情况:
"(1)系统内的线程数量会越积越多,导致性能下降严重;"
"(2)是分解次数过多,方法调用过多可能会导致栈溢出;"
//创建一个存储任务的集合
ArrayList<CountTask> subTaskList = new ArrayList<>();
long pos = start;//每个任务的起始位置
for(int i=0; i<TASKNUM;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){
sum+=Task.join(); //join()会一直等待子任务执行完毕返回执行结果
}
}
return sum;
}
public static void main(String[] args){
//创建ForkJoinPool线程池
ForkJoinPool forkJoinPool = new ForkJoinPool();
//创建一个大任务
CountTask task = new CountTask(0L,200000);
//把大任务提交给线程池
forkJoinPool.submit(task);
try{
long res = result.get();//调用任务的get()方法返回结果
}catch(InterruptedException e){
e.printStackTrace();
}catch(ExecutionException e){
e.printStackTrace();
}
}
}
保障线程安全的设计技术
这些技术可以使得我们在不必借助锁的情况下来保障线程安全,
避免锁可能导致的问题及开销。
Java运行时存储空间
/*
Java运行时(Java Runtime)空间可以分为栈区,堆区,方法区(非堆空间)。
栈空间(Stack Space)为线程执行准备一段固定大小的存储空间。每个线程都有独立的栈空间,创建线程时就为线程分配栈空间。
在线程栈中每调用一个方法就给方法分配一个 栈帧,栈帧 用于存储方法的局部变量,返回值等私有数据。
即 局部变量 存储在栈空间中, 基本变量 也是存储在栈空间中,引用类型变量值 也是存储在栈空间中,
引用的对象 存储在堆中,由于线程栈是相互独立的,一个线程不能访问另外一个线程的栈空间,
因此线程对局部变量以及只能通过当前线程的局部变量才能访问的对象进行的操作具有固定的线程安全性。
堆空间(Heap Space)用于存储对象,是在JVM启动时分配的一段可以动态扩容的内存空间,
创建对象时,在堆空间中给对象分配存储空间,实例变量就是存储在堆空间中的,
堆空间是多个线程之间可以共享的空间,因此实例变量可以被多个线程共享。
多个线程同时操作实例变量可能存在线程安全问题。
非堆空间(Non-Heap Space)用于存储常量,类的元数据等,
非堆空间也是在JVM启动时分配的一段可以动态扩容的存储空间,
类的元数据包括静态常量,类有哪些方法及这些方法的元数据(方法名,参数,返回值等)。
非堆空间也是 多个线程可以共享的,因此访问非堆空间中的静态变量也可能存在线程安全问题。
堆空间和非堆空间是线程可以共享的空间,即实例变量与静态变量是线程可以共享的,可能存在线程安全问题。
栈空间是线程的私有空间,局部变量存储在栈空间中,局部变量具有固有的线程安全性。
*/
无状态对象
对象就是数据及对数据操作的封装,对象所包含的数据称为对象的状态(State),
实例变量与静态变量称为状态变量,
如果一个类的同一个实例被多个线程共享并不会使这些线程存储共享的状态,那么该类的实例就称为无状态对象(Stateless Object).
反之,如果一个类的实例被多个线程共享会使这些线程存在共享状态,那么该类的实例称为有状态对象。
//实际上 无状态对象 就是不包含任何实例变量的对象,也不包含任何静态变量 的对象。
线程安全问题的前提是多个线程存在共享的数据,实现线程安全的一种办法就是避免在多个线程之间共享数据,
使用无状态对象就是这种办法。
不可变对象
不可变对象:一经创建它的状态就保持不变的对象,不可变对象具有固有的线程安全性。
当不可变对象现实实体的状态发生变化时,系统会创建一个新的不可变对象,就如String字符串对象。
/*
一个不可变对象需要满足以下条件:
1)类本身使用final修饰,防止通过创建子来改变它的定义。
2)所有的字段都是final修饰的,final字段在创建对象时必须显示初始化,不能被修改。
3)如果字段引用了其他状态可变的对象(集合,数组),则这些字段必须是private私有的。 */
/*
不可变对象主要的应用场景:
1)建模对象的状态变化不频繁
2)同时对一组相关数据进行写操作,可以应用不可变对象,既可以保证原子性,也可以避免锁的使用。
3)使用不可变对象作为安全可靠的Map键,HashMap键值对的存储位置与键的hashcode()有关,
如果键的内部状态发生了变化会导致键的哈希码不同,可能会影响键值对的存储位置。
如果HashMap的键是一个不可变对象,则hashcode()方法的返回值恒定,存储位置是固定的。
*/
线程特有对象
我们可以选择不共享非线程安全的对象,对于非线程安全的对象,
每个线程都创建一个该对象的实例,各个线程访问各自创建的实例,
一个线程不能访问另外一个线程创建的实例,
这种各个线程创建各自的实例,一个实例只能被一个线程访问的对象就称为 "线程特有对象"
/*
线程特有对象既保障了对 非线程安全对象的访问的线程安全,又避免了锁的开销
线程特有的对象也具有固有的线程安全性。
ThreadLocal<T>类相当于线程访问其特有对象的代理,
即各个线程通过ThreadLocal对象可以创建并访问各自线程的线程特有对象,
泛型T指定了线程的特有对象的类型,
一个线程可以使用不同的ThreadLocal实例来创建并访问不同的线程特有对象。
*/
"ThreadLocal实例为每个访问它的线程都关联了一个该线程特有的对象,ThreadLocal实例都有当前线程与特有实例之间的一个关联."
装饰器模式
装饰器模式可以用来实现线程安全,
基本思想是为非线程安全的对象创建一个相应的线程安全的外包装对象,
客户端代码不直接访问非线程安全的对象而是访问它的外包装对象.
外包装对象与非线程安全的对象具有相同的接口,即外包装对象的使用方式与非线程安全
对象的使用方式相同,而外包装对象内部通常会借助锁,以线程安全的方式调用相应的非线程安全对象的方法.
在java.util.Collections工具类中提供了一组synchronizedXXX(xxx)可以把不是线程安全的xxx集合转换为线程安全的集合,它就是采用了这种装饰器模式。
这个方法的返回值就是指定集合的外包装对象,这类集合又称为 同步集合 。
好处:"关注点分离",在这种设计中,实现同一组功能的对象的两个版本:非线程安全的对象与线程安全的对象
对于非线程安全的对象在设计时只关注要实现的功能,对于线程安全的对象只关注线程安全性。
提高锁性能的建议
减少锁持有时间
对于使用锁进行并发控制的应用程序来说
如果单个线程持有锁的时间过长,会导致锁的竞争更加激烈,会影响系统的性能。
在程序中需要尽可能的减少对锁的持有时间,
如下面代码:
public synchronized void syncMethod(){
othercode1();
mutexMethod();
othercode2();
}
在syncMethod同步方法中,假设只有mutexMethod()方法是需要同步的, otHercode1()方法与othercode2()方法不需要进行同步.
如果othercode1()与othercode2()这两个方法需要花费较长的时间,在并发量较大的情况下,这种同步方案会导致等待线程的大量增加.
//一个较好的优化方案是,只在必要时进行同步,可以减少锁的持有时间,提高系统的吞吐量,
如把上面代码改为:
public voidsyncMethod(){
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对锁的优化
//为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短.
//但是凡事都有一个度,如果对同一个锁不断的进行请求,同步和释放,也会消耗系统资源.
如:
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不会立即挂起线程,还会使用一种称为轻量级锁的优化手段.
会将共享对象的头部作为指针,指向持有锁的线程堆栈内部,来判断一个线程是否持有对象锁.
如果线程获得轻量级锁成功,就进入临界区.
如果获得轻量级锁失败,表示其他线程抢到了锁,那么当前线程的锁的请求就膨胀为重量级锁.
当前线程就转到阻塞队列中变为阻塞状态.
偏向锁,轻量级锁都是乐观锁,重量级锁是悲观锁
一个对象刚开始实例化时,没有任何线程访问它,它是可偏向的,
即它认为只可能有一个线程来访问它的时候,它会偏向这个线程.
偏向第一个线程,这个线程在修改对象头成为偏向锁时使用CAS操作,
将对象头中Threadld,改成自己的ID,之后再访问这个对象时,只需要对比ID即可,
一旦有第二个线程访问该对象,因为偏向锁不会主动释放,所以第二个线程可以查看对象的偏向状态,
当第二个线程访问对象时,表示在这个对象上已经存在竞争了,
检查原来持有对象锁的线程是否存活,如果挂了则将对象变为无锁状态,然后重新偏向新的线程;
如果原来的线程依然存活,则马上执行原来线程的栈,检查该对象的使用情况,如果仍然需要偏向锁,则偏向锁升级为轻量级锁.
轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对同一个锁的操作会错开,或者稍微等待一下(自旋),另外一个线程就会释放锁.
当自旋超过一定次数,或者一个线程持有锁,一个线程在自旋,又来第三个线程访问时,轻量级锁会膨胀为重量级锁,重量级锁除了持有锁的线程外,其他的线程都阻塞