java之多线程总结二

线程或者说多线程,是咱们处理多任务的强大工具。线程和进程是不一样的,每一个进程都是一个独立运行的程序,拥有本身的变量,且不一样进程间的变量不能共享;而线程是运行在进程内部的,每一个正在运行的进程至少有一个线程,并且不一样的线程之间能够在进程范围内共享数据。也就是说进程有本身独立的存储空间,而线程是和它所属的进程内的其余线程共享一个存储空间。线程的使用可使咱们可以并行地处理一些事情。线程经过并行的处理给用户带来更好的使用体验,好比你使用的邮件系统(outlook、Thunderbird、foxmail等),你固然不但愿它们在收取新邮件的时候,致使你连已经收下来的邮件都没法阅读,而只能等待收取邮件操做执行完毕。这正是线程的意义所在。 

实现线程的方式 

实现线程的方式有两种: java

  1. 继承java.lang.Thread,并重写它的run()方法,将线程的执行主体放入其中。
  2. 实现java.lang.Runnable接口,实现它的run()方法,并将线程的执行主体放入其中。


这是继承Thread类实现线程的示例: 数据库

  1. public class ThreadTest extends Thread {  
        public void run() {  
            // 在这里编写线程执行的主体  
            // do something  
        }  
    }  

     


这是实现Runnable接口实现多线程的示例: 编程

  1. public class RunnableTest implements Runnable {  
        public void run() {  
            // 在这里编写线程执行的主体  
            // do something  
        }  
    }  

     


这两种实现方式的区别并不大。继承Thread类的方式实现起来较为简单,可是继承它的类就不能再继承别的类了,所以也就不能继承别的类的有用的方法了。而使用是想Runnable接口的方式就不存在这个问题了,并且这种实现方式将线程主体和线程对象自己分离开来,逻辑上也较为清晰,因此推荐你们更多地采用这种方式。 

如何启动线程 

咱们经过以上两种方式实现了一个线程以后,线程的实例并无被建立,所以它们也并无被运行。咱们要启动一个线程,必须调用方法来启动它,这个方法就是Thread类的start()方法,而不是run()方法(既不是咱们继承Thread类重写的run()方法,也不是实现Runnable接口的run()方法)。run()方法中包含的是线程的主体,也就是这个线程被启动后将要运行的代码,它跟线程的启动没有任何关系。上面两种实现线程的方式在启动时会有所不一样。 

继承Thread类的启动方式: 多线程

public class ThreadStartTest {  
    public static void main(String[] args) {  
        // 建立一个线程实例  
        ThreadTest tt = new ThreadTest();  
        // 启动线程  
        tt.start();  
    }  
} 

 
实现Runnable接口的启动方式: ide

public class RunnableStartTest {  
    public static void main(String[] args) {  
        // 建立一个线程实例  
        Thread t = new Thread(new RunnableTest());  
        // 启动线程  
        t.start();  
    }  
}  


实际上这两种启动线程的方式原理是同样的。首先都是调用本地方法启动一个线程,其次是在这个线程里执行目标对象的run()方法。那么这个目标对象是什么呢?为了弄明白这个问题,咱们来看看Thread类的run()方法的实现: 工具

public void run() {  
    if (target != null) {  
        target.run();  
    }  
}  


当咱们采用实现Runnable接口的方式来实现线程的状况下,在调用new Thread(Runnable target)构造器时,将实现Runnable接口的类的实例设置成了线程要执行的主体所属的目标对象target,当线程启动时,这个实例的run()方法就被执行了。当咱们采用继承Thread的方式实现线程时,线程的这个run()方法被重写了,因此当线程启动时,执行的是这个对象自身的run()方法。总结起来就一句话,线程类有一个Runnable类型的target属性,它是线程启动后要执行的run()方法所属的主体,若是咱们采用的是继承Thread类的方式,那么这个target就是线程对象自身,若是咱们采用的是实现Runnable接口的方式,那么这个target就是实现了Runnable接口的类的实例。 

线程的状态 

