Java并发编程(一)线程创建、生命周期、控制

来源:互联网 发布:免身份证开卡软件 编辑:程序博客网 时间:2024/06/09 13:54

进程和线程

进程是处于运行中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单元。
特征:

  • 独立性:进程是系统中独立存在的实体,拥有自己的独立资源,没一个进程拥有自己私有的地址空间。没有经过允许的情况下,一个进程是不可以访问其他进程的地址空间。
  • 动态性:进程与程序区别,一个是静态指令集合,一个是正在系统中活动的指令集合,加入了事件的概念。进程具有自己的生命周期和状态,程序没有。
  • 并发性:多个进程可以在一个处理器上并发执行,不会互相影响。(注意并发与并行的区别)

    进程调度策略:

    • 共用式多任务操作策略(协作式)
    • 抢占式多任务操作策略

线程也被称作轻量级进程,线程是进程的执行单元。各个线程之间共享进程的内存空间(代码段、数据段和堆空间)及一些进程级的资源(如打开的文件),但是各个线程都拥有自己的堆栈,程序计数器和局部变量。线程的执行是抢占式的。

多线程编程优点:

  • 使用多线程可以减少程序的响应时间,如果某个操作很耗时,或者陷入长时间的等待,此时程序将不会响应鼠标和键盘等的操作,使用多线程后可以把这个耗时的线程分配到一个单独的线程去执行,从而使程序具备了更好的交互性。
  • 与进程相比,线程创建和切换开销更小,同时多线程在数据共享方面效率非常高。
  • 多CPU或者多核计算机本身就具备执行多线程的能力,如果使用单个进程,将无法重复利用计算机资源,造成资源的巨大浪费。在多CPU计算机使用多线程能提高CPU的利用率。
  • 使用多线程能简化程序的结构,使程序便于理解和维护。

创建线程

主要有三种方式

继承Thread类

