Java多线程——基本概念

来源:互联网 发布:mysql导出所有数据库 编辑:程序博客网 时间:2024/06/04 23:20

一、程序、进程、进程
程序:
一组指令集,是一个静态的实体(代码)。

进程:
是系统给“程序”分配必要资源(CPU时间、地址空间)后执行程序的动态过程,有自己的生命周期。进程是系统进行资源分配和调度的一个基本单元,每个进程都是独立的。

线程:
为什么需要“线程”?充分利用CPU空闲时间,提升程序执行效率。
执行一个程序(单一流程)时CPU大部分时间是空闲的。例如等待用户输入时CPU处于空闲,这段时间可以用来执行其他操作,比如处理已输入的内容,这就需要加入一个新的“执行流程”来完成这个处理任务,这就引入了“线程”的概念。
线程是“进程”中一个单一的连续控制流程。进程可以包含多个线程,同时完成多个任务。
线程是CPU调度和分派的基本单位,是比进程更小的能独立运行的基本单位,能并发执行。
在单处理器系统中,“并发执行”只是一个等效概念,线程按照分配的“时间片”随机交替执行。
这里写图片描述

进程与线程概念对比:
进程和线程的主要差别在于它们是操作系统不同的资源管理方式
1、进程有独立的地址空间。一个进程崩溃后,在保护模式下不会对其它进程产生影响。
2、线程是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但没有单独的地址空间。一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。
3、进程在执行过程中拥有独立的内存单元,线程共享所在进程的内存空间,进程间不需要额外的通信机制,能直接进行数据交换、同步操作,从而极大地提高了程序的运行效率。
4、线程有独立的运行栈和程序计数器。

二、创建和使用线程
上面介绍了线程的概念,那么在Java程序中,如何创建和使用线程呢。使用线程执行任务需要两个对象:任务、线程。
可执行对象(任务)在Java中用java.lang.Runnable接口定义,具体的任务需要实现这个接口。线程类是java.lang.Thread,通过Thread对象即可执行这个“任务”。创建Thread对象,即开辟一条新的线程。以下示例功能是开启两条线程,分别打印字符,代码如下:

