并发编程——3.细说线程

2023-12-26 05:56:54

这篇文章我们来详细的说一下并发编程中的线程及其相关的内容

目录

1.线程的创建

1.1通过继承Thread

1.2通过实现Runnable接口的方式

1.3使用FutureTask方式(实现Callable接口的方式)

1.4三种方式的区别

2.线程的原理

3.线程的常用方法

3.1start与run

3.2sleep方法

3.3线程的命名方法

3.4线程的让步与优先级

3.5线程的中断

3.6线程的合并、存活与守护线程

3.7线程的状态及其转换

4.小结


1.线程的创建

下面我们来看一下线程创建的几种方式

1.1通过继承Thread

第一种就是通过继承Thread类的方法来创建线程的。java中的线程主要就是使用到Thread类。

下面来看一下代码:

这个很好理解的。我们的普通类继承了Thread类,然后重写里面的run方法,就是该线程主要执行的内容。然后在测试类中创建该类对象,调用方法。很容易理解的。

这种创建线程的方式还有另外一种写法:匿名内部类!

上面这种方法创建的线程不是一次性的,都是后面可能还会用到的。如果遇到一次性的线程,我们还可以直接使用匿名内部类的方式来创建线程(也是相当于继承Thread类的方式)。

代码如下:

这里多说几句:

第一,上图中的第7、11、23行的strat方法表明线程进入就绪态,注意此时线程还没有执行,具体什么时候执行,要等CPU选中。

第二,这些线程都是异步的。按照一般的顺序,我们的代码是从上往下依次执行的,但是这里不一样,具体哪一个先执行要看CPU选中哪一个。下面看几张结果图:

1.2通过实现Runnable接口的方式

我们还可以通过实现Runnable接口的方式来创建线程。

下面来看一下代码:

这个也挺好理解的。我们实现了runnable接口里面的run方法,然后在测试类中,我们创建一下线程,就是Thread类,然后将这个实现了接口的类的实例对象作为参数传入这个线程中,然后测试类就可以执行这个线程中的内容了。

1.3使用FutureTask方式(实现Callable接口的方式)

一般情况下,使用Runnable接口、Thread实现的线程我们都是无法返回结果的。但是如果对一些场合需要线程返回的结果。就要使用 Callable、Future 这几个类。Callable只能在ExecutorService的线程池中跑,但有返回结果,也可以通过返回的Future对象查询执行状态。

Future本身也是一种设计模式,它是用来取得异步任务的结果。

下面看看其源码:

它只有一个call方法,并且有一个返回V,是泛型。可以认为这里返回V就是线程返回的结果。

下面通过具体的例子来看一下:

看一下最后的输出结果:

1.4三种方式的区别

Java中,类仅支持单继承,如果一个类继承了Thread类,就无法再继承其它类,因此,如果一个类既要继承其它的类,又必须创建为一个线程,就可以使用实现Runable接口的方式。

使用实现Callable接口的方式创建的线程,可以获取到线程执行的返回值、是否执行完成等信息。

所以具体的使用哪一种方式我们要根据具体的情况来决定。

2.线程的原理

下面,我们就深入到源码的层次,来看一下线程的原理。

上面,我们讲了创建线程的几种方式,其实他们的原理都是类似的,性能上没有太大的差别,但是我们推荐使用继承Runnable接口的方式,因为继承会有一些限制。

下面,我们看一下Thread的源码(Ctrl+点击Thread类或类的方法)

我们点击start,会看到他里面有调用start0方法,我们再点击start0方法看一下:

我们会发现start0方法是一个native方法,就是本地方法。在java中,用native修饰了的方法就表示他会执行底层的C/C++的代码。这里我们只需要知道,当我们的线程执行start方法会,java会执行底层的C/C++的代码就行了。

但是Java线程创建和调用的具体流程是怎么样的?我们看下面这张图:

我们的线程调用start方法时,Java会调用底层的用C/C++写的一些方法,而这些底层方法又会回调我们Thread类的run方法。而这个run方法,就是我们在线程中重写的方法。这就是我们第一种创建线程继承Thread类,重写run方法的原理。并且,在执行的时候,他会有一个判断,如果target不为空,则执行target中的run,而这个target就是我们的Runnable接口:

