文章目录

  • 8.9 高级并发之阻塞队列与线程池
  • 8.9.1 阻塞队列
  • 8.9.2 线程池


8.9 高级并发之阻塞队列与线程池

8.9.1 阻塞队列

前面介绍的对象互斥锁无论是synchronized+wait)/notify()机制,还是lock+await()/signal()机制,都需要自行判断何时阻塞何时唤醒,以及采用何种数据结构处理正在等待的多个线程,一旦线程同步协调,就容易产生死锁、饥饿等问题。Java.util.concurrent 包的BlockingQueue接口通过队列的方式完成线程间的高效传输数据,提供强大的功能解决了多个生产者与多个消费者共享资源的问题,实现自动阻塞机制,不必担心何时阻塞,何时唤醒。当存储区的产品满时,BlockingQueue 能自动阻塞生产者放人,唤醒消费者
取出产品;而当存储区空时,BlockingQueue 也能自动阻塞消费者取出产品,唤醒生产者放入产品。此外,BlockingQueue 还提供了独立锁的线程同步机制,与互斥锁相比,独立锁能够高效地实现生产者和消费者真正地并行运行,同时访问共享资源BlockingQueue 的核心方法如表所示。

方法

说明

offer(anObject)

将anObject加到队列中,如果队列没有空间,则返回false

offer(E o, long timeout,TimeUnit unit)

将anObject加到队列中,如果队列没有空间,设定等待的时间

put(anObject)

将anObject加到队列中,如果队列没有空间,则调用此方法的线程被阻断,直到队列中有空间再继续添加

poll(time)

取走队列中排在首位的对象

poll(long timeout,TimeUnit unit)

取走队首的对象,如果队列中没有可取的对象,则指定等待时间

take()

取走队首的对象,如果队列中没有可取的对象,则等待直到有对象可取

drain To(Collection<?super E> c)

一次性从队列中获取所有可用的数据添加到集合中

BlockingQueue接口的具体类主要有ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousBlockingQueue, 在程序中根据需来选用不同的具体类,通常ArrayBlockingQueue和LinkedBlockingQueue两类足够可以处理线程同步问题了。以下是这四种类的使用说明:

1 ) ArrayBlockingQueue :这种BlockingQueue的大小必须指定,由构造方法带-一个int参数来指明其大小,BlockingQueue中的对象以FIFO (先人先出)的顺序排序。使用时需注意,生产者放入数据和消费者获取数据,都是共用一个对象互斥锁,因此两者无法真正并行运行。

2 )LinkedBlockingQueue :这种BlockingQueue的大小没有固定,可以指定大小,也可以不指定,不指定时,构造方法可以没有参数,默认最大值由Integer.MAX_ VALUE决定,所含的对象以FIFO (先入先出)顺序排序。使用LinkedBlockingQueue时,生产者和消费者
分别采用独立的锁来控制数据同步,这也意味着在高并发的情况下,生产者和消费者可以并行地操作队列中的数据,以此来高效提升整个队列的并发性能。

3 )PriorityBlockingQueue :类似于LinkedBlockQueue,但其所含对象的排序方式不是FIFO,而是按照优先级排序。PriorityBlockingQueue 中存储的对象必须实现Comparable接口,队列通过这个接口的compare()方法确定对象的优先级。

**4 )SynchronousBlockingQueue 😗*与ArrayBlockingQueue、LinkedBlockingQueue 不同,SynchronousBlockingQueue内部并没有数据缓存空间,数据直接在生产者和消费者线程之间传递,并不会将数据缓冲到队列中,因此遍历这个队列的操作也是不允许的。生产者线程的放入操作put必须等待消费者的取出操作take完成后再执行,反过来也一样。

**示例:**采用LinkedBlockingQueue实现生产者消费者问题

代码如下:

public class Product {
    int id;
    String name;

