浅谈多线程开发

  之前看到有人提过这样一个问题:多线程开发中频繁的线程间交互会降低整体效率,反而不如直接单线程的完成效果好,这样的多线程有什么意义呢?

  要想找到这个答案,就要从线程的定义出发了。在《操作系统概念》中有明确定义:线程是CPU使用的基本单元,它由线程ID、程序计数器、寄存器集合和栈组成。由此可见,线程是应用程序(这里默认单进程应用程序)内数据共享的CPU执行单位。说到多线程问题,当然要用经典的生产者消费者问题举例子了。先码上基础版本:

 

创建产品类模拟产品

 1 public class Product {
 2     private long num;
 3 
 4     public Product(int num) {
 5         setNum(num);
 6     }
 7 
 8     public long getNum() {
 9         return num;
10     }
11 
12     public void setNum(long num) {
13         this.num = num;
14     }
15 
16     @Override
17     public String toString() {
18         return "Product{" +
19                 "num=" + num +
20                 '}';
21     }
22 }

 

创建计时器类用于测试

 1 public class TimeCounter {
 2     private static long productStart;
 3     private static long productEnd;
 4     private static long consumerStart;
 5     private static long consumerEnd;
 6 
 7     public static synchronized void beginProduct() {
 8         if (productStart <= 0)
 9             productStart = System.currentTimeMillis();
10     }
11 
12     public static synchronized void stopProduct() {
13         if (productEnd <= 0)
14             productEnd = System.currentTimeMillis();
15     }
16 
17     public static synchronized void beginConsumer() {
18         if (consumerStart <= 0)
19             consumerStart = System.currentTimeMillis();
20     }
21 
22     public static synchronized void stopConsumer() {
23         if (consumerEnd <= 0)
24             consumerEnd = System.currentTimeMillis();
25     }
26 
27     public static void printResult() {
28         System.out.printf("product:" + productStart + "-" + productEnd + ", duration=" + (productEnd - productStart) + "ms!\r\n");
29         System.out.printf("consumer:" + consumerStart + "-" + consumerEnd + ", duration=" + (consumerEnd - consumerStart) + "ms!\r\n");
30         System.out.printf("----------total:" + (consumerEnd - productStart) + "ms!\r\n");
31     }
32 }

 

统一生产者模型

 1 import java.util.Random;
 2 import java.util.concurrent.atomic.AtomicBoolean;
 3 
 4 public class Producer implements Runnable {
 5 
 6     private Random random;
 7     private final int SLEEP_TIME;
 8 
 9     public Producer() {
10         random = new Random();
11         SLEEP_TIME = 1000;
12     }
13 
14     @Override
15     public void run() {
16         try {
17             TimeCounter.beginProduct();
18             while (true) {
19                 Thread.sleep(random.nextInt(SLEEP_TIME));
20                 synchronized (Environment.class){
21                     if (Environment.productsCounter.get() < Environment.TARGET_SIZE) {
22                         Product product = new Product(Environment.productsCounter.incrementAndGet());
23                         Environment.products.putLast(product);
24                     } else {
25                         TimeCounter.stopProduct();
26                         break;
27                     }
28                 }
29             }
30         } catch (InterruptedException e) {
31             e.printStackTrace();
32             Thread.currentThread().interrupt();
33         }
34 
35     }
36 
37 }

 

 

统一消费者模型

 1 public class Consumer implements Runnable {
 2 
 3     private Random random;
 4     private final int SLEEP_TIME;
 5 
 6     public Consumer() {
 7         random = new Random();
 8         SLEEP_TIME = 1000;
 9     }
10 
11     @Override
12     public void run() {
13         try {
14             TimeCounter.beginConsumer();
15             while (true) {
16                 Product product = Environment.products.takeFirst();
17                 if (product != null) {
18                     doSomething(product);
19                     if (product.getNum() < Environment.TARGET_SIZE) {
20                         Thread.sleep(random.nextInt(SLEEP_TIME));
21                     } else {
22                         TimeCounter.stopConsumer();
23                         break;
24                     }
25                 }
26             }
27 
28             TimeCounter.printResult();
29         } catch (InterruptedException e) {
30             e.printStackTrace();
31             Thread.currentThread().interrupt();
32         }
33     }
34 
35     private void doSomething(Product product) {
36         System.out.printf("Consumer: " + product + "\r\n");
37 
38     }
39 
40 }

 

