Android线程池(十)SynchronousQueue

本文深入探讨SynchronousQueue的特性和应用场景,特别是在生产者/消费者模型中的使用方式。通过实例对比了SynchronousQueue与长度为1的阻塞队列的差异。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

xecutors给我们提供有这么一个预设线程池 :newCachedThreadPool

public static ExecutorService newCachedThreadPool()
{
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,newSynchronousQueue());
}

其中有一个SynchronousQueue,那么SynchronousQueue是什么呢?

阻塞队列分类

ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

这里写图片描述

构造函数

SynchronousQueue()
Creates a SynchronousQueue with nonfair access policy.

SynchronousQueue(boolean fair)
Creates a SynchronousQueue with the specified fairness policy.

SynchronousQueue是这样一种阻塞队列,其中每个 put 必须等待一个 take,反之亦然。同步队列没有任何内部容量,甚至连一个队列的容量都没有。

不能在同步队列上进行 peek,因为仅在试图要取得元素时,该元素才存在;

除非另一个线程试图移除某个元素,否则也不能(使用任何方法)添加元素;也不能迭代队列,因为其中没有元素可用于迭代。队列的头是尝试添加到队列中的首个已排队线程元素; 如果没有已排队线程,则不添加元素并且头为 null。

对于其他 Collection 方法(例如 contains),SynchronousQueue 作为一个空集合。此队列不允许 null 元素。

它非常适合于传递性设计,在这种设计中,在一个线程中运行的对象要将某些信息、
事件或任务传递给在另一个线程中运行的对象,它就必须与该对象同步。

对于正在等待的生产者和使用者线程而言,此类支持可选的公平排序策略。默认情况下不保证这种排序。

但是,使用公平设置为 true 所构造的队列可保证线程以 FIFO 的顺序进行访问。 公平通常会降低吞吐量,但是可以减小可变性并避免得不到服务。

简介

以上7类阻塞队列中有LinkedBlockingQueue,DelayQueue,SynchronousQueue被用在了线程池当中,其中LinkedBlockingQueue被使用在FixedThreadPool,SingleThreadExecutor中,SynchronousQueue被用在CachedThreadPool中,DelayQueue被使用在ScheduledThreadPoolExecutor中。

SynchronousQueue之所以用在CachedThreadPool中,是因为SynchronousQueue不像ArrayBlockingQueue或LinkedListBlockingQueue,SynchronousQueue内部并没有数据缓存空间,你不能调用peek()方法来看队列中是否有数据元素,因为数据元素只有当你试着取走的时候才可能存在,不取走而只想偷窥一下是不行的,当然遍历这个队列的操作也是不允许的。

SynchronousQueue的一个使用场景是在线程池里。Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收,并不会等待空闲线程产生。

1.非阻塞队列中的几个主要方法:  

add(E e):将元素e插入到队列末尾,如果插入成功,则返回true;如果插入失败(即队列已满),则会抛出异常;

remove():移除队首元素,若移除成功,则返回true;如果移除失败(队列为空),则会抛出异常;
  
offer(E e):将元素e插入到队列末尾,如果插入成功,则返回true;如果插入失败(即队列已满),则返回false;

poll():移除并获取队首元素,若成功,则返回队首元素;否则返回null;

peek():获取队首元素,若成功,则返回队首元素;否则返回null

对于非阻塞队列,一般情况下建议使用offer、poll和peek三个方法,不建议使用add和remove方法。因为使用offer、poll和peek三个方法可以通过返回值判断操作成功与否,而使用add和remove方法却不能达到这样的效果。注意,非阻塞队列中的方法都没有进行同步措施。
  
2.阻塞队列中的几个主要方法:

阻塞队列包括了非阻塞队列中的大部分方法,上面列举的5个方法在阻塞队列中都存在,但是要注意这5个方法在阻塞队列中都进行了同步措施。除此之外,阻塞队列提供了另外4个非常有用的方法:
put(E e)
take()
offer(E e,long timeout, TimeUnit unit)
poll(long timeout, TimeUnit unit)
put方法用来向队尾存入元素,如果队列满,则等待;
take方法用来从队首取元素,如果队列为空,则等待;
offer方法用来向队尾存入元素,如果队列满,则等待一定的时间,当时间期限达到时,如果还没有插入成功,则返回false;否则返回true;
poll方法用来从队首取元素,如果队列空,则等待一定的时间,当时间期限达到时,如果取到,则返回null;否则返回取得的元素;

阻塞队列的实现原理事实它和我们用Object.wait()、Object.notify()和非阻塞队列实现生产者-消费者的思路类似,只不过它把这些工作一起集成到了阻塞队列中实现。
在并发编程中,一般推荐使用阻塞队列,这样实现可以尽量地避免程序出现意外的错误。