在Java 1.4及如下的版本中,每一个线程都具备新建、可运行、阻塞、死亡四种状态,可是在Java 5.0及以上版本中,线程的状态被扩充为新建、可运行、阻塞、等待、定时等待、死亡六种。线程的状态彻底包含了一个线程重新建到运行,最后到结束的整个生命周期。线程状态的具体信息以下: 优化

  1. NEW(新建状态、初始化状态):线程对象已经被建立,可是尚未被启动时的状态。这段时间就是在咱们调用new命令以后,调用start()方法以前。
  2. RUNNABLE(可运行状态、就绪状态):在咱们调用了线程的start()方法以后线程所处的状态。处于RUNNABLE状态的线程在JAVA虚拟机(JVM)上是运行着的,可是它可能还正在等待操做系统分配给它相应的运行资源以得以运行。
  3. BLOCKED(阻塞状态、被中断运行):线程正在等待其它的线程释放同步锁,以进入一个同步块或者同步方法继续运行;或者它已经进入了某个同步块或同步方法,在运行的过程当中它调用了某个对象继承自java.lang.Object的wait()方法,正在等待从新返回这个同步块或同步方法。
  4. WAITING(等待状态):当前线程调用了java.lang.Object.wait()、java.lang.Thread.join()或者java.util.concurrent.locks.LockSupport.park()三个中的任意一个方法,正在等待另一个线程执行某个操做。好比一个线程调用了某个对象的wait()方法,正在等待其它线程调用这个对象的notify()或者notifyAll()(这两个方法一样是继承自Object类)方法来唤醒它;或者一个线程调用了另外一个线程的join()(这个方法属于Thread类)方法,正在等待这个方法运行结束。
  5. TIMED_WAITING(定时等待状态):当前线程调用了java.lang.Object.wait(long timeout)、java.lang.Thread.join(long millis)、java.util.concurrent.locks.LockSupport.packNanos(long nanos)、java.util.concurrent.locks.LockSupport.packUntil(long deadline)四个方法中的任意一个,进入等待状态,可是与WAITING状态不一样的是,它有一个最大等待时间,即便等待的条件仍然没有知足,只要到了这个时间它就会自动醒来。
  6. TERMINATED(死亡状态、终止状态):线程完成执行后的状态。线程执行完run()方法中的所有代码,从该方法中退出,进入TERMINATED状态。还有一种状况是run()在运行过程当中抛出了一个异常,而这个异常没有被程序捕获,致使这个线程异常终止进入TERMINATED状态。

一个任务进入阻塞状态,可能缘由以下:this

    1)经过调用sleep使任务进入休眠状态,在这种状况下,任务在制定的时间内不会运行。编码

    2)你经过调用wait是线程挂起,直到线程获得了notify和notifyall消息,线程才会进入就绪状态。操作系统

    3)任务在等待某个输入/输出完成。

    4)任务试图在某个对象上调用其同步控制的方法,可是对象锁不可用,由于另外一个任务已经获取了这个锁。


在Java5.0及以上版本中,线程的所有六种状态都以枚举类型的形式定义在java.lang.Thread类中了,代码以下: 

public enum State {  
    NEW,  
    RUNNABLE,  
    BLOCKED,  
    WAITING,  
    TIMED_WAITING,  
    TERMINATED;  
}  


实现同步的方式 

同步是多线程中的重要概念。同步的使用能够保证在多线程运行的环境中,程序不会产生设计以外的错误结果。同步的实现方式有两种,同步方法和同步块,这两种方式都要用到synchronized关键字。 

给一个方法增长synchronized修饰符以后就可使它成为同步方法,这个方法能够是静态方法和非静态方法,可是不能是抽象类的抽象方法,也不能是接口中的接口方法。下面代码是一个同步方法的示例: 

public synchronized void aMethod() {  
    // do something  
}  
public static synchronized void anotherMethod() {  
    // do something  
}  


线程在执行同步方法时是具备排它性的。当任意一个线程进入到一个对象的任意一个同步方法时,这个对象的全部同步方法都被锁定了,在此期间,其余任何线程都不能访问这个对象的任意一个同步方法,直到这个线程执行完它所调用的同步方法并从中退出,从而致使它释放了该对象的同步锁以后。在一个对象被某个线程锁定以后,其余线程是能够访问这个对象的全部非同步方法的。 

同步块的形式虽然与同步方法不一样,可是原理和效果是一致的。同步块是经过锁定一个指定的对象,来对同步块中包含的代码进行同步;而同步方法是对这个方法块里的代码进行同步,而这种状况下锁定的对象就是同步方法所属的主体对象自身。若是这个方法是静态同步方法呢?那么线程锁定的就不是这个类的对象了,也不是这个类自身,而是这个类对应的java.lang.Class类型的对象。同步方法和同步块之间的相互制约只限于同一个对象之间,因此静态同步方法只受它所属类的其它静态同步方法的制约,而跟这个类的实例(对象)没有关系。 

下面这段代码演示了同步块的实现方式: 

public void test() {  
    // 同步锁  
    String lock = "LOCK";  
    // 同步块  
    synchronized (lock) {  
        // do something  
    }  
    int i = 0;  
    // ...  
}  


