实战Java高并发程序设计(第二版)-chp5并行模式与算法

5.3 生产者-消费者模式

在生产者-消费者模式中,通常有两类线程,即若干个生产者线程和若干个消费者线程。生产者线程负责提交用户请求,消费者线程则负责具体处理生产者提交的任务。生产者和消费者之间则通过共享内存缓冲区进行通信。

三个生产者线程将任务提交到共享内存缓冲区,消费者线程并不直接与生产者线程通信,而是在共享内存缓冲区中获取任务,并进行处理。

生产者-消费者模式中的内存缓冲区的主要功能是数据在多线程间的共享,此外,通过该缓冲区,可以缓解生产者和消费者间的性能差。

在这里插入图片描述

BlockigQueue充当了共享内存缓冲区,用于维护任务或数据队列(PCData对象)

生产者负责创建PCData对象,并将它加入BlockigQueue队列中,

消费者则从BlockigQueue队列中获取PCData对象。

5.4 高性能的生产者-消费者模式:无锁的实现

ConcurrentLinkedQueue是一个高性能的队列,但是BlockingQueue队列只是为了方便数据共享

ConcurrentLinkedQueue队列的秘诀就在于大量使用了无锁的CAS操作

Disruptor框架别出心裁地使用了环形队列来代替普通线形队列,这个环形队列内部实现为一个普通的数组

如果队列是环形的,则只需要对外提供一个当前位置cursor,利用这个指针既可以进行入队操作,也可以进行出队操作。由于环形队列的缘故,队列的总大小必须事先指定,不能动态扩展。

Disruptor框架要求我们必须将数组的大小设置为2的整数次方。这样通过sequence &(queueSize-1)就能立即定位到实际的元素位置index,这比取余(%)操作快得多

在这里插入图片描述

图5.3显示了RingBuffer的结构。生产者向缓冲区中写入数据,而消费者从中读取数据。生产者写入数据时,使用CAS操作,消费者读取数据时,为了防止多个消费者处理同一个数据,也使用CAS操作进行数据保护。

5.4.2 使用Disruptor框架实现生产者-消费者模式的案例

/**
 * 5.4.2
 * PCData对象:代表数据
 */
public class PCData {
    private long value;
    public void set(long value){
        this.value = value;
    }
    public long get(){
        return value;
    }
}
package HIighParallel.chp5.p4.q2;

import com.lmax.disruptor.RingBuffer;

import java.nio.ByteBuffer;

public class Producer {
    private final RingBuffer<PCData> ringBuffer;
    public Producer(RingBuffer<PCData> ringBuffer){
        this.ringBuffer = ringBuffer;
    }

    /**
     * pushData()方法
     *  将产生的数据推入缓冲区,方法接收一个ByteByffer对象
     *  ByteBuffer 对象中可以用来包装任何数据类型。这里用来存储long整数
     *  pushData()方法的作用是将传入ByteBuffer对象中的数据提取出来,并装载到环形缓冲区中;
     *
     * @param bb
     */

    public void pushData(ByteBuffer bb){
        long sequence = ringBuffer.next(); //  获得下一个可用的序列号
        try{
            // 通过序列号,取得下一个空闲可用的PCData对象,
            // 并将PCData对象的数据设为期望值
            PCData event = ringBuffer.get(sequence);
            event.set(bb.getLong(0)); //Fill with data
        }
        finally {
            // 进行数据发布,只有发布后的数据才会真正被消费者看见;
            ringBuffer.publish(sequence);
        }
    }
}
package HIighParallel.chp5.p4.q2;

import com.lmax.disruptor.WorkHandler;

/**
 * 消费者的作用是读取数据进行处理;
 * 数据的读取由Disruptor框架进行封装了,onEvent()方法为框架的回调方法;
 */
public class Consumer implements WorkHandler<PCData> {

    @Override
    public void onEvent(PCData event) throws Exception {
        System.out.println(Thread.currentThread().getId() +
                ":Event: --" + event.get() * event.get() + "--");
    }
}
/**
 * 统筹规划的主函数,将生产者、消费者、数据都整合起来;
 */
public class PCMain {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executor = Executors.newCachedThreadPool();
        PCDataFactory factory = new PCDataFactory();
        // 设置缓冲区大小为1024,为2的整数次幂
        int bufferSize = 1024;
        // 创建disruptor对象,封装了整个disruptor库的使用
        Disruptor<PCData> disruptor = new Disruptor<>(factory, bufferSize,
                executor,
                ProducerType.MULTI,
                new BlockingWaitStrategy());
        // 设置用于处理数据的消费者,设置了4个消费者实例
        // 每一个消费者实例映射到一个线程中;
        disruptor.handleEventsWithWorkerPool(
                new Consumer(),
                new Consumer(),
                new Consumer(),
                new Consumer()
        );
        // 启动并初始化disruptor系统
        disruptor.start();
        RingBuffer<PCData> ringBuffer = disruptor.getRingBuffer();
        Producer producer = new Producer(ringBuffer);
        ByteBuffer bb = ByteBuffer.allocate(8);
        // 由一个生产者不断地向缓冲区存入数据;
        for (long l = 0; true; l++){
            bb.putLong(0, l);
            producer.pushData(bb);
            Thread.sleep(100);
            System.out.println(" add data " + 1);
        }
    }
}