阻塞队列使用最经典的场景就是socket客户端数据的读取和解析,读取数据的线程不断将数据放入队列,然后解析线程不断从队列取数据解析。还有其他类似的场景,只要符合生产者-消费者模型的都可以使用阻塞队列。

注意

注意1:它一种阻塞队列,其中每个 put 必须等待一个 take,反之亦然。
同步队列没有任何内部容量,甚至连一个队列的容量都没有。
注意2:它是线程安全的,是阻塞的。
注意3:不允许使用 null 元素。
注意4:公平排序策略是指调用put的线程之间,或take的线程之间。
公平排序策略可以查考ArrayBlockingQueue中的公平策略。
注意5:SynchronousQueue的以下方法很有趣:
* iterator() 永远返回空,因为里面没东西。
* peek() 永远返回null。
* put() 往queue放进去一个element以后就一直wait直到有其他thread进来把这个element取走。
* offer() 往queue里放一个element后立即返回,如果碰巧这个element被另一个thread取走了,offer方法返回true,认为offer成功;否则返回false。
* offer(2000, TimeUnit.SECONDS) 往queue里放一个element但是等待指定的时间后才返回,返回的逻辑和offer()方法一样。
* take() 取出并且remove掉queue里的element(认为是在queue里的。。。),取不到东西他会一直等。
* poll() 取出并且remove掉queue里的element(认为是在queue里的。。。),只有到碰巧另外一个线程正在往queue里offer数据或者put数据的时候,该方法才会取到东西。否则立即返回null。
* poll(2000, TimeUnit.SECONDS) 等待指定的时间然后取出并且remove掉queue里的element,其实就是再等其他的thread来往里塞。
* isEmpty()永远是true。
* remainingCapacity() 永远是0。
* remove()和removeAll() 永远是false。

使用 SynchronousQueue 实现生产者/消费者模型

在那么多java提供的用于支持并发的类里面,我想讲讲其中的 SynchronousQueue. 特别是如何使用方便的SynchronousQueue 作为交换机制来实现生产者/消费者模型。

如果没有仔细看过 SynchronousQueue 的实现,可能说不清楚为什么使用这种队列来进行生产者/消费者之间的通信。按照以往我们对队列的理解,它实际上算不上是一种队列,而只是类似于包含最多一个元素的集合。

为什么它有用呢?好吧,这里有几个原因。从生产者的角度来看,只有一个元素(或消息)可以放到队列里面。生产者需要等到消费者将队列中当前的那个 元素(或消息)消费了才能继续下一个。从消费者的角度来看,它轮询队列里面可用的下一个元素(或消息)就好了。就是这么简单,而这样的极大好处是:生产者无法以快过消费者消费的速度来产生消息。

这里有一个我最近遇到的例子:比较两个数据库表(可能非常庞大), 检测出不同的数据或者相同的数据(或备份 )。同步队列( SynchronousQueue )正是针对这类问题的便利工具。它不但允许 每个表 在自己的线程中进行处理,而且可以作为当从两个不同的数据库中读取数据可能出现超时/等待的弥补。

使用 SynchronousQueue 实现生产者/消费者模型

在那么多java提供的用于支持并发的类里面,我想讲讲其中的 SynchronousQueue. 特别是如何使用方便的SynchronousQueue 作为交换机制来实现生产者/消费者模型。

如果没有仔细看过 SynchronousQueue 的实现,可能说不清楚为什么使用这种队列来进行生产者/消费者之间的通信。按照以往我们对队列的理解,它实际上算不上是一种队列,而只是类似于包含最多一个元素的集合。

为什么它有用呢?好吧,这里有几个原因。从生产者的角度来看,只有一个元素(或消息)可以放到队列里面。生产者需要等到消费者将队列中当前的那个 元素(或消息)消费了才能继续下一个。从消费者的角度来看,它轮询队列里面可用的下一个元素(或消息)就好了。就是这么简单,而这样的极大好处是:生产者无法以快过消费者消费的速度来产生消息。

这里有一个我最近遇到的例子:比较两个数据库表(可能非常庞大), 检测出不同的数据或者相同的数据(或备份 )。同步队列( SynchronousQueue )正是针对这类问题的便利工具。它不但允许 每个表 在自己的线程中进行处理,而且可以作为当从两个不同的数据库中读取数据可能出现超时/等待的弥补。

开始了。我们先定义compare函数,它接受三个参数:源数据源、目标数据源以及表名。我使用了Spring框架的JdbcTemplate,它把处理连接和PreparedStatement的所有烦人细节都抽象掉了,非常有用。

public boolean compare( final DataSource source, final DataSource destination, final String table )  {
    final JdbcTemplate from = new  JdbcTemplate( source );
    final JdbcTemplate to = new JdbcTemplate( destination );
}

在开始真正的比较之前,先比较一下两者的行数是一个很好的做法:

if( from.queryForLong("SELECT count(1) FROM " + table ) != to.queryForLong("SELECT count(1) FROM " + table ) ) {
    return false;
}

现在,我们至少已经知道在两张表包含同样行数(的数据记录),我们可以开始进行数据比较了。(比较)算法相当简单:

为生产者和目标(生产者)数据库创建单独的线程
生产者线程(每次)从数据库表中读取一行数据并将其存放到SynchronousQueue中
消费者线程同样(每次)从数据库表中读取一行数据,然后向队列请求现有可用的行(数据)进行比较(如果有必要可等待)。最终比较两个结果集

在线程池(实现中)使用Java并发工具类库其他很棒的部分,让我们定义一个包含有固定数量线程的线程池

final ExecutorService executor = Executors.newFixedThreadPool( 2 );
final SynchronousQueue< List< ? > > resultSets = new SynchronousQueue< List< ? > >();     

下面是算法的具体描述, 消费者函数执行体可以表示成一个单独的callable

Callable< Void > producer = new Callable< Void >() {
    @Override
    public Void call() throws Exception {
        from.query( "SELECT * FROM " + table,
            new RowCallbackHandler() {
                @Override
                public void processRow(ResultSet rs) throws SQLException {
                    try {                   
                        List< ? > row = ...; // convert ResultSet to List
                        if( !resultSets.offer( row, 2, TimeUnit.MINUTES ) ) {
                            throw new SQLException( "Having more data but consumer has already completed" );
                        }
                    } catch( InterruptedException ex ) {
                        throw new SQLException( "Having more data but producer has been interrupted" );
                    }
                }
            }
        );

        return  null;
    }
};

因为Java的语法的关系,这段代码看起来有点繁琐,但其实并没有做很多事情。每个从生产者数据库表中读取的结果集都被转换成了一个链表(作为样板文件,它的具体实现被忽略了)然后将其存放到队列中。如果队列非空,生产者线程被阻塞直到消费者线程完成了它的工作。消费者线程,可以表示成下面的callable

Callable< Void > consumer = new Callable< Void >() {
    @Override
    public Void call() throws Exception {
        to.query( "SELECT * FROM " + table,
            new RowCallbackHandler() {
                @Override
                public void processRow(ResultSet rs) throws SQLException {
                    try {
                        List< ? > source = resultSets.poll( 2, TimeUnit.MINUTES );
                        if( source == null ) {
                            throw new SQLException( "Having more data but producer has already completed" );
                        }                                     

                        List< ? > destination = ...; // convert ResultSet to List
                        if( !source.equals( destination ) ) {
                            throw new SQLException( "Row data is not the same" );
                        }
                    } catch ( InterruptedException ex ) {
                        throw new SQLException( "Having more data but consumer has been interrupted" );
                    }
                }
            }
        );

        return  null;
    }
};

消费者线程对队列进行了一个相反的操作,与往队列中存放数据相反,(消费者线程)是从队列中拉取数据。如果队列为空,消费者线程被阻塞直到生产者线程向队列中添加下一行(数据)。这部分剩余的代码只有在提交那些callable后才会执行。任何从Futured 的get方法返回的异常均表明两张数据表并不包含同样的数据(或者在从数据库获取数据的过程中发生了问题)

 List< Future< Void > > futures = executor.invokeAll( Arrays.asList( producer, consumer ) );
    for( final Future< Void > future: futures ) {
        future.get( 5, TimeUnit.MINUTES );
    }

SynchronousQueue和长度为1的BlockingQueue的对比

阅读ArrayBlockingQueue源码,很容易知道有界阻塞队列的长度至少为1,也就是至少能缓存下一个数据。长度为0的阻塞队列是没有意义的,因为生产者不能生产,消费者不能消费。但是SynchronousQueue的javadoc文档提到A synchronous queue does not have any internal capacity, not even a capacity of one。也就说同步队列的容量是0,不会缓存数据。

下面的代码片段使用了长度为1的BlockingQueue,可以看到2个生产者有1个会被阻塞(因为队列已满)。

package concurrent;

import java.util.concurrent.ArrayBlockingQueue;

public class TestSynchronousQueue
{

    private static ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(1);

    public static void main(String[] args) throws Exception 
    {
        new Productor(1).start();
        new Productor(2).start();
        System.out.println("main over.");
    }

    static class Productor extends Thread
    {
        private int id;

        public Productor(int id)
        {
            this.id = id;
        }

        @Override
        public void run()
        {
            try 
            {
                String result = "id=" + this.id;
                System.out.println("begin to produce."+result);
                queue.put(result);
                System.out.println("success to produce."+result);
            } catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }
    }

    static class Consumer extends Thread 
    {
        @Override
        public void run()
        {
            try
            {
                System.out.println("consume begin.");
                String v = queue.take();
                System.out.println("consume success." + v);
            } catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }
    }
}

