Java并发编程基础(三)

3.1 线程简介

3.1.1 什么是线程

现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个Java程序,操作系统就会创建一个Java进程。现代操作系统调度的最小单元是线程,也叫轻量级进程(Light Weight Process),在一个进程里可以创建多个线程,这些线层都拥有自己的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉线程在同时执行。

执行main()方法的是一个名称为main的线程。

3.1.2 为什么使用多线程

一个线程在一个时刻只能运行在一个处理器核心上。但是随着处理器上狠心数量的增多,将计算逻辑分配到多个处理器核心上,就会显著减少程序的处理时间。

3.1.3 线程的优先级

现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下次分配。线程分配到的时间片多少也就决定了线程使用处理器资源的多少,而线程优先级决定线程需要多或少分配处理器资源的线程属性。

在Java线程中,通过一个整型成员变量priority来控制优先级,优先级范围从1-10,线程构建时可通过setPriority(int)方法来修改优先级,默认优先级是5,优先级高的线程分配时间片的数量要多于优先级低的线程。设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级。在不同JVM及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定:

线程优先级不能作为程序正确性的依赖,因为操作系统可以完全不用理会Java线程对于优先级的设定。

3.1.4 线程的状态

在给定的一个时刻,线程只能处于如下其中的一个状态:
这里写图片描述

线程在自身的生命周期中,并不是固定地处于某个状态,而是随着代码的执行在不同的状态之间进行切换:

这里写图片描述

Java将操作系统中的运行和就绪两个状态合并称为运行状态。阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态,但是阻塞在java.concurrent包中的Lock接口的线程状态却是等待状态,因为java.concurrent包中的Lock接口对于阻塞的实现均使用了LockSupport类中的相关方法。

LockSupport

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。

LockSupport中的park()和unpark()的作用分别是阻塞线程和解除阻塞线程,而且park()和unppark()不会遇到“Thread.suspend和Thread.resume锁可能引发的死锁“问题。因为park()和unpark()有许可的存在;调用park()的线程和另一个视图将其unpark()的线程之间的竞争将保持活性。

// 返回提供给最近一次尚未解除阻塞的 park 方法调用的 blocker 对象,如果该调用不受阻塞,则返回 null。
static Object getBlocker(Thread t)
// 为了线程调度,禁用当前线程,除非许可可用。
static void park()
// 为了线程调度,在许可可用之前禁用当前线程。
static void park(Object blocker)
// 为了线程调度禁用当前线程,最多等待指定的等待时间,除非许可可用。
static void parkNanos(long nanos)
// 为了线程调度,在许可可用前禁用当前线程,并最多等待指定的等待时间。
static void parkNanos(Object blocker, long nanos)
// 为了线程调度,在指定的时限前禁用当前线程,除非许可可用。
static void parkUntil(long deadline)
// 为了线程调度,在指定的时限前禁用当前线程,除非许可可用。
static void parkUntil(Object blocker, long deadline)
// 如果给定线程的许可尚不可用,则使其可用。
static void unpark(Thread thread)

LockSupport示例

示例1:

public class WaitTest1 {
 2 
 3     public static void main(String[] args) {
 4 
 5         ThreadA ta = new ThreadA("ta");
 6 
 7         synchronized(ta) { // 通过synchronized(ta)获取“对象ta的同步锁”
 8             try {
 9                 System.out.println(Thread.currentThread().getName()+" start ta");
10                 ta.start();
11 
12                 System.out.println(Thread.currentThread().getName()+" block");
13                 // 主线程等待
14                 ta.wait();
15 
16                 System.out.println(Thread.currentThread().getName()+" continue");
17             } catch (InterruptedException e) {
18                 e.printStackTrace();
19             }
20         }
21     }
22 
23     static class ThreadA extends Thread{
24 
25         public ThreadA(String name) {
26             super(name);
27         }
28 
29         public void run() {
30             synchronized (this) { // 通过synchronized(this)获取“当前对象的同步锁”
31                 System.out.println(Thread.currentThread().getName()+" wakup others");
32                 notify();    // 唤醒“当前对象上的等待线程”
33             }
34         }
35     }
36 }

示例2:

1 import java.util.concurrent.locks.LockSupport;
 2 
 3 public class LockSupportTest1 {
 4 
 5     private static Thread mainThread;
 6 
 7     public static void main(String[] args) {
 8 
 9         ThreadA ta = new ThreadA("ta");
10         // 获取主线程
11         mainThread = Thread.currentThread();
12 
13         System.out.println(Thread.currentThread().getName()+" start ta");
14         ta.start();
15 
16         System.out.println(Thread.currentThread().getName()+" block");
17         // 主线程阻塞
18         LockSupport.park(mainThread);
19 
20         System.out.println(Thread.currentThread().getName()+" continue");
21     }
22 
23     static class ThreadA extends Thread{
24 
25         public ThreadA(String name) {
26             super(name);
27         }
28 
29         public void run() {
30             System.out.println(Thread.currentThread().getName()+" wakup others");
31             // 唤醒“主线程”
32             LockSupport.unpark(mainThread);
33         }
34     }
35 }

park和wait的区别。wait让线程阻塞前,必须通过synchronized获取同步锁。

3.1.5 Daemon线程

Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会推出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。

Daemon属性需要再启动线程之前设置,不能在启动线程知乎设置。

Daemon线程被用作完成支持性工作,但是在Java虚拟机退出时,Daemon线程中的finally并不一定会被执行。

在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。

3.2 启动和终止线程

3.2.1 构造线程

在运行线程之前首先要构造一个线程对象,线程对象在构造的时候需要提供线程所需要的属性,如线程所属的线程组、线程优先级、是否是Daemon线程等信息。

3.2.2 启动线程

线程对象在初始化完成后,调用start()方法就可以启动这个线程。线程start()方法的含义是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程。

启动一个线程前,最好为这个线程设置线程名字,因为这样在使用jstack分析程序或者进行问题排查时,就会给开发人员提供一些提示。

4.2.3 理解中断

一个线程在未正常结束之前, 被强制终止是很危险的事情. 因为它可能带来完全预料不到的严重后果比如会带着自己所持有的锁而永远的休眠,迟迟不归还锁等。 所以你看到Thread.suspend, Thread.stop等方法都被Deprecated了。

那么不能直接把一个线程搞挂掉, 但有时候又有必要让一个线程死掉, 或者让它结束某种等待的状态 该怎么办呢?一个比较优雅而安全的做法是:使用等待/通知机制或者给那个线程一个中断信号, 让它自己决定该怎么办。

中断线程的使用场景:

在某个子线程中为了等待一些特定条件的到来, 你调用了Thread.sleep(10000), 预期线程睡10秒之后自己醒来, 但是如果这个特定条件提前到来的话, 来通知一个处于Sleep的线程。又比如说,线程通过调用子线程的join方法阻塞自己以等待子线程结束, 但是子线程运行过程中发现自己没办法在短时间内结束, 于是它需要想办法告诉主线程别等我了。这些情况下, 就需要中断。

中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的interrupt()方法对齐进行中断操作。

线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。如果该线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted()时依旧会返回false。

从Java的API中可以看到,许多声明抛出InterruptedException的方法(例如Thread.sleep(longmillis)方法)这些方法在抛出InterruptedException之前,Java虚拟机会先将该线程的中断标识位
清除,然后抛出InterruptedException,此时调用isInterrupted()方法将会返回false。

中断是通过调用Thread.interrupt()方法来做的。这个方法通过修改了被调用线程的中断状态来告知那个线程, 说它被中断了。对于非阻塞中的线程, 只是改变了中断状态, 即Thread.isInterrupted()将返回true; 对于可取消的阻塞状态中的线程, 比如等待在这些函数上的线程, Thread.sleep(), Object.wait(), Thread.join(), 这个线程收到中断信号后, 会抛出InterruptedException, 同时会把中断状态置回为true。但调用Thread.interrupted()会对中断状态进行复位。

4.2.4 过期的suspend()、resume()和stop()

暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop()。但这些API是过期的,不建议使用。

不建议使用的原因主要有:以suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。

正因为suspend()、resume()和stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方法,而暂停和恢复操作可以用后面提到的等待/通知机制来替代。

4.2.4 安全地终止线程

中断状态是线程的一个标识位,而中断操作是一种简便的线程间交互
方式,而这种交互方式最适合用来取消或停止任务。除了中断以外,还可以利用一个boolean变量来控制是否需要停止任务并终止该线程。

