从RocketMQ中学到的并发技巧

161 阅读5分钟

前言

前段时间无聊抄了一下RocketMQ源码,刚刚抄完存储部分,感觉有些并发写的非常好,所以总结下。

1.CountDownLatch的改造,支持重置

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class CountDownLatch2 {
    private final Sync sync;

    /**
     * Constructs a {@code CountDownLatch2} initialized with the given count.
     *
     * @param count the number of times {@link #countDown} must be invoked before threads can pass through {@link
     * #await}
     * @throws IllegalArgumentException if {@code count} is negative
     */
    public CountDownLatch2(int count) {
        if (count < 0)
            throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

    /**
     * Causes the current thread to wait until the latch has counted down to
     * zero, unless the thread is {@linkplain Thread#interrupt interrupted}.
     *
     * <p>If the current count is zero then this method returns immediately.
     *
     * <p>If the current count is greater than zero then the current
     * thread becomes disabled for thread scheduling purposes and lies
     * dormant until one of two things happen:
     * <ul>
     * <li>The count reaches zero due to invocations of the
     * {@link #countDown} method; or
     * <li>Some other thread {@linkplain Thread#interrupt interrupts}
     * the current thread.
     * </ul>
     *
     * <p>If the current thread:
     * <ul>
     * <li>has its interrupted status set on entry to this method; or
     * <li>is {@linkplain Thread#interrupt interrupted} while waiting,
     * </ul>
     * then {@link InterruptedException} is thrown and the current thread's
     * interrupted status is cleared.
     *
     * @throws InterruptedException if the current thread is interrupted while waiting
     */
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

    /**
     * Causes the current thread to wait until the latch has counted down to
     * zero, unless the thread is {@linkplain Thread#interrupt interrupted},
     * or the specified waiting time elapses.
     *
     * <p>If the current count is zero then this method returns immediately
     * with the value {@code true}.
     *
     * <p>If the current count is greater than zero then the current
     * thread becomes disabled for thread scheduling purposes and lies
     * dormant until one of three things happen:
     * <ul>
     * <li>The count reaches zero due to invocations of the
     * {@link #countDown} method; or
     * <li>Some other thread {@linkplain Thread#interrupt interrupts}
     * the current thread; or
     * <li>The specified waiting time elapses.
     * </ul>
     *
     * <p>If the count reaches zero then the method returns with the
     * value {@code true}.
     *
     * <p>If the current thread:
     * <ul>
     * <li>has its interrupted status set on entry to this method; or
     * <li>is {@linkplain Thread#interrupt interrupted} while waiting,
     * </ul>
     * then {@link InterruptedException} is thrown and the current thread's
     * interrupted status is cleared.
     *
     * <p>If the specified waiting time elapses then the value {@code false}
     * is returned.  If the time is less than or equal to zero, the method
     * will not wait at all.
     *
     * @param timeout the maximum time to wait
     * @param unit the time unit of the {@code timeout} argument
     * @return {@code true} if the count reached zero and {@code false} if the waiting time elapsed before the count
     * reached zero
     * @throws InterruptedException if the current thread is interrupted while waiting
     */
    public boolean await(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }

    /**
     * Decrements the count of the latch, releasing all waiting threads if
     * the count reaches zero.
     *
     * <p>If the current count is greater than zero then it is decremented.
     * If the new count is zero then all waiting threads are re-enabled for
     * thread scheduling purposes.
     *
     * <p>If the current count equals zero then nothing happens.
     */
    public void countDown() {
        sync.releaseShared(1);
    }

    /**
     * Returns the current count.
     *
     * <p>This method is typically used for debugging and testing purposes.
     *
     * @return the current count
     */
    public long getCount() {
        return sync.getCount();
    }

    public void reset() {
        sync.reset();
    }

    /**
     * Returns a string identifying this latch, as well as its state.
     * The state, in brackets, includes the String {@code "Count ="}
     * followed by the current count.
     *
     * @return a string identifying this latch, as well as its state
     */
    public String toString() {
        return super.toString() + "[Count = " + sync.getCount() + "]";
    }

    /**
     * Synchronization control For CountDownLatch2.
     * Uses AQS state to represent count.
     */
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

        private final int startCount;

        Sync(int count) {
            this.startCount = count;
            setState(count);
        }

        int getCount() {
            return getState();
        }

        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

        protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (; ; ) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c - 1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }

        protected void reset() {
            setState(startCount);
        }
    }
}

相比自带的CountDownLatch多了一个reset()方法,在Sync中多了一个startCount变量用来保存CountDownLatch构造参数传过来的初始计数,当reset()重新设置计数值。

好处:CountDownLatch本身设计成一个临时的,但是有些场景需要重复使用,有了reset()就避免了重复创建

2.继承线程工厂,重写newThread方法,让每个线程创建出来的都是守护进程

public class ThreadFactoryImpl implements ThreadFactory {
    private final AtomicLong threadIndex = new AtomicLong(0);
    private final String threadNamePrefix;
    private final boolean daemon;

    public ThreadFactoryImpl(final String threadNamePrefix) {
        this(threadNamePrefix, false);
    }

    public ThreadFactoryImpl(final String threadNamePrefix, boolean daemon) {
        this.threadNamePrefix = threadNamePrefix;
        this.daemon = daemon;
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r, threadNamePrefix + this.threadIndex.incrementAndGet());
        thread.setDaemon(daemon);
        return thread;
    }
}

这里用到的是一个工厂模式,将我们的后台服务进程如检测磁盘的占用率,设置成守护进程。也就是说我们的后台服务就像GC一样,等待最后一个用户进程结束时,随着jvm的结束而结束。