这段代码的输出结果如下:

main over.  
begin to produce.id=1  
begin to produce.id=2  
success to produce.id=1  

现在我们将长度为1的阻塞队列换成SynchronousQueue试试看

package concurrent;

import java.util.concurrent.SynchronousQueue;

public class TestSynchronousQueue
{
    private static SynchronousQueue<String> queue = new SynchronousQueue<String>();

    public static void main(String[] args) throws Exception 
    {
        new Productor(1).start();
        new Productor(2).start();
        System.out.println("main over.");
    }

    static class Productor extends Thread
    {
        private int id;

        public Productor(int id)
        {
            this.id = id;
        }

        @Override
        public void run()
        {
            try 
            {
                String result = "id=" + this.id;
                System.out.println("begin to produce."+result);
                queue.put(result);
                System.out.println("success to produce."+result);
            } catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }
    }

    static class Consumer extends Thread 
    {
        @Override
        public void run()
        {
            try
            {
                System.out.println("consume begin.");
                String v = queue.take();
                System.out.println("consume success." + v);
            } catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }
    }
}

这段代码的输出结果如下:可以看到2个生产者线程都被阻塞了,无法进行生产。

main over.  
begin to produce.id=1  
begin to produce.id=2  

可以看出SynchronousQueue和BlockingQueue的区别了:在没有消费者的情况下,长度为1的阻塞队列可以让生产者生产1个商品并存储在阻塞队列中;而同步队列不允许生产者进行生产。可以看到同步队列有这样的特性:producer waits until consumer is ready, consumer waits until producer is ready。

下面的代码用2个生产者和1个消费者,可以分别使用阻塞队列和同步队列看下输出的情况。

public static void main(String[] args) throws Exception   
{  
    new Consumer().start();  
    Thread.sleep(200);  

    new Productor(1).start();  
    new Productor(2).start();  
    System.out.println("main over.");  
}  

stackoverflowerror上这篇文章很好地演示了SynchronousQueue的使用场景:

I have a requirement for a task to be executed asynchronously while discarding any further requests until the task is finished.

public class SyncQueueTester {  
    private static ExecutorService executor = new ThreadPoolExecutor(1, 1,   
            1000, TimeUnit.SECONDS,   
            new SynchronousQueue<Runnable>(),  
            new ThreadPoolExecutor.DiscardPolicy());  

    public static void main(String[] args) throws InterruptedException {  
        for (int i = 0; i < 20; i++) {  
            kickOffEntry(i);  

            Thread.sleep(200);  
        }  

        executor.shutdown();  
    }  

    private static void kickOffEntry(final int index) {  
        executor.  
            submit(  
                new Callable<Void>() {  
                    public Void call() throws InterruptedException {  
                        System.out.println("start " + index);  
                        Thread.sleep(1000); // pretend to do work  
                        System.out.println("stop " + index);  
                        return null;  
                    }  
                }  
            );  
    }  
}  

SynchronousQueue简单使用

经典的生产者-消费者模式,操作流程是这样的:
有多个生产者,可以并发生产产品,把产品置入队列中,如果队列满了,生产者就会阻塞;
有多个消费者,并发从队列中获取产品,如果队列空了,消费者就会阻塞;
如下面的示意图所示:

这里写图片描述

SynchronousQueue 也是一个队列来的,但它的特别之处在于它内部没有容器,一个生产线程,当它生产产品(即put的时候),如果当前没有人想要消费产品(即当前没有线程执行take),此生产线程必须阻塞,等待一个消费线程调用take操作,take操作将会唤醒该生产线程,同时消费线程会获取生产线程的产品(即数据传递),这样的一个过程称为一次配对过程(当然也可以先take后put,原理是一样的)。
我们用一个简单的代码来验证一下,如下所示:

package com.concurrent;

import java.util.concurrent.SynchronousQueue;

public class SynchronousQueueDemo {
    public static void main(String[] args) throws InterruptedException {
        final SynchronousQueue<Integer> queue = new SynchronousQueue<Integer>();

        Thread putThread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("put thread start");
                try {
                    queue.put(1);
                } catch (InterruptedException e) {
                }
                System.out.println("put thread end");
            }
        });

        Thread takeThread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("take thread start");
                try {
                    System.out.println("take from putThread: " + queue.take());
                } catch (InterruptedException e) {
                }
                System.out.println("take thread end");
            }
        });

        putThread.start();
        Thread.sleep(1000);
        takeThread.start();
    }
}

一种输出结果如下:

put thread start
take thread start
take from putThread: 1put thread end
take thread end

从结果可以看出,put线程执行queue.put(1) 后就被阻塞了,只有take线程进行了消费,put线程才可以返回。可以认为这是一种线程与线程间一对一传递消息的模型。

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值