这就是我们第二种实现Runnable接口的原理。

3.线程的常用方法

下面,我们来讲一下线程中常用的方法

3.1start与run

方法功能说明
public void start ( )启动一个新线程,Java虚拟机调用此线程的run方法

start方法只是让线程进入就绪态,里面的代码并不是立刻执行。

每个线程对象的start方法只能调用一次,如果调用多次就会报错

public void run ( )线程启动后会自动调用该方法,里面方法的就是该线程完成的任务。(也可以手动调用)如果在构造Thread对象时传递了Runnable参数,则线程启动后会调用我们传入的Runnable方法。

下面来看一下二者的区别

类型:run方法是同步方法(就是按照代码顺序,从上到下依次执行),而start方法是异步方法。

作用:run方法的作用是存放任务代码,而start方法的作用是启动线程

线程数量方法:执行run方法不会产生新的线程,而执行start方法会创建新的线程

调用次数:run方法可以被执行无数次,而start方法只能被执行一次,原因就在于线程不能被重复启动。

3.2sleep方法

下面看一下线程的sleep方法

方法功能说明
public static void sleep(long time)

让当前线程休眠多少毫秒再继续执行

Thread.sleep(0):让操作系统立刻进行一次CPU竞争

就是让线程睡眠的,在哪个线程中加,就让哪个线程睡眠

下面看一下代码:

我没有加sleep方法时,输出结果有两种,但是当我给主线程加上sleep方法,输出结果就只有一种了,如下如所示:

注意:

其它线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出lnterruptedExceptio

建议用TimeUnit的sleep 代替Thread 的 sleep来获得更好的可读性。其底层还是sleep方法。

在循环访问锁的过程中,可以加入sleep让线程阻塞时间,防止大量占用cpu资源。

3.3线程的命名方法

方法功能说明
public void setName(String name)给当前线程起名字
public void getName()

获取当前线程的名称

线程存在默认名称:子线程是Thread-索引

主线程是main

public static Thread currentThread()获取当前线程的对象,代码在哪个线程中执行就获取哪个线程的对象

?下面来看一下代码:

看一下结果:

这就是几个很简单的方法,不多说。

3.4线程的让步与优先级

下面我们来看一下线程的让步与优先级

public static native void yield()提示线程调度器尽力让出当前线程对CPU的使用主要是为了测试和调试
public final int getPriority()返回此线程的优先级
public final void setPriority(int priority)更改此线程的优先级,常用1,5,10Java中规定线程优先级是1~10的整数,较大的优先级,能提高该线程被CPU调度的机率

注意:

Thread.yield()方法作用是:暂停当前正在执行的线程对象(即放弃当前拥有的cup资源),并执
行其他线程。
yield()做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield(),达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

yield()方法并不能保证线程一定会让出CPU资源,它只是一个提示,告诉调度器当前线程愿意让出CPU资源。具体是否让出CPU资源,还是由调度器决定。

下面来看一下实例:

看一下结果:

3.5线程的中断

下面来看一下线程的中断

public void interrupt()中断这个线程,异常处理机制
public static boolean interrupted()判断当前线程是否被打断,清除打断标记
public boolean interrupted()判断当前线程是否被打断,不清除打断标记

注意:如果一个方法正在睡眠,你将其打断,则会报打断异常。上面所说的清除打断标记就是撤销打断。即如果一个线程被打断了,然后你使用那个方法,就可以撤销该线程的打断。interrupt,线程的中断,这只是一种协同机制,只是修改标识位而已,不会立刻停止线程。

3.6线程的合并、存活与守护线程

下面看一下线程合并与存活的方法

public final void join()等待这个线程结束
public final void join(long millis)等待这个线程死亡millis毫秒,0意味着永远等待
public final native boolean isAlive()线程是否存活(还没有运行完毕)
public final void setDaemon(boolean on)将此线程标记为守护线程或用户线程(就是普通线程)

