多线程设计模式-串行线程封闭模式

定义:通过将多个并发的任务存入队列实现任务的串行化,并为这些串行化的任务创建唯一的一个工作者线程进行处理

由于只有唯一的工作者线程访问,对其的访问无须加锁,因此避开了锁的的开销和锁带来的问题

串行线程封闭模式

Serializer: 负责对外暴露服务接口以及工作者线程的生命周期管理,并将客户端对其的并发任务调用转换为相应的任务以实现服务串行化
    service: 表示将串行线程封闭模式对外暴露的服务方法,该方法将客户端对其的并发调用串行化
    init: 初始化串行线程封闭模式对外暴露的服务,该方法启动工作者线程
    shutdown: 停止线程串行模式对外暴露的服务,该方法停止工作者线程
WorkerThread: 工作者线程,负责接收Serializer提交的并发任务以及这些任务的执行    
    submit: 用于Serializer提交并发任务,并将这些任务串行化
    dispatch: 用于执行串行化的任务
Queue: Serializer和WorkerThread间的缓冲区,用于实现并发任务的串行化
    enqueue: 用于并发任务入队列
    dequeue: 用于任务出队列
NonThreadSafeObject: 工作者线程执行任务时所需访问的非线程安全对象

需要下载一批文件,但是不希望是建立大量的网络连接,因为大量的网络IO会增加系统负担,此时可以将并行任务串行化。这种情况可以考虑使用队列,下列代码使用阻塞队列ArrayBlockingQueue,将任务一个一个取出来处理,同时也避免额外增添锁的使用,减小开销。事实上,ArrayBlockingQueue内部中使用了ReentrantLock来实现阻塞,保证了串行化,同时也保证了线程安全。

package com.bruce.serialThreadConfinement;

import com.bruce.twoPhaseTermination.AbstractTerminatableThread;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPClientConfig;
import org.apache.commons.net.ftp.FTPReply;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

/**
* @Author: Bruce
* @Date: 2019/6/3 19:07
* @Version 1.0
*/
public class WorkerThread extends AbstractTerminatableThread {

    private Logger LOG = LoggerFactory.getLogger(WorkerThread.class);

    protected final BlockingQueue<String> workQueue;
    private final FTPClient ftpClient;
    private final String outputDir;
    private String servWorkingDir;

    public WorkerThread(String outputDir, final String ftpServer, final String userName,
                        final String password, String servWorkingDir) throws Exception {
        this.workQueue = new ArrayBlockingQueue<String>(100);
        this.outputDir = outputDir + '/';
        this.servWorkingDir = servWorkingDir;
        this.ftpClient = initFTPClient(ftpServer, userName, password);
    }

    public void download(String file) {
        try {
            workQueue.put(file);
            terminationToken.reservations.incrementAndGet();
        } catch (InterruptedException e) {
            ;
        }
    }

    protected FTPClient initFTPClient(String ftpServer, String userName, String password) throws Exception {
        FTPClient ftpClient = new FTPClient();

        FTPClientConfig config = new FTPClientConfig();
        ftpClient.configure(config);

        int reply;
        ftpClient.connect(ftpServer);

        System.out.println(ftpClient.getReplyString());

        reply = ftpClient.getReplyCode();

        if (!FTPReply.isPositiveCompletion(reply)) {
            ftpClient.disconnect();
            throw new RuntimeException("FTP server refused connection.");
        }

        boolean isOK = ftpClient.login(userName, password);

        if (isOK) {
            System.out.println(ftpClient.getReplyString());
        } else {
            throw new RuntimeException("Failed to login." + ftpClient.getReplyString());
        }

        reply = ftpClient.cwd(servWorkingDir);
        if (!FTPReply.isPositiveCompletion(reply)) {
            ftpClient.disconnect();
            throw new RuntimeException("Failed to change working directory. reply: " + reply);
        } else {
            System.out.println(ftpClient.getReplyString());
        }

        ftpClient.setFileType(FTP.ASCII_FILE_TYPE);
        return ftpClient;
    }

    @Override
    protected void doRun() throws Exception {
        String file = workQueue.take();
        LOG.info("Downloading %s", file);
        boolean isOK;
        try (OutputStream os = new BufferedOutputStream(new FileOutputStream(outputDir + file))) {
            isOK = ftpClient.retrieveFile(file, os);
            if (!isOK) {
                LOG.error("Failed to download %s", file);
            }
        } finally {
            terminationToken.reservations.decrementAndGet();
        }
    }

    @Override
    protected void doCleanup(Exception cause) {
        try {
            ftpClient.disconnect();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
package com.bruce.serialThreadConfinement;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

/**
* @Author: Bruce
* @Date: 2019/6/3 19:06
* @Version 1.0
*/
public class MessageFileDownloader {

    private final WorkerThread workerThread;

    public MessageFileDownloader(String outputDir, final String ftpServer, final String userName, final String password,
                                 final String servWorkingDir) throws Exception {
        Path path = Paths.get(outputDir);
        if (!path.toFile().exists()) {
            Files.createDirectories(path);
        }
        workerThread = new WorkerThread(outputDir, ftpServer, userName, password, servWorkingDir);
    }

    public void init() {
        workerThread.start();
    }

    public void shutdown() {
        workerThread.terminate();
    }

    public void downloadFile(String file) {
        workerThread.download(file);
    }
}
package com.bruce.serialThreadConfinement;

/**
* @Author: Bruce
* @Date: 2019/6/3 22:29
* @Version 1.0
*/
public class SimpleClient {

    private static final MessageFileDownloader DOWNLOADER;

    static {
        MessageFileDownloader mfd = null;
        try {
            mfd = new MessageFileDownloader(getWorkingDir("serialThreadConfinement"), "192.168.8.201", "ftp",
                    "123456", "~/messages");
            mfd.init();
        } catch (Exception e) {
            e.printStackTrace();
        }
        DOWNLOADER = mfd;
    }

    public static void main(String[] args) throws InterruptedException {
        DOWNLOADER.downloadFile("abc.xml");
        DOWNLOADER.downloadFile("123.xml");
        DOWNLOADER.downloadFile("xyz.xml");

        Thread.sleep(1000);

        DOWNLOADER.shutdown();
    }

    public static String getWorkingDir(String subDir) {
        if (null == subDir) {
            subDir = "";
        }
        String dir = System.getProperty("user.dir");
        dir = dir.replaceAll("\\\\", "/");
        dir = dir + subDir + "/";
        return dir;
    }
}

详细的源代码可以见我的Github

适用的场景有
1. 需要使用非线程安全对象,又不希望引入锁
2. 任务的执行涉及I/O操作,但是不希望有过多的I/O线程增加上下文切换

参考资料

黄文海 Java多线程编程实战指南(设计模式篇)

黄文海的Github

猜你喜欢

转载自blog.csdn.net/u010145219/article/details/91337918
今日推荐