浅谈多线程开发
之前看到有人提过这样一个问题:多线程开发中频繁的线程间交互会降低整体效率,反而不如直接单线程的完成效果好,这样的多线程有什么意义呢?
要想找到这个答案,就要从线程的定义出发了。在《操作系统概念》中有明确定义:线程是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,这是由于耗时的文件读写导致的,因此说阙值的影响因素也是要考虑的,但是不管在任何项目中,只要不超过阙值数量的线程,还是一定能提高系统效率的。