最后是调用主函数

 1 import java.util.concurrent.BlockingDeque;
 2 import java.util.concurrent.LinkedBlockingDeque;
 3 import java.util.concurrent.atomic.AtomicBoolean;
 4 import java.util.concurrent.atomic.AtomicInteger;
 5 
 6 public class Environment {
 7 
 8     public final int PRODUCT_SIZE = 10;  //产品缓冲区内可容纳量
 9     public static BlockingDeque<Product> products;  //产品缓冲区
10     public static AtomicInteger productsCounter; //当前产品总数
11     public static final int TARGET_SIZE = 100;  //目标产品数
12     private Thread[] producers;  //生产者线程们
13     private Thread[] consumers;//消费者线程们
14 
15     public Environment() {
16         products = new LinkedBlockingDeque<>(PRODUCT_SIZE);
17         productsCounter = new AtomicInteger(0);
18         producers = new Thread[1];
19         consumers = new Thread[1];
20     }
21 
22     public void start() {
23         for (int i = 0; i < producers.length; i++) {
24             producers[i] = new Thread(new Producer());
25             producers[i].start();
26         }
27         for (int i = 0; i < consumers.length; i++) {
28             consumers[i] = new Thread(new Consumer());
29             consumers[i].start();
30         }
31     }
32 
33     public static void main(String[] args) {
34         Environment environment = new Environment();
35         environment.start();
36     }
37 
38 }

 

  在这套代码中,为了生产TARGET_SIZ数量的产品,我们使用生产者线程们和消费者线程们来完成整个流程,生产者们生产的产品暂时放在最大容量为PRODUCT_SIZE的缓冲区内保存,消费者们从缓冲区中取出产品,并通过doSomething()来消费掉这个产品,至于缓冲区使用BlockingDeque和产品数量使用AtomicInteger都能保证线程访问的同步性。这样就建立了一个闭合系统,可以通过修改线程们的数量来改变当前系统的总效率。

 

  显而易见,线程们的数量在一定范围内与当前系统总用时成负相关的,此时的系统效率是随着线程数量递增的,但是当把线程们的数量分配到足够大,也就是系统用大部分时间创建线程们,而线程们做的生产工作时间小到忽略不计,此时这套系统的总用时又变长了很多,也就是系统效率开始降低。这也就不难理解了,线程们的数量和当前系统效率是呈二次曲线关系的。

  从操作系统角度考量,在阙值内,多个CPU当然可以并行多组线程执行一系列操作,不同CPU只有对共享变量缓冲区和产品数量进行同步读写,而这相比较他们线程内各自任务的耗时来讲可以小到忽略,因此CPU在执行线程代码时,大部分时间用来执行生产和消费操作,几乎不用阻塞等待同步操作。但是当超过阙值的线程数量时,共享变量的读写访问量加大,必然引起更多的线程阻塞等待,虽然CPU还是有相同时间执行单纯的生产消费操作,但是总用时却由于线程之间的严重阻塞而增加了,因此系统总效率反而降低。

 

  返回到文章开篇提到的问题,其中所提到的现象很可能是在线程数量超过阙值之后而产生的。当然也就不难理解,在使用多线程中要保证在阙值以内最大能力的提高总效率,而不是造成超过阙值之后产生一系列低效问题。

 

  这使我想到在之前犯过的一个低级错误,需求就是读取很大容量的文件,过滤掉其中不必要的内容,之后把修改后的内容写入新文件。很容易被大内容的文件误导,使用多线程读取文件的不同行,在内存修改后再写入到新文件中。然而我似乎忘记了,这个需求中的短板在于新文件的写入。读取文件部分线程几个无所谓,关键是内存操作完成后,写入新文件要怎么使用线程?使用单线程写入单个文件,本来就是最简单的模式,如果使用多线程写入单个文件,必然多个线程要等待文件的写锁,相比较CPU反而增加了多线程的分配唤醒等操作;如果使用多线程写入多个文件,最后再将多个文件合并到一个文件中,最后的合并操作对CPU来说和单线程写入单文件的用时是一样的,因此增加的是前半部分文件写入读取操作和单文件的写锁等待上。因此用单线程写入单文件反而是效率最高的。其实在这个需求中线程的阙值就是1,这是由于耗时的文件读写导致的,因此说阙值的影响因素也是要考虑的,但是不管在任何项目中,只要不超过阙值数量的线程,还是一定能提高系统效率的。

 

posted on 2019-03-30 14:56  白少木丿  阅读(292)  评论(0编辑  收藏  举报

导航