并发编程——3.细说线程
这篇文章我们来详细的说一下并发编程中的线程及其相关的内容
目录
1.3使用FutureTask方式(实现Callable接口的方式)
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,10 | Java中规定线程优先级是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.小结
这篇文章我们详细的介绍了一下线程的相关内容,包括线程的创建方式及其区别,线程的底层原理,线程中的常用方法,线程的状态及其之间的转换。这些内容基本上囊括了单个线程的所有内容,下一篇文章我们将介绍一下线程池的相关内容。
?
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!