    public Product(int id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "Product{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}
public class Producer implements Runnable {
    BlockingQueue<Product> queue;//利用阻塞队列存储

    public Producer(BlockingQueue<Product> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        try{
            Product item1 = new Product(1,"ipad");
            System.out.println("I have made a product: "+Thread.currentThread().getName());
            queue.put(item1);//放入产品
            System.out.println("I put in a product:"+item1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Consumer implements Runnable {
    BlockingQueue<Product> queue;

    public Consumer(BlockingQueue<Product> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        try{
            Product temp = queue.take();//取出产品,如果队列为空,会阻塞当前线程
            System.out.println("I took out "+temp);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}
public class ProductConsumer {
    public static void main(String[] args) {
        //创建一个LinkdeBlockQueue
        BlockingQueue<Product>queue = new LinkedBlockingQueue<Product>();
        //创建Consumer对象和Producer对象
        Consumer consumer = new Consumer(queue);
        Producer producer = new Producer(queue);
        //创建5个Consumer线程和5个Producer线程
        for (int i = 0; i < 5; i++) {
            new Thread(producer,"Producer"+(i+1)).start();
            new Thread(consumer,"consumer"+(i+1)).start();
        }
    }
}

运行结果如下:

I have made a product: Producer1
I have made a product: Producer4
I have made a product: Producer5
I have made a product: Producer2
I have made a product: Producer3
I put in a product:Product{id=1, name='ipad'}
I took out Product{id=1, name='ipad'}
I put in a product:Product{id=1, name='ipad'}
I put in a product:Product{id=1, name='ipad'}
I put in a product:Product{id=1, name='ipad'}
I took out Product{id=1, name='ipad'}
I took out Product{id=1, name='ipad'}
I took out Product{id=1, name='ipad'}
I put in a product:Product{id=1, name='ipad'}
I took out Product{id=1, name='ipad'}

该例首先创建一个LinkedBlockingQueue用于放置产品,然后创建5个Producer和5个Consumer, Producer 每生产一 个产品会调用queue.put() 把产品放入队列中,Consumer 调用queue.take()从队列中取出产品,在放人和取出操作中自动进行阻塞处理,适用于大量线程的并发处理,非常方便,并且代码也简洁,推荐在线程编程采用这种方式。

8.9.2 线程池

大多数网络服务器程序都离不开多线程,每当一个请求到达时,立即需要一个单独的线程为请求服务,但当有大量请求并发访问时,假设一个服务器一天 要处理50 000个请求,服务器不断创建和销毁对象的开销很大,这种方式会因线程创建得太多而消耗过多的内存,影响到执行效率。为了减少创建和销毁线程的次数,并能重复利用线程执行多个任务,引人“池”的概念,线程池提供了限制系统中执行线程数量的解决方案。**线程池是在任务到来之前,预先创建一定数目线程的机制,可以根据系统环境,自动或手动设置线程数量,创建线程放人空闲队列中,这些线程均处于睡眠状态,不消耗CPU,仅占用非常少量的内存空间。**当接收到一个请求时,缓冲池为该请求分配一个空闲线程,将请求传人此线程中运行,进行处理。如果预先创建的线程都处于运行状态,即创建的线程不够使用,线程池可再创建一定数量的新线程,用于处理更多的请求。如果请求不多,系统比较清闲,也可以移除一部分一直处于停用状态的线程。如此采用线程池机制以达到运行的最佳效果,既不至于浪费资源,又不会造成系统拥挤影响效率。

java.util.concurrent.Executors类提供了创建4种线程池newSingle ThreadExecutor、new-FixedThreadPool、newScheduledThreadPool、newCachedThreadPool的方法,如下表所示。

方法

说明

ScheduledExecutorServicenewSingleThreadExecutor()

创建-一个单线程的线程池,它只有一个工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行

ExecutorServicenewFixedThreadPool(int nThreads)

创建一个可重用固定保存nThreads个线程的线程池,超出的线程在队列中等待

ScheduledExecutorServicenewScheduledThreadPool(int corePoolSize)

创建一个可保存线程数为corePoolSize的线程池,可设置定时运行及周期性执行任务

ExecutorServicenewCachedThreadPool()

创建一个无界限的线程池,如果线程池长度超过任务数量,可回收60秒不执行任务的空闲线程。若任务数量增多,则自动

新建线程

ExecutorService是一个接口,ExecutorService有 两个具体类ThreadPoolExecutor和ScheduledTheadPoolExecutor,其中ThreadPoolExecutor是线程池中最核心的类,继承自AbstractExecutorsService类,创建线程池的工作由它的构造方法完成。ThreadPoolExecutor的构造方法定义如下:

ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAlive Time,TimeUnit :unit, 
BlockihgQueue< Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)

下面解释各个参数的含义。

corePoolSize:线程池的大小,即可放置的线程数量。
maximumPoolSsize: 线程池最多能创建的线程数。
keepAliveTime:表示空闲线程没有任务执行时,最多等待多少时间会终止。
unit:参数keepAliveTime的时间单位,可设置为天、小时、分钟、秒、毫秒、微秒及纳秒。
workQueue:存放线程的阻塞队列。
threadFactory:创建新线程时使用的线程工厂。
handler:对某些异常的处理程序,比如超出线程范围和队列容量而使执行被阻塞的异常。

从参数列表中可看出,ThreadPoolExecutor 构造方法提供了创建线程池的必要参数。如果请求执行的线程数量少于corePoolSize,则不排队,直接执行;反之,Executor 将请求加入队列,而不添加新的线程。线程的排队方式由工作队列workQueue决定,

ArrayBlockingQueue、LinkedBlockingQueue以及synchronousQueue三种阻塞队列可供选用,此队列仅保持由execute()方法提交的Runnable 任务,关于阻塞队列前面已经介绍过。

ThreadPoolExecutor类中有几个常用的方法: excute(runnable command)、submit()、shutdown()

●execute(runnable command)是个重要的方法,在ExecutorService的父接口中声明,由ThreadPoolExecutor完成具体的实现,该方法负责向线程池提交请求并由线程池来执行。

●submit()方法在ExecutorService接口中声明,在AbstractExecutorService中实现,该
方法与execute()功能相似,也是向线程池提交执行请求,不同的是它提供执行的返
回结果。

●Shutdown( 在最后用于关闭线程池。

**示例:**创建一个newCachedThreadPool类型的线程池,其他类型线程池的创建方式与之类似

代码如下:

public class ExecutorTest {
    public static void main(String[] args) {
        //创建一个可调节大小的线程池
        ExecutorService pool_1 = Executors.newCachedThreadPool();

//        //创建一个单线程运行的线程池
//        ExecutorService pool_2 = Executors.newSingleThreadExecutor();
//        //创建一个可重复固定线程数的线程池
//        ExecutorService pool_3= Executors.newFixedThreadPool(5);
//        //创建一个可可重用固定线程数的线程池
//        ExecutorService pool_4= Executors.newScheduledThreadPool(1);

        //创建5个线程
        Thread thread_1 = new MyThread();
        Thread thread_2 = new MyThread();
        Thread thread_3 = new MyThread();
        Thread thread_4 = new MyThread();
        Thread thread_5 = new MyThread();
        
        //将线程放入线程池中进行执行
        pool_1.execute(thread_1);
        pool_1.execute(thread_2);
        pool_1.execute(thread_3);
        pool_1.execute(thread_4);
        pool_1.execute(thread_5);


    }
}
class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+" is running");
    }
}

运行结果如下:

pool-1-thread-3 is running
pool-1-thread-4 is running
pool-1-thread-2 is running
pool-1-thread-1 is running
pool-1-thread-5 is running