以下是主要步骤:
(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
(2)创建Thread子类的实例,即创建了线程对象。
(3)调用线程对象的start()方法来启动该线程。

   public class TestThread extends Thread {        int i;        public void run() {            for (; i < 100; i++)            System.out.println(getName() + " " + i);        }    }   public void main(String[] args) {       Thread mThread = new TestThread();       mThread.start();    } 

使用继承Thread类的方式,多个线程之间无法共享线程类的实例变量。

实现Runnable接口

以下是主要步骤:
(1)自定义类并实现Runnable接口,实现run()方法。
(2)创建Thread子类的实例,用实现Runnable接口的对象作为target实例化该Thread对象。
(3)调用Thread的start()方法来启动该线程。

    public class TestRunnable implements Runnable {        int i;        public void run() {            for (; i < 100; i++)              System.out.println(Thread.currentThread().getName() + " " + i);        }    }  public void main(String[] args) {       TestRunnable mTestRunnable = new TestRunnable();             Thread mThread = new Thread(mTestRunnable);       mThread.start();    }

使用实现Runnable接口的方式,多个线程之间可以共享线程类的实例变量。

使用Callable和Future

Callable接口实际是属于Executor框架中的功能类,Callable接口与Runnable接口的功能类似,但提供了比Runnable更强大的功能,主要表现为:

  • Callable中的call()方法提供一个返回值
  • Callable中的call()方法可以抛出异常

两个问题:
1. Callable不是Runnable的子接口,无法直接作为Thread的target?
2. call()不是直接调用,而是作为线程执行体被调用,那么如何获取返回值呢?

Java5提供Future接口来代表Callable接口中call()方法返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口,且实现了Runnable接口,可以直接作为Thread的target。

Future接口提供了如下几个方法控制它关联的Callable任务:

boolean cancle(boolean mayInterruptIfRunning)V get()//获取Callable任务返回值,该方法将导致线程阻塞,必须等到子线程结束才会得到返回值V get(long timeout, TimeUnit unit)boolean isCancelled()boolean isDone()

以下是主要步骤:
(1)创建Callable接口实现类,实现call()方法,再创建Callable实现类实例。
(2)用FutureTask类来包装Callable对象。
(3)使用FutureTask对象作为Thread的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行后的返回值。

      public void main(String[] args) {            FutureTask<String> task = new FutureTask<>(new Callable<String>() {                @Override                public String call() throws Exception {                    return "Hello World";                }            });            new Thread(task).start();            try {                System.out.print(task.get()); //阻塞线程            } catch (Exception e) {                e.printStackTrace();            }        }

三种方式对比

采用Runnable/Callable:

优点:

  • 线程类只是实现接口,还可以继承其他类
  • 多个线程可以共享同一个target对象,适合多个线程处理同一份资源情况

缺点:

  • 编程稍复杂,需要用Thread.currentThread()访问当前线程

采用Thread

优点:

  • 编程简单,使用this即可获取当前线程

缺点:

  • 不能继承另外父类

线程生命周期

线程状态

(1). 新建状态(New):新创建了一个线程对象。
(2). 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
(3). 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
(4). 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

  • 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
  • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
  • 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

(5). 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

这里写图片描述

控制线程

join线程

当在某个程序执行流中调用其他线程的join方法时,调用线程将被阻塞,直到被join的线程执行完为止。
join()方法通常由使用线程的程序调用,以将大问题分解成许多小问题,每个小问题分配一个线程。当所有的小问题都处理后,再调用主线程来进一步操作。

join() //等待被join的线程执行完成join(long millis) //如果在millis毫秒内还没有执行结束,则不再等待join(long millis,int nanos) //millis毫秒加nanos微秒
        public class JoinThread extends Thread {            public void run() {               for (int i = 0; i < 100; i++) {                   System.out.print(getName() + " " + i);               }            }        }        public void main(String[] args) throws Exception{            for (int i = 0; i < 100; i++) {                if (i == 20) {                    JoinThread jt = new JoinThread();                    jt.start();                    jt.join();                }                System.out.print(Thread.currentThread().getName() + " " + i);            }        }

后台线程

后台线程是为其他线程提供服务的,又称“守护线程”或“精灵线程”。JVM垃圾回收线程就是典型的后台线程。

特征:所有前台线程都死亡,后台线程自动死亡。

setDaemon(true); /**将线程转换为守护线程*/isDaemon(); //判断是否后台线程
...DeamonThread t = new DeamonThread();t.setDaemon(true);t.start(); 

注意setDaemon必须在start之前执行,否则引发IllegalThreadStateException。

线程睡眠sleep

调用Thread类静态方法实现当前运行线程睡眠。调用后当前线程处于阻塞状态,等待睡眠时间到。

static void sleep(long millis)static void sleep(long millis, int nanos)

线程让步yield

调用Thread类静态方法实现当前运行线程让步。调用后当前线程处于就绪状态,等待被CPU调用。

sleep()方法与yield()方法区别:

  • sleep()方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程优先级。yield()方法只会给同优先级或更高优先级线程执行机会。
  • sleep()方法将当前线程转入阻塞状态,而yield()强制将当前线程转入就绪状态,因此完全可能某个线程调用yield()后立即再次获得CPU资源。
  • sleep()方法申明抛出InterruptException异常,要么捕捉要么显示抛出,而yield()没有申明抛出任何异常。
  • sleep()比yield()有更好的移植性,不建议yield()控制并发线程执行。

线程优先级

Thread类提供了setPriority(int newPriority)、getPriority()方法设置和返回线程优先级。setPriority参数可以是一个整数,1-10之间,也可以使用Thread类三个静态常量:

  • MIN_PRIORITY(0)
  • NORM_PRIORITY(5)
  • MAX_PRIORITY(10)

虽然Java提供10个优先级,但这些优先级仍然需要操作系统支持,不同操作系统上优先级并不相同,如Windows2000仅提供7个优先级,因此为保证程序可移植性,最好使用上面三个静态常量。

注:默认情况下,一个线程和它父线程有相同的优先级。

线程的底层实现

在Window系统和Linux系统上,Java线程的实现是基于一对一的线程模型,所谓的一对一模型,实际上就是通过语言级别层面程序去间接调用系统内核的线程模型,即我们在使用Java线程时,Java虚拟机内部是转而调用当前操作系统的内核线程来完成当前任务。

这里需要了解一个术语,内核线程(Kernel-Level Thread,KLT),它是由操作系统内核(Kernel)支持的线程,这种线程是由操作系统内核来完成线程切换,内核通过操作调度器(Scheduler)进而对线程执行调度,并将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这也就是操作系统可以同时处理多任务的原因。

由于我们编写的多线程程序属于语言层面的,程序一般不会直接去调用内核线程,取而代之的是一种轻量级的进程(Light Weight Process),也是通常意义上的线程,由于每个轻量级进程都会映射到一个内核线程,因此我们可以通过轻量级进程调用内核线程,进而由操作系统内核将任务映射到各个处理器,这种轻量级进程与内核线程间1对1的关系就称为一对一的线程模型。如下图
这里写图片描述
如图所示,每个线程最终都会映射到CPU中进行处理,如果CPU存在多核,那么就可以并行执行多个线程任务。

原创粉丝点击