对于做为同步锁的对象并无什么特别要求,任意一个对象均可以。若是一个对象既有同步方法,又有同步块,那么当其中任意一个同步方法或者同步块被某个线程执行时,这个对象就被锁定了,其余线程没法在此时访问这个对象的同步方法,也不能执行同步块。 

synchronized和Lock 

Lock是一个接口,它位于Java 5.0新增的java.utils.concurrent包的子包locks中。concurrent包及其子包中的类都是用来处理多线程编程的。实现Lock接口的类具备与synchronized关键字一样的功能,可是它更增强大一些。java.utils.concurrent.locks.ReentrantLock是较经常使用的实现了Lock接口的类。下面是ReentrantLock类的一个应用实例: 

private Lock lock = new ReentrantLock();  
public void testLock() {  
    // 锁定对象  
    lock.lock();  
    try {  
        // do something  
    } finally {  
        // 释放对对象的锁定  
        lock.unlock();  
    }  
}  


lock()方法用于锁定对象,unlock()方法用于释放对对象的锁定,他们都是在Lock接口中定义的方法。位于这两个方法之间的代码在被执行时,效果等同于被放在synchronized同步块中。通常用法是将须要在lock()和unlock()方法之间执行的代码放在try{}块中,而且在finally{}块中调用unlock()方法,这样就能够保证即便在执行代码抛出异常的状况下,对象的锁也老是会被释放,不然的话就会为死锁的产生增长可能。 

使用synchronized关键字实现的同步,会把一个对象的全部同步方法和同步块看作一个总体,只要有一个被某个线程调用了,其余的就没法被别的线程执行,即便这些方法或同步块与被调用的代码之间没有任何逻辑关系,这显然下降了程序的运行效率。而使用Lock就可以很好地解决这个问题。咱们能够把一个对象中按照逻辑关系把须要同步的方法或代码进行分组,为每一个组建立一个Lock类型的对象,对实现同步。那么,当一个同步块被执行时,这个线程只会锁定与当前运行代码相关的其余代码最小集合,而并不影响其余线程对其他同步代码的调用执行。 

关于死锁 

死锁就是一个进程中的每一个线程都在等待这个进程中的其余线程释放所占用的资源,从而致使全部线程都没法继续执行的状况。死锁是多线程编程中一个隐藏的陷阱,它常常发生在多个线程共用资源的时候。在实际开发中,死锁通常隐藏的较深,不容易被发现,一旦死锁现象发生,就必然会致使程序的瘫痪。所以必须避免它的发生。 

程序中必须同时知足如下四个条件才会引起死锁: 

  1. 互斥(Mutual exclusion):线程所使用的资源中至少有一个是不能共享的,它在同一时刻只能由一个线程使用。
  2. 持有与等待(Hold and wait):至少有一个线程已经持有了资源,而且正在等待获取其余的线程所持有的资源。
  3. 非抢占式(No pre-emption):若是一个线程已经持有了某个资源,那么在这个线程释放这个资源以前,别的线程不能把它抢夺过去使用。
  4. 循环等待(Circular wait):假设有N个线程在运行,第一个线程持有了一个资源,而且正在等待获取第二个线程持有的资源,而第二个线程正在等待获取第三个线程持有的资源,依此类推……第N个线程正在等待获取第一个线程持有的资源,由此造成一个循环等待。


线程池 

线程池就像数据库链接池同样,是一个对象池。全部的对象池都有一个共同的目的,那就是为了提升对象的使用率,从而达到提升程序效率的目的。好比对于Servlet,它被设计为多线程的(若是它是单线程的,你就能够想象,当1000我的同时请求一个网页时,在第一我的得到请求结果以前,其它999我的都在郁闷地等待),若是为每一个用户的每一次请求都建立一个新的线程对象来运行的话,系统就会在建立线程和销毁线程上耗费很大的开销,大大下降系统的效率。所以,Servlet多线程机制背后有一个线程池在支持,线程池在初始化初期就建立了必定数量的线程对象,经过提升对这些对象的利用率,避免高频率地建立对象,从而达到提升程序的效率的目的。 

下面实现一个最简单的线程池,从中理解它的实现原理。为此咱们定义了四个类,它们的用途及具体实现以下: 

1.Task(任务):这是个表明任务的抽象类,其中定义了一个deal()方法,继承Task抽象类的子类须要实现这个方法,并把这个任务须要完成的具体工做在deal()方法编码实现。线程池中的线程之因此被建立,就是为了执行各类各样数量繁多的任务的,为了方便线程对任务的处理,咱们须要用Task抽象类来保证任务的具体工做统一放在deal()方法里来完成,这样也使代码更加规范。 
Task的定义以下: 