5.4.4 CPU Cache的优化:解决伪共享问题

[
在这里插入图片描述
在这里插入图片描述

伪共享问题:

​ CPU中的高速缓存;在高速缓存中,读写数据的最小单位为缓存行(Cache Line),它是从贮存复制到缓存的最小单位,一般为32字节到128字节;

​ 当两个变量存放在一个缓存行时,在多线程访问中,可能会影响彼此的性能。在图5.4中,假设变量X和Y在同一个缓存行,运行在CPU1上的线程更新了变量X,那么CPU2上的缓存行就会失效,同一行的变量Y即使没有修改也会变成无效,导致Cache无法命中。接着,如果在CPU2上的线程更新了变量Y,则导致CPU1上的缓存行失效(此时,同一行的变量X变得无法访问)

一种可行的做法就是在变量X的前后空间都先占据一定的位置(把它叫作padding,用来填充用的)。这样,当内存被读入缓存时,这个缓存行中,只有变量X一个变量实际是有效的,因此就不会发生多个线程同时修改缓存行中不同变量而导致变量全体失效的情况,如图5.5所示。

public final class FalseSharing implements Runnable{
    public final static int NUM_THREADS = 2;
    public final static long ITERATIONS = 500L * 1000L * 1000L;
    private final int arrayIndex;

    // 准备一个数组longs,数组元素个数和线程数量一致
    // 每个线程都会访问自己对应的longs中的元素
    private static VolatileLong[] longs = new VolatileLong[NUM_THREADS];
    static {
        for (int i = 0; i < longs.length; i++) {
            longs[i] = new VolatileLong();
        }
    }

    public FalseSharing(final int arrayIndex){
        this.arrayIndex = arrayIndex;
    }

    public static void main(String[] args) throws InterruptedException {
        final long start = System.currentTimeMillis();
        runTest();
        System.out.println("duration = " + (System.currentTimeMillis() - start));
    }

    private static void runTest() throws InterruptedException {
        Thread[] threads = new Thread[NUM_THREADS];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new FalseSharing(i));
        }

        for (Thread t: threads){
            t.start();
        }

        for (Thread t: threads) {
            t.join();
        }
    }

    @Override
    public void run() {
        long i = ITERATIONS + 1;
        while ( 0 != --i){
            longs[arrayIndex].value = i;
        }
    }

    public final static class VolatileLong{
        public volatile long value = 0L;
        // 准备了七个long型变量用来填充缓存。
        public long p1, p2, p3, p4, p5, p6, p7;
    }
}

5.5 Future模式

Future模式是多线程开发中非常常见的一种设计模式,它的核心思想是异步调用。当我们需要调用一个函数方法时,如果这个函数执行得很慢,那么我们就要进行等待。但有时候,我们可能并不急着要结果。因此,我们可以让被调者立即返回,让它在后台慢慢处理这个请求。

对于Future模式来说,虽然它无法立即给出你需要的数据,但是它会返回一个契约给你,将来你可以凭借这个契约去重新获取你需要的信息。

5.5.1 Future模式的主要角色

在这里插入图片描述
在这里插入图片描述

5.5.2 Future模式的简单实现

Java StringBuffer 和 StringBuilder 类

当对字符串进行修改的时候,需要使用 StringBuffer 和 StringBuilder 类。

和 String 类不同的是,StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象。

StringBuilder 类在 Java 5 中被提出,它和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全的(不能同步访问)。

由于 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。然而在应用程序要求线程安全的情况下,则必须使用 StringBuffer 类。

/**
 * 5.5.2 Future模式中Data接口
 * 有两个重要的实现,
 *  1. RealData,也就是真实数据
 *  2. FutureData,用来提取RealData的一个“订单”
 */
public interface Data {
    public String getResult();
}
/**
 * RealData是最终需要使用的数据模型。它的构造很慢。用sleep()函数模拟这个过程,简单地模拟一个字符串的构造。
 */
public class RealData implements Data {
    protected String result;

    @Override
    public String getResult() {
        return result;
    }

    public RealData(String para) {
        // RealData的构造可能很慢,这里使用sleep模拟
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < 10; i++) {
            sb.append(para);
            try {
                // 使用sleep,代替一个很慢的操作过程
                Thread.sleep(100);
            } catch (InterruptedException e) {
            }
            result = sb.toString();
        }
    }