3.交换容器的设计思想

public class GroupCommitService extends Thread {
    class GroupCommitRequest {
    }

    private volatile List<GroupCommitRequest> requestsWrite = new ArrayList<GroupCommitRequest>();//任务提交容器。
    private volatile List<GroupCommitRequest> requestsRead = new ArrayList<GroupCommitRequest>(); //任务执行容器

    public synchronized void putRequest(final GroupCommitRequest request) {
        //客户端提交同步刷盘任务到GroupCommitService线程
        synchronized (this.requestsWrite) {
            this.requestsWrite.add(request);
        }
    }

    private void swapRequests() {
        //写和读的互相交换一下
        List<GroupCommitRequest> tmp = this.requestsWrite;
        this.requestsWrite = this.requestsRead;
        this.requestsRead = tmp;
    }


    private void doCommit() {
        synchronized (this.requestsRead) {//获取
            if (!this.requestsRead.isEmpty()) {
                for (GroupCommitRequest req : this.requestsRead) {

                }
                this.requestsRead.clear();
            }
        }
    }

    @Override
    public void run() {
        while (true) { //服务没有被停止
            try {
                //如果没有待处理的任务,则休息10ms,即每10ms空转一次,
                Thread.sleep(10);
                //进行提交操作。
                this.doCommit();
                //交换请求。
                this.swapRequests();
            } catch (Exception e) {

            }
        }
    }
}

简单写了个demo,核心就是将任务提交和任务执行做分离,避免了锁的冲突,我感觉它是一个读写分离的思想。

4.大量CompletableFuture使用

import java.util.concurrent.CompletableFuture;

public class Test01 {

    public static void main(String[] args) {
        Test01 test01 = new Test01();
        test01.asyncProcessRequest("test");
        System.out.println("main结束");
    }

    public void asyncProcessRequest(String msg){
        asyncPutMessageProcessor(msg).thenAcceptAsync((r) -> {
            System.out.println("asyncPutMessageProcessor: " + r);
        });
    }

    public CompletableFuture<String>asyncPutMessageProcessor(String msg){
        CompletableFuture<String> putMessageResult = asyncPutMessage(msg);
        return handlePutMessageResultFuture(putMessageResult);
    }

    public CompletableFuture<String> asyncPutMessage(String message) {
        if (message == null) return CompletableFuture.completedFuture("ERROR");
        CompletableFuture<String> putResultFuture = asyncPutCommitLogMessage(message);
        putResultFuture.thenAccept((result) -> {
           //做处理
            System.out.println("asyncPutMessage:"+message);
        });
        return putResultFuture;
    }

    public CompletableFuture<String> handlePutMessageResultFuture(CompletableFuture<String> putMessageResult) {
        return putMessageResult.thenApply((r) -> handlePutMessageResult(r));
    }

    public String handlePutMessageResult(String r) {
        if (r == "OK" || r == "ERROR") return r;
        return "UNKNOWN";
    }

    public CompletableFuture<String> asyncPutCommitLogMessage(String msg) {
        CompletableFuture<String> flushResultFuture = submitFlushRequest(msg);
        CompletableFuture<String> replicaResultFuture = submitReplicaRequest(msg);
        return flushResultFuture.thenCombine(replicaResultFuture, (flushStatus, replicaStatus) -> {
            if (flushStatus != "OK" || replicaStatus != "OK") {
               return "ERROR";
            }
            return "OK";
        });
    }

    //写本地
    public CompletableFuture<String> submitFlushRequest(String msg) {
        if (msg == null) return CompletableFuture.completedFuture("ERROR");
        return putMessage(msg,"Flush");//偷懒写法
    }

    //写从节点
    public CompletableFuture<String> submitReplicaRequest(String msg) {
        if (msg == null) return CompletableFuture.completedFuture("ERROR");
        return putMessage(msg,"Replica");

    }



    public CompletableFuture<String> putMessage(String message, String type){
        CompletableFuture<String> flushOKFuture = new CompletableFuture<>();


        Thread thread = new Thread(() -> {
            // 生成一个10至30之间的随机数作为睡眠时间(单位:秒)
            long sleepTime = (long) (Math.random() * 21 + 10);

            try {
                System.out.println("线程将睡眠 " + sleepTime + " 秒");
                Thread.sleep(sleepTime * 1000); // 注意转换为毫秒
                System.out.println((type ==  "Flush"? "Flush :":"Replica :") +message);
                flushOKFuture.complete("OK");

            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                System.out.println("线程被中断");
            }

            // 睡眠结束后执行的操作
            System.out.println("线程执行后续操作");
        });

        // 启动线程
        thread.start();

        return flushOKFuture;
    }

参考同步双写,写了一个小demo。

5.CopyOnWriteArrayList的使用(cow写时复制思想)

private final CopyOnWriteArrayList<MappedFile> mappedFiles = new CopyOnWriteArrayList<MappedFile>();

我硬凑一下,就是感觉这个地方用的比较好,这个是在MappedFileQueue类使用的,每个MappedFile都代表一个文件,MappedFileQueue是一个目录下所有文件的合集。为什么说用的好呢?因为mappedFiles里面的文件很少增加和删除,但是读的情况非常多。以commitlog为例,查找消息就会查询mappedFiles列表。那么增加和删除呢?每个commitlog大小为1G,写满才会创建新的,删除如果不是磁盘占用高的话每天才会清除一次超过三天的文件。所以说是“读多写少”。