本章目标
- 了解进程与线程的区别
- 掌握java多线程的两种实现方式及区别,继承Thread类、实现Runnable接口
- 了解线程的状态变化
- 了解多线程的主要操作方法
- 了解同步及死锁的概念
- 了解线程的生命周期
索引
9.1 进程与线程
9.2 Java中线程的实现
9.3 线程的状态
9.4 线程操作的相关方法
9.1 进程与线程
进程是程序的一次动态执行过程,它经历了从代码加载、执行到执行完毕的一个顽症过程,这个过程也是从进程本身从产生、发展到最终消亡的过程。多进程操作系统能同时运行多个进程(程序),由于CPU具备分时机制,所以每个进程都能循环获得自己的CPU时间片。 由于CPU执行速度非常快,使得所有程序好像是在“同时”运行一样。
多线程是实现并发机制的一种有效手段。进程和线程一样,都是实现并发的一个基本单位。 线程是比进程更小的执行单位,线程是在进程的基础上进行的进一步划分。所谓多线程是指一个进程在执行过程中可以产生多个线程,这些线程可以同时存在、同时运行, 一个进程可能包含了多个同时执行的线程。 进程与线程的区别如下图所示:
Java的多线程打破了传统程序中一次只能运行一个程序块的束缚。 所谓的线程(Thread)是指程序的运行流程,多线程机制则是指同时运行多个程序块,使程序运行的效率变得更高,也可克服传统程序语言所无法解决的问题。例如,有些包含循环的线程可能要使用一段时间来运算,此时便可让另一个线程来做其他的处理。
线程与进程的区别,可以参见此处的博文来理解!!!
9.2 Java中线程的实现
Java中实现多线程有两种方法: 一种是继承Thread类;另一种就是实现Runnable接口。
9.2.1 继承Thread类
Thread类是在java.lang包中定义的,一个类只要继承了Thread类,此类就称为多线程操作类。 在Thread子类中,必须明确地覆写run()方法,此方法为线程的主体。
线程类的定义语法如下:
class 类名称 extends Thread{ //继承Thread类
属性...; //类中定义属性
方法...; //类中定义方法
public void run(){ //覆写父类Thread中的run()方法,此方法是线程的主体
线程主体;
}
}
下面使用以上的格式进行多线程的实现,范例如下:
class MyThread extends Thread { //继承Thread类
private String name; //在类中定义属性,表示线程的名称
public MyThread(String name){ //通过构造方法设置name的内容
this.name = name;
}
public void run(){ //覆写Thread类中的run()方法
for(int i = 0; i < 5; i++){ //输出
System.out.println(this.name + " is running, i = " + i);
try {
Thread.sleep(500);
}catch(Exception e){
e.printStackTrace();
}
}
}
}
public class ThreadDemo01{
public static void main(String args[]){
MyThread mt1 = new MyThread("Thread AAA"); //实例化对象
MyThread mt2 = new MyThread("Thread BBB");
<span style="color:#ff0000;"><del>mt1.run();</del></span> //调用线程的主体!!!
<span style="color:#ff0000;"><del>mt2.run();</del></span>
}
}
输出结果为:
-------------------------------------------------
Thread AAA is running, i = 0
Thread AAA is running, i = 1
Thread AAA is running, i = 2
Thread AAA is running, i = 3
Thread AAA is running, i = 4
Thread BBB is running, i = 0
Thread BBB is running, i = 1
Thread BBB is running, i = 2
Thread BBB is running, i = 3
Thread BBB is running, i = 4
-------------------------------------------------
从输出结果可以看出,程序是先执行完线程mt1对象之后再执行线程mt2对象,并没有交错执行,也就是说,此时线程实际上并没有被启动,还是属于传统的顺序式的执行方法,那么该如何启动线程呢?
如果要正确地启动线程,是不能直接调用线程的run()方法的,而是应该调用从Thread类中继承而来的start()方法,具体代码如下:
class MyThread extends Thread { //继承Thread类
private String name; //在类中定义属性,表示线程的名称
public MyThread(String name){ //通过构造方法设置name的内容
this.name = name;
}
public void run(){ //覆写Thread类中的run()方法
for(int i = 0; i < 5; i++){ //输出
System.out.println(this.name + " is running, i = " + i);
try { //在线程运行时等待0.5s,更好的看出线程是否同时运行
Thread.sleep(500);
}catch(Exception e){
e.printStackTrace();
}
}
}
}
public class ThreadDemo01{
public static void main(String args[]){
MyThread mt1 = new MyThread("Thread AAA"); //实例化对象
MyThread mt2 = new MyThread("Thread BBB");
/*
mt1.run(); //调用线程的主体!!!
mt2.run();
*/
<span style="color:#ff0000;">mt1.start();</span> //启动多线程
<span style="color:#ff0000;">mt2.start();</span>
}
}
此时的运行结果是
-------------------------------------------------
Thread AAA is running, i = 0
Thread BBB is running, i = 0
Thread AAA is running, i = 1
Thread BBB is running, i = 1
Thread AAA is running, i = 2
Thread BBB is running, i = 2
Thread AAA is running, i = 3
Thread BBB is running, i = 3
Thread AAA is running, i = 4
Thread BBB is running, i = 4
-------------------------------------------------
此时,两个线程是同时交错运行的。哪个线程对象抢到了CPU资源,哪个线程就可以运行,所以程序每次的运行结果是不一样的。在线程启动时虽然调用的是start()方法,但实际上调用的却是run()方法的主体。
- 为什么启动线程不能直接使用run()方法
线程的允许需要本机操作系统的支持。 Thread类中start()方法的定义如下:
public synchronized void start(){
if (threadStatus != 0)
throw new IllegalThreadStateException();
...
start0();
...
}
private <span style="color:#ff0000;">native</span> void start0();
从start()方法在Thread类中的定义可以发现, 在一个类中的start()方法调用时可能会抛出“IllegalThreadStateException”异常,一般在重复调用statr()方法时会抛出这个异常。而且实际上此处真正调用的是start0()方法,此方法在声明处使用了native关键字声明。。native关键字表示调用本机的 操作系统函数,因为多线程的实现需要依靠底层操作系统支持。
如果一个类通过继承Thread类来实现,那么只能调用一次start()方法;如果调用多次,则会抛出“IllegalThreadStateException”异常。如下例子,
class MyThread extends Thread { //继承Thread类
private String name; //在类中定义属性,表示线程的名称
public MyThread(String name){ //通过构造方法设置name的内容
this.name = name;
}
public void run(){ //覆写Thread类中的run()方法
for(int i = 0; i < 5; i++){ //输出
System.out.println(this.name + " is running, i = " + i);
try {
Thread.sleep(500);
}catch(Exception e){
e.printStackTrace();
}
}
}
}
public class ThreadDemo01{
public static void main(String args[]){
MyThread mt1 = new MyThread("Thread AAA"); //实例化对象
<span style="color:#ff0000;">mt1.start();</span> //启动多线程
<span style="color:#ff0000;"><del>mt1.start();</del></span> //再次启动线程
}
}
运行结果如下
-------------------------------------------------
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.start(Unknown Source)
at ThreadDemo01.main(ThreadDemo01.java:21)
Thread AAA is running, i = 0
Thread AAA is running, i = 1
Thread AAA is running, i = 2
Thread AAA is running, i = 3
Thread AAA is running, i = 4
执行错误-------------------------------------------------
多线程如果依靠继承Thread类来实现的,那么必定会受到单继承局限的影响。所以一般来讲,实现多线程还可以通过实现Runnable接口完成。
9.2.2 实现Runnable接口
在java中也可以通过实现Runnable接口的方法实现多线程, Runnable接口中只定义了一个抽象方法 public void run();
使用Runnable接口实现多线程的格式如下:
class 类名称 implements Runnable{ //实现Runnable接口
属性...; //类中定义属性
方法...; //类中定义方法
public void run(){ //覆写Runnable接口中的抽象方法run()
线程主体;
}
}
使用实现Runnable接口来实现多线程,例如:
class MyThread implements Runnable{
private String name;
public MyThread(String name){
this.name = name;
}
public void run(){
for (int i = 0 ; i < 5; i++){
System.out.println(name + " is running, i = " + i);
try{
Thread.sleep(500);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
以上代码通过实现Runnable接口实现多线程,但是这样一来就会有一个新的问题: 想要启动一个多线程必须要使用start()方法完成,如果继承了Thread类,则可以直接从Thread类中使用start()方法,但是现在实现的是Runnable接口,那么该如何启动多线程呢??实际上,此时还是要依靠Thread类完成启动,在Thread类中提供了一下两个构造方法:
<span style="font-size:14px;color:#ff0000;">public Thread(Runnable target)</span>
<span style="font-size:14px;color:#ff0000;">public Thread(Runnable target, String name)</span>
这两个构造方法都可以接受Runnable的子类实例对象,所以就俄可以依靠此点来启动多线程。。如下所示:
class MyThread implements Runnable{ //实现Runnable接口
private String name;
public MyThread(String name){ //通过构造方法设置属性内容
this.name = name;
}
public void run(){ //覆写Runnable接口中的抽象方法run()
for (int i = 0 ; i < 5; i++){
System.out.println(name + " is running, i = " + i);
try{
Thread.sleep(500);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
public class ThreadDemo02{
public static void main(String args[]){
MyThread mt1 = new MyThread("Thread A"); //实例化Runnable接口的子类对象
MyThread mt2 = new MyThread("Thread B");
Thread t1 = new Thread(mt1); //实例化Thread类对象
Thread t2 = new Thread(mt2);
t1.start(); //启动线程
t2.start();
}
}
运行结果如下:
-------------------------------------------------
Thread A is running, i = 0
Thread B is running, i = 0
Thread A is running, i = 1
Thread B is running, i = 1
Thread A is running, i = 2
Thread B is running, i = 2
Thread A is running, i = 3
Thread B is running, i = 3
Thread A is running, i = 4
Thread B is running, i = 4
-------------------------------------------------
****************从以上两种实现可以发现,无论使用哪种方法,最终都必须依靠Thread类才能启动多线程********************
9.2.3 Thread类和Runnable接口
通过Thread类和Runnable接口都可以实现多线程,那么两者有哪些联系和区别呢? 下面观察Thread类的定义。
public class Thread extends Object implements Runnable
从Thread类的定义可以清楚地发现,Thread类也是Runnable接口的子类,但在Thread类中并没有完全地实现Runnable接口中的run()方法。下面是Thread类的部分定义:
private Runnable target;
public Thread(Runnable target, String name){
init(null, target, name, 0);
}
private void init(ThreadGroup g, Runnable target, String name, long stackSize){
...
this.target = target;
...
}
public void run(){
if (target != null){
target.run();
}
}
从上面定义可以发现,在Thread类中的run()方法调用的是Runnable接口中的run()方法,也就是说此方法是由Runnable子类完成的,所以如果要通过继承Thread类实现多线程,则必须覆写run()方法。
Thread和Runnable的子类都同时实现了Runnable接口,之后将Runnable的子类实例放到了Thread类之中,如下图所示,这种操作模式和代理设计类似。
实际上Thread类和Runnable接口之间在使用上也是有区别的。如果一个类继承Thread类,则不适合于多个县城共享资源,而实现了Runnable接口,就可以方便地实现资源的共享。。。
例如: 利用Thread类实现多线程
class MyThread extends Thread{ //继承Thread类
private int ticket = 2; //一共5张票
public void run(){ //覆写run()方法
for(int i = 0; i < 100; i++){ //超出票数的循环
if(ticket > 0){ //判断是否有剩余票
System.out.println("卖票: ticket = " + ticket--);
}
}
}
}
public class ThreadDemo04{
public static void main(String args[]){
MyThread mt1 = new MyThread(); //定义线程对象
MyThread mt2 = new MyThread();
mt1.start();
mt2.start();
}
}
输出结果是:
-------------------------------------------------
卖票: ticket = 2
卖票: ticket = 1
卖票: ticket = 2
卖票: ticket = 1
-------------------------------------------------
以上程序通过Thread类实现多线程,程序中启动了2个线程,但是2个线程却分别卖了各自的2张票,并没有达到资源共享的目的。
接下来,利用Runnable接口实现多线程:
class MyThread implements Runnable{
private int ticket =2;
public void run(){
for(int i = 0; i < 100; i++){
if(ticket > 0){
System.out.println("卖票:ticket = " + ticket--);
}
}
}
}
public class ThreadDemo05{
public static void main(String args[]){
MyThread mt = new MyThread();
Thread t1 = new Thread(mt);
Thread t2 = new Thread(mt);
t1.start();
t2.start();
new Thread(mt).start();
}
}
运行结果如下: 达到了资源的共享:
-------------------------------------------------
卖票:ticket = 2
卖票:ticket = 1
-------------------------------------------------
程序运行结束!
从上面程序的运行结果发现,虽然启动了三个线程,但是3个线程一共才卖出了2张票,即ticket属性被所有的线程对象共享。
可见,实现Runnable接口相对于继承Thread类来说,有如下的显著优势:
- 适合多个相同程序代码的线程去处理同一资源的情况
- 可以避免由于java的单继承特性带来的局限性
- 增强了程序的健壮性,代码能够被多个线程共享,代码与数据是相对独立的。
思考,为什么以下程序也实现不了资源的共享: 由一个线程类分别构造了2个对象,所以各自有各自的数据,对象之间是数据不共享的。
class MyThread implements Runnable{
private int ticket =2;
public void run(){
for(int i = 0; i < 100; i++){
if(ticket > 0){
System.out.println("卖票:ticket = " + ticket--);
}
}
}
}
public class ThreadDemo05{
public static void main(String args[]){
MyThread mt1 = new MyThread();
MyThread mt2 = new MyThread();
Thread t1 = new Thread(mt1);
Thread t2 = new Thread(mt2);
t1.start();
t2.start();
}
}
以上代码运行结果是:
-------------------------------------------------
卖票:ticket = 2
卖票:ticket = 1
卖票:ticket = 2
卖票:ticket = 1
-------------------------------------------------
代码与上面通过Runnable接口实现多线程的例子有何区别, 此时ThreadDemo07只实例化了一个MyThread对象, 然后t1、t2、t3共享了该对象资源!!!
<pre class="java" name="code">class MyThread implements Runnable{
private String name;
private int ticket = 3;
public MyThread(String name){
this.name = name;
}
public void run(){
for(int i = 0; i < 100; i++){
if (ticket > 0){
System.out.println(Thread.currentThread().getName() + "卖票: " + ticket--);
}
}
}
}
public class ThreadDemo05{
public static void main(String args[]){
MyThread mt = new MyThread("Thread A");
Thread t1 = new Thread(mt);
Thread t2 = new Thread(mt);
t1.start();
t2.start();
new Thread(mt).start();
}
}
运行结果是: 因为票数过少,所以只会出现一个进程名称!!!
-------------------------------------------------
Thread-0卖票: 3
Thread-0卖票: 2
Thread-0卖票: 1
-------------------------------------------------
9.3 线程的状态
要想实现多线程,必须在主线程(main方法就是一个主线程)中创建新的线程对象,任何线程一般具有5种状态,即: 创建、运行、阻塞、终止。 线程状态的转移与方法之间的关系可用下图表示: 图片用的是作者DreamSea的。
- 创建状态 在程序中用构造方法创建了一个线程对象后,新的线程对象便处于新建状态,此时,它已经有了相应的内存空间和其他资源,但还处于不可运行状态。新建一个线程对象可采用Thread类的构造方法来实现,例如“Thread thread = new Thread()”。
- 就绪状态 新建线程对象后,调用该线程的start()方法就可以启动线程。当线程启动时,线程进入了就绪状态。此时,线程将进入线程队列排队,等待CPU服务,这表明它已经具备了运行条件。
- 运行状态 当就绪状态的线程被调用并获得处理器资源时,线程就进入了运行状态。此时,自动调用该线程对象的run()方法。run()方法定义了该线程的操作和功能。
- 阻塞状态 一个正在执行的线程在某些特殊情况下,如被认为挂起或需要执行耗时的输入/输出操作时,将让出CPU并暂时中止自己的执行,进入阻塞状态。在可执行状态下,如果调用sleep()、suspend()、wait()等方法,线程都将进入阻塞状态。堵塞时,线程不能进入排队队列,只有当引起堵塞得原因被消除后,线程才可以转入就绪状态。
- 终止状态 线程调用stop()方法时或run()方法执行结束后,即处于死亡状态。处于死亡状态的线程不具有继续运行的能力。
9.4 线程操作的相关方法
9.4.1 取得和设置线程名称
9.4.2 判断线程是否启动
9.4.3 线程的强制运行
9.4.4 线程的休眠
9.4.5 中断线程
9.4.6 后台线程
9.4.7 线程的优先级
9.4.8 线程的礼让