天天看点

万字长文,线程攻略,夯实基础很重要!

点击上方 "后端架构师"关注, 星标或置顶一起成长

后台回复“大礼包”有惊喜礼包!

万字长文,线程攻略,夯实基础很重要!

每日英文

be alike flower. spread beauty and happiness wherever you stay; irrespective of your surroundings. 

像花儿一样,无论身在何处,不管周遭环境如何,都依然潇洒的绽放自己的美丽,活出自己的精彩。

每日掏心话

其实我们每个人都拥有时光机器,有的能把我们带回从前,叫做回忆;有的能带我们迈向未来,被称为梦想。

来自:谭嘉俊 | 责编:乐乐

链接:juejin.im/user/2400989124522446

后端架构师(id:study_tech)第 1055 次推文 图 / 图虫

往日回顾:11 月全国程序员平均工资出炉

   正文   

/   前言   /

本文章讲解的内容是java线程,建议对着示例项目阅读文章,本文章分析的相关的源码基于java development kit(jdk) 13。

threaddemo地址:

github.com/tanjiajunbeyond/threaddemo

/   概述   /

在说线程的概念之前,先说下进程的概念,进程是代码在数据集合上的一次运行活动,它是系统进行资源分配和调度的基本单位。一个进程至少有一个线程,线程是进程中的实体,线程本身是不会独立存在的,进程中的多个线程可以共享进程的资源(例如:内存地址、文件i/o等),也可以独立调度。

有以下三种方式实现线程:

使用内核线程实现

使用用户线程实现

使用用户线程和轻量级线程混合实现

java语言统一处理了不同硬件和操作系统平台的线程操作,一个线程是一个已经执行start()方法而且还没结束的java.lang.thread类的实例,其中thread类的所有关键方法都是本地方法(native method)来的,这意味着这些方法没有使用或者无法使用平台相关的手段来实现。

/   线程状态切换   /

java语言定义了六种线程状态,要注意的是,在任意一个时间点,一个线程有且只有五种线程状态的其中一种,这五种线程状态如下所示:

新建(new):线程创建后尚未启动的状态。

运行(runable):线程正在等待着cpu为它分配执行时间,进入就绪(ready)状态,等到cpu分配执行时间后,线程才真正执行,进入正在运行(running)状态。

无限期等待(waiting):这种状态下的线程不会被cpu分配执行时间,它们需要其他线程显示地唤醒。以下方法会让线程进入这种状态:

没有设置参数timeout的object.wait()方法

没有设置参数timeout的thread.join()方法

locksupport.park方法

限期等待(timed waiting):这种状态下的线程不会被cpu分配执行时间,但是它无需其他线程显示地唤醒,会在一定时间内由系统自动唤醒。以下方法会让线程进入这种状态:

thread.sleep()方法

有设置参数timeout的object.wait()方法

有设置参数timeout的thread.join()方法

locksupport.parknanos()方法

locksupport.parkuntil()方法

阻塞(block):线程被阻塞的状态,在等待着一个排他锁。

结束(terminated):线程已终止,并且已经结束执行的状态。

/   线程创建和运行   /

java语言提供了三种创建线程的方式,如下所示:

代码如下所示:

这种方式的优点是在run方法内可以使用this获取当前线程,无须使用thread.currentthread方法;缺点是因为java的类只能继承一个类,所以继承thread类之后,就不能继承其他类了,而且因为任务和代码没有分离,如果多个线程执行相同的任务时,需要多份任务代码。

要注意的是,调用了start方法后,线程正在等待着cpu为它分配执行时间,进入就绪(ready)状态,等到cpu分配执行时间后,线程才真正执行,进入正在运行(running)状态。

这种方式的优点是因为java的类可以实现多个接口,所以这个类就可以继承自己需要的类了,而且任务和代码分离,如果多个线程执行相同的任务时,可以公用同一个任务的代码,如果需要对它们区分,可以添加参数进行区分。

前面两种方式都没有返回值,futuretask可以有返回值。

/   wait和notify   /

wait()方法、wait(long timeoutmillis)方法、wait(long timeoutmillis, int nanos)方法、notify()方法和notifyall()方法都是object类的方法。

当一个线程调用共享变量的wait系列方法时,这个线程进入等待状态,直到使用下面两种方式才会被唤醒:

其他线程调用该共享变量的notify系列方法(notify()方法或者notifyall()方法)。

其他线程调用该共享变量所在的线程的interrupt()方法后,该线程抛出interruptedexception异常返回。

要注意的是,需要获取到该共享变量的监视器锁才能调用wait方法,否则会抛出illegalmonitorstateexception异常,可以使用以下两种方式获得对象的监视器锁:

调用被关键字synchronized修饰的方法,代码如下所示:

执行同步代码块,代码如下所示:

源码如下所示:

这个方法实际上调用了wait(long timeoutmillis)方法,参数timeoutmillis的值是0l。它的行为和调用wait(0l, 0)方法是一致的。