注意:

join方式指的是主线程等待子线程执行完毕之后,再执行主线程的方法,这个方法比sleep方法要灵活很多。

拓展:

  • 默认情况下,我们创建的线程都是用户线程(普通线程),进程需要等待所有的线程执行完毕后,进程才会结束。
  • 守护线程.setDaemon(true):设置守护线程
  • 想要查看线程到底是用户线程还是守护线程,可以通过 Thread.isDameon()方法来判断,如果返回的结果是true,则为守护线程,反之则为用户线程。
  • 当所有的用户线程退出后,守护线程会立马结束。

应用:垃圾回收器线程属于守护线程;Tomcat用来接受处理外部的请求的线程就是守护线程。

3.7线程的状态及其转换

下面我们来看一下线程的状态和状态间的转换

状态名称

说明

new初始状态,线程被构建,但是还没有调用start()方法
runnable运行状态,Java线程将操作系统中的就绪态和运行态两种状态统称为“运行中”
blocked阻塞状态,表示线程阻塞于锁
waiting等待状态,表示线程进入等待状态,进入该状态表示当前线程需要其他线程通知(notify或者notifyAll)
Time_Waiting超时等待状态,可以指定等待时间自己返回
terminated终止状态,表示当前线程已经执行完毕

下面用一张图来看一下:

下面详细的说一下线程各种状态间的转换,主要就是blocked、waiting、Timed waiting三种状态的转换,以及他们是如何进入下一状态最终进入Runnable的

Blocked进入Runnable

想要从Blocked状态进入Runnable状态,就必须让线程获得monitor锁,但是如何想进入其他状态那么就相对比较特殊,因为它是没有超时机制的,也就是不会主动进入。

Waiting进入Runnable

只有当执行了LockSupport.unpark(),或者join的线程运行结束,或者被中断时才可以进入Runnable状态

注意:

  • 如果通过其他线程调用 notify() 或 notifyAll() 来唤醒它,则它会直接进入Blocked状态,这里大家可能会有疑问,不是应该直接进入 Runnable 吗?这里需要注意一点,因为唤醒 Waiting 线程的线程如果调用 notify() 或 notifyAll(),要求必须首先持有该 monitor锁,这也就是我们说的 wait()、notify必须在 synchronized 代码块中。
  • 所以处于Waiting 状态的线程被唤醒时拿不到该锁,就会进入Blocked 状态,直到执行了 notify() 或者 notifyAll() 的唤醒它的线程执行完毕并释放 monitor锁,才可能轮到它去抢夺这把锁,如果它能抢到,就会从Blocked 状态回到 Runnable 状态。

Timed Waiting进入Runnable

同样在Timed Waiting 中执行 notify() 和 notifyAll() 也是一样的道理,它们会先进入Blocked状态,然后抢夺锁成功后,再回到 Runnable 状态。

注意:

  • 但是对于Timed Waiting而言,它存在超时机制,也就是说如果超时时间到了那么就会系统自动直接拿到锁,或者 当 join 的线程执行结束 / 调用了LockSupport.unpark()?/ 被中断 等情况都会直接进入Runnable 状态,而不会经历Blocked状态

总结:

  • 线程的状态是按照上图箭头方向走的,比如线程从New状态是不可以直接进入Blocked状态的,它需要先经历 Runnable 状态。
  • 线程生命周期不可逆:一旦进入 Runnable 状态就不能回到New 状态;一旦被终止就不可能再有任何状态的变化。
  • 所以一个线程只能有一次New和Terminated 状态,只有处于中间状态才可以相互转换。也就是这两个状态不会参与相互转化

4.小结

这篇文章我们详细的介绍了一下线程的相关内容,包括线程的创建方式及其区别,线程的底层原理,线程中的常用方法,线程的状态及其之间的转换。这些内容基本上囊括了单个线程的所有内容,下一篇文章我们将介绍一下线程池的相关内容。


?

文章来源:https://blog.csdn.net/m0_52096593/article/details/133937114
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。