/**
 * FutureData实现了一个快速返回的RealData包装。
 * 它只是一个包装,或者说是一个RealData的虚拟实现。
 * 因此,它可以很快被构造并返回。
 * 当使用FutrueData的getResult()方法时,
 * 如果实际的数据没有准备好,那么程序就会阻塞,
 * 等RealData准备好并注入FutureData中才最终返回数据。
 */
public class FutureData implements Data {

    protected RealData realData = null; // FutureData是RealData的包装
    protected boolean isReady = false;

    public synchronized void setRealData(RealData realData) {
        if (isReady) {
            return;
        }
        this.realData = realData;
        isReady = true;
        notifyAll();   // RealData已经被注入,通知getResult()方法
    }

    @Override
    public synchronized String getResult() {
        while (!isReady) {
            try {
                wait();  // 一直等待,直到RealData被注入
            } catch (InterruptedException e) {
            }
        }
        return realData.result; // 由realData实现
    }
}
/**
 * 客户端程序,Client主要实现了获取FutureData,
 * 开启构造RealData的线程,并在接受请求后,很快返回FutureData。
 * 注意,它不会等待数据真的构造完毕再返回,而是立即返回FutureData,
 * 即使这个时候FutureData内并没有真实数据。
 */
public class Client {
    public Data request(final String queryStr){
        final FutureData future = new FutureData();
        new Thread(){
            public void run(){
                // RealData的构建很慢,所以在单独的线程中进行;
                RealData realData = new RealData(queryStr);
                future.setRealData(realData);
            }
        }.start();
        return future;    // FutureData会被立即饭hi
    }

    public static void main(String[] args) {
        Client client = new Client();
        // 这里会立即返回,因为得到的是FutureData,而不是RealData
        Data data = client.request("name");
        System.out.println("请求完毕");
        try {
            // 这里用一个sleep代替对其他业务逻辑的处理
            // 在处理这些逻辑的过程中,RealData被创建,从而充分利用了等待时间
            Thread.sleep(2000);
        }catch (InterruptedException e){

        }
        // 使用真实的数据
        System.out.println("数据 = " + data.getResult());
    }
}

5.5.3 JDK中的Future模式

在这里插入图片描述

Future模式的基本结构,如图5.9所示。

其中Future接口类似于前文描述的订单或者说是契约。通过它,你可以得到真实的数据。RunnableFuture继承了Future和Runnable两个接口,其中run()方法用于构造真实的数据。它有一个具体的实现FutureTask类。

FutureTask类有一个内部类Sync,一些实质性的工作会委托Sync类实现。

而Sync类最终会调用Callable接口,完成实际数据的组装工作。

/**
 * 上述代码实现了Callable接口,
 * 它的call()方法会构造我们需要的真实数据并返回。
 */

import java.util.concurrent.Callable;

public class RealData implements Callable<String> {

    private String para;
    public RealData(String para){
        this.para = para;
    }

    @Override
    public String call() throws Exception {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < 10; i++) {
            sb.append(para);
            try {
                Thread.sleep(100);
            }catch (InterruptedException e){
            }
        }
        return sb.toString();
    }
}
public class FutureMain {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 构造FutureTask对象实例
        FutureTask<String> future = new FutureTask<>(new RealData("a"));
        ExecutorService executor = Executors.newFixedThreadPool(1);
        // 执行FutureTask,相当于上例中的client.request("a")发送请求
        // 在这里开启线程进行RealData的call()方法执行
        // 将FutureTask提交给线程池
        executor.submit(future);
        System.out.println("请求完毕");
        try {
            // 这里依然可以做额外的数据操作,使用sleep代替其他业务逻辑的处理
            Thread.sleep(2000);
        }catch (InterruptedException e){

        }
        // 相当于5.5.2 节中的data.getResult()方法,取得call()方法的返回值
        // Future.get()方法得到Future的处理结果,但是这个方法是阻塞的
        System.out.println("数据= " + future.get());
    }
}

5.5.4 Guava对Future模式的支持

/**
 * 使用Futures 工具类将FutureCallback接口注册到给定的Future中,
 * 从而增加了对Future的异常处理。
 */
public class FutureDemo01 {
    public static void main(String[] args) throws InterruptedException {
        // 使用MoreExecutors.listeningDecorator()方法将一个普通的线程池
        // 包装为一个包含通知功能的Future线程池;
        ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
        // 将Callable任务提交到线程池中,并得到一个ListenableFuture;
        //与Future相比,ListenableFuture拥有完成时的通知功能
        ListenableFuture<String> task = service.submit(new RealData("x"));

        Futures.addCallback(task, new FutureCallback<String>() {
            @Override
            public void onSuccess(@Nullable String s) {
                System.out.println("异步处理成功, result=" + 0);
            }

            @Override
            public void onFailure(Throwable throwable) {
                System.out.println("异步处理失败,e=" + throwable);
            }
        }, MoreExecutors.newDirectExecutorService());

        System.out.println("main task done.....");
        Thread.sleep(3000);
    }
}