public class TestThread {    public static void main(String[] args) {        PirntMsg printA = new PirntMsg("A");// 创建可执行对象        PirntMsg printB = new PirntMsg("B");        Thread thread1 = new Thread(printA);// 创建线程,并加入可执行对象        Thread thread2 = new Thread(printB);        thread1.start();//启动线程,执行任务        thread2.start();    }}/** * 打印消息的任务 */class PirntMsg implements Runnable {    private String msg;    public PirntMsg(String msg) {        this.msg = msg;    }    public void run() {        System.out.println(msg); // run()方法中定义,任务的具体逻辑。    }}

执行任务即可打印出A、B两个字符。字符打印顺序是随机的,这是CUP任务调度的随机性造成的。举个不太恰当的比喻,Runnable对象就像一辆货车,Thread对象则是货车行驶的公路。
当然开启线程也可以这与定义:

public class TestBlock {    public static void main(String[] args) {        PrintThread print  = new PrintThread("A");        print.start();    }}class PrintThread extends Thread{    private String msg;    public PrintThread(String msg) {        this.msg = msg;    }    public void run() {        super.run();        System.out.println(msg);    }}

这种方式将任务与执行任务的机制混合在了一起,不推荐这与写。通过实现Runnable的方式定义线程任务,可以同时继承其他类,通用性更高。

除了实现Runnable接口定义任务外,还可以通过实现Callable接口定义任务。这种方式在后面讲到线程池的时候再介绍。

三、Thread类
这里简单介绍Thread类的几个常用方法,start()、isAlive()、setPriority(int newPriority)、join()、sleep(int millis)、yield()和interrupt()。
1、start()
Java虚拟机调用线程的run()方法,启动线程执行任务。启动后,新线程和当前宿主线程并发执行,子线程任务执行完成后,自动关闭。需要注意的是,线程如果已经处于运行状态,不可重复启动,否则抛出异常。

2、isAlive()
检测当前线程是否还“活着”(已经启动,并且没有死亡)。

3、setPriority(int newPriority)
设置线程的优先级。优先级采用数值定义,具体取值范围与操作系统有关。优先级越高的线程,获取CPU调度的机会越大,即一定时间内,优先级高的线程占用的执行时间越长,越快完成任务。Java中定义了三个常量:Thread.MIN_PRIORITY、Thread.NORM_PRIORITY、Thread.MAX_PRIORITY,依次代表低、中等、高优先级。

4、join()
等待此线程执行完毕后,再继续原线程。示例如下:

public class TestJoin {    public static void main(String[] args) {        Thread threadA = new Thread(new PirntA());        threadA.start();    }}class PirntA implements Runnable {    public void run() {        for (int i = 0; i < 5; i++) {            System.out.print("A");            if (i == 2) {                Thread threadB = new Thread(new PirntB());                threadB.start();                try {                    threadB.join();// 插入PirntB任务执行                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }    }}class PirntB implements Runnable {    public void run() {        for (int i = 0; i < 5; i++) {            System.out.print("B");        }    }}

启动线程PirntA打印5个字符A,打印完第三个字符后,join一个线程PirntB打印5个B。执行结果为: AAABBBBBAA。
新线程join()后会立执行,即使当前线程处于阻塞状态也会正常执行插入的线程。另外重载方法 join(long millis),可指定原线程等待插入线程的毫秒数。例如在threadA中调用threadB.join(2000),则插入threadB后,threadA只阻塞2000ms,不论threadB是否执行完成,threadA都将继续执行。

5、sleep(int millis)
参数millis代表毫秒数(千分之一秒)。可以让调用它的线程沉睡(停止运行)指定的时间,之后自动醒来,变为可运行状态,但这并不表示它马上就会被运行,因为线程调度机制(见下文)恢复线程的运行也需要时间。调用sleep()需要捕获InterruptedException异常,这是因为线程在sleep()期间可能被其它线程调用Thread类的interrupt()打断。如果不捕获这个异常,线程就会异常终止。sleep()不会释放同步锁。

try {            Thread.sleep(2000); // 睡眠2秒        } catch (InterruptedException e) {            e.printStackTrace();        }

这里顺便讲讲Object类的wait()方法:
wari()和wait(long timeout,int nanos)方法都是基于wait(long timeout)方法实现的。调用某个对象的wait()后,调用的线程就会转入等待状态,等待别的线程再次调用这个对象的notify()或者notifyAll()方法唤醒它;或者等到指定的最大等待时间,线程自动醒来。wait()同样可能被interrupt()中断,效果同sleep()方法被中断一样。wait()会释放同步锁,且不限于这个被调用了wait()方法的对象。

6、yield()
Thread.yield()方法作用是:暂停正在执行的线程,回到可运行状态。使用yield()的目的是让出cpu时间,给予其他线程执行的机会。但无法保证yield()达到让步目的,因为让步的线程可能再次被线程调度程序选中。注意:yield()不会导致线程阻塞,只是简单地让出时间片。

7、interrupt()
interrupt()用于中断线程。它通过修改被调用线程的中断状态来告知这个线程它被中断了。对于处于非阻塞状态(就绪、运行状态)的线程,只是改变了中断状态,Thread.isInterrupted()将返回true;对于处于阻塞状态的线程, 比如等待在这些函数上的线程:Thread.sleep()、Object.wait()、 Thread.join(),这个线程收到中断信号后, 会抛出InterruptedException,同时会把中断状态置回为true。可用Thread.interrupted()复位中断状态。
注意:
a、中断一个线程只是为了引起该线程的注意,被中断线程可以决定如何应对中断,线程不会终止。但是当线程被阻塞的时候,如被wait或 join阻塞时,它没有占用CPU,此时调用它的interrput()方法,线程不会修改自己的中断状态的,这会产生一个InterruptedException异常。
b、不是所有的阻塞方法收到中断后都可以取消阻塞状态。I/O类会阻塞等待 I/O 完成,但是不抛出 InterruptedException,且不会退出阻塞状态。
c、尝试获取一个内部锁的操作(进入一个 synchronized 块)是不能被中断的,但是 ReentrantLock 支持可中断的获取模式即 tryLock(long time, TimeUnit unit)。

中断线程的使用场景:
线程中为了等待一些特定条件的到来, 调用了Thread.sleep(10000),预期线程睡10秒之后自己醒来, 但是如果这个特定条件提前到来的话,可以用interrupt()来通知线程提前醒来。

//Interrupted的经典使用代码        public void run(){                try{                     ....                     while(!Thread.currentThread().isInterrupted()&& more work to do){                            // do more work;                     }                }catch(InterruptedException e){                            // thread was interrupted during sleep or wait                }                finally{                           // cleanup, if required                }        }    

四、线程的生命周期

这里写图片描述

1、新建(new)
当创建Thread类的一个实例(new Thread())时,此线程进入新建状态(未被启动)。

2、就绪(runnable)
调用线程的start()后就进入就绪状态,就绪状体是可运行状态,还没开始运行,正在等待CPU为他分配时间。此时线程正在就绪队列中排队等候得到CPU资源。

3、运行(running)
就绪状态的线程获得CPU时间后,进入运行状态开始执行任务(run()方法体)。如果本次CPU分配的时间片用完,或者线程主动调用yield(),线程将回到就绪状态。

4、堵塞(blocked)
运行状态的线程,被调用了jion()、sleep()方法,或者线程中锁对象的wait()方法,线程会让出CPU并暂停自己的执行,即进入堵塞状态。阻塞解除后,线程将回到就绪状态。

5、死亡(dead)
当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不能再进入就绪状态。
自然终止:正常运行run()方法后终止
异常终止:调用stop()方法让一个线程终止运行

1 0
原创粉丝点击