public abstract class Task {  
    public enum State {  
        /* 新建 */NEW, /* 执行中 */RUNNING, /* 已完成 */FINISHED  
    }  
    // 任务状态  
    private State state = State.NEW;  
    public void setState(State state) {  
        this.state = state;  
    }  
    public State getState() {  
        return state;  
    }  
    public abstract void deal();  
}  


2.TaskQueue(任务队列):在同一时刻,可能有不少任务须要执行,而程序在同一时刻只能执行必定数量的任务,当须要执行的任务数超过了程序所能承受的任务数时怎么办呢?这就有了先执行哪些任务,后执行哪些任务的规则。TaskQueue类就定义了这些规则中的一种,它采用的是FIFO(先进先出,英文名是First In First Out)的方式,也就是按照任务到达的前后顺序执行。 
TaskQueue类的定义以下: 

import java.util.Iterator;  
import java.util.LinkedList;  
import java.util.List;  
public class TaskQueue {  
    private List<Task> queue = new LinkedList<Task>();  
    // 添加一项任务  
    public synchronized void addTask(Task task) {  
        if (task != null) {  
            queue.add(task);  
        }  
    }  
    // 完成任务后将它从任务队列中删除  
    public synchronized void finishTask(Task task) {  
        if (task != null) {  
            task.setState(Task.State.FINISHED);  
            queue.remove(task);  
        }  
    }  
    // 取得一项待执行任务  
    public synchronized Task getTask() {  
        Iterator<Task> it = queue.iterator();  
        Task task;  
        while (it.hasNext()) {  
            task = it.next();  
            // 寻找一个新建的任务  
            if (Task.State.NEW.equals(task.getState())) {  
                // 把任务状态置为运行中  
                task.setState(Task.State.RUNNING);  
                return task;  
            }  
        }  
        return null;  
    }  
}  

3.addTask(Task task)方法用于当一个新的任务到达时,将它添加到任务队列中。这里使用了LinkedList类来保存任务到达的前后顺序。finishTask(Task task)方法用于任务被执行完毕时,将它从任务队列中清除出去。getTask()方法用于取得当前要执行的任务。
TaskThread(执行任务的线程):它继承自Thread类,专门用于执行任务队列中的待执行任务。

public class TaskThread extends Thread {  
    // 该线程所属的线程池  
    private ThreadPoolService service;  
    public TaskThread(ThreadPoolService tps) {  
        service = tps;  
    }  
    public void run() {  
        // 在线程池运行的状态下执行任务队列中的任务  
        while (service.isRunning()) {  
            TaskQueue queue = service.getTaskQueue();  
            Task task = queue.getTask();  
            if (task != null) {  
                task.deal();  
            }  
            queue.finishTask(task);  
        }  
    }  
}  


4.ThreadPoolService(线程池服务类):这是线程池最核心的一个类。它在被建立了时候就建立了几个线程对象,可是这些线程并无启动运行,但调用了start()方法启动线程池服务时,它们才真正运行。stop()方法能够中止线程池服务,同时中止池中全部线程的运行。而runTask(Task task)方法是将一个新的待执行任务交与线程池来运行。 
ThreadPoolService类的定义以下: 

import java.util.ArrayList;  
import java.util.List;  
public class ThreadPoolService {  
    // 线程数  
    public static final int THREAD_COUNT = 5;  
    // 线程池状态  
    private Status status = Status.NEW;  
    private TaskQueue queue = new TaskQueue();  
    public enum Status {  
        /* 新建 */NEW, /* 提供服务中 */RUNNING, /* 中止服务 */TERMINATED,  
    }  
    private List<Thread> threads = new ArrayList<Thread>();  
    public ThreadPoolService() {  
        for (int i = 0; i < THREAD_COUNT; i++) {  
            Thread t = new TaskThread(this);  
            threads.add(t);  
        }  
    }  
    // 启动服务  
    public void start() {  
        this.status = Status.RUNNING;  
        for (int i = 0; i < THREAD_COUNT; i++) {  
            threads.get(i).start();  
        }  
    }  
    // 中止服务  
    public void stop() {  
        this.status = Status.TERMINATED;  
    }  
    // 是否正在运行  
    public boolean isRunning() {  
        return status == Status.RUNNING;  
    }  
    // 执行任务  
    public void runTask(Task task) {  
        queue.addTask(task);  
    }  
    protected TaskQueue getTaskQueue() {  
        return queue;  
    }  
}  

完成了上面四个类,咱们就实现了一个简单的线程池。如今咱们就可使用它了,下面的代码作了一个简单的示例:

public class SimpleTaskTest extends Task {  
    @Override  
    public void deal() {  
        // do something  
    }  
    public static void main(String[] args) throws InterruptedException {  
        ThreadPoolService service = new ThreadPoolService();  
        service.start();  
        // 执行十次任务  
        for (int i = 0; i < 10; i++) {  
            service.runTask(new SimpleTaskTest());  
        }  
        // 睡眠1秒钟,等待全部任务执行完毕  
        Thread.sleep(1000);  
        service.stop();  
    }  
}

 

固然,咱们实现的是最简单的,这里只是为了演示线程池的实现原理。在实际应用中,根据状况的不一样,能够作不少优化。好比: 

  • 调整任务队列的规则,给任务设置优先级,级别高的任务优先执行。
  • 动态维护线程池,当待执行任务数量较多时,增长线程的数量,加快任务的执行速度;当任务较少时,回收一部分长期闲置的线程,减小对系统资源的消耗。

事实上Java5.0及以上版本已经为咱们提供了线程池功能,无需再从新实现。这些类位于java.util.concurrent包中。 

Executors类提供了一组建立线程池对象的方法,经常使用的有一下几个: 

public static ExecutorService newCachedThreadPool() {  
    // other code  
}  
public static ExecutorService newFixedThreadPool(int nThreads) {  
    // other code  
}  
public static ExecutorService newSingleThreadExecutor() {  
    // other code  
}  

newCachedThreadPool()方法建立一个动态的线程池,其中线程的数量会根据实际须要来建立和回收,适合于执行大量短时间任务的状况;newFixedThreadPool(int nThreads)方法建立一个包含固定数量线程对象的线程池,nThreads表明要建立的线程数,若是某个线程在运行的过程当中由于异常而终止了,那么一个新的线程会被建立和启动来代替它;而newSingleThreadExecutor()方法则只在线程池中建立一个线程,来执行全部的任务。 

这三个方法都返回了一个ExecutorService类型的对象。实际上,ExecutorService是一个接口,它的submit()方法负责接收任务并交与线程池中的线程去运行。submit()方法可以接受Callable和Runnable两种类型的对象。它们的用法和区别以下: 
Runnable接口:继承Runnable接口的类要实现它的run()方法,并将执行任务的代码放入其中,run()方法没有返回值。适合于只作某种操做,不关心运行结果的状况。
Callable接口:继承Callable接口的类要实现它的call()方法,并将执行任务的代码放入其中,call()将任务的执行结果做为返回值。适合于执行某种操做后,须要知道执行结果的状况。

不管是接收Runnable型参数,仍是接收Callable型参数的submit()方法,都会返回一个Future(也是一个接口)类型的对象。该对象中包含了任务的执行状况以及结果。调用Future的boolean isDone()方法能够获知任务是否执行完毕;调用Object get()方法能够得到任务执行后的返回结果,若是此时任务尚未执行完,get()方法会保持等待,直到相应的任务执行完毕后,才会将结果返回。 

咱们用下面的一个例子来演示Java5.0中线程池的使用: 

import java.util.concurrent.*;  
public class ExecutorTest {  
    public static void main(String[] args) throws InterruptedException,  
            ExecutionException {  
        ExecutorService es = Executors.newSingleThreadExecutor();  
        Future fr = es.submit(new RunnableTest());// 提交任务  
        Future fc = es.submit(new CallableTest());// 提交任务  
        // 取得返回值并输出  
        System.out.println((String) fc.get());  
        // 检查任务是否执行完毕  
        if (fr.isDone()) {  
            System.out.println("执行完毕-RunnableTest.run()");  
        } else {  
            System.out.println("未执行完-RunnableTest.run()");  
        }  
        // 检查任务是否执行完毕  
        if (fc.isDone()) {  
            System.out.println("执行完毕-CallableTest.run()");  
        } else {  
            System.out.println("未执行完-CallableTest.run()");  
        }  
        // 中止线程池服务  
        es.shutdown();  
    }  
}  
class RunnableTest implements Runnable {  
    public void run() {  
        System.out.println("已经执行-RunnableTest.run()");  
    }  
}  
class CallableTest implements Callable {  
    public Object call() {  
        System.out.println("已经执行-CallableTest.call()");  
        return "返回值-CallableTest.call()";  
    }  
}  

运行结果: 
已经执行-RunnableTest.run()
已经执行-CallableTest.call()
返回值-CallableTest.call()
执行完毕-RunnableTest.run()
执行完毕-CallableTest.run()

使用完线程池以后,须要调用它的shutdown()方法中止服务,不然其中的全部线程都会保持运行,程序不会退出。

相关文章
相关标签/搜索