目标
了解进程与线程的区别。
掌握Java线程的两种实现方式及其区别。
了解线程的操作状态。
进程与线程
DOS系统有一个非常明显的特点,只有一中病毒之后系统会立刻死机,因为传统的DOS是采用单进程的处理方式,所以只能有一个程序运行,其他程序无法运行。
Windows系统中,即使出现了病毒,系统照样可以使用,因为在windows中采用的是多进程的处理方式,那么在同一个时间段上会有多个程序同时运行。
线程实际上就是在进程的基础之上进一步划分,从WORD来看,可以吧拼写检查当做一个进程进行处理。当然会存在多个线程。如果一个进程没有了,则线程肯定会消失,那么如果线程消失了,但进程未必会消失。而且所有的线程都是在进程的基础之上并发(同时运行)。
如果现在同时运行多个任务,则所有的系统资源将是共享的,被所有线程所公用,但是程序处理需要CPU,传统的单核CPU来说,在同一时间段上会有多个程序执行,但是在同一个时间点上只能存在一个程序运行,也就是说所有程序都要抢占CPU资源。
但是现在的CPU已经发展到多核的状态了,在一个电脑上可能会存在多个CPU,那么这个时候就可以非常清楚的发现多线程操作间是如何并发执行的。
java的多线程实现
在java中如果想实现多线程可以采用以下两种方式:
继承Thread类。
实现Runnable接口。
Thread类
继承Thread类
Thread类是在java.lang包中定义的,一个类只要继承了Thread类,此类就称为多线程的操作类,在Thread子类之中,必须明确的覆写Thread类中的run()方法,此方法为线程的主体。
java.lang包会在程序运行时自动导入,所以无需手工编写import语句。
一个类继承了Thread类之后,那么此类就具备了多线程的操作功能。
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) ;
}
}
};
public class ThreadDemo01{
public static void main(String args[]){
MyThread mt1 = new MyThread("线程A ") ; // 实例化对象
MyThread mt2 = new MyThread("线程B ") ; // 实例化对象
mt1.run() ; // 调用线程主体
mt2.run() ; // 调用线程主体
}
};
从此处的运行结果看,并未出现抢夺资源的现象。
以上程序是先执行完A之后再执行B,并未达到所谓并发执行的效果。
因为以上程序实际上还是按照古老的形式调用的,通过对象.方法,但是如果想启动一个线程必须使用Thread类中定义的start()方法。
一旦调用start()方法,实际上最终调用的就是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) ;
}
}
};
public class ThreadDemo02{
public static void main(String args[]){
MyThread mt1 = new MyThread("线程A ") ; // 实例化对象
MyThread mt2 = new MyThread("线程B ") ; // 实例化对象
mt1.start() ; // 调用线程主体
mt2.start() ; // 调用线程主体
}
};
观察调用start()方法后确实起到了程序并发运行的效果,哪个线程抢夺到了CPU资源,哪个线程就先运行。
问题1:
为什么不直接调用run()方法,而是通过start()调用呢?
如果要想解决这样的问题,查看java.lang.Thread类,发现定义如下:
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
start0();
if (stopBeforeStart) {
stop0(throwableFromStop);
}
}
private native void start0();
start()方法有可能抛出异常。
stopBeforeStart是一个boolean类型的变量。
native 关键字表示的是一个由java调用本机操作系统函数的一个关键字。在java中,运行Java程序调用本机的操作系统的函数以完成特定的功能。
证明:如果现在要是想实现多线程的话,则肯定需要操作系统的支持,因为多线程操作中牵扯到一个抢占CPU的情况,要等待CPU进行调度,那么这一点肯定需要操作系统的底层支持,所以使用了native调用本机的系统函数,而且在各个操作系统中多线程的实现底层代码肯定是不同的,所以使用native关键字也可以让JVM自动调整不同的JVM的实现。
threadStatus也表示一种状态,如果线程已经启动了,再调用start()方法就有可能产生异常。
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) ;
}
}
};
public class ThreadDemo03{
public static void main(String args[]){
MyThread mt1 = new MyThread("线程A ") ; // 实例化对象
mt1.start() ; // 调用线程主体
mt1.start() ; // 错误
}
};
Runnable接口
实现Runnable接口:在Java中也可以通过实现Runnable接口的方式实现多线程,Runnable接口中只定义了一个抽象方法。
public void run();
通过Runnable接口实现多线程:
class MyThread implements Runnable{ // 实现Runnable接口,作为线程的实现类
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) ;
}
}
};
如果想启动线程则肯定依靠Thread类,但是之前如果直接继承了Thread类,则可以将start()方法直接继承下来并使用,但是在Runnable接口中并没有此方法。
Thread类的构造:
public Thread(Runnable target);
就利用以上的构造方法启动多线程。
class MyThread implements Runnable{ // 实现Runnable接口,作为线程的实现类
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) ;
}
}
};
public class RunnableDemo01{
public static void main(String args[]){
MyThread mt1 = new MyThread("线程A ") ; // 实例化对象
MyThread mt2 = new MyThread("线程B ") ; // 实例化对象
Thread t1 = new Thread(mt1) ; // 实例化Thread类对象
Thread t2 = new Thread(mt2) ; // 实例化Thread类对象
t1.start() ; // 启动多线程
t2.start() ; // 启动多线程
}
};
从程序运行效果可以发现,已经完成了多线程的功能。
Thread类与Runnable接口
Thread类的定义:public class Thread extends Object implements Runnable
从定义的格式上可以发现,Thread类也是Runnable接口的子类。
从类的关系上来看,之前的做法非常类似于代理设计模式,Thread类完成比线程主体更多的操作,例如:分配CPU资源,判断是否已经启动等等。
Thread类与Runnable接口的区别
使用Thread类在操作多线程的时候无法达到资源共享的目的,而使用Runnable接口实现的多线程操作可以实现资源共享。
class MyThread 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--) ;
}
}
}
};
public class ThreadDemo04{
public static void main(String args[]){
MyThread mt1 = new MyThread() ; // 实例化对象
MyThread mt2 = new MyThread() ; // 实例化对象
MyThread mt3 = new MyThread() ; // 实例化对象
mt1.run() ; // 调用线程主体
mt2.run() ; // 调用线程主体
mt3.run() ; // 调用线程主体
}
};
发现一共卖出了15张票。证明:三个线程各自卖各自的票,也就是说并没有达到资源共享的目的。
因为在每一个MyThread对象中都包含各自的ticket属性。
如果现在使用Runnable接口呢?同样启动多个线程,那么所有的线程将卖出共同的5张票。
class MyThread implements Runnable{ // 继承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--) ;
}
}
}
};
public class RunnableDemo02{
public static void main(String args[]){
MyThread mt = new MyThread() ; // 实例化对象
new Thread(mt).run() ; // 调用线程主体
new Thread(mt).run() ; // 调用线程主体
new Thread(mt).run() ; // 调用线程主体
}
};
从运行的结果看,虽然现在启动了三个线程,但是三个线程一共才卖出了5张票,达到了资源共享的目的。
Thread类与Runnable接口的使用结论
实现Runnable接口比继承Thread类有如下的明显优点:
适合多个相同程序代码的线程去处理同一个资源。
可以避免由于单继承局限所带来的影响。
增强了程序的健壮性,代码能够被多个线程共享,代码与数据是独立的。
综合以上来看,开发中使用Runnable接口是最适合的。
线程的状态:
多线程在操作中也是有一个固定的操作状态的:
创建状态:准备好了一个多线程对象,Thread t = new Thread()
就绪状态:调用了start()方法,等待CPU进行调度。
运行状态:执行run()方法。
阻塞状态:暂时停止执行,可能将资源交给其他线程使用。
终止状态(死亡状态):线程执行完毕了,不再进行的使用了。
进程挂起、阻塞和睡眠的区别?
阻塞是进程在等待某种资源,但是不能马上得到,必须等待别的进程释放资源才能继续,属于被动无法得到时间片,内核就切换其他进程运行。
休眠一般为主动的放弃一段CPU时间。挂起是运行时间片到了,内核要调度其他进程运行,被动式的失去CPU。