一文读懂Java多线程原理
2021-06-08 15:03
前言
线程池,故名思意,就是一个存放线程的池子,学术一点的说法,就是一组存放线程资源的集合。为什么有线程池这一概念地产生呢?想想以前我们都是需要线程的时候,直接自己手动来创建一个,然后执行完任务我们就不管了,线程就是我们执行异步任务的一个工具或者说载体,我们并没有太多关注于这个线程自身生命周期对于系统或环境的影响,而只把重心放在了多线程任务执行完成的结果输出,然后目的达到了,但是真正忽略了线程资源的维护和监控等问题。随着大型系统大量多线程资源的使用,对多线程疏于重视、维护和管理而对资源占用和拉低性能的影响逐渐扩大,才引起了人们的思考。
多线程的创建和销毁在多线程的生命周期中占有很大比重,这一部分其实很占用资源和性能,如果使用线程来执行简单任务,而因为线程本身的维护成本已经超出任务执行的效益,这是得不偿失的,于是就产生了线程池。通过使用线程池,将线程的生命周期管控起来,同时能够方便地获取到线程、复用线程,避免频繁地创建和销毁线程带来额外性能开销,这大概就是线程池引入的背景和初衷吧。
一、多线程创建方式
1.1、继承Thread类创建线程类
1.实现步骤
定义一个继承Thread类的子类,并重写该类的run()方法;
创建Thread子类的实例,即创建了线程对象;
调用该线程对象的start()方法启动线程。
2.核心代码
`class SomeThead extends Thraad { public void run() { //do something here
}
}
public static void main(String[] args){
SomeThread oneThread = new SomeThread();
//启动线程
oneThread.start();
}`
1.2、实现Runnable接口创建线程类
1.实现步骤
定义Runnable接口的实现类,并重写该接口的run()方法;
创建Runnable实现类的实例,并以此实例作为Thread的target对象,即该Thread对象才是真正的线程对象。
2.核心代码
class SomeRunnable implements Runnable { public void run() { //do something here } } Runnable oneRunnable = new SomeRunnable(); Thread oneThread = new Thread(oneRunnable); oneThread.start();
1.3、通过Callable和Future创建线程
1.实现步骤
创建Callable接口的实现类,并实现call()方法,改方法将作为线程执行体,且具有返回值。
创建Callable实现类的实例,使用FutrueTask类进行包装Callable对象,FutureTask对象封装了Callable对象的call()方法的返回值
使用FutureTask对象作为Thread对象的target创建并启动新线程
调用FutureTask对象的get()方法获取子线程执行结束后的返回值。
2.核心代码
`//1.创建Callable接口的实现类,并实现call()方法 public class SomeCallable01 implements Callable { @Override public Integer call() throws Exception { int i = 0; for(;i
public static void main(String[] args) {
//2.创建Callable实现类的实例
SomeCallable01 ctt = new SomeCallable01();
//3.使用FutrueTask类进行包装Callable对象,FutureTask对象封装了Callable对象的call()方法的返回值
FutureTask ft = new FutureTask(ctt);
//开启ft线程
for(int i = 0;i 复制代码
}`
二、创建线程方式的区别
1.使用继承Thread类的方式创建多线程
1)优势
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
2)劣势
线程类已经继承了Thread类,所以不能再继承其他父类。(有单继承的局限性)
创建多线程时,每个任务有成员变量时不共享,必须加static才能做到共享
2.使用实现Runnable类的方式创建多线程
1)优势
避免了单继承的局限性、多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
2)劣势
比较复杂、访问线程必须使用Thread.currentThread()方法、无返回值。
3.使用实现Callable接口的方式创建多线程
1)优势
有返回值、避免了单继承的局限性、多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
2)劣势
比较复杂、访问线程必须使用Thread.currentThread()方法
4.Runnable和Callable的区别
1)Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
3)call方法可以抛出异常,run方法不可以。
4)运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果future.get()。
三、多线程调度
3.1、调度策略
时间片:线程的调度采用时间片轮转的方式 抢占式:高优先级的线程抢占CPU
3.2、Java的调度方法
1)对于同优先级的线程组成先进先出队列(先到先服务),使用时间片策略
2)对高优先级,使用优先调度的抢占式策略
3.3、线程的优先级
等级:
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5
方法:
`getPriority():返回线程优先级
setPriority(int newPriority):改变线程的优先级`
备注:
高优先级的线程要抢占低优先级的线程的cpu的执行权。但是仅是从概率上来说的,高优先级的线程更有可能被执行。并不意味着只有高优先级的线程执行完以后,低优先级的线程才执行。
四、多线程状态管理
4.1、线程睡眠---sleep
1)概述
如果我们需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread的sleep方法。
2)线程睡眠方法
在指定的毫秒数内让正在执行的线程休眠:
sleep(long millis) 在指定的毫秒数加指定的纳秒数内让正在执行的线程休眠:
sleep(long millis,int nanos)
3)代码实现
sleep是静态方法,最好不要用Thread的实例对象调用它,因为它睡眠的始终是当前正在运行的线程,而不是调用它的线程对象,它只对正在运行状态的线程对象有效。
`public class SynTest { public static void main(String[] args) { new Thread(new CountDown(),"倒计时").start(); } }
class CountDown implements Runnable{ int time = 10; public void run() { while (true) { if(time>=0){ System.out.println(Thread.currentThread().getName() + ":" + time--); try { Thread.sleep(1000); //睡眠时间为1秒 } catch (InterruptedException e) { e.printStackTrace(); } } } } }`
4)备注
Java线程调度是Java多线程的核心,只有良好的调度,才能充分发挥系统的性能,提高程序的执行效率。但是不管程序员怎么编写调度,只能最大限度的影响线程执行的次序,而不能做到精准控制。因为使用sleep方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果调用Thread.sleep(1000)使得线程睡眠1秒,可能结果会大于1秒。
4.2、线程让步---yield
1)概述
yield()方法和sleep()方法有点相似,它也是Thread类提供的一个静态的方法,它也可以让当前正在执行的线程暂停,让出cpu资源给其他的线程。但是和sleep()方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。yield()方法只是让当前线程暂停一下,重新进入就绪的线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行。
实际上,当某个线程调用了yield()方法暂停之后,优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程更有可能获得执行的机会,当然,只是有可能,因为我们不可能精确的干涉cpu调度线程。
2)代码实现
public class Test1 {
public static void main(String[] args) throws InterruptedException {
new MyThread("低级", 1).start();
new MyThread("中级", 5).start();
new MyThread("高级", 10).start();
}
}
class MyThread extends Thread {
public MyThread(String name, int pro) {
super(name);// 设置线程的名称
this.setPriority(pro);// 设置优先级
}
@Override
public void run() {
for (int i = 0; i 复制代码
3)sleep和yield的区别
①sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态。
②sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常。
③sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行。
4.3、线程合并---join
1)概述
线程的合并的含义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行时,Thread类提供了join方法来完成这个功能,注意,它不是静态方法。
简而言之:
? 当B线程执行到了A线程的.join()方法时,B线程就会等待,等A线程都执行完毕,B线程才会执行。join可以用来临时加入线程执行。
2)线程合并方法
它有三个重载方法:
? 当前线程等该加入该线程后面,等待该线程终止。
void join()
? 当前线程等待该线程终止的时间最长为 millis 毫秒。
? 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
void join(long millis)
? 等待该线程终止的时间最长为 millis 毫秒 + nanos
? 纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
void join(long millis,int nanos)
3)代码实现
public static void main(String[] args) throws InterruptedException {
yieldDemo ms = new yieldDemo();
Thread t1 = new Thread(ms,"张三吃完还剩");
Thread t2 = new Thread(ms,"李四吃完还剩");
Thread t3 = new Thread(ms,"王五吃完还剩");
t1.start();
t1.join();
t2.start();
t3.start();
System.out.println( "主线程");
}`
Thread t = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r = 10;
});
t.start();
// 让主线程阻塞 等待t线程执行完才继续执行
// 去除该行,执行结果为0,加上该行 执行结果为10
t.join();
log.info("r:{}", r);
// 运行结果
13:09:13.892 [main] INFO thread.TestJoin - r:10
复制代码
4.4、设置线程的优先级
1)概述
每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会。与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。
每个线程默认的优先级都与创建它的父线程具有相同的优先级,在默认情况下,main线程具有普通优先级。
2)涉及优先级方法
Thread类提供了setPriority(int newPriority)和getPriority()方法来设置和返回一个指定线程的优先级,其中setPriority方法的参数是一个整数,范围是1~·0之间,也可以使用Thread类提供的三个静态常量:
MAX_PRIORITY =10 MIN_PRIORITY =1 NORM_PRIORITY =5
3)代码实现
public class Test1 {
public static void main(String[] args) throws InterruptedException {
new MyThread("高级", 10).start();
new MyThread("低级", 1).start();
}
}
class MyThread extends Thread {
public MyThread(String name,int pro) {
super(name);//设置线程的名称
setPriority(pro);//设置线程的优先级
}
@Override
public void run() {
for (int i = 0; i 复制代码
4)备注
虽然Java提供了10个优先级别,但这些优先级别需要操作系统的支持。不同的操作系统的优先级并不相同,而且也不能很好的和Java的10个优先级别对应。所以我们应该使用MAX_PRIORITY、MIN_PRIORITY和NORM_PRIORITY三个静态常量来设定优先级,这样才能保证程序最好的可移植性。
4.5、后台(守护)线程
1)概述
守护线程使用的情况较少,但并非无用,举例来说,JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。
默认情况下,java进程需要等待所有线程都运行结束,才会结束,有一种特殊线程叫守护线程,当所有的非守护线程都结束后,即使它没有执行完,也会强制结束。
2)涉及方法
调用线程对象的方法setDaemon(true),则可以将其设置为守护线程。
将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用。 该方法首先调用该线程的 checkAccess 方法,且不带任何参数。这可能抛出 SecurityException(在当前线程中)。
public final void setDaemon(boolean on)
参数:
on - 如果为 true,则将该线程标记为守护线程。
抛出:
IllegalThreadStateException - 如果该线程处于活动状态。
SecurityException - 如果当前线程无法修改该线程。
3)守护线程的用途
守护线程通常用于执行一些后台作业,例如在你的应用程序运行时播放背景音乐,在文字编辑器里做自动语法检查、自动保存等功能。
java的垃圾回收也是一个守护线程。守护线的好处就是你不需要关心它的结束问题。例如你在你的应用程序运行的时候希望播放背景音乐,如果将这个播放背景音乐的线程设定为非守护线程,那么在用户请求退出的时候,不仅要退出主线程,还要通知播放背景音乐的线程退出;如果设定为守护线程则不需要了。
4.6、停止线程
1)概述
Thread.stop()、Thread.suspend、Thread.resume、Runtime.runFinalizersOnExit这些终止线程运行的方法已经被废弃了,使用它们是极端不安全的。
正确停止线程的方法:
? 第一:正常执行完run方法,然后结束掉。
? 第二:控制循环条件和判断条件的标识符来结束掉线程。
2)实现代码示例
class MyThread extends Thread { int i=0; boolean next=true; @Override public void run() { while (next) { if(i==10) next=false; i++; System.out.println(i); } } }
4.7、线程打断---interrupt
1)什么是中断(interrupt)
? 中断只是一种协作机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现;
? 每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;
? 通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。
打断标记:线程是否被打断,true表示被打断了,false表示没有
2)涉及方法
isInterrupted()方法:
获取线程的打断标记(哪个线程对象调用就检查谁的) ,调用后不会修改线程的打断标记
interrupt()方法:
中断this线程(哪个线程对象调用即中断谁)。如果这个需要被中断线程处于阻塞状态(sleep、wait、join),那么它的中断状态就会被清除,并且抛出异常(InterruptedException)。这个中断并非真正的停止掉线程,而是将它的中断状态设置成“停止”的状态,线程还是会继续运行,至于怎么停止掉该线程,还是要靠我们自己去停止,该方法只是将线程的状态设置成“停止”的状态,即true。
打断正常线程 ,线程不会真正被中断,但是线程的打断标记为true。
interrupted()方法:
检查当前线程是否被中断,与上面的interrupt()方法配合一起用。线程的中断状态将会被这个方法清除,也就是说:如果这个方法被连续成功调用两次,第二次
调用将会返回false(除非当前线程在第一次调用之后和第二次调用之前又被中断了)。
也就是说:调用后清空打断标记 即如果获取为true 调用后打断标记为false (不常用)
4.8、线程堵塞
线程的阻塞可以分为好多种,从操作系统层面和java层面阻塞的定义可能不同,但是广义上使得线程阻塞的方式有下面几种:
1)BIO阻塞,即使用了阻塞式的io流
2)sleep(long time) 让线程休眠进入阻塞状态
3)a.join() 调用该方法的线程进入阻塞,等待a线程执行完恢复运行
4)sychronized或ReentrantLock 造成线程未获得锁进入阻塞状态
5)获得锁之后调用wait()方法 也会让线程进入阻塞状态
6)LockSupport.park() 让线程进入阻塞状态