在公众号后端架构师后台回复“java”,获取java面试题和答案。

参数timeoutmillis是等待的最大时间,也就是超时时间,单位是毫秒。它的行为和调用wait(timeoutmillis, 0)方法是一致的。

要注意的是,如果传入了负数的timeoutmillis,就会抛出illegalargumentexception异常。

这个方法实际上调用了wait(long timeoutmillis)方法;参数timeoutmillis是等待的最大时间,也就是超时时间,单位是毫秒;参数nanos是额外的时间,单位是纳秒,范围是0~999999(包括999999)。

只有在参数nanos大于0的时候,参数timeoutmillis才会自增。

在一个线程上调用共享变量的notify方法后,会唤醒这个共享变量上调用wait系列方法后进入等待状态的线程。要注意的是,一个共享变量可能有多个线程在等待,具体唤醒哪个等待的线程是随机的。

被唤醒的线程不能立即从wait系列方法返回后继续执行,它需要获取到该共享变量的监视器锁才能返回,也就是说,唤醒它的线程释放了该共享变量的监视器锁,被唤醒的线程不一定能获取到该共享变量的监视器锁,因为该线程还需要和其他线程去竞争这个监视器锁,只有竞争到这个监视器锁后才能继续执行。

只有当前线程获取到该共享变量的监视器锁后,才能调用该共享变量的notify系列方法,否则会抛出illegalmonitorstateexception异常。

notifyall()方法可以唤醒所有在该共享变量上因为调用wait系列方法而进入等待状态的线程。

/   sleep()方法--让线程睡眠   /

sleep()方法是thread类的一个静态方法。当一个正在执行的线程调用了这个方法后,调用线程会暂时让出指定睡眠时间的执行权,不参与cpu的调度,但是不会让出该线程所拥有的监视器锁。指定的睡眠时间到了后,sleep()方法会正常返回,线程处于就绪状态,然后参与cpu调度,获取到cpu资源后继续运行。

要注意的是,如果在睡眠期间其他线程调用了该线程的interrupt()方法中断了该线程,就会在调用sleep方法的地方抛出interruptedexception异常而返回。

下面来看一个生产者消费者问题(producer-consumer problem)的例子:

repository类是一个存储库,存放产品,代码如下所示:

以下是测试代码,我先把生产者所在的线程睡眠(sleep)一秒,把消费者所在的线程睡眠三秒,这样就可以制造出生产速度大于消费速度的场景,代码如下所示:

运行上面的代码,大约十秒后手动结束进程,结果如下所示:

然后我把生产者所在的线程睡眠三秒,把消费者所在的线程睡眠一秒,这样就可以制造出生产速度小于消费速度的场景,代码如下所示:

上面的结果都符合预期,我解释一下,当发现队列满了后,就会调用变量queue的wait()方法,该生产者线程就会被进入等待状态,并且释放queue对象的监视器锁,让其他生产者线程和消费者线程去竞争这个监视器锁,打破了死锁产生的四个条件中的请求并持有条件,避免发生死锁,同样的,当发现队列空了后,也会调用变量queue的wait()方法,该消费者线程会进入等待状态,并且释放queue对象的监视器锁,让其他消费者线程和生产者线程去竞争这个监视器锁,打破了死锁的四个条件中的请求并持有条件,避免发生死锁。

在公众号后端架构师后台回复“offer”,获取算法面试题和答案。

上文提到的死锁产生的四个条件会在后面详细讲解。

/   join系列方法--等待线程执行终止   /

join系列方法是thread类的一个普通方法。它可以处理一些需要等待某几个任务完成后才能继续往下执行的场景。

这个方法实际上调用了join(final long millis)方法,参数millis的值是0。

参数millis是等待时间,单位是毫秒。

要注意的是,如果传入了负数的millis,就会抛出illegalargumentexception异常。

这个方法实际上调用了join(final long millis)方法;参数millis是等待时间,单位是毫秒;参数nanos是额外的时间,单位是纳秒,范围是0~999999(包括999999)。

只有在参数nanos大于0的时候,参数millis才会自增。

我在写深入了解volatile关键字这篇文章的时候,其中一个例子使用到了这个方法,代码如下所示:

在这个示例代码中调用join()方法目的是为了让这十个子线程运行结束后,主线程才结束,保证这十个子线程都能全部运行结束。

/   yield()--让出cpu执行权   /

yield()方法是thread类的一个静态方法。当一个线程调用这个方法后,当前线程告诉线程调度器让出cpu执行权,但是线程调度器可以无条件忽略这个请求,如果成功让出后,线程处于就绪状态,它会从线程就绪队列中获取一个线程优先级最高的线程,当然也有可能调度到刚刚让出cpu执行权的那个线程来获取cpu执行权。源码如下所示:

它和sleep()方法的区别是:当线程调用sleep()方法时,它会被阻塞指定的时间,在这个期间线程调度器不会去调度其他线程,而当线程调用yield()方法时,线程只是让出自己剩余的cpu时间片,线程还是处于就绪状态,并没有被阻塞,线程调度器在下一次调度时可能还会调度到这个线程执行。

