对Java多线程得了解

来源:互联网 发布:梨园淘宝城贵吗 编辑:程序博客网 时间:2024/05/29 15:41

博主最近对多线程查看了很多资料,之前也很少用到,不过掌握多线程是每个程序员必须掌握的一项技能,今天将从3个大方面展开对多线程进行逐一得讲解。

1.什么是多线程

说到多线程,那么我们必须先了解什么是进程,什么又是线程?

DOS系统有一个非常明显的特点,只有一中病毒之后系统会立刻死机,因为传统的DOS是采用单进程的处理方式,所以只能有一个程序运行,其他程序无法运行。
Windows系统中,即使出现了病毒,系统照样可以使用,因为在windows中采用的是多进程的处理方式,那么在同一个时间段上会有多个程序同时运行。
线程实际上就是在进程的基础之上进一步划分,如果一个进程没有了,则线程肯定会消失,那么如果线程消失了,但进程未必会消失。而且所有的线程都是在进程的基础之上并发(同时运行)的。

下面用一幅图直观得表达一下:
这里写图片描述

如果现在同时运行多个任务,则所有的系统资源将是共享的,被所有线程所公用,但是程序处理需要CPU,传统的单核CPU来说,在同一时间段上会有多个程序执行,但是在同一个时间点上只能存在一个程序运行,也就是说所有程序都要抢占CPU资源。
但是现在的CPU已经发展到多核的状态了,在一个电脑上可能会存在多个CPU,那么这个时候就可以非常清楚的发现多线程操作间是如何并发执行的。

java中得多线程技术

在java中如果想实现多线程可以采用以下两种方式:
- 继承Thread类。
- 实现Runnable接口。

线程的运行流程

多线程在操作中也是有一个固定的操作状态的:
创建状态:准备好了一个多线程对象,Thread t = new Thread()
就绪状态:调用了start()方法,等待CPU进行调度。
运行状态:执行run()方法。
阻塞状态:暂时停止执行,可能将资源交给其他线程使用。
终止状态(死亡状态):线程执行完毕了,不再进行的使用了。
进程挂起、阻塞和睡眠的区别?
阻塞是进程在等待某种资源,但是不能马上得到,必须等待别的进程释放资源才能继续,属于被动无法得到时间片,内核就切换其他进程运行。休眠一般为主动的放弃一段CPU时间。挂起是运行时间片到了,内核要调度其他进程运行,被动式的失去CPU。

Thread类

继承Thread类
Thread类是在java.lang包中定义的,一个类只要继承了Thread类,此类就称为多线程的操作类,在Thread子类之中,必须明确的覆写Thread类中的run()方法,此方法为线程的主体。

// 继承Thread类,作为线程的实现类class MyThread extends Thread{ // 表示线程的名称private String name ; // 通过构造方法配置name属性 } public MyThread(String name){ this.name = name ; // 重写run()方法,作为线程 的操作主体  public void 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运行,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()方法。

// 继承Thread类,作为线程的实现类class MyThread extends Thread{ // 表示线程的名称private String name ; // 通过构造方法配置name属性 public MyThread(String name){ this.name = name ; } // 重写run()方法,作为线程 的操作主体public void 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() ;      }};

运行结果为:

线程A运行,i=0线程A运行,i=1线程A运行,i=2线程B运行,i=0线程A运行,i=3线程B运行,i=1线程A运行,i=4线程B运行,i=2线程A运行,i=5线程B运行,i=3线程A运行,i=6线程B运行,i=4线程A运行,i=7线程B运行,i=5线程B运行,i=6线程B运行,i=7线程B运行,i=8线程A运行,i=8线程A运行,i=9线程B运行,i=9

观察调用start()方法后确实起到了程序并发运行的效果,哪个线程抢夺到了CPU资源,哪个线程就先运行。

观察上面2个程序后我们为什么不直接调用run()方法,而是通过start()调用呢?

public synchronized void start() { if (threadStatus != 0) throw new IllegalThreadStateException();start0();if (stopBeforeStart) {  stop0(throwableFromStop);    } }  private native void start0();

start()方法有可能抛出异常。
stopBeforeStart是一个boolean类型的变量。
native 关键字表示的是一个由java调用本机操作系统函数的一个关键字。在java中,运行Java程序调用本机的操作系统的函数以完成特定的功能。
证明:如果现在要是想实现多线程的话,则肯定需要操作系统的支持,因为多线程操作中牵扯到一个抢占CPU的情况,要等待CPU进行调度,那么这一点肯定需要操作系统的底层支持,所以使用了native调用本机的系统函数,而且在各个操作系统中多线程的实现底层代码肯定是不同的,所以使用native关键字也可以让JVM自动调整不同的JVM的实现。
threadStatus也表示一种状态,如果线程已经启动了,再调用start()方法就有可能产生异常。

// 继承Thread类,作为线程的实现类class MyThread extends Thread{ // 表示线程的名称 private String name ; // 通过构造方法配置name属性 public MyThread(String name){ this.name = name ; } // 覆写run()方法,作为线程 的操作主体public void 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{ // 表示线程的名称private String name ; // 通过构造方法配置name属性public MyThread(String name){ this.name = name ; } public void run(){ // 重写run()方法,作为线程 的操作主体 for(int i=0;i<10;i++){ System.out.println(name + "运行,i = " + i) ; }     }};

如果想启动线程则肯定依靠Thread类,但是之前如果直接继承了Thread类,则可以将start()方法直接继承下来并使用,但是在Runnable接口中并没有此方法。

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 = 3 ; // 表示一共有3张票 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() ; // 调用线程主体     }};

运行结果为:

卖票:ticket = 3卖票:ticket = 2卖票:ticket = 1卖票:ticket = 3卖票:ticket = 2卖票:ticket = 1卖票:ticket = 3卖票:ticket = 2卖票:ticket = 1

发现一共卖出了9张票。证明:三个线程各自卖各自的票,也就是说并没有达到资源共享的目的,因为在每一个MyThread对象中都包含各自的ticket属性。
如果现在使用Runnable接口呢?同样启动多个线程,那么所有的线程将卖出共同的3张票。

// 继承Thread类,作为线程的实现类class MyThread implements Runnable{ // 表示一共有3张票private int ticket = 3 ; // 重写run()方法,作为线程 的操作主体  public void 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() ; // 调用线程主体     }};

运行结果为:

卖票:ticket = 3卖票:ticket = 2卖票:ticket = 1

从运行的结果看,虽然现在启动了三个线程,但是三个线程一共才卖出了3张票,达到了资源共享的目的。

整理干货

实现Runnable接口比继承Thread类有如下的明显优点:
- 适合多个相同程序代码的线程去处理同一个资源。
- 可以避免由于单继承局限所带来的影响。
- 增强了程序的健壮性,代码能够被多个线程共享,代码与数据是独立的。
- 综合以上来看,开发中使用Runnable接口是最适合的。

希望大家会对多线程有更深的理解,如有异议,请大家指出共同学习,我们不止会New。

原创粉丝点击