public class Shutdown {
   public static void main(String[] args) throws Exception {
      Runner one = new Runner();
      Thread countThread = new Thread(one, "CountThread");
      countThread.start();
      // 睡眠1秒,main线程对CountThread进行中断,使CountThread能够感知中断而结束
      TimeUnit.SECONDS.sleep(1);
      countThread.interrupt();
      Runner two = new Runner();
      countThread = new Thread(two, "CountThread");
      countThread.start();
      // 睡眠1秒,main线程对Runner two进行取消,使CountThread能够感知on为false而结束
      TimeUnit.SECONDS.sleep(1);
      two.cancel();
    }

    private static class Runner implements Runnable {
        private long i;
        private volatile boolean on = true;
        @Override
        public void run() {
            while (on && !Thread.currentThread().isInterrupted()){
                i++;
            }
            System.out.println("Count i = " + i);
        }
        public void cancel() {
            on = false;
        }
    }
}

示例在执行过程中,main线程通过中断操作和cancel()方法均可使CountThread得以终止。这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,因此这种终止线程的做法显得更加安全和优雅。

3.3 线程间通信

线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码一步一步地执行,直到终止。如果多个线程能够相互配合完成工作,这将会带来巨大的价值。

3.3.1 volatile和synchronized关键字

任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED状态。

这里写图片描述

任意线程对Object(Object由synchronized保护)的访问,首先要获得Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。

3.3.2 等待/通知机制

等待/通知的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类java.lang.Object上:

这里写图片描述

等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

调用wait()、notify()以及notifyAll()时需要注意的细节:
(1)使用wait()、notify()和notifyAll()时需要先对调用对象加锁。
(2)调用wait()方法后,线程又RUNNING变为WAITING,并将当前线程放置到对象等待队列,当前线程释放锁。
(3)notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或notifyAll()的线程释放锁之后,等待线程才有机会从wait()返回。
(4)notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为BLOCKED。
(5)从wait()方法返回的前提是获得了调用对象的锁。

等待/通知机制依托于同步机制,目的就是确保等待线程从wait()方法返回时能够感知到通知线程对变量做出的修改。

这里写图片描述

  • 线程获取不到锁而阻塞时,是存在于同步队列中(SynchronizedQueue);
  • 线程执行wait()后,将进入等待队列(WaitQueue),当收到notify()/notifyAll()并释放锁后由等待队列移动至同步队列中;

3.3.3 等待/通知的经典范式

该范式分为两部分,分别针对等待方(消费者)和通知方(生产者)。

等待方遵循如下原则:

1)获取对象的锁。
2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。
3)条件满足则执行对应的逻辑。
对应的伪代码如下:

synchronized(对象){
    while(条件不满足){
        对象.wait();
    }
    对应的处理逻辑
}

通知方遵循如下原则:

1)获得对象的锁。
2)改变条件。
3)通知所有等待在对象上的线程。
对应的伪代码如下:

synchronized(对象){
    改变条件
    对象.notifyAll();
}

3.3.4 管道输入/输出流

管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存。

管道输入/输出流包括如下四种具体实现:PipedOutputStream、PipedInputStream、PipedReader和PiledWriter,前两种面向字节,后两种面向字符。

public class Piped {
    public static void main(String[] args) throws Exception{
        PipedWriter out = new PipedWriter();
        PipedReader in = new PipedReader();
        //将输出流和输入流进行连接,否则在使用时抛出IOException
        out.connect(in);
        Thread printThread = new Thread(new Print(in),"PrintThread");
        printThread.start();
        int receive = 0;
        try{
            while((receive = System.in.read()) != -1){
                out.write(receive);
            }
        }finally{
            out.close();
        }
    }

    static class Print implements Runnable{
        private PipedReader in;
        public Print(PipedReader in){
            this.in = in;
        }
        public void run(){
            int receive = 0;
            try{
                while((receive = in.read()) != -1){
                    System.out.print((char)receive);
                }
            }catch(IOException ex){

            }
        }
    }
}

对于Piped类型的流,必须先要进行绑定,也就是调用connect()方法,如果没有将输入/输出流绑定起来,对于该流的访问将会抛出异常。

3.3.5 Thread.join()的使用