/   线程中断   /

在java中,线程中断是一种线程间的协作模式。

要注意的是,通过设置线程的中断标志并不能立刻终止线程的执行,而是通过被中断的线程的中断标志自行处理。

interrupt()方法可以中断线程,如果是在其他线程调用该线程的interrupt()方法,会通过checkaccess()方法检查权限,这有可能抛出securityexception异常。假设有两个线程,分别是线程a和线程b,当线程a正在运行时,线程b可以调用线程a的interrupt()方法来设置线程a的中断标志为true并且立即返回,前面也提到过,设置标志仅仅是设置标志而已,线程a实际上还在运行,还没被中断;如果线程a因为调用了wait系列方法、join()方法或者sleep()方法而被阻塞,这时候线程b调用线程a的interrupt()方法,线程a会在调用这些方法的地方抛出interruptedexception异常。源码如下所示:

isinterrupted()方法可以用来检测当前线程是否被中断,如果是就返回true,否则返回false。源码如下所示:

interrupted()方法是thread类的一个静态方法。它可以用来检测当前线程是否被中断,如果是就返回true,否则返回false。它和上面提到的isinterrupted()方法的不同的是:如果发现当前线程被中断,就会清除中断标志,并且这个方法是静态方法,可以直接调用thread.interrupted()使用。源码如下所示:

isinterrupted()方法和interrupted()方法都是调用了isinterrupted(boolean clearinterrupted)方法,源码如下所示:

参数clearinterrupted是用来判断是否需要重置中断标志。

从源码可得知,interrupted()方法是通过获取当前线程的中断标志,而不是获取调用interrupted()方法的实例对象的中断标志。

/   线程上下文切换   /

在多线程编程中,线程的个数一般大于cpu的个数,但是每一个cpu只能被一个线程使用,为了让用户感觉多个线程在同时执行,cpu的资源分配策略采用的是时间片轮换策略,时间片轮换策略是指给每个线程分配一个时间片,线程会在时间片内占用cpu执行任务。

线程上下文切换是指当前线程使用完时间片后,处于就绪状态,并且让出cpu给其他线程占用,同时保存当前线程的执行现场,用于再次执行时恢复执行现场。

/   线程死锁   /

线程死锁是指两个或者两个以上的线程在执行的过程中,因为互相竞争资源而导致互相等待的现象,如果没有外力的情况下,它们会一直互相等待,导致无法继续执行下去。

死锁的产生必须具备以下四个条件:

互斥条件:指资源只能由一个线程占用,如果其他线程要请求使用该资源,就只能等待,直到占用资源的线程释放该资源。

请求并持有条件:指一个线程已经占有至少一个资源,但是还想请求占用新的资源,而新的资源被其他线程占用,所以当前线程会被阻塞,但是阻塞的同时不释放自己获取的资源。

不可剥夺条件:指线程获取到的资源在自己使用完毕之前不能被其他线程占用,只有在自己使用完毕后才会释放该资源。

环路等待条件:指发生在死锁时,必然存在一个线程——资源的环形链,举个例子:有一个线程集合{thead0, thread1, thread2, ……, threadn),其中thread0等待thread1占用的资源,thread1等待thread2占用的资源,thread2等待thread3占用的资源,……,threadn等待thread0占用的资源。

那如何避免死锁呢?只要打破其中一个条件就可以避免死锁,不过基于操作系统的特性,只有请求并持有条件和环路等待条件是可以破坏的。

/   用户线程和守护线程   /

java中的线程分为两类:用户线程(user thread)和守护线程(daemon thread)。在java虚拟机启动的时候会调用main方法,main方法所在的线程就是一个用户线程,同时java虚拟机还会启动很多守护线程,例如:垃圾回收线程。

只需要调用thread类的setdaemon(boolean on)方法,并且参数on设为true,就可以使该线程成为守护线程。

用户线程和守护线程的区别是当最后一个用户线程结束后,java虚拟机进程才会正常结束,而守护线程是否结束不影响java虚拟机进程的结束。

总结一下:如果我们希望在主线程结束后,子线程继续工作,等到子线程结束后才让java虚拟机进程结束,我们可以把线程设为用户线程;如果我们希望在主线程结束后,java虚拟机进程也立即结束,我们可以把线程设为守护线程。

github地址:

github.com/tanjiajunbeyond

ps:欢迎在留言区留    下你的观点,一起讨论提高。如果今天的文章让你有新的启发,欢迎转发分享给更多人。

万字长文,线程攻略,夯实基础很重要!

欢迎加入后端架构师交流群,在后台回复“007”即可。

猜你还想看

阿里、腾讯、百度、华为、京东最新面试题汇集

自从上线了 prometheus 监控告警,真香!

刚刚,python 之父放弃退休,64岁的他宣布加入微软!

一步步实现 redis 搜索引擎

嘿,你在看吗?

万字长文,线程攻略,夯实基础很重要!