一文读懂Java多线程原理
前言
线程池,故名思意,就是一个存放线程的池子,学术一点的说法,就是一组存放线程资源的集合。为什么有线程池这一概念地产生呢?想想以前我们都是需要线程的时候,直接自己手动来创建一个,然后执行完任务我们就不管了,线程就是我们执行异步任务的一个工具或者说载体,我们并没有太多关注于这个线程自身生命周期对于系统或环境的影响,而只把重心放在了多线程任务执行完成的结果输出,然后目的达到了,但是真正忽略了线程资源的维护和监控等问题。随着大型系统大量多线程资源的使用,对多线程疏于重视、维护和管理而对资源占用和拉低性能的影响逐渐扩大,才引起了人们的思考。
多线程的创建和销毁在多线程的生命周期中占有很大比重,这一部分其实很占用资源和性能,如果使用线程来执行简单任务,而因为线程本身的维护成本已经超出任务执行的效益,这是得不偿失的,于是就产生了线程池。通过使用线程池,将线程的生命周期管控起来,同时能够方便地获取到线程、复用线程,避免频繁地创建和销毁线程带来额外性能开销,这大概就是线程池引入的背景和初衷吧。
一、多线程创建方式
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<10;i++) { System.out.println(Thread.currentThread().getName()+" "+i); } return i; }
public static void main(String[] args) {//2.创建Callable实现类的实例SomeCallable01 ctt = new SomeCallable01();//3.使用FutrueTask类进行包装Callable对象,FutureTask对象封装了Callable对象的call()方法的返回值FutureTask<Integer> ft = new FutureTask<>(ctt);//开启ft线程for(int i = 0;i < 21;i++){System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);if(i==20)//i为20的时候创建ft线程{//4.使用FutureTask对象作为Thread对象的target创建并启动新线程new Thread(ft,"有返回值的线程FutureTask").start();}}//ft线程结束时,获取返回值try{ //5.调用FutureTask对象的get()方法获取子线程执行结束后的返回值。System.out.println("子线程的返回值:"+ft.get());//get()方法会阻塞,直到子线程执行结束才返回} catch (InterruptedException e){e.printStackTrace();} catch (ExecutionException e){e.printStackTrace();}
}
复制代码
}`
二、创建线程方式的区别
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 < 30; i++) { System.out.println(this.getName() + "线程第" + i + "次执行!"); if (i % 5 == 0) Thread.yield(); } }
}
复制代码
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 < 100; i++) { System.out.println(this.getName() + "线程第" + 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() 让线程进入阻塞状态
本文首发于java黑洞网,csdn同步更新
一文读懂Java多线程原理相关推荐
- java中date类型如何赋值_一文读懂java中的Reference和引用类型
简介 java中有值类型也有引用类型,引用类型一般是针对于java中对象来说的,今天介绍一下java中的引用类型.java为引用类型专门定义了一个类叫做Reference.Reference是跟jav ...
- 一文读懂Java中File类、字节流、字符流、转换流
一文读懂Java中File类.字节流.字符流.转换流 第一章 递归:File类: 1.1:概述 java.io.File 类是文件和目录路径名的抽象表示,主要用于文件和目录的创建.查找和删除等操作. ...
- 一文读懂贝叶斯原理(Bayes‘ theorem)
一文读懂贝叶斯原理(Bayes' theorem) 前言:贝叶斯定理是18世纪英国数学家托马斯·贝叶斯(Thomas Bayes)提出得重要概率论理论.以下摘一段 wikipedia 上的简介: 一. ...
- JVM(一)一文读懂Java编译全过程
一文读懂Java编译全过程 java代码首先要通过前端编译器编译成.class字节码文件,然后再按一定的规则加载到JVM(java 虚拟机)内运行,有三种运行方式,解释模式(javac).编译模式(C ...
- 一文读懂CDN加速原理
一文读懂CDN加速原理 什么是 CDN 工作原理 传统访问过程 CDN 访问过程 组成要素 智能调度 DNS 缓存功能服务 负载均衡设备 内容 Cache 服务器 共享存储 名词解释 CNAME记录( ...
- java多线程 模型_一篇文章读懂Java多线程模型
要真正了解Java的多线程,我们还要从进程和线程的概念说起 进程 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础.在早期 ...
- 夯实Java基础系列17:一文搞懂Java多线程使用方式、实现原理以及常见面试题
本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial 喜欢的话麻烦点下 ...
- 多线程的实现方式_一文搞懂Java多线程使用方式、实现原理以及常见面试题
本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial 喜欢的话麻烦点下 ...
- 使用线程锁(lock)实现线程同步_一文搞懂Java多线程使用方式、实现原理以及常见面试题...
本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial 喜欢的话麻烦点下 ...
最新文章
- Python爬虫爬取部分学校的新闻标题、时间、对应的新闻链接
- Algorithm:C++语言实现之图论算法相关(图搜索广度优先BFS、深度优先DFS,最短路径SPF、带负权的最短路径Bellman-ford、拓扑排序)
- mysql jdbc linux,linux mysql jdbc 权限问题_MySQL
- python爬虫怎么做_python爬虫怎么做?
- python学习手册-Python学习手册
- Sourcegraph 代码搜索
- 力的吸引,是否因为负动量
- 【进阶版】 机器学习之强化学习、蒙特卡罗、AlphaGo原理浅析(22)
- Silverlight下载-Silverlight 1.1 Tools下载
- 51汇编——矩阵键盘
- java keytool证书cer,keytool 生成cer证书
- lingo入门教程之二 --- 集合运用
- 2019电商数据分析师实战项目教程 电商数据分析报告 电商运营数据分析 电商数据分析流程
- 原创 关于微信拼车小程序开发的需求分析(分析建模2)
- 计算机产品选型与配置,高校校园网设备的选型和配置.DOC
- 青村茶舍||“城乡居民基本养老保险”社会治理创新活动
- mysql signal_[MySQL]MySQL的signal sqlstate
- win10如何开启电源高性能模式
- FPGA进阶(2):基于I2C协议的EEPROM驱动控制
- Ruby way Rails way Milky way
热门文章
- access中case替代方法
- (96)分频器设计(任意分频器)
- (71)信号发生器DDS方波设计 (一)(第15天)
- 5 呼吸灯verilog与Systemverilog编码
- oracle脏块,检查点队列上的最早脏的数据块再次被修改以何种顺序刷到磁盘
- java中用new创建一个对象的过程解析_【漫画】Java对象的创建和访问全过程详解...
- gorm软删除_gorm的简单使用和注意事项
- pymol怎么做底物口袋表面_汽车表面有划痕怎么办?建议大家这样做,自己动手就解决...
- vivado fpga最最简单的入门--led闪烁 创建工程+代码输入+添加引脚约束完整具体流程
- jboss7 java版本_jboss-as-7.1.1.Final与jdk1.8不兼容解决方案