关于进程、线程、多线程等概念的理解,请看我关于操作系统的个人博客:https://blog.calvinhaynes.top/2021/05/26/cao-zuo-xi-tong-ji-chu-gai-nian-cao-zuo-xi-tong-xi-lie-yi/
使用多线程的优点
1、提高应用程序的响应。(举例:假如上传一首专辑封面的时候,上传原图成功后,才会再生成专辑封面,这段处理也需要程序来完成,如果生成封面的时间成本比较长,单线程的执行效率就不如多线程了,用户的体验也会不好)
2、提高CPU资源的利用率。(依旧是上例,如果处理上传图片操作的服务器是个灭霸级的服务器,何不利用多线程把它榨干呢)
3、改善程序的结构。(单线程的结构需要标记每次处理各个节点的状态,利用多线程的话可以处理完一个销毁一个线程,程序结构更加清晰,比如,将用户的请求放在一个线程中,响应后销毁此线程)
关于回调函数
什么是回调函数?
我们绕点远路来回答这个问题。
编程分为两类:系统编程(system programming)和应用编程(application programming)。所谓系统编程,简单来说,就是编写库;而应用编程就是利用写好的各种库来编写具某种功用的程序,也就是应用。系统程序员会给自己写的库留下一些接口,即API(application programming interface,应用编程接口),以供应用程序员使用。所以在抽象层的图示里,库位于应用的底下。
当程序跑起来时,一般情况下,应用程序(application program)会时常通过API调用库里所预先备好的函数。但是有些库函数(library function)却要求应用先传给它一个函数,好在合适的时候调用,以完成目标任务。这个被传入的、后又被调用的函数就称为回调函数(callback function)。
打个比方,有一家旅馆提供叫醒服务,但是要求旅客自己决定叫醒的方法。可以是打客房电话,也可以是派服务员去敲门,睡得死怕耽误事的,还可以要求往自己头上浇盆水。这里,“叫醒”这个行为是旅馆提供的,相当于库函数,但是叫醒的方式是由旅客决定并告诉旅馆的,也就是回调函数。而旅客告诉旅馆怎么叫醒自己的动作,也就是把回调函数传入库函数的动作,称为登记回调函数(to register a callback function)。如下图所示(图片来源:维基百科):
可以看到,回调函数通常和应用处于同一抽象层(因为传入什么样的回调函数是在应用级别决定的)。而回调就成了一个高层调用底层,底层再回过头来调用高层的过程。(我认为)这应该是回调最早的应用之处,也是其得名如此的原因。
通过上述回调函数的描述,我们可以发现,回调函数和多线程的配合是紧密相关的,流程大致是:创建一个线程,将后台任务加入线程,同时将回调函数(可能是后台任务处理完后的一些操作)作为参数传给线程,此线程执行后台任务完成后执行回调函数。
JVM允许程序运行多个线程,通过java.lang.Thread类体现
Thread类的特性
- 每个线程都是通过某个特定的Thread对象的run()方法完成操作的,把run()方法的主体称为线程体
- 通过该Thread对象的start()方法启动此线程,并非直接调用run()方法
//1.定义子类继承Thread类
class MyThread extends Thread{
//2.子类中重写Thread类中的run()方法
@Override
public void run() {
//此线程要执行的操作
}
}
public class ThreadTest{
public static void main(String[] args) {
//3.创建Thread子类对象,即创建一个线程对象
MyThread thread = new MyThread();
//4.调用线程对象的start方法:启动线程,调用run()方法
thread.start();
}
}
注意事项(关于原因会出单独一篇短文):
- 不能用run方法启动线程
- start方法只能调用一次,不可以再用一次start方法创建新的线程(要想创建一个新的线程只能重新创建一个新的对象,再调用start方法)
//1.定义子类,实现Runnable接口
class MyThread implements Runnable{
//2.子类中重写Runnable接口中的run方法。
@Override
public void run() {
//此线程要执行的操作
}
}
public class ThreadTest{
public static void main(String[] args) {
//3.通过Thread类含参构造器创建线程对象。
MyThread thread = new MyThread();
//4.将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
Thread myThread = new Thread(thread);
//5.调用Thread类创建对象的start方法:开启线程,调用Runnable子类接口的run方法。
myThread.start();
}
}
注意事项(关于原因会出单独一篇短文):
- 不能用run方法启动线程
- start方法只能调用一次,不可以再用一次start方法创建新的线程(要想创建一个新的线程只能重新创建一个新的对象,再调用start方法)
//1、创建一个实现Callable的实现类
class MyThread implements Callable{
//2、重写实现call()方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
//此线程要执行的操作
}
}
public class ThreadTest3 {
public static void main(String[] args) {
//3、创建Callable接口实现的对象
MyThread numThread = new MyThread();
//4、将此Callable接口实现类的对象作为参数传递到FutureTask的构造器中
FutureTask task = new FutureTask(numThread);
//5、将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start方法
new Thread(task).start();
try {
//6、获取Callable中call方法的返回值
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值
Object retVal = task.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
public ThreadPoolExecutor(int corePoolSize,//线程池核心线程数目
int maximumPoolSize,//线程池线程最大数目
long keepAliveTime,//线程存活时间
TimeUnit unit,//keepAliveTime的时间单位
BlockingQueue<Runnable> workQueue,//阻塞任务队列
ThreadFactory threadFactory,//线程工厂
RejectedExecutionHandler handler)//线程饱和策略
corePoolSize(线程池核心线程数目):
maximumPoolSize(线程池线程最大数目):
keepAliveTime(线程存活时间):
workQueue(阻塞任务队列):
存放任务的阻塞队列。如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到该队列当中,注意只要超过了 corePoolSize 就会把任务添加到该缓存队列,添加可能成功也可能不成功,如果成功的话就会等待空闲线程去执行该任务,若添加失败(一般是队列已满),就会根据当前线程池的状态决定如何处理该任务(若线程数 < maximumPoolSize 则新建线程;若线程数 >= maximumPoolSize,则会根据拒绝策略做具体处理)。
常用阻塞队列
ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
一般使用后两个,阻塞队列的选取对线程池的影响很大。
ThreadFactory(线程工厂):用来为线程池创建线程,当我们不指定线程工厂时,线程池内部会调用Executors.defaultThreadFactory()创建默认的线程工厂,其后续创建的线程优先级都是
Thread.NORM_PRIORITY`。如果我们指定线程工厂,我们可以对产生的线程进行一定的操作。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。
handler(线程饱和策略):拒绝执行策略。当线程池的缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。 当队列中有任务时才notify对应线程从队列中取出消息进行执行。 使得在线程不至于一直占用cpu资源。
CPU密集型任务
尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
IO密集型任务
可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。
混合型任务
根据实际情况考虑。
//1.newFixedThreadPool:创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
//2.newScheduledThreadPool:适用于执行延时或者周期性任务。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
/**
* Creates a new {@code ScheduledThreadPoolExecutor} with the
* given core pool size.
*
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @throws IllegalArgumentException if {@code corePoolSize < 0}
*/
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,//此处super指ThreadPoolExecutor
new DelayedWorkQueue());
}
//3.newSingleThreadExecutor:创建一个单线程的线程池,适用于需要保证顺序执行各个任务。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
//4.newCachedThreadPool:用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
//1.创建一个固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
//2.创建一个单线程的线程池
ExecutorService threadPool = Executors.newSingleThreadExecutor();
//3.创建一个可以无限扩大的线程池
ExecutorService threadPool = Executors.newCachedThreadPool();
//4.创建一个适用于执行延时或者周期性任务的线程池
ExecutorService threadPool = Executors.newScheduledThreadPool(5);
注意:以上四种方法都不建议使用,可以看下下面这张阿里巴巴Java开发手册的经典截图:
从图片中可以看出不允许使用Executors的根本原因其实只有一个,就是规避资源耗尽,防止OOM,那为什么会导致OOM呢?
首先引入一个极致实例来康康:
有的小伙伴可能发现你的选项中没有VM options(这也是我傻了,找了好半天的坑),哈哈这个贼蠢,看这里:
- Modify options,调整展示哪些选项
- 点开之后就是以下这个界面,勾选中Add VM options就可以了,之后再遇到其他选项找不到就看看这里,由此可见英语学习和仔细多么重要,所以看到英语不要惧怕,都仔细看一看,不懂的单词查一查,有了这种英语的思维才能更好编程
package com.calvinhaynes.java;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created with IntelliJ IDEA.
* Description:测试Executors会导致的OOM的问题
* User: CalvinHaynes
* Date: 2021-05-20
* Time: 16:38
*/
public class ExecutorsTest {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
while(true){
executor.submit(new SubThread());
}
}
}
class SubThread implements Runnable {
@Override
public void run() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
//do nothing
}
}
}
使用Executors类中的newCachedThreadPool()方法创建无限扩大线程池,然后无限提交任务,过一会儿就会报OOM异常如下图:
有关于这种异常产生的原因可以看后面的英文:Java heap space,表示Java堆空间不够,当应用程序申请更多的内存,而Java堆内存已经无法满足应用程序对内存的需要,将抛出这种异常。
也就是说由于线程的不断创建,最终导致内存满了,JVM报出OOM异常。
其他几种方法也会导致不同的OOM异常产生,本文就不细致讲解了,毕竟有点复杂,这样会导致这篇文章过长。
package com.calvinhaynes.java;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* Created with IntelliJ IDEA.
* Description:测试ThreadPoolExecutor留给用户自行处理的三个方法
*
* protected void beforeExecute(Thread t, Runnable r) // 任务执行前被调用
* protected void afterExecute(Runnable r, Throwable t) // 任务执行后被调用
* protected void terminated() // 线程池结束后被调用
*
* User: CalvinHaynes
* Date: 2021-05-16
* Time: 15:11
*/
public class ThreadPoolTest5 {
public static void main(String[] args) {
ExecutorService executor = new ThreadPoolExecutor(1, 1, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1)) {
@Override protected void beforeExecute(Thread t, Runnable r) {
System.out.println("beforeExecute is called");
}
@Override protected void afterExecute(Runnable r, Throwable t) {
System.out.println("afterExecute is called");
}
@Override protected void terminated() {
System.out.println("terminated is called");
}
};
executor.submit(() -> System.out.println("this is a task"));
executor.shutdown();
}
}
在Thread类中存在一个枚举类,其中说明了JVM中线程的五种状态。
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
*
* - {@link Object#wait() Object.wait} with no timeout
* - {@link #join() Thread.join} with no timeout
* - {@link LockSupport#park() LockSupport.park}
*
*
* A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called Object.wait()
* on an object is waiting for another thread to call
* Object.notify() or Object.notifyAll() on
* that object. A thread that has called Thread.join()
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
*
* - {@link #sleep Thread.sleep}
* - {@link Object#wait(long) Object.wait} with timeout
* - {@link #join(long) Thread.join} with timeout
* - {@link LockSupport#parkNanos LockSupport.parkNanos}
* - {@link LockSupport#parkUntil LockSupport.parkUntil}
*
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
WAITING,TIMED_WAITING:在JVM中线程的两种等待状态。
流程分析:
- new一个新线程对象后,该线程对象就处于新建状态。
- 调用该对象的start()方法,该线程就进入了就绪状态。
- 就绪状态获得了CPU资源,进入了运行状态
- 在运行状态中如果执行了sleep()睡眠方法、suspend()挂起方法、wait()等待方法、join()等待方法、等待同步监视器(这些后文会提到),则会进入阻塞状态
- 同样的,调用与上述方式相对立的方式(图片中)即可离开阻塞状态,继续回到就绪状态等待CPU调度
- 当运行状态执行完线程中的run()方法,或调用stop()方法、出现错误或异常未处理,则线程死亡
建议每一个方法可以自己依次敲一下试一下
public void start()
使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
public void run()
如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。
public final void setName(String name)
改变线程名称,使之与参数 name 相同。
public final void getName(String name)
获取线程名称。
public final void setPriority(int priority) 更改线程的优先级。
public final void setDaemon(boolean on)
将该线程标记为守护线程或用户线程。
public final void join(long millisec)
在线程a中调用线程b的join(), 此时线程a进入阻塞状态, 直到线程b完全执行完以后, 线程a才结束阻塞状态,等待该线程终止的时间最长为 millisec 毫秒。
public final void stop()
当执行此方法时,强制结束当前线程.
public void interrupt()
中断线程。
public final boolean isAlive()
测试线程是否处于活动状态。
public static void yield() 释放当前CPU的执行权,暂停当前正在执行的线程对象,并执行其他线程。
public static void sleep(long millisec)
在指定的毫秒数内让当前正在执行的线程休眠(阻塞状态),此操作受到系统计时器和调度程序精度和准确性的影响。
public static native Thread currentThread()
返回当前代码执行的线程。
这是一个native方法,native关键字是用于Java和其他语言(C++)协作时用的,也就是只native关键字修饰的函数不是用Java写的,所以实际上调用的是jvm.cpp中的JVM_CurrentThread函数,具体细节内容未来深究。
一个典型的线程安全问题,卖票问题由于多线程导致发生错票重票:
/** * Created with IntelliJ IDEA. * Description: * 例子:创建三个窗口卖票,总票数为100张.使用继承Thread类的方式 * * 目前存在线程安全问题,待解决 * User: CalvinHaynes * Date: 2021-04-19 * Time: 21:11 */class Window extends Thread{ private static int ticket = 100; @Override public void run() { while(true){ if(ticket > 0){ System.out.println(Thread.currentThread().getName() + ":" + "卖票咯,票号为:" + ticket); ticket--; }else{ break; } } }}public class WindowTest { public static void main(String[] args) { Window window1 = new Window(); Window window2 = new Window(); Window window3 = new Window(); window1.start(); window2.start(); window3.start(); }}
如果window1线程执行过程中进入阻塞状态,这时其他两个线程操作ticket就会导致错票的现象。
极端状态:同时有两个线程被阻塞,则会导致多卖两张票
如果window1线程在输出票号和ticket减一的操作之间进入阻塞状态,则会导致另一个进程在输出票号时输出一个相同的票号,即发生了重票现象。
解决线程安全问题有三种方式:同步代码块、同步方法、Lock锁
synchronized(同步监视器){需要被同步的代码}
说明:
操作共享数据的代码,即为需要被同步的代码。 -->不能包含代码多了,也不能包含代码少了。
共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
要求:多个线程必须要共用同一把锁。(即同步锁也是多个线程的共享对象)
锁的选择
补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。(前提是当前类是唯一类)
class Window3 implements Runnable{
private int ticket = 100;
// 任意对象都可以当锁
// Object obj = new Object();
// Dog dog = new Dog();
@Override
public void run() {
// Object obj = new Object();//这相当于三个对象(多个线程必须同一把锁)
while(true){
synchronized(this){//当前的唯一Windows3对象作为同步监视器
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + ":" + "卖票咯,票号为:" + ticket);
ticket--;
}else{
break;
}
}
}
}
}
class Dog{
}
public class WindowTest3 {
public static void main(String[] args) {
Window3 window3 = new Window3();
Thread th1 = new Thread(window3);
Thread th2 = new Thread(window3);
Thread th3 = new Thread(window3);
th1.setName("窗口一");
th2.setName("窗口二");
th3.setName("窗口三");
th1.start();
th2.start();
th3.start();
}
}
将要同步的代码放到一个方法中,再将方法声明为synchronized同步方法,在run()方法中调用此同步方法。
说明
/**
* Created with IntelliJ IDEA.
* Description:使用同步方法解决实现Runnable接口的窗口安全问题
*
* 关于同步方法的总结:
* 1. 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
* 2. 非静态的同步方法,同步监视器是:this
* 静态的同步方法,同步监视器是:当前类本身
* User: CalvinHaynes
* Date: 2021-04-22
* Time: 10:19
*/
class Window5 implements Runnable{
private int ticket = 100;
@Override
public void run() {
while(true){
show();
}
}
private synchronized void show(){//同步监视器:this(隐式定义)
// synchronized (this) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}
// }
}
}
public class WindowTest5 {
public static void main(String[] args) {
Window5 window5 = new Window5();
Thread t5_1 = new Thread(window5);
Thread t5_2 = new Thread(window5);
Thread t5_3 = new Thread(window5);
t5_1.setName("窗口一");
t5_2.setName("窗口二");
t5_3.setName("窗口三");
t5_1.start();
t5_2.start();
t5_3.start();
}
}
JDK5.0之后,可以通过实例化ReentrantLock对象,在所需要同步的语句前,调用ReentrantLock对象的lock()方法,实现同步锁,在同步语句结束时,调用unlock()方法结束同步锁
synchronized和lock的异同:
- Lcok是显式锁(需要手动开启和关闭锁),synchronized是隐式锁,出作用域自动释放。
- Lock只有代码块锁,synchronized有代码块锁和方法锁。
- 使用Lcok锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的拓展性(提供更多的子类)
class WindowLock implements Runnable{
private int ticket = 100;
//1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try {
//2.调用锁定方法
lock.lock();
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
ticket--;
}else{
break;
}
} finally {
//3.调用解锁方法
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
WindowLock windowLock = new WindowLock();
Thread l1 = new Thread(windowLock);
Thread l2 = new Thread(windowLock);
Thread l3 = new Thread(windowLock);
l1.setName("窗口一");
l2.setName("窗口二");
l3.setName("窗口三");
l1.start();
l2.start();
l3.start();
}
}
死锁即是指多个线程因资源竞争而造成的一种僵局(deadlock),多个线程互相等待,彼此都处于阻塞状态,都无法继续前进。
某计算机系统中有一台打印机和一台输入设备,进程Process1正在占用打印机,但同时又提出使用输入设备的请求,但是此时输入设备正在被Process2进程占用,而Process2在未释放输入设备之前,又提出使用打印机的请求,这样就会导致Process1和Process2互相无休止的等待,此时两个进程进入了死锁状态。
通常系统中拥有的不可剥夺资源,其数量不足以满足多个进程运行的需要,使得进程在运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。只有对不可剥夺资源的竞争才可能产生死锁,对可剥夺资源的竞争是不会引起死锁的。
进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程 P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时,两者都会因为所需资源被占用而阻塞。
Java中死锁最简单的情况是,一个线程T1持有锁L1并且申请获得锁L2,而另一个线程T2持有锁L2并且申请获得锁L1,因为默认的锁申请操作都是阻塞的,所以线程T1和T2永远被阻塞了。导致了死锁。这是最容易理解也是最简单的死锁的形式。但是实际环境中的死锁往往比这个复杂的多。可能会有多个线程形成了一个死锁的环路,比如:线程T1持有锁L1并且申请获得锁L2,而线程T2持有锁L2并且申请获得锁L3,而线程T3持有锁L3并且申请获得锁L1,这样导致了一个锁依赖的环路:T1依赖T2的锁L2,T2依赖T3的锁L3,而T3依赖T1的锁L1。从而导致了死锁。
综上,产生死锁的根本原因应该是:
1.线程1在获得锁1时又去申请了锁2,在未释放自己的锁的情况下去申请另外一把锁。
2.默认锁申请的操作时阻塞的
public class ThreadTest {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
//匿名对象1
new Thread(){
@Override
public void run() {
synchronized (s1){
s1.append("a");
s2.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2){
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
//匿名对象2
new Thread(){
@Override
public void run() {
synchronized (s2){
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1){
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
}
}
由以上实例可以看出在匿名线程对象一中占有着锁s1,同时申请锁s2,在匿名线程对象二中占有着锁s2,同时申请锁s1,形成死锁。
注意加锁顺序:线程一定要按照一定的顺序加锁,注意好各个锁的连带关系做一个排序,排序靠后的锁一定要等到排序靠前的锁释放之后才能加锁(避免嵌套锁)
加锁时限:线程尝试获取锁的时候加上时间限制,超过设置的时限则放弃请求,并且释放自己占有的锁
死锁检测
wait()
notify()/notifyAll()
这两种方法都定义在类:Object中
//线程通信例子:使用两个线程交替打印1~100
class Number implements Runnable{
private int number;
@Override
public void run() {
while(true){
synchronized (this) {
this.notify();
if(number <= 100){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
}else{
break;
}
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread thread1 = new Thread(number);
Thread thread2 = new Thread(number);
thread1.setName("线程一");
thread2.setName("线程二");
thread1.start();
thread2.start();
}
}
Java多线程是一门比较深的学问,如果之前学过操作系统基础课程的话,会学的更透彻一点,因为博主也刚刚接触操作系统,所以可能有些地方说的也欠妥,希望大佬们可以指正,互相交流学习。
重在理解这个过程,代码层面重点学习线程池,目前也是互联网企业常用的方式。
Java编程思想(第四版)
https://cloud.tencent.com/developer/article/1638175
https://www.bilibili.com/video/BV1Kb411W75N?t=8&p=528
https://www.zhihu.com/question/19801131/answer/27459821
我是一个热爱IT技术和音乐的Dream Catcher,正在努力培养计算机的深度和广度认知,也会和大家伙儿分享我的音乐,大家伙儿多多关照 (๑❛ᴗ❛๑)
联系我的话,可以邮箱或者私信哦!!谢谢大家咯(*≧▽≦)
My Social Link:
我的个人博客站:https://blog.calvinhaynes.top/
我的知乎主页:https://www.zhihu.com/people/eternally-92-61
我的B站主页:https://space.bilibili.com/434604897
我的CSDN主页:https://blog.csdn.net/qq_45772333
我的邮箱:[email protected]
我的Github主页:https://github.com/CalvinHaynes
我的码云主页:https://gitee.com/CalvinHaynes
喜欢我的文章的话,不妨留下你的大拇指,点个赞再走,您的支持是我创作的最大动力,也欢迎指正博客中存在的问题,谢谢呐(~ ̄▽ ̄)~