如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回。线程Thread除了提供join()方法之外,还提供了join(long millis)和join(longmillis,int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时时间里没有终止,那么将会从该超时方法中返回。

当线程终止时,会调用线程自身的notifyAll()方法,会通知所有等待在该线程对象上的线程。可以看到join()方法的逻辑结构与等待/通知经典范式一致,即加锁、循环和处理逻辑3个步骤。

3.3.6 ThreadLocal的使用

ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。

可以通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。

3.4 线程应用实例

3.4.1 等待超时模式

等待超时模式的伪代码:

public synchronized Object get(long mills) throws InterruptedException{
    long future = System.currentTimeMillis()+mills;
    long remaining = mills;
    //当重试大于0并且result返回值不满足要求
    while((result == null) && remaining >0){
        wait(remaining);
        remaining = future-Sustem.currentTimeMillis();
    }
    return result;
}

等待超时模式就是在等待/通知范式基础上增加了超时控制。

3.4.2 简单的数据库连接池示例

使用等待超时模式来构造一个简单的数据库连接池,在示例中模拟从连接池中获取、使用和释放连接的过程,而客户端获取连接的过程被设定为等待超时的模式,也就是在1000毫秒内如果无法获取到可用连接,将会返回给客户端一个null。设定连接池的大小为10个,然后通过调节客户端的线程数来模拟无法获取连接的场景。

首先看一下连接池的定义。它通过构造函数初始化连接的最大上限,通过一个双向队列来维护连接,调用方需要先调用fetchConnection(long)方法来指定在多少毫秒内超时获取连接,当连接使用完成后,需要调用releaseConnection(Connection)方法将连接放回线程池,示例如下:

public class ConnectionPool{
    private LinkedList<Connection> pool = new LinkedList<Connection>();
    public ConnectionPool(int initialSize){
        if(initialSize>0){
            for(int i = 0;i<initialSize;i++){
                pool.addLast(ConnectionDriver.createConnection());
            }
        }
    }

    public void releaseConnection(Connection connection){
    if(connection != null) {
        synchronized(pool){
            //连接释放后需要进行通知,这样其他消费者能够感知到连接池中已经归还了一个连接
            pool.addLast(connection);
            pool.notifyAll();
        }
    }
}
//在mills内无法获取到连接,将会返回null
public Connection fetchConnection(long mills) throws InterruptedException{
    synchronized(pool){
        //完全超时
        if(mills <= 0){
            while(pool.isEmpty()){
                pool.wait();
            }
            return pool.removeFirst();
        }else{
            long future = System.currentTimeMillis()+mills;
            long remaining = mills;
            while(pool.isEmpty() && remaining > 0){
                pool.wait(remaining);
                remaining = future - System.currentTimeMillis();
                Connection result = null;
                if(!pool.isEmpty()){
                    result = pool.removeFirst();
                }
                return result;
            }
        }
    }

}

一个示例来测试简易数据库连接池的工作情况,模拟客户端ConnectionRunner获取、使用、最后释放连接的过程,当它使用时连接将会增加获取到连接的数量,反之,将会增加未获取到连接的数量,示例如下:

public class ConnectionPoolTest{
    static ConnectionPool pool = new ConnectionPool(10);
    //保证所有ConnectionRunner能够同时开始
    static CountDownLatch start = new CountDownLatch(1);
    //main线程将会等待所有ConnectionRunner结束后才能继续执行
    static CountDownLatch end;
    public static void main(String[] args) throws Exception{
        //线程数量,可以修改线程数量进行观察
        int threadCount = 10;
        end = new CountDownLatch(threadCount);
        int count = 20;
        AtomicInteger got = new AtomicInteger();
        AtomicInteger notGot = new AtomicInteger();
        for(int i = 0;i<threadCount;i++){
            Thread thread = new Thread(new ConnectionRunner(count,get,notGot),"ConnectionRunnerThread");
            thread.start();
        }
        start.countDowm();
        end.await();
        System.out.println("taotal invoke: "+(threadCount * count));
        System.out.println("got connection: "+got);
        System.out.println("not got connection: "+notGot);
    }

    static class ConnectionRunner implements Runnable{
        int count;
        AtomicInteger got;
        AtomicInteger notGot;
        public ConnectionRunner(int count,AtomicInteger got,AtomicInteger notGot){
            this.count = count;
            this.got = got;
            this.notGot = notGot;
        }

        public void run(){
            try{
                start.await();
            }catch(Exception ex){
            }
            while(count>0){
                try{
                    //线程池中获取连接,如果1000ms无法获取到,将会返回Null
                    //分别统计连接获取的数量got和未获取到的数量notGot
                    Connection connection = pool.fetchConnection(1000);
                    if(connection != null){
                        try{
                            connection.createStatement();
                            connection.commit();
                        }finally{
                            pool.releaseConnection(connection);
                            get.incrementAndGet();
                        }
                    }else{
                        notGot.incrementAndGet();
                    }
                }catch(Exception ex){
                }finally{
                    count--;
                }
            }
            end.countDown();
        }
    }
}

上述示例中使用了CountDownLatch来确保ConnectionRunnerThread能够同时开始执行,并且在全部结束之后,才使main线程从等待状态中返回。

CountDownLatch
CountDownLatch这个类能够使一个线程等待其他线程完成各自的工作后再执行。例如,应用程序的主线程希望在负责启动框架服务的线程已经启动所有的框架服务之后再执行。

CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。
构造器中的计数值(count)实际上就是闭锁需要等待的线程数量。这个值只能被设置一次,而且CountDownLatch没有提供任何机制去重新设置这个计数值。

与CountDownLatch的第一次交互是主线程等待其他线程。主线程必须在启动其他线程后立即调用CountDownLatch.await()方法。这样主线程的操作就会在这个方法上阻塞,直到其他线程完成各自的任务。

其他N 个线程必须引用闭锁对象,因为他们需要通知CountDownLatch对象,他们已经完成了各自的任务。这种通知机制是通过 CountDownLatch.countDown()方法来完成的;每调用一次这个方法,在构造函数中初始化的count值就减1。所以当N个线程都调 用了这个方法,count的值等于0,然后主线程就能通过await()方法,恢复执行自己的任务。

使用场景
1、实现最大的并行性:有时我们想同时启动多个线程,实现最大程度的并行性。例如,我们想测试一个单例类。如果我们创建一个初始计数为1的CountDownLatch,并让所有线程都在这个锁上等待,那么我们可以很轻松地完成测试。我们只需调用 一次countDown()方法就可以让所有的等待线程同时恢复执行。
2、开始执行前等待n个线程完成各自任务:例如应用程序启动类要确保在处理用户请求前,所有N个外部系统已经启动和运行了。

3.4.3 线程池技术及其示例

每个请求都创建一个线程,会使操作系统频繁的进行线程上下文切换,无故增加系统的负载,线程的创建和消亡都需要耗费系统资源。

一个简单线程池接口定义:

public interface ThreadPool<Job extends Runnable>{
    //执行一个Job,这个Job需要实现Runnable
    void execute(Job job);
    //关闭线程池
    void shutdown();
    //增加工作者线程
    void addWorkers(int num);
    //较少工作者线程
    void removeWorker(int num);
    //得到正在等待执行的任务数量
    int getJobSize();
}

客户端可以通过execute(Job)方法将Job提交入线程池执行,而客户端自身不用等待Job的执行完成。除了execute(Job)方法以外,线程池接口提供了增大/减少工作者线程以及关闭线程池的方法。这里工作者线程代表着一个重复执行Job的线程,而每个由客户端提交的Job都将进入到一个工作队列中等待工作者线程的处理。

线程池接口的默认实现:

public class DefaultThreadPool<Job extends Runnable> implements ThreadPool<Job>{
    //线程池最大限制数
    private static final int MAX_WORKER_NUMBERS = 10;
    //线程池默认的数量
    private static final int DEFAULT_WORKER_NUMBERS 5;
    //线程池的最小数量
    private static final int MIN_WORKER_NUMBERS = 1;
    //这是一个工作列表,将会向里面插入工作
    private final LinkedList<Job> jobs = new LinkedList<Job>();
    //工作者列表
    private final List<Worker> workers = Collections.synchronizedList(new ArrayList<Worker>());
    //工作者线程数量 
    private int workerNum = DEFAULT_WORKER_NUMBERS;
    //线程编号生成
    private AtomicLong threadNum = new AtomicLong();

    public DefaultThreadPool(){
        initializeWorkers(DEFAULT_WORKER_NUMBERS);
    }

    //初始化线程工作者
    private void initializeWorkers(int num){
        for(int i = 0;i<num;i++){
            Worker worker = new Worker();
            workers.add(worker);
            Thread thread = new Thread(worker,"ThreadPool-Worker-"+threadNum.incrementAndGet());
            thread.start();
        }
    }

    public DefaultThreadPool(int num){
        workerNum = num  ;//判断是否在最大和最小之间
        initializeWorkers(workerNum);
    }

    public void execute(Job job){
        if(job != null){
            //添加一个工作,然后进行通知
            synchronized(jobs){
                jobs.addLast(job);
                jobs.notify();
            }
        }
    }

    public void shutdown(){
        for(Worker worker : workers){
            worker.shutdown();
        }
    }

    public void addWorkers(int num){
        synchronized(jobs){
            //限制新增的Worker数量不能超过最大值
            if(num+this.workerNum>MAX_WORKER_NUMBERS)
                NUM = MAX_WORKERS_NUMBERS - this.workerNum;
            initializeWorkers(num);
            this.workerNum += num;
        }   
    }

    public void removeWorker(int num){
        synchronized(jobs){
            if(num >= this.workerNum){
                throw new IllegalArgumentException("beyond workNUm");
            }
            //按照给定的数量停止Worker
            int count = 0;
            while(count < num){
                Worker worker = workers.get(count);
                if(workers.remove(worker)){
                    worker.shutdown();
                    count++;
                }
            }
            this.workerNum -= count;
        }
    }

    public int getJobSize(){
        return jobs.size();
    }

    //工作者,负责消费任务
    class Worker implements Runnable{
        //是否工作
        private volatile boolean running = true;
        public void run(){
            while(running){
                Job job = null;
                synchronized(jobs){
                    //如果工作者列表是空的,就wait
                    while(jobs.isEmpty()){
                        try{
                            jobs.wait();
                        }catch(InterruptedException ex){
                            //感知到外部对WorkerThread的中断操作,返回
                            Thread.currentThread().interrupt();
                            return;
                        }
                    }
                    //取出一个Job
                    job = jobs.removeFirst();
                }
            }
            if(job != null){
                try{
                    job.run();
                }catch(Exception ex){
                    //忽略Job执行中的Exception
                }
            }
        }

        public void shutdown(){
            running = false;
        }
    }   
}

从线程池的实现可以看到,当客户端调用execute(Job)方法时,会不断地向任务列表jobs中添加Job,而每个工作者线程会不断地从jobs上取出一个Job进行执行,当jobs为空时,工作者线程进入等待状态。

添加一个Job后,对工作队列jobs调用了其notify()方法,而不是notifyAll()方法,因为能够确定有工作者线程被唤醒,这时使用notify()方法将会比notifyAll()方法获得更小的开销(避免将等待队列中的线程全部移动到阻塞队列中)。

线程池的本质就是使用了一个线程安全的工作队列连接工作者线程和客户端线程,客户端线程将任务放入工作队列后便返回,而工作者线程则不断地从工作队列上取出工作并执行。当工作队列为空时,所有的工作者线程均等待在工作队列上,当有客户端提交了一个任务之后会通知任意一个工作者线程,随着大量任务呗提交,更多地工作者线程会被唤醒。

3.4.4 基于线程池技术的简单Web服务器

使用前一节中的线程池来构造一个简单的Web服务器,这个Web服务器用来处理HTTP请求,目前只能处理简单的文本和JPG图片内容。这个Web服务器使用main线程不断地接受客户端Socket的连接,将连接以及请求提交给线程池处理,这样使得Web服务器能够同时处理多个客户端请求:

public class SimpleHttpServer{
    //处理HttpRequest的线程池
    static ThreadPool<HttpRequestHandler> threadPool = new DefaultThreadPool<HttpRequestHandler>(1);

    //SimpleHttpServer的根路径
    static String basePath;
    static ServerSocket serverSocket;

    //服务监听端口
    statuc int port = 8080;
    public static void setPort(int port){
        if(port>0){
            SimpleHttpServer.port = port;
        }
    }

    public static void setBasePath(String basePath){
        if(basePath != null && new File(basePath).exists() && new File(basePath).isDirectory()){
            SimpleHttpServer.basePath = basePath;
        }
    }

    public static void start() throws Exception{
        serverSocket = new ServerSocket(port);
        Socket socket = null;
        while((socket = serverSocket.accept()) != null){
            //接收一个客户端Socket,生成一个HttpRequestHandler,放入线程池执行
            threadPool.execute(new HttpRequestHandler(socket));
        }
        serverSocket.close();
    }

    static class HttpRequestHandler implements Runnable{
        private Socket socket;
        public HttpRequestHandler(Socket socket){
            this.socket = socket;
        }

        public void run(){
            String line = null;
            BufferedReader br = null;
            BufferedReader reader = null;
            PrintWriter out = null;
            InputStream in = null;
            try{
                socket.getInputStream();
                .....
            }
            ...
        }
    }

    //关闭流或者Socket
    private static void close(Closeable... closeables){
    if(closeables != null){
        for(Closeable closeable : closeables){
            try{
                closeable.close();
            }catch(Exception ex){
            }
        }
    }
}

这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值