第9章 认识多线程
本章目标:
- 了解进程与线程的区别
- 掌握Java线程的两种实现方式及其区别
- 了解线程的操作状态
一、进程与线程
DOS系统有一个非常明显的特点,只要一中病毒之后系统就会立刻死机。因为传统的DOS系统采用的是单进程的处理方式。所以只能有一个程序独立运行,其他程序无法运行。
Windows系统中,即使出现了病毒,系统照样可以正常使用,因为在Windows中采用的是多进程的处理方式,那么在同一个时间段上会有多个程序同时运行。
对于Word来说,每次启动一个Word之后实际上都是在操作系统上分配了一个进程。
线程实际上是进程基础上的进一步划分,从Word来看,可以把拼写检查当做一个线程进行处理。当然,会同时存在多个线程。
如果一个进程没有了,那么线程一定会消失。那么如果线程消失了,进程未必消失。而且,所有的线程都是在进程基础上并发执行的。
如果同时运行多个任务,则所有的系统资源将是共享的,被所有线程所公用。但是程序处理需要CPU,传统的单核CPU来说,在同一个时间段上会有多个程序执行,但是在同一个时间点上只能存在一个程序运行。也就是说,所有的程序都要抢占CPU资源。
现在的CPU已经发展到多核状态了,在一个电脑上会存在多个CPU,那么这个时候就可以非常清楚地发现多线程操作间是如何进行并发执行的。
二、Java的多线程实现
在Java中如果想要实现多线程可以采用以下两种方式:
- 继承Thread类
- 实现Runnable接口
1、Thread类
Thread类是在java.lang包中定义的一个类。只要继承了Thread类,此类就称为多线程操作类。在Thread子类之中,必须明确地覆写Thread类中的run方法,此方法为线程的主体。
多线程的定义语法:
class 类名称 extends Thread{ //继承Thread类
属性...; //类中定义属性
方法...; //类中定义方法
//覆写Thread类中的run方法,此方法是线程的主体
public void run(){
线程主体;
}
}
java.lang包会在程序运行时自动导入,所以无需手工书写import语句。
一个类继承了Thread类之后,那么此类就具有了多线程的操作功能。
package com.java.Thread;
public class ThreadDemo01 {
public static void main(String[] args){
MyThread mt1 = new MyThread("线程A "); //实例化对象
MyThread mt2 = new MyThread("线程B "); //实例化对象
mt1.run(); //调用线程主体
mt2.run(); //调用线程主体
}
}
class MyThread extends Thread{ //继承Thread类,作为线程的实现类
private String name; //表示线程的名称
public MyThread(String name){
this.name = name; //通过构造方法配置name属性
}
public void run(){ //覆写run方法,作为线程的操作主体
for(int i = 0; i < 10; i++){
System.out.println(name + "运行, i = " + i);
}
}
}
以上完成了一个线程的操作类,直接使用此类就可以完成功能。观察运行效果:
线程A 运行, i = 0
线程A 运行, i = 1
线程A 运行, i = 2
线程A 运行, i = 3
线程A 运行, i = 4
线程A 运行, i = 5
线程A 运行, i = 6
线程A 运行, i = 7
线程A 运行, i = 8
线程A 运行, i = 9
线程B 运行, i = 0
线程B 运行, i = 1
线程B 运行, i = 2
线程B 运行, i = 3
线程B 运行, i = 4
线程B 运行, i = 5
线程B 运行, i = 6
线程B 运行, i = 7
线程B 运行, i = 8
线程B 运行, i = 9
以上得程序是先执行完A再执行B,并没有达到所谓的并发执行的效果。
因为以上的程序实际上还是按照古老的形式调用的,通过 对象.方法 ,但是如果要想启动一个线程必须使用Thread类中定义的start()方法。
一旦调用start()方法,实际上最终调用的是run()方法。
package com.java.Thread;
public class ThreadDemo02 {
public static void main(String[] args){
MyThread02 mt1 = new MyThread02("线程A "); //实例化对象
MyThread02 mt2 = new MyThread02("线程B "); //实例化对象
mt1.start(); //调用线程主体
mt2.start(); //调用线程主体
}
}
class MyThread02 extends Thread{ //继承Thread类,作为线程的实现类
private String name; //表示线程的名称
public MyThread02(String name){
this.name = name; //通过构造方法配置name属性
}
public void run(){ //覆写run方法,作为线程的操作主体
for(int i = 0; i < 10; i++){
System.out.println(name + "运行, i = " + i);
}
}
}
程序运行效果:
线程A 运行, i = 0
线程B 运行, i = 0
线程B 运行, i = 1
线程B 运行, i = 2
线程B 运行, i = 3
线程B 运行, i = 4
线程B 运行, i = 5
线程B 运行, i = 6
线程B 运行, i = 7
线程B 运行, i = 8
线程B 运行, i = 9
线程A 运行, i = 1
线程A 运行, i = 2
线程A 运行, i = 3
线程A 运行, i = 4
线程A 运行, i = 5
线程A 运行, i = 6
线程A 运行, i = 7
线程A 运行, i = 8
线程A 运行, i = 9
从效果来看,确实是并发执行的。哪个线程抢占到了CPU资源,哪个线程就执行。
问题:为什么不直接调用run()方法,而是通过start()调用呢?
如果要想解决这样的难题,则肯定要打开Thread类的定义。在JDK的src.zip中全部都是JAVA的源程序代码,直接找到java.lang.Thread类,就可以打开Thread类的定义。
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) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
start()方法有可能抛出异常。
native关键字表示的是一个由Java调用本机操作系统函数的一个关键字,运行Java程序调用本机的操作系统的函数以完成特定的功能。
证明,如果现在要是想实现多线程的话,则肯定需要操作系统的支持,因为多线程操作中牵扯到到一个抢占CPU的情况,要等待CPU进行调度。那么这一点肯定需要操作系统的底层支持,所以使用了native调用本机的系统函数。而且在各个操作系统中多线程的实现底层代码肯定是不同的,所以使用native关键字也可以让JVM自动去调整不同的JVM实现。
threadStatus也表示6一种状态,如果线程已经启动了再调用start()方法的时候就有可能产生异常。
package com.java.Thread;
public class ThreadDemo03 {
public static void main(String[] args){
MyThread03 mt1 = new MyThread03("线程A "); //实例化对象
MyThread03 mt2 = new MyThread03("线程B "); //实例化对象
mt1.start(); //调用线程主体
mt1.start(); //错误
}
}
class MyThread03 extends Thread{ //继承Thread类,作为线程的实现类
private String name; //表示线程的名称
public MyThread03(String name){
this.name = name; //通过构造方法配置name属性
}
public void run(){ //覆写run方法,作为线程的操作主体
for(int i = 0; i < 10; i++){
System.out.println(name + "运行, i = " + i);
}
}
}
运行效果:
2、Runnable接口
在Java也可以通过实现Runnable接口的方式实现多线程,Runnable接口中只定义了一个抽象方法。
public void run();
通过Runnable接口实现多线程:
class 类名称 implements Runnable{ //实现Runnable接口
属性...; //类中定义属性
方法...; //类中定义方法
public void run(){ //覆写Runnable接口里的run()方法
线程主体;
}
}
class RunnableThread01 implements Runnable{ //实现Runnable接口,作为线程的实现类
private String name; //表示线程的名称
public RunnableThread01(String name){
this.name = name; //通过构造方法配置name属性
}
public void run(){ //覆写run方法,作为线程的操作主体
for(int i = 0; i < 10; i++){
System.out.println(name + "运行, i = " + i);
}
}
}
如果想要启动线程则肯定依靠Thread类,但是之前如果直接继承了Thread类,则可以将start()方法继承下来并使用,但是在Runnable接口中并没有此方法。
Thread类的构造:
public Thread(Runnable target)
就利用以上的构造方法,启动多线程。
package com.java.Thread;
public class RunnableDemo01 {
public static void main(String[] args){
RunnableThread01 mt1 = new RunnableThread01("线程A "); //实例化对象
RunnableThread01 mt2 = new RunnableThread01("线程B "); //实例化对象
Thread t1 = new Thread(mt1); //实例化Thread类对象
Thread t2 = new Thread(mt2); //实例化Thread类对象
t1.start(); //启动多线程
t2.start(); //启动多线程
}
}
class RunnableThread01 implements Runnable{ //实现Runnable接口,作为线程的实现类
private String name; //表示线程的名称
public RunnableThread01(String name){
this.name = name; //通过构造方法配置name属性
}
public void run(){ //覆写run方法,作为线程的操作主体
for(int i = 0; i < 10; i++){
System.out.println(name + "运行, i = " + i);
}
}
}
运行效果:
线程A 运行, i = 0
线程B 运行, i = 0
线程B 运行, i = 1
线程B 运行, i = 2
线程B 运行, i = 3
线程B 运行, i = 4
线程B 运行, i = 5
线程B 运行, i = 6
线程B 运行, i = 7
线程B 运行, i = 8
线程B 运行, i = 9
线程A 运行, i = 1
线程A 运行, i = 2
线程A 运行, i = 3
线程A 运行, i = 4
线程A 运行, i = 5
线程A 运行, i = 6
线程A 运行, i = 7
线程A 运行, i = 8
线程A 运行, i = 9
从运行结果来看,已经完成了多线程的功能。
3、Thread类与Runnable接口
(1)Thread类与Runnable接口的联系
Thread定义:
public class Thread
extends Object
implements Runnable
从定义格式上可以发现,Thread类也是Runnable接口的子类。
Thread类的定义:
从类的关系上来看,之前的做法类似于代理设计模式,Thread类完成比线程主体更多的操作。例如:分配CPU资源,判断是否已经启动等等。
(2)Thread类与Runnable接口的区别
使用Thread类在操作多线程的时候无法达到资源共享的目的,而使用Runnable接口实现的多线程操作可以实现资源共享。
package com.java.Thread;
public class ThreadDemo04 {
public static void main(String[] args){
MyThread04 mt1 = new MyThread04(); //实例化对象
MyThread04 mt2 = new MyThread04(); //实例化对象
MyThread04 mt3 = new MyThread04(); //实例化对象
mt1.run(); //调用线程主体
mt2.run(); //
mt3.run();
}
}
class MyThread04 extends Thread{ //继承Thread类,作为线程的实现类
private int ticket = 5; //表示一共有5张票
public void run(){ //覆写run方法,作为线程的操作主体
for(int i = 0; i < 100; i++){
if(this.ticket > 0)
System.out.println("卖票, ticket = " + ticket--);
}
}
}
运行效果:
卖票, ticket = 5
卖票, ticket = 4
卖票, ticket = 3
卖票, ticket = 2
卖票, ticket = 1
卖票, ticket = 5
卖票, ticket = 4
卖票, ticket = 3
卖票, ticket = 2
卖票, ticket = 1
卖票, ticket = 5
卖票, ticket = 4
卖票, ticket = 3
卖票, ticket = 2
卖票, ticket = 1
一共卖出了15张票。证明3个线程各自卖各自的5张票,也就是说现在没有达到资源共享的目的。
因为在每一个MyThread04对象中都包含各自的ticket属性。
如果现在使用Runnable接口呢?同样启动多个线程,那么所有的线程将卖出共同的5张票。
package com.java.Thread;
public class RunnableDemo02 {
public static void main(String[] args){
RunnableThread02 mt = new RunnableThread02(); //实例化对象
new Thread(mt).run(); //调用线程主体
new Thread(mt).run();
new Thread(mt).run();
}
}
class RunnableThread02 implements Runnable{ //实现Runnable接口,作为线程的实现类
private int ticket = 5; //表示一共有5张票
public void run(){ //覆写run方法,作为线程的操作主体
for(int i = 0; i < 100; i++){
if(this.ticket > 0)
System.out.println("卖票, ticket = " + ticket--);
}
}
}
运行效果:
卖票, ticket = 5
卖票, ticket = 4
卖票, ticket = 3
卖票, ticket = 2
卖票, ticket = 1
从运行结果来看,现在启动了3个线程,但是3个线程一共才卖出了5张票。所以达到了资源共享的目的。
3、Thread类与Runnable接口的使用结论
(1)实现Runnable接口比继承Thread类有如下的明显优点:
- 适合多个相同程序代码的线程去处理同一个资源。
- 可以避免由于单继承局限所带来的影响。
增强了程序的健壮性,代码能够被多个线程共享,代码与数据是独立的。
综合以上来看,开发中使用Runnable接口是最合适的。
三、线程的状态
多线程在操作中也是有一个固定的状态:
- 创建状态:准备好了一个多线程的对象,Thread t = new Thread()
- 就绪状态:调用了start()方法,等待CPU进行调度
- 运行状态:执行run()方法
- 阻塞状态:暂时停止执行,可能将资源交给其他线程使用
终止状态(死亡状态):线程执行完毕,不再使用。
状态之间的转换:
实际上,线程调用start()方法的时候不是立刻启动的,而是要等待CPU进行调度。
四、总结
1、线程与进程的区别,关系:
- 线程是在进程的基础上划分的。
- 线程消失了进程不会消失,进程如果消失了线程肯定会消失。
2、Java进程实现的两种方式:
- 继承Thread类。
- 实现Runnable接口。
3、线程的启动
通过start()方法完成,需要进行CPU调度。调用start()实际上调用的是run()方法。