java创建线程的两种方式及源码解析
创建线程的方式有很多种,下面我们就最基本的两种方式进行说明。主要先介绍使用方式,再从源码角度进行解析。
- 继承Thread类的方式
- 实现Runnable接口的方式
这两种方式是最基本的创建线程的方式,其实核心也就是Thread类,后面分析源码会讲到,下面先介绍使用方式。
一:继承Thread类的方式创建线程
1,创建线程步骤
- 创建一个子类继承于Thread类
- 子类重写Thread类的run方法,方法内实现子线程要完成的功能
- 创建一个子类的对象
- 调用子类对象的start()的方法,该方法有两个作用:启动此线程;调用重写的run方法。
代码如下:
package com.yefengyu.thread; //1,创建一个子类继承于Thread类
public class SubThread extends Thread { //2,子类重写Thread类的run方法,方法内实现子线程要完成的功能
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Thread ...." + i);
}
}
}
package com.yefengyu.thread; public class TestThread { public static void main(String[] args) {
//3,创建一个子类的对象。
SubThread subThread = new SubThread();
//4,调用线程的start()的方法。该方法有两个作用:启动此线程;调用响应的run方法。不能显示调用run方法,因为这样不能开启线程
subThread.start(); for (int i = 0; i < 5; i++) {
System.out.println("main ...." + i);
}
}
}
每次执行结果都不相同,这是因为多个线程都在获取cpu的执行权,cpu执行到谁,就执行谁。但是要明确一点,在某个时刻,单个cpu只能执行一个线程,cpu进行着快速切换,已达到看上去是并行执行的效果,这就是线程的一个特点:随机性,哪个线程抢到cpu资源,就执行该线程,至于执行多长时间,是cpu说了算。
main ....0
Thread ....0
Thread ....1
Thread ....2
main ....1
main ....2
main ....3
main ....4
Thread ....3
Thread ....4
2,设置与获取线程名称
由于默认的线程名称没有可读性,因此设置一个线程名称还是比较重要的,Thread类有个构造方法:
public Thread(String name) {
init(null, null, name, 0);
}
因此子类增加线程名称则比较简单:
package com.yefengyu.thread; //1,创建一个子类继承于Thread类
public class SubThread extends Thread { //通过构造函数设置线程名称,当然使用set方法也可以
public SubThread(String name) {
super(name);
} //2,子类重写Thread类的run方法,方法内实现子线程要完成的功能
@Override
public void run() {
for (int i = 0; i < 5; i++) {
//下面两种方法都会获取线程名称
System.out.println(this.getName()+" ... " + i);
System.out.println(Thread.currentThread().getName()+" *** " + i);
}
}
}
package com.yefengyu.thread; public class TestThread { public static void main(String[] args) {
//(3)创建一个子类的对象。
SubThread subThread1 = new SubThread("my-thread11111");
SubThread subThread2 = new SubThread("my-thread22222");
//(4)调用线程的start()的方法。该方法有两个作用:启动此线程;调用响应的run方法。不能显示调用run方法,因为这样不能开启线程
subThread1.start();
subThread2.start(); for (int i = 0; i < 5; i++) {
System.out.println("main ...." + i);
}
}
}
结果:
main ....0
my-thread22222 ... 0
my-thread11111 ... 0
my-thread22222 *** 0
my-thread22222 ... 1
my-thread22222 *** 1
main ....1
my-thread22222 ... 2
my-thread11111 *** 0
my-thread22222 *** 2
main ....2
main ....3
main ....4
my-thread22222 ... 3
my-thread11111 ... 1
my-thread11111 *** 1
my-thread11111 ... 2
my-thread22222 *** 3
my-thread22222 ... 4
my-thread22222 *** 4
my-thread11111 *** 2
my-thread11111 ... 3
my-thread11111 *** 3
my-thread11111 ... 4
my-thread11111 *** 4
3,实战:汽车票买票程序
假如车站有3张票,三个窗口,多线程如何卖票?
package com.yefengyu.thread; public class SubThread extends Thread { //票的总数
private int ticket = 3; public SubThread(String name) {
super(name);
} @Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "卖票 " + ticket--);
}
}
}
}
package com.yefengyu.thread; public class TestThread { public static void main(String[] args) {
//三个线程模拟三个窗口同时卖票
SubThread subThread1 = new SubThread("my-thread11111");
SubThread subThread2 = new SubThread("my-thread22222");
SubThread subThread3 = new SubThread("my-thread33333");
subThread1.start();
subThread2.start();
subThread3.start();
}
}
结果和我们想的大不相同,竟然每个线程都卖了3张票,一共卖了9张票,这是不可以忍受的。
my-thread11111卖票 3
my-thread11111卖票 2
my-thread11111卖票 1
my-thread33333卖票 3
my-thread22222卖票 3
my-thread33333卖票 2
my-thread22222卖票 2
my-thread33333卖票 1
my-thread22222卖票 1
修改1:使用静态变量:
//票的总数
private static int ticket = 3;
定义静态可以解决卖出多余票的情况,但是这种变量一般不定义静态的,因为静态属性生命周期太长。
修改2:new一个SubThread实例,多次启动
package com.yefengyu.thread; public class TestThread {
public static void main(String[] args) {
//一个线程对象多次启动
SubThread subThread1 = new SubThread("my-thread11111");
subThread1.start();
subThread1.start();
subThread1.start();
}
}
出现异常:
my-thread11111卖票 3Exception in thread "main"
my-thread11111卖票 2
my-thread11111卖票 1
java.lang.IllegalThreadStateException
at java.lang.Thread.start(Thread.java:708)
at com.yefengyu.thread.TestThread.main(TestThread.java:8)
源码这样说:线程不是NEW状态是不可以调用start方法的,调用会报异常,也就是一个线程启动之后不能再启动。关于线程状态后面博文会讲到。
/**
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
如何解决卖票程序中的问题呢?使用Runnable接口。
二:实现Runnable接口的方式创建多线程
1,创建线程步骤
- 编写一个类实现Runnable接口
- 该类实现run方法
- 创建该类的对象
- 在创建 Thread 时作为一个参数来传递并启动
2,代码演示
package com.yefengyu.thread; public class SubThread implements Runnable { //票的总数
private int ticket = 3; @Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "卖票 " + ticket--);
}
}
}
}
package com.yefengyu.thread; public class TestThread {
public static void main(String[] args) { //创建该类的对象
SubThread subThread = new SubThread(); //在创建 Thread 时作为一个参数来传递并启动
new Thread(subThread, "线程1").start();
new Thread(subThread, "线程2").start();
new Thread(subThread, "线程3").start();
}
}
运行结果如下,是我们想要的结果。
线程1卖票 3
线程3卖票 2
线程2卖票 1
稍微研究一下,第一种继承Thread类的方式,new了3次SubThread实例,那么实例的变量ticket也有三个,线程分别拥有各自的ticket。而实现Runnable接口的方式,数据只存在与SubThread这个实例对象中,代码中只需new一次,因此只有一份ticket数据,而将持有这份数据的对象通过构造方法传入到多个线程中的时候,线程对象只是执行的载体,真实数据只有一份,因此Runnable接口的实现方式适合多个相同的程序代码的线程去处理同一个资源。
3,本节小总结
- 采用继承Thread类方式
- 优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程。
- 缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类
- 采用实现Runnable接口方式:
- 优点:线程类只是实现了Runable接口,还可以继承其他的类。可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况。
- 缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。
三:源码分析
1,理论分析
多线程是java开发中必不可少的一项技术点,下面主要研究Thread类,通过分析该类了解多线程执行的过程,为以后的线程池等高级技术打下坚实基础。
当我们使用多线程进行开发的时候,最开始学习的例子就是使用Thread类。使用步骤如下,和上面演示的对比,简化了步骤:
编写一个类,继承Thread
重写run方法
通过调用start方法启动线程。
后来又有一种方法,实现Runnable接口,主要步骤如下:
编写一个类,实现Runnable 接口,重写run方法
将该类的对象传入Thread类中
通过调用start方法启动线程。
通过上面,我们来分析一下,线程执行离不开Thread类,最后都要使用start方法启动线程。我们可以想到start方法是一个入口方法,它可以做很多事。假如start方法做了如下的事情:
do x
do y
do run
do z
我们不关心x、y、z具体是什么,只要明白start方法是一个入口方法,它做了很多事情,但是在某一步,它调用了 run 方法。而run方法是我们必须实现的,也就是我们自己实现的逻辑在start里面被执行了。接着我们考虑下 run 方法,怎么样才能自己定义run方法,然后在run方法里面写自己的逻辑?
一种方法是,在Thread类里面,我们定义一个抽象方法 run,这个时候,必须有子类来实现。这是模板设计模式思想。
另一种方法是提供一个接口,并且接口中有个方法 run,Thread类持有这个接口(通过属性持有,再通过构造器传入),并且Thread类也有个run方法(为啥也要有个run方法后面会提到),该run方法调用接口的run方法。此时只要编写一个类实现接口,重写run方法,并传入Thread类,那么Thread类在执行start方法的时候,会调用自身的run方法,该run方法又会调用接口实现的run方法,这是策略设计模式思想。
以上两种模式就是实现Thread类和实现Runnable接口的实现原理。需要注意的是,Thread类本身也实现了Runnable接口,那么Thread类本身拥有run方法则水到渠成。
2,源码分析
Runnable的源码很简单:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
Thread类的属性非常多,我们暂时不管,注意有一个属性,它是对Runnable接口的引用。
private Runnable target;
有了属性,我们需要看如何传入这个属性值:
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
init方法内容很多,但是关于target变量,只有一句:
this.target = target;
因此可以判断,上面两个构造器,第一个没有给Runnable赋值,值为null,第二个通过参数进行赋值。
我们再看下Thread类:
public class Thread implements Runnable
Thread类实现了Runnable接口,因此必须实现run方法:
@Override
public void run() {
if (target != null) {
target.run();
}
}
对于这个run方法,如果是使用继承Thread类重写run方法;那么这里面的内容将会被覆盖,如果是实现Runnable接口重写run方法,那么此处就会调用接口实现的run方法。这就让两种实现多线程的方式得以共存。
这个run方法何时调用?在start方法中:
public synchronized void start() { if (threadStatus != 0)
throw new IllegalThreadStateException(); group.add(this); boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) { }
}
} private native void start0();
注意start方法中的start0方法和最后一行start0方法。该start0方法是native方法,实质是调用run方法,此处暂时不做详解。
Thread类使用模板设计模式,模板方法是start,start方法里面的start0方法才能真正启动线程、调用了Thread类的run方法。
3,Runnable接口的好处:
- 适合多个相同的程序代码的线程去处理同一个资源
- 可以避免java中的单继承的限制
- 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
- 线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
java创建线程的两种方式及源码解析的更多相关文章
- 线程系列1--Java创建线程的几种方式及源码分析
线程--创建线程的几种方式及源码分析 开始整理下线程的知识,感觉这块一直是盲区,工作中这些东西一直没有实际使用过,感觉也只是停留在初步的认识.前段时间一个内推的面试被问到,感觉一脸懵逼.面试官说,我的 ...
- Java创建线程的两种方式
方式 继承Thread类 实现Runnable方法 实例 #继承Thread类 public class ThreadTest2 extends Thread { private int thread ...
- 当阿里面试官问我:Java创建线程有几种方式?我就知道问题没那么简单
这是最新的大厂面试系列,还原真实场景,提炼出知识点分享给大家. 点赞再看,养成习惯~ 微信搜索[武哥聊编程],关注这个 Java 菜鸟. 昨天有个小伙伴去阿里面试实习生岗位,面试官问他了一个老生常谈的 ...
- 【java并发】传统线程技术中创建线程的两种方式
传统的线程技术中有两种创建线程的方式:一是继承Thread类,并重写run()方法:二是实现Runnable接口,覆盖接口中的run()方法,并把Runnable接口的实现扔给Thread.这两种方式 ...
- Java并发基础01. 传统线程技术中创建线程的两种方式
传统的线程技术中有两种创建线程的方式:一是继承Thread类,并重写run()方法:二是实现Runnable接口,覆盖接口中的run()方法,并把Runnable接口的实现扔给Thread.这两种方式 ...
- 创建线程的两种方式比较Thread VS Runnable
1.首先来说说创建线程的两种方式 一种方式是继承Thread类,并重写run()方法 public class MyThread extends Thread{ @Override public vo ...
- Java新建线程的两种方式
Java新建线程有两种方式,一种是通过继承Thread类,一种是实现Runnable接口,下面是新建线程的两种方式. 我们假设有个竞赛,有一个选手A做俯卧撑,一个选手B做仰卧起坐.分别为两个线程: p ...
- Java创建线程的四种方式
Java创建线程的四种方式 1.继承Thread类创建线程 定义Thread类的子类,并重写该类的run方法,run()方法的内容就是该线程执行的内容 创建Thread子类的实例,即创建了线程对象. ...
- Java中创建线程的两种方式
创建线程的第一种方式: 创建一个类继承Thread 重写Thread中的run方法 (创建线程是为了执行任务 任务代码必须有存储位置,run方法就是任务代码的存储位置.) 创建子类对象,其实就是在创建 ...
随机推荐
- 使用IP在局域网内访问System.Net.HttpListenerException:“拒绝访问。”
记录一下,自己写的程序之前运行没有遇到这个问题,突然遇到这个问题,找了一圈没有找到有效的解决方案,到最后发现,以管理员身份运行程序即可.简单记录一下. 还有就是 .UseUrls("http ...
- Ionic创建混合App(一)
前言 最近公司要开始做App项目,最终选定了ionic开发方案,在这里我将学习的过程记录在这里,一方面避免自己忘记,另一方面方便大家交流学习.这里我们采用的是 Ionic2 + Angular2 : ...
- FFmpeg从入门到出家(HEVC在RTMP中的扩展)
由金山云视频云技术团队提供:FFmpeg从入门到出家第三季: 为推进HEVC视频编码格式在直播方案中的落地,经过CDN联盟讨论,并和主流云服务厂商达成一致,规范了HEVC在RTMP/FLV中的扩展,具 ...
- Docker实战部署应用——Tomcat
Tomcat 部署 拉取tomcat镜像 docker pull tomcat:8 创建tomcat容器 创建tomcat容器用于 Web应用,并且进行目录映射 docker run -id --na ...
- mysql查询时间戳转换
mysql查询时间戳转换 SELECT FROM_UNIXTIME(create_time) FROM tablename; 更新时间为七天以后 UPDATE t_rebate_trade_item ...
- [NOIP2009]最优贸易(图论)
[NOIP2009]最优贸易 题目描述 CC 国有 \(n\) 个大城市和 \(m\) 条道路,每条道路连接这 \(n\) 个城市中的某两个城市.任意两个城市之间最多只有一条道路直接相连.这 \(m\ ...
- python列表转json树菜单
1.列表数据 data = [ { 'id': 1, 'parent_id': 2, 'name': "Node1" }, { 'id': 2, 'parent_id': 5, ' ...
- 一、ARM
1.1 ARM 分类 1.1.1 版本号分类 以前分类的是 ARM7,ARM9... ARM11,在 ARM11 之后,就是以 Cortex 系列分类了: Cortex-R:应用在实时系统上的系列 C ...
- JSP和Servlet及浏览器与tomcat交互过程
JSP与SERVLET区别 JSP在本质上就是Servlet,但是两者的创建方式不一样. JSP由HTML代码和JSP标签构成,可以方便地编写动态网页.因此在实际应用中采用Servlet来控制业务流程 ...
- SPOJ QTREE - Query on a tree 【树链剖分模板】
题目链接 引用到的大佬博客 代码来自:http://blog.csdn.net/jinglinxiao/article/details/72940746 具体算法讲解来自:http://blog.si ...