5.6 并行流水线

执行过程中有数据相关性的运算都是无法完美并行化的。

5.7 并行搜索

一种简单的策略就是将原始数据集合按照期望的线程数进行分割。如果我们计划使用两个线程进行搜索,那么就可以把一个数组或集合分割成两个。每个线程各自独立搜索,当其中有一个线程找到数据后,立即返回结果即可。

并行二分查找

public class Search {
    // 待搜索的数组
    static int[] arr= {5, 52, 6, 3, 4, 10, 8, 100, 35, 78, 64, 31, 77, 90,
            45, 53, 89, 78, 1,2 };
    // 定义线程池
    static ExecutorService pool = Executors.newCachedThreadPool();
    // 定义线程数量
    static final  int Thread_num = 2;
    // 用于存放结果,存放的是元素在arr数组中的下标,默认-1表示未找到元素
    static AtomicInteger result = new AtomicInteger(-1);

    public static void createArray(){
        arr = new int[1000*1000*1000];
        Random r = new Random();
        for (int i = 0; i < arr.length; i++) {
            arr[i] = i;
        }
    }

    /**
     *
     * @param searchValue
     * @param beginPos 搜索的起始位置
     * @param endPos 搜索的结束位置
     * @return
     */
    public static int search(int searchValue, int beginPos, int endPos){
        int i = 0;
        for (i = beginPos; i < endPos; i++) {
            //
            if (result.get()>=0){
                return result.get();
            }
            // 若条件(arr[i] == searchValue)成立,表示当前线程找到了需要的数据
            // 那么就将结果保存到result变量,
            // 这里使用CAS操作,若设置失败则表示其它线程已经先一步找到了结果
            if (arr[i] == searchValue){
                // 如果设置失败,表示其它线程已经先找到了
                if (!result.compareAndSet(-1, i)){
                    return result.get();
                }
                return i;
            }
        }
        return -1;
    }

    public static class SearchTask implements Callable<Integer>{
        int begin, end, searchValue;
        public SearchTask(int searchValue, int being, int end){
            this.begin = being;
            this.end = end;
            this.searchValue = searchValue;
        }
        public Integer call(){
            int re = search(searchValue, begin, end);
            return re;
        }
    }

    // 并行查找函数,根据线程数量对arr数组进行划分,并建立对应的任务提交给线程池处理
    public static int pSearch(int searchValue) throws ExecutionException, InterruptedException {
        int subArrSize = arr.length/Thread_num+1;
        List<Future<Integer>> re = new ArrayList<Future<Integer>>();
        //将原始数组arr划分为若干段,根据划分结果建立子任务
        // 每一个子任务都会返回一个Future对象,通过Future对象可以获得线程组得到的最终结果
        // 线程之间通过result共享彼此的信息,只要一个线程成功返回后,其他线程都会立即返回
        for (int i = 0; i < arr.length; i+=subArrSize) {
            int end = i+subArrSize;
            if (end >= arr.length) end=arr.length;
            re.add(pool.submit(new SearchTask(searchValue, i, end)));
        }
        for (Future<Integer> fu:re){
            if (fu.get()>=0) return fu.get();
        }
        return -1;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        int pos = pSearch(2);
        pool.shutdown();
        System.out.println(pos);
    }

}

5.8 并行排序

冒泡排序的并行化改进:奇偶交换排序

5.9 并行算法:矩阵乘法

5.10 NIO

5.10.1 Java IO

5.10.2 使用NIO进行网络编程

在这里插入图片描述

要了解NIO,首先需要知道在NIO中的

Channel。Channel有点类似于流,一个Channel可以和文件或者网络Socket对应。如果Channel对应着一个Soceket,那么往这个Channel中写数据,就等于向Socket中写入数据。

Buffer。大家可以简单地把Buffer理解成一个内存区域或者byte数组。数据需要包装成Buffer的形式才能和Channel交互(写入或者读取)。

Selector(选择器)。在Channel的众多实现中,有一个SelectableChannel实现,表示可被选择的通道。

任何一个SelectableChannel都可以将自己注册到一个Selector中,因此这个Channel就能为Selector所管理。而一个Selector可以管理多个SelectableChannel。当SelectableChannel的数据准备好时,Selector就会接到通知,得到那些已经准备好的数据,而SocketChannel就是SelectableChannel的一种。因此,它们构成了如图5.20所示的结构

5.10.3 使用NIO实现客户端

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值