多线程
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/a69334e9713343c2e33119fec4934f28.png)
文章目录
1. 什么是进程和线程?
1.1 什么是进程?
首先我们来看一下进程的概念:
进程:是指内存中一个运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程,进程也是程序一次执行过程,是系统运行程序的基本单位。系统运行一个程序既是从进程的创建,运行到消亡的过程
看到这里大家可能都不太理解,其实我也不太清楚,不过看了宜春大佬的博客,有了小小的收获。就跟我们玩游戏差不多,点击游戏,游戏会把加载到内存中运行,系统为给他分配资源(这就是大家玩游戏卡的原因,内存只有1g,给他分了0.99g,不卡才怪。)此时进程内存的程序都可以叫做进程,退出游戏,就是进程从内存中销毁了。这里借用一下宜春大佬的图
1.2 什么是线程?
线程是进程中的一个执行单位,负责当前程序中程序的执行,一个进程中至少有一个线程,也就是说一个进程可以有多个线程。而多个线程组成进程也就是应用程序就是多线程程序
1.3什么是多线程?
多线程就是多个线程同时运行或者交替运行,以前电脑只有一个cpu,多线程运行都是交替运行(并发),现在都是4核8核的,可以同时运行8个线程,做到真正的并行,
单核CPU:交替运行,并发
多核CPU:同时运行,并行
多线程并不能提高程序的运行速度,但是能提高程序的运行效率,让CPU的利用率更高
理解并发和并行
什么是并发:在一个时间段内同时发生,并不是同时发生
什么是并行:同一时刻发生,真正的同时运行
并发的感觉就是同时运行,其实是cpu分配的时间很小,交替运行的时间很短,给人的感觉是同时运行
1.4 什么是线程的优先级?
说起线程调度优先级这个概念,就好比大家去面试工作,学历高,有项目经验的面试通过的概率就高,线程也是同样的道理,一般线程有10个级别准确地说是11个级别,级 别为0的线程是JVM的,应用程序不能设置该级别),值越小获得机会越小,
线程优先级具有继承性,比如A线程启动B线程,则B的优先级和A一样。
线程优先级具有随机性,也就是说优先级高的线程不一定每一次都执行完,只是被执行的可能性更大。
后续用到getPriority()方法获取线程的优先级。
1.5 为什么提倡使用多线程而不是多进程?
线程和进程类似,但线程是一个比进程更小的执行单位,是程序执行的最小单位,一个进程在执行过程中可能产生多个线程,与进程不同的是同类的多个线程可以共享同一个内存空间和一组系统资源。所以系统产生一个线程,或者多线程之间协调工作的消耗比进程少得多,也正因为如此,线程也被称为轻量级进程。同时线程是程序执行的最小单位。使用多线程而不是用多进程去进行并发程序的设计,是因为线程间的切换和调度的成本远远小于进程。
使用多线程,多线程会将程序运行方式从串行运行变为并发运行,效率会有很大提高
2. 创建线程的四种方式
2.1 继承Thread类
Java使用Java.lang.Thread类代表线程,所有的线程对象都是Thread类或其子类的实例。每个线程的作用是完成一定的任务。实际上就是执行一段程序流也就是顺序执行的代码,Java使用线程执行体来代表这段程序流,也就是我们说的run()中的代码。Java通过继承Thread类来创建并启动多线程的步骤如下。
- 定义一个类,使用关键extends继承Thread,并重写Thread类的run()方法,该方法就代表了线程需要完成的任务,就是我们说的线程执行体。
- 创建Thread子类的实例,也就是使用new关键字创建线程对象。
- 调用线程对象的start()方法启动线程,注意,此方法只是启动线程,并不一定执行该线程,还需要得到CPU的资源才能执行,只是让进程进入了read状态。
代码如下:
//定义一个MyThread使用关键字extends继承Thread,并重写run()方法
public class MyThread extends Thread {
//定义线程名称的构造方法
public MyThread(String name) {
//调用父类的String参数的构造方法,指定线程的名称
super(name);
}
//线程执行体,完成该线程执行的逻辑
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(getName()+" 正在执行:"+i);
}
}
}
测试类
//测试类
public class Test {
public static void main(String[] args) {
//创建MyThread对象,并命名为新的线程
MyThread myThread=new MyThread("新的线程");
myThread.start();//启动线程
//main也是一个线程,主线程执行for循环
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+" 正在执行:"+i);
}
}
}
测试结果:
首先我们看到并不像以前那样代码顺序执行,大家多运行几次,每次都不一样,这是因为开启了两个线程,第一个是我们定义MyThread类,第二个就是main方法主线程。多线程可以同时运行。
Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。但是如果我们已经继承了别的类了,就不能继承Thread类,只能实现Runnable接口或者Callable接口。
我们可以看下Thead类的源码:
//可以看到Thread类也是实现Runnable接口
public class Thread implements Runnable {
/* 导致此线程开始执行; Java Virtual Machine调用此线程的run方法。
结果是两个线程同时运行:当前线程(从调用返回到start方法)和另一个线程(执行其run方法)。
不止一次启动线程永远不合法。
特别是,一旦完成执行,线程可能无法重新启动。
@exception IllegalThreadStateException如果线程已经启动。
@see #run()
@see #stop()*/
//1.在mian主线程调用start()方法,进入到这里
public synchronized void start() {
//此判断当前线程只能被启动一次,不能被重复启动
if (threadStatus != 0)
throw new IllegalThreadStateException();
/**
*通知组该线程即将启动,
*这样它就可以添加到组的线程列表中
*并且该组的未启动计数可以递减
*/
group.add(this);
boolean started = false;
try {
//启动线程
start0();
started = true;
} finally {
try {
//如果线程启动失败,从线程组移除该线程。
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
//会调用 JVM_StartThread 这个方法,JVM层面去启动一个线程
private native void start0();
总结一下:
Java创建线程之后,必须调用start()方法才能启动线程。该方法会通过虚拟机启动一个本地线程,本地线程的创建会调用当前系统去创建的线程的方式创建线程,并且线程被执行的时候会回调run()方法进行业务逻辑的处理。大概意思就是:系统给你留了一个接口,你要用这个接口调用的run()方法。该系统给你提供一系列的操作。
2.2 实现Runnable接口
如果一个类已经继承了别的类怎么办,就无法直接继承Thread来创建线程,这时候只能实现Runnable接口,Java是单继承,多实现。
自定义一个类并实现Runnable接口,重写其run()方法。
//自定义一个RunnableDemo类,实现Runnable接口
public class RunnableDemo implements Runnable {
@Override
public void run() {
for (int i = 0; i <3 ; i++) {
//输出当前线程名字和正在执行的次数
System.out.println(Thread.currentThread().getName()+" 正在执行:"+i);
}
}
}
测试类
为了启动自定义类RunnableDemo,需要首先实例化一个Thread,并传入RunnableDemo 实例:
//测试类
public class Test {
public static void main(String[] args) {
//创建runnableDemo类对象
RunnableDemo runnableDemo=new RunnableDemo();
//实例化一个Thread并传入自己的RunnableDemo 实例
Thread thread=new Thread(runnableDemo,"runnableDemo");
thread.start();//启动线程
//主线程执行for循环
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName()+" 正在执行:"+i);
}
}
}
运行结果:
其实多运行几次,大家就会发现每次的输出结果都是不一样的,这是因为多线程回去抢夺CPU资源,谁抢到了就执行那个线程。Main线程和runnableDemo两个线程就一直在争抢CPU。
实际上,当传入一个Runnable target(目标)参数给Thread后,Thread的run()方法就会调用target.run(),参考JDK源代码:
@Override
public void run() {
if (target != null) {
target.run();
}
}
2.3 实现Callable接口
实现Callable接口和Runnable接口差不多,但是也有不同点。
不同点:
- 重写方法:Callable是call()方法,Runnable是run()方法。
- 有无返回值:Callable有返回值,需要使用get()得出返回值,可能会阻塞当前线程,Runnable没有返回值
- 是否抛出异常:Callable会抛出异常,Runnable不会抛出异常
- 实现方式:Callable需要配合FutureTask类使用,Runnable不需要
看下Runnable和Callable就一目了然
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
@FunctionalInterface
public interface Callable<V> {
//V是泛型,这个小伙伴应该都清楚,不清楚的回去好好看看基础
V call() throws Exception;
}
自定义一个类实现Callable接口,重写其call()方法,这里的call()和run()方法差不多,都是执行具体的业务逻辑,不同的是call()有返回值和会抛出异常。
//自定义一个类CallableDemo,实现Callable接口
public class CallableDemo implements Callable {
@Override
public Integer call() throws Exception {
//这里为了测试就返回一个简单的值
return 1024;
}
}
测试类
//测试类
public class Test {
public static void main(String[] args) {
//创建CallableDemo类对象
CallableDemo callableDemo=new CallableDemo();
//传入callableDemo实例到FutureTask
FutureTask<Integer> futureTask=new FutureTask<>(callableDemo);
//因为Thread构造函数没有Callable类型,只能借用FutureTask作为中间类来实现,类似于中介
new Thread(futureTask).start();
//捕捉一下异常
try {
Integer result = futureTask.get();
System.out.println("线程返回值:"+result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
2.4 匿名内部类的方式和Lambda表达式创建线程
这个就是对上面创建子类实现接口的一种简化方式,并不是新的创建方式。大家需要了解下,不然以后看别人代码看不懂那就尴尬了。话不多说上代码:
public class Test2 {
public static void main(String[] args) {
//使用匿名内部类的方式创建线程
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
},"我是匿名线程").start();
//使用Lambda表达式创建线程
new Thread(()->{
System.out.println(Thread.currentThread().getName());
},"我是Lambda线程").start();
}
}
2.5 使用线程池创建线程
这个等后面讲线程池的时候再说吧,现在说大家都没有啥概念,以后大家用的基本上都是使用线程池创建线程。先跟大家说下其他三种创建线程的优先级。
采用继承Thread类方式:
(1)优点:编写简单,直接继承Thread类就行。如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程。
(2)缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类。
采用实现Runnable接口方式:
(1)优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
(2)缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。
采用实现Callable接口方式:
(1)优点:线程类只是实现了Callable接口,还可以继承其他的类。可以得到线程计算的值。
(2)缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。还需要捕捉异常和创建线程还需要借助FutureTask类。
总结:
如果一个类继承Thread类,就不能继承其他类,不适合做资源共享。而实现了Callable和Runnable接口的适合资源共享。需要使用线程返回值的可以使用Runnable接口。
实现Runnable接口和Callable比继承Thread类的优势:
1.适合多个相同代码的线程去处理同一个资源。
2.可以避免java中单继承的限制。
3.增加代码的健壮性,实现解耦。代码可以被多个线程共享,代码和数据独立。
4.线程池中只能放入实现Runnable或Callable类线程,不能放入继承Thread的类
5.Callable可以得到返回值。
所以说,如果推荐选择那种方式尽量选择Runnable接口,如果需要有返回值的选Callable接口。其实学到了后面的线程池都使用线程池创建的方式创建线程,使用线程池的方式也是最推荐的一种方式,不过入门还是要先理解一下这几种方式。
3. 多线程常用API
3.1 Thread类
- Thread():无参的构造函数,用于创建一个新的Thread。
- Thread(Runnable target):用于构建一个新的Thread,该线程使用了指定target的run()方法。
- Thread(ThreadGroup group ,Runnable target):用于在指定的线程组中构建一个新的Thread,该线程使用了指定target的run()方法。
- currentThread():获得当前运行线程的对象引用。
- interrupt():将当前线程置为中断状态。
- sleep(long millis):使当前运行的线程进入睡眠状态,睡眠时间至少为指定毫秒数。
- join():等待这个线程结束,即在一个线程中调用other.join(),将等待other线程结束后才继续本线程。
- yield():当前执行的线程让出CPU的使用权,从运行状态进入就绪状态,让其他就绪线程执行。
- getId():获取线程ID
- stop():停止线程,不建议使用,已过时
- setDaemon():设置为守护线程,随着主线程一起销毁。
3.2 Object类
- wait():让当前线程进入等待阻塞状态,直到其他线程调用了此对象的notify()或notifyAll()方法后,当前线程才被唤醒进入就绪状态.
- notify():唤醒在此对象监控器(锁对象)上等待的单个线程。
- notifyAll():唤醒在此对象监控器(锁对象)上等待的所有线程。
注意:wait(),notify(),notifyAll(),都依赖于同步锁,而同步锁是对象持有的,且每一个对象只有一个,所以这些方法定义在Object类中,而不是Thread类中。
3.2 yield()、sleep()、wait()比较
yield():让线程从运行状态进入就绪状态,不会释放它所持有的同步锁。
sleep():让线程从运行状态进入阻塞状态,不会释放它锁持有的同步锁。
wait():让线程从运行状态进入等待阻塞状态,并且会释放它所持有的同步锁。
4. 多线程的运行状态
如果想要去深入了解一下的话也是可以的:Java线程的6种状态及切换
5. 多线程通信
5.1 线程间通信的几种实现方式
线程间通信的模型有两种:共享内存和消息传递,以下方式基本都是这两种模型来实现的。
5.1.1 使用volatile关键字
基于volatile关键词来实现线程间相互通信是使用共享内存得思想,大致意思就是多个线程同时监听一个变量,当这个变量发生变化时,线程能够感知并执行相应得业务,这就是最简单得一种实现方式。
题目:有两个线程A、B,A线程向一个集合里面依次添加元素"a"字符串,一共添加十次,当添加到第五次的时候,希望B线程能够收到A线程的通知,然后B线程执行相关的业务操作。
测试代码:
public class VolatileDemo {
//定义一个共享变量来实现通信,它需要用volatile来修饰,否则线程不能及时感知
static volatile boolean flag = false;
public static void main(String[] args) throws InterruptedException {
List<String> list=new ArrayList<>();
//实现线程A
Thread threadA= new Thread(()->{
for (int i = 1; i <=10 ; i++) {
list.add("a");
System.out.println(Thread.currentThread().getName()+"向list中添加一个元素,此时list中的元素个数为:" + list.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size()==5)
flag=true;
}
},"A");
Thread threadB=new Thread(()->{
while (true){
if (flag){
System.out.println(Thread.currentThread().getName()+"正在执行相关业务");
break;
}
}
},"B");
//需要先启动线程B
threadB.start();
//休眠3s,确保B启动
Thread.sleep(1000);
//在启动线程A
threadA.start();
}
}
测试结果:
5.1.2 使用Object类的wait() 和 notify() 方法
Object类提供了线程通信的方法,watit(),notify()和notifyAll(),他们是多线程通信的基础。而这种实现方式的思想自然是线程间通信。
注意:使用wait()和notify(),必须配合synchronized使用,因为wait()让线程等待并释放锁,notify唤醒线程,不释放锁,notifyAll()唤醒当前锁上的随机线程。
题目:有两个线程A、B,A线程输出A,B线程输出B。其结果为ABABAB…AB
//第一步:编写一个资源类
class PrintAB{
boolean flag=true;//true输出A,false输出B
//输出A
public synchronized void printA() throws InterruptedException {
//第二步:判断,干活,通知
while (!flag){//如果不是true则等待
this.wait();
}
System.out.print("A");
flag=false;
this.notify();//唤醒其他线程
}
//输出B
public synchronized void printB() throws InterruptedException {
while (flag){
this.wait();
}
System.out.print("B");
flag=true;
this.notify();
}
}
//测试wait()和notify()线程间通信。
public class TestSync2 {
public static void main(String[] args) throws InterruptedException {
PrintAB printAB=new PrintAB();
for (int i = 0; i < 10; i++) {
new Thread(()->{
try {
printAB.printA();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "AA").start();
new Thread(()->{
try {
printAB.printB();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "BB").start();
}
}
}
5.1.3 使用JUC工具类CountDownLatch
JUC(java.util.concurrent):在JDK1.5出现,这是一个处理多线程的工具包。
CountDownLatch是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程执行完后再执行。例如,应用程序的主线程希望在负责启动框架服务的线程已经启动所有框架服务之后执行。CountDownLatch是通过一个计数器来实现的,计数器初始化值为线程的数量,每当一个线程完成了自己的任务后,计数器的值相应的也要减一。当计数器等于0的时候,表示所有的线程都已完成任务。然后再闭锁上等待的线程就可以恢复执行任务。
举个例子,就是晚自习上课,上完课必须等所有人走完才能锁门。如果有人一直不走就要等他走了才能锁门,当然不可能无限制等他(毕竟各位大佬还要回家陪女朋友的)。所以必须加个时间限制,比如再等她亿点点个小时。
题目:有六个学生上晚自习,人走了班长才能锁门。
//有六个学生上晚自习,人走了班长才能锁门。
public class CountDownlatchDemo {
public static void main(String[] args) {
//初始化CountDownLatch的线程数量为6
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <3 ; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName() +"号学生离开了教室");
//线程完成任务,计数器减一
countDownLatch.countDown();
},String.valueOf(i)).start();
}
try {
//阻塞,等计算器变为0,执行后面的,超时3s,会报错
countDownLatch.await(3, TimeUnit.SECONDS);
//countDownLatch.await();
System.out.println(Thread.currentThread().getName() +"班长锁门了");
} catch (Exception e) {
//超时并不会执行这一行
System.out.println("报错");
e.printStackTrace();
}finally {
//关闭一些资源
}
}
}
测试结果:
5.1.4 使用ReentrantLock和Condition
ReentrantLock是接口Lock的实现类,相比synchronized的非公平锁,ReentrantLock可以实现非公平锁和公平锁
//创建一个非公平锁,默认是非公平锁
Lock lock=new ReentrantLock();
//Lock lock=new ReentrantLock(false);
//创建一个公平锁,构造参数true
Lock lock=new ReentrantLock(true);
一个ReentrantLock对象可以同时绑定多个对象,ReentrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程,而不是像synchronized那样要么随机唤醒一个线程,要么唤醒全部线程。
题目:三个线程彼此通信,分别输出AABBCC。
代码演示:
//题目:三个线程彼此通信,分别输出AABBCC。
class PrintABC{
//1:输出A 2:输出B 3:输出C
private int flag=1;
private Lock lock=new ReentrantLock();
private Condition condition1=lock.newCondition(); //线程A的条件
private Condition condition2=lock.newCondition();//线程B的条件
private Condition condition3=lock.newCondition(); //线程C的条件
//输出A
public void printA(){
try {
lock.lock();//获取锁
while (flag==1){
System.out.print(Thread.currentThread().getName());
flag=2;
//唤醒线程B
condition2.signal();
}
//线程A等待
condition1.await();
}catch (Exception e){
e.printStackTrace();
}finally {
//释放锁
lock.unlock();
}
}
//输出B
public void printB(){
try {
lock.lock();//获取锁
while (flag==2){
System.out.print(Thread.currentThread().getName());
flag=3;
//唤醒线程C
condition3.signal();
}
//线程B等待
condition2.await();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
//输出C
public void printC(){
try {
lock.lock();//获取锁
while (flag==3){
System.out.print(Thread.currentThread().getName());
flag=1;
//唤醒线程A
condition1.signal();
}
//线程C等待
condition3.await();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
测试类:
public static void main(String[] args) {
//创建一个PrintABC对象
PrintABC printABC=new PrintABC();
//启动A,B,C三个线程
while (true){
new Thread(()->{printABC.printA();},"A").start();
new Thread(()->{printABC.printB();},"B").start();
new Thread(()->{printABC.printC();},"C").start();
}
}
运行结果:
总结:可以看到线程按照我们的计划那样交替输出ABC,我们可以通过Condition类来指定唤醒那个线程。
5.1.5 基本LockSupport实现线程间的阻塞和唤醒
LockSupport是一种十分灵活的实现线程间阻塞和唤醒的工具。使用它不用关注是等待线程先进行,还是唤醒线程先运行。但是需要知道线程的名字。
题目:有两个线程A、B,A线程向一个集合里面依次添加元素"a"字符串,一共添加十次,当添加到第五次的时候,希望B线程能够收到A线程的通知,然后B线程执行相关的业务操作。
测试代码:
代码演示:
//题目:有两个线程A、B,A线程向一个集合里面依次添加元素"a"字符串,一共添加十次,当添加到第五次的时候,希望B线程能够收到A线程的通知,然后B线程执行相关的业务操作。
public class LockSupportDemo {
public static void main(String[] args) {
List<String> list=new ArrayList<>();
Thread threadB=new Thread(()->{
if (list.size()!=5){
LockSupport.park();//阻塞线程
}
System.out.println(Thread.currentThread().getName()+"正在执行相关业务");
},"B");
Thread threadA=new Thread(()->{
for (int i = 0; i <10 ; i++) {
list.add("a");
System.out.println(Thread.currentThread().getName()+"向list中添加一个元素,此时list中的元素个数为:" + list.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size()==5)
LockSupport.unpark(threadB);//唤醒B线程
}
},"A");
//启动线程A
threadA.start();
//启动线程B
threadB.start();
}
}
测试结果:
5.2 如何停止线程
线程属于一次性消耗品,在执行完run()方法后便会正常结束,线程结束后就会被销毁,不能再次start()。只能重新建立一个新的线程对象,但有时run()方法是永远不会结束的,例如在程序中使用线程进行Socket监听请求,或是其他的需要循环处理的任务。在这种情况下,一般是将这些任务放在一个循环中,如while循环。当需要结束线程时,如何退出线程呢?
终止线程的三种方法:
- 使用stop方法强行终止线程(不推荐使用,Thread.stop, Thread.suspend, Thread.resume 和Runtime.runFinalizersOnExit 这些终止线程运行的方法已经被废弃,使用它们是极端不安全的!)
- 设置退出标志,使线程正常退出,也就是当run()方法完成后线程终止
- 使用interrupt()方法中断线程
后两种方法都可以实现线程的正常退出;第一种方法相当于电脑断电关机一样,是不安全的方法。
5.2.1 使用stop()方法(已过时,不建议使用)
程序中可以直接使用 thread.stop();来停止线程,但是是极不安全的,就好比突然关闭计算机电源,可能导致未知的错误。不安全的主要是: thread.stop()调用之后,创建子线程的线程就会抛出ThreadDeatherror的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用stop方法来终止线程。
代码演示:
public static void main(String[] args) throws InterruptedException {
//定义一个线程,无限循环输出一句话
Thread thread=new Thread(()->{
for (int i = 0; i <10000 ; i++) {
System.out.println("正在无限无限循环中。。。。"+i);
}
});
//启动线程
thread.start();
//延迟1ms
Thread.sleep(1);
//停止线程
thread.stop();
System.out.println("线程结束。。。");
}
运行结果:
5.2.2 使用退出标志来终止线程
一般线程run()结束,线程就会终止,但是遇到一些while循环的线程,我们只能打破循环条件才能终止线程。最直接的就是设置一个boolean类型的标志,并通过设置这个标志是true还是false来控制while条件是否退出。
代码示例:
class MyThread extends Thread{
//退出标志,使用volatile修饰,修改后的值可以被其他线程感知
public volatile boolean exit = false;
@Override
public void run() {
while (!exit){
System.out.println("正在无限无限循环中。。。。");
}
}
}
public static void main(String[] args) throws InterruptedException {
MyThread thread=new MyThread();
//启动线程
thread.start();
//延迟3s
thread.sleep(3000);
//停止线程
thread.exit=true;
thread.join();
System.out.println("线程结束。。。");
}
测试结果:
5.2.3 使用interrupt()来中断当前线程
使用interrupt()方法来中断线程有两中情况
1.线程处于阻塞状态:如使用了sleep(),同步锁的wait(),socket中的receiver()和accep()。会使线程处于阻塞状态。当调用线程的interrupt()方法时,会抛出InterruptedException异常,阻塞的那个方法抛出这个异常时,通过代码去捕捉异常,然后break跳出循环状态。从而让我们有机会结束这个线程。通常很多人认为只要调用interrupt方法线程就会结束,实际上是错的, 一定要先捕获InterruptedException异常之后通过break来跳出循环,才能正常结束run方法。
代码示例:
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(()->{
while (true){
try {
Thread.sleep(300);
System.out.println("正在无限无限循环中。。。。");
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("线程结束。。。");
break;//捕获到异常之后,执行break跳出循环。
}
}
});
//启动线程
thread.start();
//延迟3s
thread.sleep(3000);
thread.interrupt();//终止线程
}
测试结果:
2.线程处于非阻塞状态:使用isinterrupt()来判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会变成true。和使用自定义的标志来控制循环时一样的道理。
public void run() {
while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出
System.out.println("正在无限无限循环中。。。。");
}
}
总结:
为何要区分阻塞状态和非阻塞状态两种情况,是因为在阻塞状态时,如果调用interrupt()方法时,系统不仅会抛出InterruptedException异常,还会调用Interrupted()函数。调用时能获取到中断状态是true的状态,调用之后会复位中断状态为false。所以异常抛出之后通过isInterrupted()是获取不到中断标志是true的状态,从而不能退出循环。因此在线程未进入阻塞状态的代码段是可以通过isInterrupted()来判断是否发生来控制循环。在进入阻塞状态后要通过捕获异常,然后break退出循环。因此使用interrupt()来退出线程的最好的方式应该是两种情况都要考虑:
代码实例:
public void run() {
while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出
try {
Thread.sleep(300);
System.out.println("正在无限无限循环中。。。。");
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("线程结束。。。");
break;//捕获到异常之后,执行break跳出循环。
}
}
}
6. 多线程安全
6.1 线程安全问题
线程安全问题主要是共享资源竞争的问题,也就是在多线程情况下,一个或者多个线程同时抢占同一资源导致出现一些不必要的问题。比如,多个售票员卖火车票的问题。就是对共享资源(车票),多个售票员(多个线程)售卖,可能出现线程安全问题。这里简单实现一个线程安全问题代码。
对共享资源的多线程实现一般使用Runnable接口,因为Thread无法对共享资源访问,主要实现过程:实例化三个Thread,并传入同一个RunnableDemo实例作为参数。最后开启三条相同参数的线程。
代码实例:
class RunnableDemo implements Runnable{
//共享资源 ,比如车票,库存数量
public int num=100;
@Override
public void run() {
while (num>0){
System.out.println(Thread.currentThread().getName()+" num:"+num);
num--;
}
}
}
public class TestSync {
public static void main(String[] args) {
//实例化RunnableDemo
RunnableDemo runnableDemo=new RunnableDemo();
//构建三个线程并传入runnableDemo
Thread threadA= new Thread(runnableDemo,"A");
Thread threadB= new Thread(runnableDemo,"B");
Thread threadC= new Thread(runnableDemo,"C");
threadA.start();
threadB.start();
threadC.start();
}
}
测试结果
相信大家看了结果会发现,A,B,C三个线程在运行,安全问题就出在线程会出现相同的结果,比如100出现了三次,如果更改循环条件也可能出现负数。也就是我们100个门票,你卖了比200个,那多出来的门票咋办。这种情况我们怎么解决呢,这个时候就需要线程同步了。
6.2 如何解决线程安全问题:线程同步
上面的测试结果可以看出多个线程对共享资源访问时可能会过度消耗和虚拟消耗,我们需要线程同步来保证对共享资源的正常访问。那什么是线程同步呢?
线程同步:在线程使用一个资源时为其加锁,这样其他的线程便不能访问那个资源,直到解锁后才能访问。这样做的结果就是所以线程间会资源竞争,但是所有竞争的资源是同步的,刷新的,动态的,不会因为线程间的竞争,导致资源过度消耗和虚拟消耗。
什么是管程:
Monitor监视器,就是锁是一种同步机制,保证同一个时间,只有一个线程访问被保护数据或代码 JVM同步基于进入和退出,使用管程对象实现
6.2.1 使用synchronized关键字解决线程同步
synchronized关键字: java语言的关键字,可以用来给对象和方法或者代码块加锁。当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当多个线程并发访问同一个对象的Object中的这个加锁代码块时,一段时间内只能有一个线程得到执行,其他线程必须等待当前线程执行完这个代码块释放锁才能执行该代码块。它包括两种用法:synchronized修饰的同步代码块和同步方法。
也就是实现线程同步的两种方式是:
- 使用synchronized同步代码块
- 使用synchronized创建synchronized()同步方法
第一种方法:使用同步代码块
语法使用:
synchronized (锁对象){
//可能会出现线程安全问的代码,比如访问共享资源
}
使用同步代码块注意:
- 通过代码块的锁对象,可以是任意的对象。
- 必须保证多个线程使用的锁是同一个,不然起不到加锁的作用。
- 锁对象的作用就是把同步代码块锁住,同一时刻,最多允许一个线程执行。
我们还是以上面的线程安全为例,使用同步代码块举例。
代码实例:
class RunnableDemo implements Runnable{
//共享资源 ,比如车票,库存数量
public int num=10;
Object o=new Object();//同步锁
@Override
public void run() {
synchronized (o){
//可能会出现线程安全问的代码,比如访问共享资源
while (num>0){
System.out.println(Thread.currentThread().getName()+" num:"+num);
num--;
}
}
}
}
测试结果:
main方法没有任何改动,这里为了大家看的比较清楚,把num的值修改为10.大家可以看到没有出现虚拟消耗和过度消耗的问题
同步代码块的原理:
使用了一个锁对象,叫同步锁,对象锁,也叫同步监视器。当开启多个线程的时候,多个线程就开始抢夺CPU的执行权。比如A线程首先得到执行,就会执行开始run()方法,遇到同步代码块时,首先会检查是否有锁对象。发现有,则获取该锁对象。执行同步代码块中的代码。之后当CPU切换线程时。比如B线程得到执行,也开始执行run()。但是遇到同步代码块时检查是否有锁对象时发现没有锁对象,B线程就被阻塞。等待A线程执行完毕同步代码块,然后释放锁对象。B线程才可以获取从而进入同步代码块中执行。同步中的线程,没有执行完毕是不会释放锁的。这样便实现了线程对临界区的互斥访问。保证了共享数据的安全。
缺点:频繁的获取释放锁对象,降低程序效率。
第一种方法:使用同步方法
使用步骤:
1.把同步代码块中的代码抽取出来,封装到一个方法里
2.使用synchronized关键字修饰该方法
语法:
权限修饰符 synchronized 返回值类型 方法名(参数列表){
//可能会出现线程安全问的代码,比如访问共享资源
}
代码实例:
class RunnableDemo implements Runnable{
//共享资源 ,比如车票,库存数量
public int num=100;
@Override
public void run() {
while (true){
sell();//调用下面的sell方法
}
}
//访问了共享数据的代码抽取出来,放到一个方法sell中
public synchronized void sell(){
while (num>0){
System.out.println(Thread.currentThread().getName()+" num:"+num);
num--;
}
}
}
同步方法也是靠锁锁住同步代码,这里的锁就是Runnable实现类的对象,也就是this,谁调用方法,就是谁。
其实说到同步方法,就不得不提静态同步方法,就是在普通方法上添加一个static。此时锁对象就不是this。静态同步方法的锁对象是本类的class属性,class文件对象(反射)
代码实例:
class RunnableDemo implements Runnable{
//共享资源 ,比如车票,库存数量
public static int num=100;//共享数据也要加上static,不然不能访问
@Override
public void run() {
while (true){
sell();//调用下面的sell方法
}
}
//访问了共享数据的代码抽取出来,放到一个方法sell中
public static synchronized void sell(){//添加static关键字修饰
while (num>0){
System.out.println(Thread.currentThread().getName()+" num:"+num);
num--;
}
}
}
6.2.2 使用Lock锁解决线程同步
我们通常说的JUC就是java.util.concurrent为我们多线程开发提供很多帮助类。其中Lock接口是位于java.util.concurrent.locks.Lock,它是JDK1.5之后出现的,Lock中常用的方法。
void lock();获取锁
void unlock();释放锁
Lock接口的一个实现类java.util.concurrent.locks.ReentrantLock implements Lock接口,接下来我们就用它去实现。
使用步骤:
- 在Runnable实现类的成员变量中创建一个ReentrantLock对象。
- 在可能产生线程安全问题的代码前该对象调用lock方法获取锁
- 在可能产生线程安全问题的代码后该对象调用unlock方法获取锁。大家获取锁后一定要释放锁资源,不然后续可能导致未知问题。synchronized是帮我们自动释放锁的。Lock必须我们手动释放锁。最好写在finally代码中。
代码示例:
class RunnableDemo implements Runnable{
//共享资源 ,比如车票,库存数量
public static int num=100;//共享数据也要加上static,不然不能访问
//第一步:在Runnable实现类RunnableDemo的成员变量创建一个ReentrantLock对象。
Lock lock=new ReentrantLock();
@Override
public void run() {
while (true){
sell();//调用下面的sell方法
}
}
//访问了共享数据的代码抽取出来,放到一个方法sell中
public void sell(){//添加static关键字修饰
try {
//在可能出现线程安全的代码前该对象获取锁
lock.lock();
while (num>0){
System.out.println(Thread.currentThread().getName()+" num:"+num);
num--;
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();//释放锁
}
}
}
6.2.3 三种方法总结
第一种:
使用synchronized同步代码块:锁可以是任意的对象,但是必须保证多线程使用的是同一个对象锁。
第二种:
使用synchronized同步方法:锁是当前对象this,谁调用锁对象就是谁。
使用synchronized静态同步方法:锁是其class对象*,可以用this.getClass()获取,也可以用当前类名.class获取。
第三种:
Lock锁方法:该方法提供的方法远远多于synchronized方式,主要在Runable实现类的成员变量创建一个ReentrantLock对象,并使用该对象调用lock方法获取锁以及unlock方法释放锁。
6.2.4 Lock和synchronized区别
区别:
- 来源:synchronized是java的关键字,是内置的语言实现,而Lock只是一个接口。
- 异常是否释放锁:synchronized在发生异常时会自动释放锁,因此不会导致死锁的发生。而Lock发生异常时,不会主动释放占有的锁,必须收到unlock()来释放锁,可能会引起死锁的发生。所以最好同步代码使用try …catch包裹起来,在finally中释放锁,避免产生死锁。
- 是否响应中断:Lock等待锁的过程中可以使用interrupt()来中断等待,而synchronized只能等待锁的释放,不能响应中断。
- 是否知道获取锁:Lock可以使用tryLock来知道有没有获取锁,synchronized不能。
- Lock可以提高多个线程进行读操作的效率(可以通过ReentrantReadWriteLock实现读写分离)
- 在性能上来说:如果竞争资源不激烈,二者的性能是差不多的。而当竞争资源非常激烈(也就是大量线程同时竞争),此时Lock的效率要远远高于synchronize。但是最新的开发版本也对synchronized进行了优化。所以说,具体使用时要根据情况选择。
- 通信方式:synchronize使用Object对象的wat()和notify(),notifyAll()调度机制。Lock使用的是Condition进行线程之间的调度。
7. Java并发包和队列
7.1 集合的线程安全问题
Java中的集合有哪些,复习一下Java基础,一个就是单一值的Collection接口(ArrayList,Vector,LinkedList,HashSet,CopyOnWriteArrayList等)。另一个就是有键值对的Map接口(HashMap,Hashtable,TreeMap等)。其中线程安全的是Vector,Hashtable。如果我们在多线程使用线程不安全的集合,会抛出异常java.util.ConcurrentModificationException。
下面用ArrayList示范多线程安全问题:
//测试集合线程安全
public class TestSync {
public static void main(String[] args) {
//创建ArrayList集合
List<Integer> list=new ArrayList<>();
//创建20个线程,不断添加元素
for (int i = 0; i < 20; i++) {
new Thread(()->{
list.add(new Random().nextInt(10));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
测试结果:
可以看到多线程环境下操作ArrayList集合会出现异常ConcurrentModificationException(当方法检测到对象的并发修改,但不允许这种修改时,抛出此异常)。那我们如何解决这个问题呢。有以下三种办法。
- 第一种:使用Collections工具类中的synchronizedList()方法。
// 第一种:使用Collections中的synchronizedList()方法
List<Integer> list= Collections.synchronizedList(new ArrayList<>());
- 第二种:使用java.util.concurrent并发包下的CopyOnWriteArrayList类。
// 第二种:使用java.util.concurrent并发包下的CopyWriterList类。
List<Integer> list=new CopyOnWriteArrayList<>();
- 第三种:使用线程安全类Vector
List<Integer> list=new Vector<>();
测试结果:
总结:经过三种方法都可以实现多线程操作集合的安全,但是三种实现方法的原理不一样,
第一种:SynchronizedList 的实现里,get, set, add 等操作都加了 mutex 对象锁,再将操作委托给最初传入的 list。
这就是以组合的方式,将非线程安全的对象,封装成线程安全对象,而实际的操作都是在原非线程安全对象上进行,只是在操作前给加了同步锁。
第二种:CopyOnWriteArrayList是通过加锁,volatile和数组拷贝来保证数据安全。除了加锁之外,CopyOnWriteArrayList底层数组还被volatile修饰。意思是一旦数组被修改其他线程立马能够感知到。
public boolean add(E e) {
final ReentrantLock lock = this.lock;
//1.加锁
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//2.从原数组拷贝新数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
//3.在新数组上操作
newElements[len] = e;
//4.将新数组赋给数组容器
setArray(newElements);
return true;
} finally {
//5.释放锁
lock.unlock();
}
}
第三种:就是使用synchronized加锁。
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
总结:所以在多线程环境下我们要使用线程安全的集合。List推荐使用CopyOnWriterArrayList。Set的推荐使用CopyOnWriteArraySet。Map的推荐使用ConcurrentHashMap。这里就不对ConcurrentHashMap举例了。后续会对其源码详解。大家知道如何用就行。
7.2 JUC中的常用类
JUC包中有很多帮助我们开发多线程的常用类,比如在本文章 5.1.3 使用JUC工具类CountDownLatch中介绍的CountDownLatch类。接下来继续介绍CyclicBarrier(循环栅栏)和Semaphore(信号量)
7.2.1 CyclicBarrier
现实生活中我们可能遇到这样一个场景,比如吃饭要等所有人到齐了才能吃饭,开会也要等到人齐了才能开会。玩英雄联盟必须十个人进入游戏才能开始。
在JUC包中为我们提供了一个同步工具类能给很好的模拟这类场景。他就是CyclicBarrier类。利用CyclicBarrier类可以实现一组线程相互等待。当所有线程都到达某个线程点后再继续后续操作。
在CyclicBarrier类内部有一个计数器,每个线程在到底屏障点的时候都会调用await(
)方法来将自己阻塞,此时计数器会减一。当计算器减为0的时候所有因调用await(
)方法而将阻塞的线程将被唤醒。这就是实现一组线程相互等待的原理。
这里简单实现一下,比如等十个人到齐了才能吃饭
代码实例:
public class TestCyclicBarrierDemo {
public static void main(String[] args) {
//创建循环栅栏,第一个参数是目标障碍数
CyclicBarrier cyclicBarrier=new CyclicBarrier(8,()->{
System.out.println("所有人到齐了,可以干饭了");
});
//启动十个线程,模仿十个人分别到场
for (int i = 1; i <=10 ; i++) {
new Thread(()->{
try {
System.out.println("第"+ Thread.currentThread().getName() +"位客人到了");
//计数器减一
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
测试结果:
总结:
可以看出是等所有的客人到齐才能去干饭。这里跟CountDownLatch有点类似,但是CyclicBarrier比它更加强大。
区别:
- CyclicBarrier的计数器由自己控制,而CountDownLatch的计数器是由使用者来控制,在CyclicBarrier中线程调用await方法不仅会将自己阻塞还会将计数器减1,而在CountDownLatch中线程调用await方法只是将自己阻塞而不会减少计数器的值。
- CountDownLatch只能拦截一轮,而CyclicBarrier可以实现循环拦截。
7.2.2 Semaphore
Semaphore是一个线程同步的辅助类,可以维护当前访问自身的线程个数,并提供了同步机制。Semaphore是一种计数信号量,用于管理一组资源,内部是基于AQS的共享模式。使用Semaphore可以控制同时访问资源的线程个数。例如:实现一个文件允许的并发访问数。停车场停车问题。
常用方法:
1.Semaphore(int permits):构造方法,创建具有给定许可数 的计数信号量并设置为非公平信号量
2.Semaphore(int permits,boolean fair):构造方法,当fair为true时,创建具有给定许可数的计数信号量并设置为公平信号量
3.void acquire():从此信号量获取一个许可前将线程一直阻塞,相当于一个车占一个车位
4.void acquire(int n):从此信号量获取给定数目许可,在提供这些许可前将线程一直阻塞,比如n=3,相当于一个车占三个车位。
5.void release():释放一个许可,将其返回给信号量,就如同车开走返回一个车位。
6.void release(int n):释放n个许可,将其返回给信号量。
6.int availablePermits():当前可用的许可数。
//测试8个汽车停在三个车位
public class SemaphoreDemo {
public static void main(String[] args) {
//设置信号量为3个,对应三个停车位
Semaphore semaphore=new Semaphore(3);
for (int i = 1; i <=8 ; i++) {
new Thread(()->{
try {
semaphore.acquire();//获取许可
System.out.println(Thread.currentThread().getName()+"号停到了车位");
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName()+"号准备释放许可");
System.out.println("当前许可数为:"+semaphore.availablePermits());
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();//释放许可
}
},String.valueOf(i)).start();
}
}
}
总结:Semaphore主要用于控制当前活动线程的数目,就如同停车场系统一般,Semaphore就像门卫,用于控制总共允许停车的停车位个数。对于每一辆车来说就是一个线程,线程需要通过acquire()方法获得许可,而release()方法释放许可,如果许可数达到最大活动数,那么调用acquire()方法后会进入等待队列,必须等待获得许可的线程释放许可,才能继续合理运行下去。
7.3 并发队列
7.3.1 前言
说下多线程的并发队列前先给大家说下两种数据结构队列和栈。不了解这两个后续的并发队列可能会有点难度
1.什么是队列?
是一种数据结构,该队列的元素遵循先进先出的原则。就是当我们添加元素时添加到队列的尾部,当我们获取元素时,他会返回队列头部的元素。也就是先插入的列的元素也是最先出列的,类似于排队吃饭。
2.什么是栈?
也是一种数据结构,该队列的元素遵循后进先出的原则。,就是只能从栈顶插入元素(入栈),栈顶获取元素(出栈)。因为只有栈顶可以操作,先插入的元素就会在栈底,只能先处理后进入的元素,这种队列优先处理最近发生的事件。
3.什么是并发队列
在JDK中提供了两套并发队列实现。一个是ConcurrentLinkedQueu为代表的高性能队列,另一个是BlockingQueue接口为代表的阻塞队列,无论那种都继承了Queue。
7.3.2 ConcurrentLinkedQueue
定义:
ConcurrentLinkedQueue是一个适用于高并发场景下的队列。通过无锁的方式,来实现了高并发状态下的高性能。通常ConcurrentLinkedQueue的性能要好于BlockingQueue,它是一个基于链接节点的无界线程安全队列,该队列的元素遵循先进先出的原则。头是最先加入的,尾是最后加入的。该队列不允许null元素。
常用方法:
add()和offer():都是加入元素的方法,在ConcurrentLinkedQueue中这两个方法没有任何区别。
poll()和peek():都是取头元素节点,区别在于前者会删除元素,后者不会。
代码实例:
public class ConcurrentLinkedQueueDemo {
public static void main(String[] args) {
//创建ConcurrentLinkedQueue对象
ConcurrentLinkedQueue<Integer> concurrentLinkedQueue=new ConcurrentLinkedQueue();
//测试add()和offer()添加元素
concurrentLinkedQueue.add(1);
concurrentLinkedQueue.offer(2);
System.out.println(concurrentLinkedQueue);
//peek()
Integer peek = concurrentLinkedQueue.peek();
System.out.println("peek: "+peek+"剩余元素: "+concurrentLinkedQueue);
//测试poll(),发现会删除取到的元素。
Integer poll = concurrentLinkedQueue.poll();
System.out.println("poll: "+poll+"剩余元素: "+concurrentLinkedQueue);
}
}
测试结果:
7.3.2 BlockingQueue
定义:
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列,这两个附加的操作是:
1.当队列为空时,获取元素的线程会等待队列变为非空。
2.当队列满时,存储元素的线程会等待队列可用。
阻塞队列时线程安全的。
用途:阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者只能从容器获取元素。
常用方法
更多详细方法可以翻阅官方文档:
jdk在线中文文档
add(e):将指定元素插入到此队列尾部(如果立即可行且不会超过该队列的容量),在成功时返回true,如果此队列已满,则抛出IllegalStateException异常。
offer(e):将指定元素插入到此队列尾部(如果立即可行且不会超过该队列的容量),在成功时返回true,如果此队列已满,则返回false;
put(e):将指定元素插入到此队列尾部,当阻塞队列满时,生产者继续往队列里put元素,队列将会一直阻塞生产线程,直到put数据or响应中断退出。
offer(E e, long timeout, TimeUnit unit):将指定元素插入到此队列尾部,当阻塞队列满时,队列会阻塞生产线程一段时间直到超时退出。
remove(): 从此双端队列移除第一次出现的指定元素。也就是从队列头部移除,如果阻塞队列为空时,则会抛出NoSuchElementException异常。
poll(): 从此双端队列移除第一次出现的指定元素。也就是从队列头部移除,成功则返回队列里的元素,失败则返回null。take():获取并移除此双端队列表示的队列的头部(即此双端队列的第一个元素),当阻塞队列为空时,消费线程从队列里take()元素,队列会一直阻塞消费线程直到队列可用。
poll(long timeout, TimeUnit unit): 从此双端队列移除第一次出现的指定元素。也就是从队列头部移除,当阻塞队列为空时,队列会阻塞消费线程一段时间直到超时退出。
element();返回队列的第一个元素。
peek():获取但不移除此双端队列表示的队列的头部(即此双端队列的第一个元素);如果此双端队列为空,则返回 null。
1、ArrayBlockingQueue
定义:
ArrayBlockindQueue是一个有边界的阻塞队列,它的内部实现是一个数组,我们必须在其初始化的时候指定其容量大小,容量大小一旦指定不能改变。ArrayBlockindQueue是以先进先出的方式存储数据,最新插入的元素在尾部,最新移除的对象是头部。
//创建一个容量3的ArrayBlockingQueue对象
ArrayBlockingQueue<Integer> arrayBlockingQueue=new ArrayBlockingQueue<>(3);
//添加元素
arrayBlockingQueue.add(1);
arrayBlockingQueue.add(2);
arrayBlockingQueue.add(3);
boolean offer = arrayBlockingQueue.offer(4);
System.out.println("offer:"+offer+"arrayBlockingQueue:"+arrayBlockingQueue);
2、LinkedBlockingQueue
定义:
LinkedBlockingQueue阻塞队列的大小是配置可选的,如果我们初始化时指定容量大小,那就是有界的,如果不指定就是没有边界的,说是无边界的,但是其实还是有边界的,其最大值是Integer.MAX_VALUE。他的内部实现是一个链表。和ArrayBlockingQueue队列一样,是以先进先出的方式存储数据,最新插入的元素在尾部,最新移除的对象是头部。
3、PriorityBlockingQueue
定义:
PriorityBlockingQueue是一个没有边界的队列,它的排序规则和java.util.PrioritQueue一样,需要注意的是,PriorityBlockingQueue允许插入null元素,所有插入PriorityBlockingQueue的对象必须实现java.lang.Comparable接口,队列优先级的排序规则就是按照我们对这个接口的实现来定义的。另外,我们可以从PriorityBlockingQueue获得一个迭代器Iterator,但这个迭代器并不保证按照优先级顺序进行迭代。
4、SynchronousQueue
定义:
SynchronousQueue队列内部仅允许容纳一个元素。当一个线程插入一个元素后会被阻塞,除非这个元素被另一个线程消费。
5、DelayQueue
定义:
Delayed 元素的一个无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部 是延迟期满后保存时间最长的 Delayed 元素。如果延迟都还没有期满,则队列没有头部,并且 poll 将返回 null。当一个元素的 getDelay(TimeUnit.NANOSECONDS) 方法返回一个小于等于 0 的值时,将发生到期。即使无法使用 take 或 poll 移除未到期的元素,也不会将这些元素作为正常元素对待。例如,size 方法同时返回到期和未到期元素的计数。此队列不允许使用 null 元素。
5、LinkedBlockingDeque
定义:
LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列,即可以从队列的两端插入和移除元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。LinkedBlockingDeque是可选容量的,在初始化时可以设置容量防止其过度膨胀,如果不设置,默认容量大小为Integer.MAX_VALUE
6、LinkedTransferQueue
定义:
LinkedTransferQueue是一个由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,LinkedTransferQueue多了tryTransfer和transfer方法。
LinkedTransferQueue采用一种预占模式。意思就是消费者线程取元素时,如果队列不为空,则直接取走数据,若队列为空,那就生成一个节点(节点元素为null)入队,然后消费者线程被等待在这个节点上,后面生产者线程入队时发现有一个元素为null的节点,生产者线程就不入队了,直接就将元素填充到该节点,并唤醒该节点等待的线程,被唤醒的消费者线程取走元素,从调用的方法返回。我们称这种节点操作为“匹配”方式。
7.3.3使用BlockingQueue模拟生产者与消费者
生产线程:
//生产者线程
public class ProducerThread implements Runnable{
private BlockingQueue<Integer> blockingQueue;//阻塞队列
private volatile boolean flag=true;//终止线程标志
private static AtomicInteger count = new AtomicInteger();
public ProducerThread(BlockingQueue<Integer> blockingQueue) {
this.blockingQueue = blockingQueue;
}
//停止线程
public void stopThread() {
this.flag = false;
}
@Override
public void run() {
try{
System.out.println(Thread.currentThread().getName()+"启动线程");
while (flag){
System.out.println(Thread.currentThread().getName()+"正在生产数据");
Integer data=count.incrementAndGet();
//将数据存入队列中,超时退出
boolean offer = blockingQueue.offer(data, 2, TimeUnit.SECONDS);
if (!offer){
System.out.println(Thread.currentThread().getName()+"存入"+data+"到队列中,失败");
}
System.out.println(Thread.currentThread().getName()+"存入"+data+"到队列中,成功");
Thread.sleep(1000);
}
}catch (Exception e){
e.printStackTrace();
}finally {
System.out.println(Thread.currentThread().getName()+"退出线程");
}
}
}
消费线程:
//消费者线程
public class ConsumerThread implements Runnable {
private BlockingQueue<Integer> blockingQueue;//阻塞队列
private volatile boolean flag=true;//终止线程标志
public ConsumerThread(BlockingQueue<Integer> blockingQueue) {
this.blockingQueue = blockingQueue;
}
//停止线程
public void stopThread() {
this.flag = false;
}
@Override
public void run() {
try{
System.out.println(Thread.currentThread().getName()+"启动线程");
while (flag){
System.out.println(Thread.currentThread().getName()+"正在从队列中获取数据");
//将数据存入队列中,超时退出
Integer data= blockingQueue.poll( 2, TimeUnit.SECONDS);
if (data!=null){
System.out.println(Thread.currentThread().getName()+"拿到队列中的数据data:" + data);
Thread.sleep(1000);
}else {
System.out.println(Thread.currentThread().getName()+"超过2s没拿到数据");
flag=false;
}
}
}catch (Exception e){
e.printStackTrace();
}finally {
System.out.println(Thread.currentThread().getName()+"退出线程");
}
}
}
测试结果:
//测试生产者线程和消费线程
public class TestPrdAndComsm {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> blockingQueue =new ArrayBlockingQueue<>(10);
ProducerThread producerThread1 = new ProducerThread(blockingQueue);
ProducerThread producerThread2 = new ProducerThread(blockingQueue);
ConsumerThread consumerThread1 = new ConsumerThread(blockingQueue);
Thread t1 = new Thread(producerThread1,"生产者-1 ");
Thread t2 = new Thread(producerThread2,"生产者-2 ");
Thread c1 = new Thread(consumerThread1,"消费者");
t1.start();
t2.start();
c1.start();
// 执行2s后,生产者不再生产
Thread.sleep(2* 1000);
producerThread1.stopThread();
producerThread2.stopThread();
}
}
8. 线程池
8.1 什么是线程池?
首先看下定义:
线程池(Thread Pool):是一种线程使用模式,线程过度会带来调度开销,进而影响缓存局部性和整体性能,而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免短时间任务时创建和消耗线程的代价,线程池不仅能够保证内核的充分利用,还能防止过度调度
看完大家是不是有点蒙蔽😵,其实我跟大家差不多,一开始也是懵逼的,只有通过不断的学习才能获得新的知识,其实线程池可以看成一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,从而不需要反复创建线程而消耗过度的资源。
使用线程池的优点:
1.降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
2.提高响应速度:当任务到达时,任务可以不需要的等到线程创建就能立即执行。
3.提高线程的可管理性,可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而使服务器宕机(线程是稀有资源,不可能无限制创建线程,每个线程需要大约1MB内存,线程越多,消耗的资源越多,就会使服务器宕机)。使用线程池还可以统一调度,分配和监控。
8.2 线程池如何使用?
Java里面线程池的最顶级接口是java.util.concurrent.Executor接口,但严格意义上讲Executor并不是一个线程池,而只是执行线程的工具,真正的线程池接口是java.util.concurrent.ExecutorService,为我们封装了几种常见的功能线程池,等下后面会介绍。不过底层的原理还是通过java.util.concurrent.ThreadPoolExecutor实现。底层原理我们会详细介绍,因为以后项目上都是手动创建线程池。常用的线程池都会存在一些问题。
- Executor顶层接口就是用来执行该方法的。源码如下:
public interface Executor {
void execute(Runnable command);
}
- ExecutoService继承了Executor接口,还增加了新的方法,拥有了初步管理线程池的方法。
- Executors是一个工具类,快速创建线程池都是用的它,后面介绍的newFixedThreadPool,newCachedThreadPool,newSingleThreadExecutor,ScheduledThreadPool 这些线程都是通过Executors创建。
- public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行。Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。
8.2.1 四种常见的线程池?
以后大家创建线程池的时候,其实手动创建更好,因为这样可以更加明确线程池的运行规则,避免资源耗尽的风险。
自动创建线程池(即直接调用JDK封装好的构造方法)可能带来一些风险。现在只是让大家先熟悉下线程池。
8.2.1.1 定长线程池(newFixedThreadPool)
源码:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
- 特点:只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列(其LinkedBlockingQueue最大值是Integer.MAX_VALUE)。
- 应用场景:控制线程最大的并发数。
- 缺点:因为LinkedBlockingQueue的最大值是Integer.MAX_VALUE,当处理任务的速度赶不上增加任务的速度,其任务队列不断增加,使用newFixedThreadPool容易造成大量内存占用,可能会导致OOM。
8.2.1.2 可缓存线程池(newCachedThreadPool)
源码:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
- 特点:没有核心线程,非核心线程数量无限,执行完闲置60s回收,任务队列为不存储元素的队列
- 应用场景:执行大量,耗时少的任务。
- 缺点:因为非核心线程数量为Integer.MAX_VALUE,这可能会创建非常多的线程,从而导致OOM。
8.2.1.3 单线程化线程池(newSingleThreadExecutorl)
源码:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
- 特点:只有一个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列
- 应用场景:不适合并发但可能引起IO阻塞性及影响UI线程响应的操作。如何数据库操作,文件操作
- 缺点:当请求堆积的时候,可能会占用大量的内存线程池,原理和newFixedThreadPool一样,只是线程数量设为了1
8.2.1.4 定时线程池(ScheduledThreadPool )
源码:
private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue(), threadFactory);
}
- 特点:核心线程数量固定,非核心线程数量无限,执行完10s回收,任务队列为延时阻塞对列
- 应用场景:执行定时或周期的任务。
- 缺点:因为非核心线程数量为Integer.MAX_VALUE,这可能会创建非常多的线程,从而导致OOM。
8.2.1.5 测试可缓存线程池(newCachedThreadPool)
相信大家现在对Executors提供的线程池有了大概的了解,下面就让我进入代码的实践。
使用线程池的步骤:
1.创建线程池对象
2.创建Runnable接口子类对象(task)
3.使用submit()方法,提交Runnable接口子类对象(take task)
4.关闭线程池(一般不操作这步)
Runnable实现类:
class MyTask implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"正在执行任务");
}
}
测试类:
public static void main(String[] args) {
//创建线程池对象
ExecutorService threadPool = Executors.newCachedThreadPool();
for (int i = 0; i <Integer.MAX_VALUE ; i++) {
threadPool.submit(new MyTask());
}
//关闭线程池
// threadPool.shutdown();
}
测试结果:
总结:
虽然使用Executors创建线程池很方便,但现在已经不建议使用了,而是采用ThreadPoolExecutor 方式创建,为了让创建线程池的人了解线程池运行规则,规避资源消耗的风险
其实 Executors 的 4 个功能线程有如下弊端:
- FixedThreadPool 和 SingleThreadExecutor:主要问题是堆积的请求处理队列均采用 LinkedBlockingQueue,可能会耗费非常大的内存,甚至 OOM。
- CachedThreadPool 和 ScheduledThreadPool:主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。
8.3 线程池原理
我们通过Executors快速创建的线程池,其实底层都是使用ThreadPoolExecutor类。看下newCachedThreadPool源码
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
线程池的真正实现类是 ThreadPoolExecutor,其构造方法有如下4种:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
可以看到,其构造函数需要如下几个参数:
- corePoolSize:核心线程数,默认情况下,核心线程数会一直存活,但是当allowCoreThreadTimeOut设置为true时,核心线程也会超时回收。
- maximumPoolSize:线程池所能容纳的最大线程数,也就是核心线程数+非核心线程数不能超过maximumPoolSize。当活跃线程数达到该数值后,后续的新任务将会阻塞。
- keepAliveTime:回收非核心线程的等待时间,如果线程池当前线程数超过corePoolSize,那么如果多余的非核心线程数空闲时间超过keepAliveTime,它们会被回收,如果设置了allowCoreThreadTimeOut设置为true时,核心线程也会被回收。
- unit:指定keepAliveTime参数的时间单位,常用的有:TimeUnit.MILLISECONDS(毫秒),TimeUnit.SECONDS(秒),TimeUnit.MINUTES(分)。
- workQueue:任务队列,通过线程池execute()方法提交的Runnable对象会存储在该参数中,其采用阻塞队列实现。
- threadFactory :线程工厂,用于指定为线程池创建线程的方式。
- handle:拒绝策略,当达到最大线程数时需要执行的饱和策略。
8.3.1 线程池工作原理
当此时新来一个任务需要执行,线程池会怎么处理?
首先我们看下图,让我们对上面的参数有个了解。其工作原理流程图如下:
线程池在完成初始化后,默认情况下,线程池中并没有任何线程,线程池会等待有任务到来时,再创建新线程去执行任务
- 当核心线程数小于corePoolSize时,即使其他线程处于空闲状态,也会创建一个新核心线程来运行任务。
- 如果线程数等于(或大于)corePoolSize但是少于maximumPoolSize,则将任务放入阻塞队列
- 如果队列已满,并且线程数小于maximumPoolSize,则会创建一个新非核心线程来运行任务。
- 如果队列已满,并且线程数已经等于或者大于maximumPoolSize,则会执行拒绝策略,拒绝任务。
- 如果设置了超时时间,处于超时的非核心线程会在超时后被回收,设置了allowCoreThreadTimeOut设置为true时,核心线程也会被回收。
总结:
是否需要增加线程的判断顺序:1、corePoolSize 2、workQueue 3、maximumPoolSize
是否回收线程判断顺序:1、超时keepAliveTime 2、非核心线程
举个例子:
线程池的核心线程数corePoolSize大小为4,最大池maxPoolSize大小为8,队列workQueue为100。keepAliveTime 为60s。
因为线程中的请求最多会创建4个,然后任务将被添加到队列中,直到达到100。当队列已满时,将创建新的线程,最多到8个线程,如果再来任务,就拒绝。非核心线程超过60s会被回收。
8.3.2 线程池参数
8.3.2.1 核心线程数corePoolSize和最大线程数maximumPoolSize
项目当中,corePoolSize和maximumPoolSize该如何设置呢,这个可不能随便设置,不然轻则导致效率变低,重则导致内存溢出,直接OOM了。那么该如何设置呢?
首先要看任务类型:
- CPU密集型(计算Hash,加密等):一般设置为CPU核心数的1-2倍左右。
- IO密集型(读写数据库,文件,网络读写等):可以参考Brain Goetz专家推荐的计算方法:线程数=CPU核心数*(1+平均等待时间/平均工作时间
当然如果需要更精确的线程数量,那就需要根据不同的程序去做压测,这样就能得到比较合适的线程数量。还是需要多测试。
8.3.2.2 keepAliveTime超时时间和unit时间单位
这个需要根据自己实际的业务来设置
8.3.2.3 任务队列(workQueue)
任务队列是基于阻塞队列实现的,即采用生产者消费者模式,在 Java 中需要实现 BlockingQueue 接口。但 Java 已经为我们提供了 7 种阻塞队列的实现:这个在前面已经详细介绍了,这里简单回顾下。
- ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列(数组结构可配合指针实现一个环形队列)。
- LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列,在未指明容量时,容量默认为 Integer.MAX_VALUE。
- PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列,对元素没有要求,可以实现 Comparable 接口也可以提供 Comparator 来对队列中的元素进行比较。跟时间没有任何关系,仅仅是按照优先级取任务。
- DelayQueue:类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现 Delayed 接口,通过执行时延从队列中提取任务,时间没到任务取不出来。
- SynchronousQueue: 一个不存储元素的阻塞队列,消费者线程调用 take() 方法的时候就会发生阻塞,直到有一个生产者线程生产了一个元素,消费者线程就可以拿到这个元素并返回;生产者线程调用 put() 方法的时候也会发生阻塞,直到有一个消费者线程消费了一个元素,生产者才会返回。
- LinkedBlockingDeque: 使用双向队列实现的有界双端阻塞队列。双端意味着可以像普通队列一样 FIFO(先进先出),也可以像栈一样 FILO(先进后出)。
- LinkedTransferQueue: 它是ConcurrentLinkedQueue、LinkedBlockingQueue 和 SynchronousQueue 的结合体,但是把它用在 ThreadPoolExecutor 中,和 LinkedBlockingQueue 行为一致,但是是无界的阻塞队列。
总结:注意有界队列和无界队列的区别,如果使用有界队列,当队列已满并超过最大线程数时就会执行拒绝策略。而如果使用无界队列,因为任务队列可以永远添加任务,所以设置的maximumPoolSize没有任何意思。
8.3.2.4 线程工厂(threadFactory)
- 线程工厂指定创建线程的方式,需要实现ThreadFactory接口,并实现 newThread(Runnable r) 方法。该参数可以不用指定。通常默认使用Executors.defaultThreadFactory()
- 创建出来的线程都在同一个线程组,拥有同样的NORM_PRIORITY优先级并且都不是守护线程。
- 如果自己指定ThreadFactory,那么就可以改变线程名、线程组、优先级、是否是守护线程等。
源码:
// 验证第一点,新的线程是由ThreadFactory创建的,默认使用Executors.defaultThreadFactory()
public static ThreadFactory defaultThreadFactory() {
return new DefaultThreadFactory();
}
......
static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
// 验证第二点,创建出来的线程都在同一个线程组,拥有同样的NORM_PRIORITY优先级并且都不是守护线程。
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
8.3.2.5 任务拒绝策略(handler)
任务拒绝时间:
- 当Executor关闭时,提交新任务会被拒绝。
- 当Executor中最大线程数和阻塞队列都已饱和时。
拒绝策略需要实现RejectedExecutionHandler接口并实现rejectedExecution(Runnable r, ThreadPoolExecutor e) 方法。不过 Executors 框架已经为我们实现了 4 种拒绝策略:
- AbortPolicy(默认):丢弃任务并抛出 RejectedExecutionException 异常。
- DiscardPolicy:丢弃任务,但是不抛出异常,可以配合这种模式进行自定义的处理方式。
- DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。
- CallerRunsPolicy:由调用线程来处理任务,这个策略有两大好处:
1.任务拒绝后让提交任务的线程去执行,比如主线程调用了execute()方法,任务爆满拒绝后让主线程代劳执行,避免了业务损失。
2.给主线程代劳执行,主线程此刻就不能继续添加新任务了,就必须把代劳的任务执行完才可以执行新任务,执行代劳任务的时候线程池的其他任务也能同时执行完一些,给了线程池的线程执行任务的缓冲时间。
8.4 如何停止线程池
8.4.1 线程池状态
线程池一共五种状态
8.4.2 使用shutdown停止线程池
不一定立即停止,执行了该方法后,后面再请求执行的任务会被拒绝,当前线程正在执行的任务和任务队列正在等待的任务还是会执行完才会停止线程。即存量任务等待执行完毕,新任务拒绝。
代码实例:
class ShutDownTask implements Runnable{
@Override
public void run() {
try {
Thread.sleep(500);
System.out.println(Thread.currentThread().getName()+"执行任务");
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "被中断了");
}
}
}
public class TestShutDown {
public static void main(String[] args) throws InterruptedException {
//创建一个固定容量的线程池
ExecutorService pool = Executors.newFixedThreadPool(8);
for (int i = 0; i <100 ; i++) {
pool.execute(new ShutDownTask());
}
Thread.sleep(1500);// 先执行1500ms再去停止
pool.shutdown();//关闭线程池
pool.execute(new ShutDownTask());
}
}
测试结果:
执行了**shutdown()**之后,再来一个任务会被拒绝,抛出异常 Java.util.concurrent.RejectedExecutionException…Shutting down, pool size = 8, active threads = 8 queued tasks = 76, completed tasks = 16,已经提示正在关闭了,后面的任务不接受了,队列还有76个任务等待,正在执行8个,已经完成16个。
但是总不能每一次执行一个新任务都看是否被拒绝来判断是否正在停止把?于是有了 isShutdown方法。
8.4.3 isShutdown
只有执行了shutdown() 方法,isShutDown() 方法就会返回true。只要把上面代码executorService.shutdown(); 前后各添加一句System.out.println(“isShowDown:”+pool.isShutdown());打印完成后,任务队列还有任务在继续执行打印。
代码实例:
System.out.println("isShowDown:"+pool.isShutdown());
pool.shutdown();//关闭线程池
System.out.println("isShowDown:"+pool.isShutdown());
测试结果:
8.4.4 isTerminated
可以判断线程池是否已经完全终止。
public static void main(String[] args) throws InterruptedException {
//创建一个固定容量的线程池
ExecutorService pool = Executors.newFixedThreadPool(8);
for (int i = 0; i <100 ; i++) {
pool.execute(new ShutDownTask());
}
Thread.sleep(1500);// 先执行1500ms再去停止
pool.shutdown();//关闭线程池
//false 因为此时队列还有任务
System.out.println("isTerminated:"+pool.isTerminated());
Thread.sleep(10000);
//true 等待时间够长,任务已经介绍
System.out.println("isTerminated:"+pool.isTerminated());
}
测试结果:
中间打印false,因为此时还有任务没有执行完毕
等待10s后,最后打印true,表示线程池已经完全停止。
8.4.5 awaitTermination
测试一段时间内任务是否执行完毕,做判断用,执行这个方法后指定时间内,线程处于阻塞状态,线程运行完毕则返回true,否则返回false,代表超时。
代码实例:
public static void main(String[] args) throws InterruptedException {
//创建一个固定容量的线程池
ExecutorService pool = Executors.newFixedThreadPool(8);
for (int i = 0; i <100 ; i++) {
pool.execute(new ShutDownTask());
}
Thread.sleep(1500);// 先执行1500ms再去停止
pool.shutdown();//关闭线程池
boolean b = pool.awaitTermination(1, TimeUnit.SECONDS);
//1s内肯定执行不完,阻塞等待1s后返回false
System.out.println(b);
// 7s内执行完了,阻塞等待着,然后提前执行完就直接返回true
boolean b1 = pool.awaitTermination(7L, TimeUnit.SECONDS);
System.out.println(b1);
}
有3种情况会返回**awaitTermination()**会返回值,返回前是阻塞的。
1.所有任务执行完毕了,返回 true。
2.等待的时间到了,返回 false。
3.等待过程被中断,抛出 InterruptedException。
8.4.6 shudownNow
终止所有正在执行的任务,线程池正在执行的线程收到中断信号,并停止处理等待队列中的任务,最后将所有未执行的任务以列表的形式返回。
代码示例:
public static void main(String[] args) throws InterruptedException {
//创建一个固定容量的线程池
ExecutorService pool = Executors.newFixedThreadPool(8);
for (int i = 0; i <100 ; i++) {
pool.execute(new ShutDownTask());
}
Thread.sleep(1500);// 先执行1500ms再去停止
List<Runnable> list = pool.shutdownNow();//关闭线程池
}
8.4.7 线程池的暂停与恢复
接下来我们使用ThreadPoolExecutor手动创建一个线程池,并新增暂停和恢复功能。
代码示例:
//使用ThreadPoolExecutor创建一个线程池
public class MyThreadPool extends ThreadPoolExecutor {
private final ReentrantLock lock=new ReentrantLock();
//返回与此Lock实例一起使用的Condition实例
private Condition unpaused =lock.newCondition();
private boolean isPaused;
// ====下面这些构造不用看
public MyThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
public MyThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
}
public MyThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
}
public MyThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
}
// ============================上面这些构造不用看========================
//线程执行之前调用
protected void beforeExecute(Thread t, Runnable r) {
System.out.println(Thread.currentThread().getName()+"钩子====");
try{
//获取锁
lock.lock();
while (isPaused){//当isPaused为true时,阻塞线程
unpaused.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();//释放锁
}
}
//暂停线程
private void pause() {
lock.lock();
try {
isPaused = true;
} finally {
lock.unlock();
}
}
//恢复线程
public void resume() {
lock.lock();
try {
isPaused = false;
unpaused.signalAll();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
//创建线程池
MyThreadPool myThreadPool = new MyThreadPool(10, 20, 60, TimeUnit.SECONDS, new LinkedBlockingQueue());
for (int i = 0; i <1000 ; i++) {
myThreadPool.execute(()->{
try {
System.out.println(Thread.currentThread().getName()+"我被执行");
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
//休眠1s开始暂停线程池
Thread.sleep(1000);
myThreadPool.pause();
System.out.println("线程被暂停了");
Thread.sleep(2000);
myThreadPool.resume();
System.out.println("线程被恢复了");
}
}
测试结果:
8.4.8 Fork/Join框架基本使用
1.什么是Fork/Join?
Java.util.concurrent.ForkJoinPool由Java大师Doug Lea主持编写,它可以将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出。
- fork :把一个复杂任务进行拆分,大事化小
- join:把拆分任务的结果进行合并
2.Fork/Join框架基本使用
我们做个简单的示例,在这个示例中我们计算了1-100累加后的值:
代码示例:
/**
* 计算1+2+...+100的值。
* 第一步:创建一个类并继承RecursiveTask
*/
public class MyTask extends RecursiveTask<Integer> {
//拆分成差值不能超过10,计算10以内的值
private static final int VALUE=10;
private int begin;//拆分开始值
private int end;//拆分结果值
private int result;//返回结果
public MyTask(int begin, int end) {
this.begin = begin;
this.end = end;
}
/**
*第二步: 根据拆分条件拆分任务
* @return
*/
@Override
protected Integer compute() {
if(begin>end){
System.out.println("begin不能大于end");
}else if(end-begin<VALUE){
/**
* 如果条件成立,说明这个任务所需要计算的数值分为足够小了
* 可以正式进行累加计算了
*/
for (int i = begin; i <=end ; i++) {
result+=i;
}
System.out.println("开始计算的部分:begin = " + begin + ";end = " + end+";result = "+result);
}else {//第三步:进一步拆分
int middle=(begin+end)/2;
//拆分左边
MyTask task01=new MyTask(begin,middle);
//拆分右边
MyTask task02=new MyTask(middle+1,end);
//调用方法拆分
task01.fork();
task02.fork();
//第四步:合并结果
result=task01.join()+task02.join();
}
return result;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建MyTask对象
MyTask myTask=new MyTask(1,100);
//创建分支合并池对象
ForkJoinPool forkJoinPool=new ForkJoinPool();
ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(myTask);
//获取最终合并结果
Integer result=forkJoinTask.get();
System.out.println("result: "+result);
//关闭池对象
forkJoinPool.shutdown();
}
测试结果:
9. Java中的锁分类和区别
9.1 公平锁和非公平锁
公平锁:是指多个线程按照申请锁的顺序来获取锁
非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,有可能会造成优先级反转或者饥饿现象。
可能大家对这个概念还不太理解,那我举个不恰当的例子,就是你去饭店吃饭,人很多需要排队。如果是公平锁呢,那就必须等前面人吃完饭才能轮到你,你是VIP用户都不行。但是如果是非公平锁呢,那就是VIP用户先进,如果没有充钱,只能排到明天(或者永远轮不到你,只能挨饿,就是这么真实),这就是饥饿现象。
对于Java的ReentrantLock来说,通过其构造参数来指定该锁是否是公平锁,默认是非公平锁,当构造参数传值为true时,是公平锁,非公平锁的优点在于吞吐量比公平锁大。
源码示例 :
//非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
//true 公平锁 false 非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
对于synchronized而言,也是一个非公平锁,由于其并不像ReentrantLock是通过AQS来实现线程调度,所以并没有任何办法让其变为公平锁。
9.2 可重入锁
可重入锁又叫递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法后会自动获取锁
- Java的ReentrantLock是一个显式的可重入锁
- 对于synchronized而言,也是一个隐式的可重入锁,可重入锁的一个好处就是一定程度上避免死锁。
代码示例:
public class TestSync {
synchronized void getA() throws Exception{
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"进入了A方法");
getB();//调用getB()
}
synchronized void getB() throws Exception{
System.out.println(Thread.currentThread().getName()+"进入了B方法");
}
//测试
public static void main(String[] args) throws Exception {
TestSync t=new TestSync();
t.getA();
}
}
测试结果:
上面的代码就是一个可重入锁的一个特点,如果不是可重入锁的话,getB可能不会被当前线程执行,可能造成死锁。
9.3 独享锁和共享锁
独享锁:是指该锁一次只能被一个线程所持有。
共享锁:是指该锁可以被多个线程所持有。
- 对于实现Lock接口的ReentrantLock而言,其是独享锁。
- 但是对于Lock接口的另一个实现类ReentrantReadWriteLock,其读锁是共享锁(使用readLock()方法),其写锁是独占锁(使用writeLock()方法)。读锁的共享锁可以保证并发读是非常高效的,读写,写读和写写都是互斥的。
- 独享锁和共享锁都是通过AQS实现的,通过实现不同的方法,来实现独享或者共享。
9.4 互斥锁和读写锁
上面讲的独享锁和共享锁就是一种广义的说法,互斥锁和读写锁就是具体的实现。
互斥锁在Java的具体实现就是:ReentrantLock
读写锁在Java的具体实现就是:ReentrantReadWriteLock
注意:在Jdk 1.8说明,写锁可以降级为读锁,但是读锁不能升级为写锁
。
降级过程为:获得写锁>获得读锁>释放写锁>释放读锁
代码示例:
//写锁降级为读锁
public class ReentrantReadWriteLockDemo2 {
public static void main(String[] args) {
ReentrantReadWriteLock rwLock=new ReentrantReadWriteLock();
//获取写锁
ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
//获取读锁
ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
//演示写锁降级
writeLock.lock();//获得写锁
System.out.println("获得写锁");
readLock.lock();//获得读锁
System.out.println("获得读锁");
/**
* 不能读锁升级写锁
* readLock.lock();//获得读锁
* System.out.println("获得读锁");
* writeLock.lock();//获得写锁
* System.out.println("获得写锁");
*/
//释放锁
writeLock.unlock();
readLock.unlock();
}
}
测试结果:
9.5 悲观锁和乐观锁
- 悲观锁和乐观锁不是指具体的什么类型的锁,而是指
在看待并发同步
的角度。 - 悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改,因此对于同一个数据的并发操作,悲观锁采取加锁的形式,悲观的认为,不加锁的并发操作一定会出问题。
- 乐观锁则认为对于同一个数据的并发操作,是不会发生修改的,在更新数据的时候,会采用尝试更新,不断更新的方式更新数据,乐观的认为,不加锁的并发操作不会发生问题。
从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
悲观锁:在Java中的使用,就是利用各种锁。
乐观锁:在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
9.6 分段锁
- 分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
- 我们以ConcurrentHashMap来说一下分段锁的含义和设计思想,ConcurrentHashMap中的分段锁称为Segment,它类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表,同时又是一个ReentrantLock(Segment继承ReentrantLock)。
- 当需要put元素的时候,并不是对整个HashMap进行加锁,而是先通过HashCode来知道它要存储在那个分段上,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
- 但是,在获取size时,就是获取HashMap全局信息的时候,就需要获取所有的分段锁才能统计。
- 分段锁的设计目的就是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一段进行加锁操作。
9.7 偏向锁/轻量级锁/重量级锁
- 这三种锁是指锁的状态,并且是针对synchronized
- 在Java 5通过引入锁升级机制来实现高效synchronized,这三种锁的状态是通过对象监视器在对象头中的字段来表明的,
- 偏向锁:是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
偏向锁的取消:
偏向锁是默认开启的,而且开始时间一般是比应用程序启动慢几秒,如果不想有这个延迟,那么可以使用
-XX:BiasedLockingStartUpDelay=0;
如果不想要偏向锁,那么可以通过**-XX:-UseBiasedLocking = false**来设置;
- 轻量级锁:是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的方式尝试获取锁,不会阻塞,提高性能。
- 重量级锁:是指当锁是轻量级锁的时候,另一个线程虽然是自旋,但是不可能一直自旋一下,当自旋一定次数的时候,还没有获取到锁,就会阻塞,该锁膨胀为重量级锁,重量级锁会让其他申请的线程进入阻塞,性能降低。
- 注意:为了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了;偏向锁升级为轻量级锁也不能再降级为偏向锁。一句话就是锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态。
这几种锁的优缺点(偏向锁、轻量级锁、重量级锁)
9.7 自旋锁
- 在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式获取锁。
- 这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
- 自旋锁是采用让当前线程不停的在循环体内执行实现的,当循环的条件被其他线程改变时,才能进入临界区。
代码示例:
public class SpinLock {
/**
* AtomicReference是作用是对”对象”进行原子操作。
* 提供了一种读和写都是原子性的对象引用变量。
* 原子意味着多个线程试图改变同一个AtomicReference(例如比较和交换操作)
* 将不会使得AtomicReference处于不一致的状态。
* n compareAndSet(V expect,V update):
* 如果预期值(expect) ==当前值(update),则以原子方式将该值设置为给定的更新值。
* 如果成功,则返回 true。返回 false 指示实际值与预期值不相等。
*/
private AtomicReference<Thread> sign=new AtomicReference<>();
public void lock(){
//获取当前线程
Thread current = Thread.currentThread();
/**
* 预测值为null,当前值为current
* 如果预期值(expect) ==当前值(update),则以原子方式将该值设置为给定的更新值。
*/
while(!sign .compareAndSet(null, current)){
}
}
public void unlock (){
//获取当前线程
Thread current = Thread.currentThread();
sign .compareAndSet(current, null);
}
}
使用了CAS原子操作,lock函数将预测值为null,如果预测与当前值相等,就设置owner为当前线程,unlock函数将owner设置为null,并且预测值为当前线程。
当有第二个线程调用操作时由于owner值不为空,导致循环一直被执行,直到第一个线程调用unlock函数将owner设置为null,第二个线程才能进入临界区。
由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。
注:该例子为非公平锁,获得锁的先后顺序,不会按照进入lock的先后顺序进行。
9.8 死锁
什么是死锁?
死锁是指两个或者两个以上的进程在运行过程中,因为争夺资源而造成一种相互等待的现象,若无外力作用,它们都不能继续执行下去。举个例子,线程A持有锁A,尝试获取锁B,但是线程B持有锁B,尝试获取锁A。此时因为线程A和B都在请求对方资源造成阻塞。如果没有外力作用,可能永久等待。
如下图:
产生死锁的原因?
可归结为如下两点:
a. 竞争资源
- 系统中的资源可以分为两类
1.可剥夺资源:是指某进程在获得该类资源后,该资源可以在被其他进程或者系统剥夺。CPU和主存均属于可剥夺资源。
2.另一类是不可剥夺资源:当系统把这些资源分配给某个进程后,再不能强行收回,只能再进程用完自行释放,如打印机,锁资源等。 - 产生死锁中的竞争资源之一指的是竞争不可剥夺资源。举个例子。系统中只有一个打印机,假如进程P1已经占用了打印机,但是进程P2继续要求打印机打印将会发生阻塞。
- 产生死锁中竞争资源另一种是竞争临时资源(临时资源包括硬件中断、信号、消息、缓冲区内的消息等),通常消息通信顺序进行不当,则会产生死锁
b. 进程间推进顺序非法
- 若进程P1持有资源R1,进程P2持有资源R2,系统处于不安全状态,因为这两个进程再向前推进,便可能发生死锁。
举个例子。当进程P1持有资源R1,并请求资源R2时,因为R2已经被进程P2所占用而阻塞。当进程P2持有资源R2,并请求资源R1时,因为R1已经被进程P1所占用也发生阻塞。于是发生进程死锁。
产生死锁的四个必要条件?
- 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源只能由一个进程占用。
- 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不可剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时自己释放。
- 环路等待条件:在发生死锁时,必须存在一条进程—资源的环行链。
解决死锁的基本方法
发生死锁是一个非常严重的事情,所以我们在项目开发中应该避免死锁。要在事前预防死锁,事后解除死锁。
预防死锁:
前面我们已经了解产生死锁的4个必要条件,如果我们破坏其中的一个条件便可避免死锁。
- 破坏"
互斥
"条件:就是在系统里面取消互斥,若资源不被一个进程独占使用,那么死锁肯定不会发生的,但一般互斥
条件是无法破坏的,因此,在死锁预防里注意是破坏其他三个必要条件,而不去涉及破坏互斥
条件。 - 破坏
请求
和保持
条件:在系统中不允许进程在已获得某种资源的情况下,申请其他资源,就是想出一个办法,阻止进程在持有资源的同时申请其他资源。
方法一:所有进程在运行之前,必须一次性地申请在整个运行过程中所需的全部资源,这样,该进程在整个运行期间,便不会再提出资源请求,从而破坏了请求
条件。系统在分配资源时,只要有一种资源不能满足进程的要求,即使其他所需的各资源都空闲,也不分配给该进程,而让该进程等待。由于该进程在等待期间未占有任何资源,于是破坏了保持
条件。
该方法优点:简单,易行且安全
缺点:资源被严重浪费,严重恶化了资源的利用率,使进程经常发生饥饿现象。
方法二:要求每个进程在提出新的资源申请前,释放掉它所占有的资源,这样,一个进程在申请资源S时,必须先把它之前占有的资源R释放掉,才能提出对S的申请,即使它很快会用到资源R。 - 破坏不可剥夺条件:允许对资源实行抢夺
方法一:如果占有某些资源的一个进程进行进一步资源请求被拒绝,则该进程必须释放它最初占有的资源,如果有必要,可再次请求这些资源和另外的资源。
方法二:如果一个进程请求当前被另一个进程占有的一个资源,则操作系统可以抢占另一个进程,要求它释放资源,只有在任意两个进程的优先级都不相同的条件下,该方法才能预防死锁。 - 破坏循环等待`条件:将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出。这样做就能保证系统不出现死锁。
死锁的解除
一旦检测出死锁,就应立即釆取相应的措施,以解除死锁。死锁解除的主要两种方法:
- 抢占资源。从一个或多个进程中抢占足够数量的资源,分配给死锁进程,以解除死锁状态。
- 终止(或撤销)进程。终止(或撤销)系统中的一个或多个死锁进程,直至打破循环环路,使系统从死锁状态解脱出来。
总结:
写程序时应该尽量避免同时获得多个锁,如果一定有必要这么做,则有一个原则:如果所有线程在需要多个锁时都按相同的先后顺序(常见的是按Mutex变量的地址顺序)获得锁,则不会出现死锁。比如一个程序中用到锁1、锁2、锁3,它们所对应的Mutex变量的地址是锁1<锁2<锁3,那么所有线程在需要同时获得2个或3个锁时都应该按锁1、锁2、锁3的顺序获得。如果要为所有的锁确定一个先后顺序比较困难,则应pthread_mutex_trylock调用代替pthread_mutex_lock 调用,以免死锁。
死锁检测
1、Jstack命令
jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息。 Jstack工具可以用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。
2、JConsole工具
Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。它用于连接正在运行的本地或者远程的JVM,对运行在Java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。而且本身占用的服务器内存很小,甚至可以说几乎不消耗。
手动写一个死锁并使用工具检测
代码示例:
/**
* 测试死锁
*/
public class TestDeadLock {
public static void main(String[] args) {
Object o1=new Object();//资源o1
Object o2=new Object();//资源o2
new Thread(()->{
synchronized (o1){
try {
System.out.println(Thread.currentThread().getName()+"获取到资源o1");
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName()+"尝试获取资源o2");
synchronized (o2){
System.out.println(Thread.currentThread().getName()+"获取到资源o2");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(()->{
synchronized (o2){
try {
System.out.println(Thread.currentThread().getName()+"获取到资源o2");
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName()+"尝试获取资源o1");
synchronized (o1){
System.out.println(Thread.currentThread().getName()+"获取到资源o1");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
测试结果:
可以看到程序一直在运行,都在等待各自拥有的资源,这就是线程死锁。
如何验证发生死锁
- 使用Jstack查看:
第一步:在idea中进入到安装jdk的bin目录下。
第二步:利用jps命令查看进程号,在windos系统中使用。类似于linux中ps -ef查看当前进程。
第三步:使用jstack 进程号
来跟踪堆栈
- 使用JConsole工具
第一步:打开我的电脑,找到自己Jdk的安装目录bin。
第二步:双击进入,然后选择检测的进程。
第三步:根据自己的需要查看
9.9 数据库表锁和行锁
行锁:锁的作用范围是
行
级别。
表锁:锁的作用范围是整张表。
数据库能够确定那些行需要锁的情况下使用行锁,如果不知道会影响哪些行的时候就会使用表锁。
举个例子:
一个用户表user,有主键id和年龄age
当你使用update … where id=?这样的语句时,数据库明确知道会影响哪一行,它就会使用行锁;
当你使用update … where age=?这样的的语句时,因为事先不知道会影响哪些行就可能会使用表锁