java模拟耗时操作_Java多线程演进之路

首先在Java里面我们为什么要使用多线程呢?虽然在编程问题中有相当大一部分都可以通过顺序编程来解决,然而,对于某些问题,如果能够并发执行程序中的多个部分,则会变得非常方便甚至非常必要.比如在android应用编程里,所有耗时的操作,比如网络请求,都会在一个独立的线程里执行,以免阻塞UI线程,导致APP卡顿造成糟糕的用户体验.其次,使用多线程可以更好的利用cpu的资源.

有两个重要的概念我们要搞清楚,并行与并发:

  • 并行:多个cpu同时执行不同任务,是真正的同时
  • 并发:通过cpu调度算法,让多个任务看上去像是同时执行,实际上从cpu操作层面看不同任务是间隔执行的

如图所示:

7d52a6f88d5b44d0baa57d96457f9eb2.png

虽然使用多线程有这些好处,但是当并行执行的任务开始产生互相干涉时,实际的并发问题就会接迥而至.故障可能还是偶尔出现的,不确定性的,这对于编程来说非常致命.因此我也会讨论如何实现线程安全.

JDK1.0:使用Thread和Runnable实现多线程

实现Runnable接口:

public class Task1 implements Runnable {
    Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public void run() {
        try {
            // 模拟耗时操作
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            logger.error("interrupted", e);
        }
        System.out.println(getClass().getSimpleName() + "执行完成");
    }
}

或者继承Thread类:

public class Task2 extends Thread {
    Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public void run() {
        try {
            // 模拟耗时操作
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            logger.error("interrupted", e);
        }
        System.out.println(getClass().getSimpleName() + "执行完成");
    }
}

测试:

public class MultiThreadTest {
    @Test
    public void test() throws Exception {
        Thread thread1 = new Thread(new Task1());
        thread1.start();

        Task2 thread2 = new Task2();
        thread2.start();

        TimeUnit.SECONDS.sleep(5);
    }
}

JDK1.5:使用ExecutorService实现多线程

ExecutorService在JDK1.5开始提供,旨在让我们避免直接管理Thread类对象,同时ExecutorService实现了线程复用,提高了效率.

创建一个什么样的ExecutorService的实例(即线程池)需要根据具体应用场景而定,不过Java给我们提供了一个Executors的辅助类,它可以帮助我们很方便的创建各种类型ExecutorService线程池,Executors一共可以创建下面这四类线程池:

  1. newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程.
  2. newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待.
  3. newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行.
  4. newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行.

还提供了新的Callable<T>类,用于得到线程的执行结果.如下:

public class Task3 implements Callable<String> {
    Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public String call() {
        try {
            // 模拟耗时操作
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            logger.error("interrupted", e);
        }
        System.out.println(getClass().getSimpleName() + "执行完成");
        return getClass().getSimpleName() + " success";
    }
}

测试:

public class MultiThreadTest {
    @Test
    public void test1() throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();

        executorService.execute(new Task1());

        Future<String> future4 = executorService.submit(new Task3());
        System.out.println(future4.get());

        executorService.awaitTermination(5, TimeUnit.SECONDS);
    }
}

JDK1.7:使用ForkJoinPool实现并行执行

ForkJoinPool的优势在于,可以充分利用多cpu,多核cpu的优势,把一个任务拆分成多个“小任务”,把多个“小任务”放到多个处理器核心上并行执行;当多个“小任务”执行完成之后,再将这些执行结果合并起来。

例如,我们想要计算从1到1百万的总和,使用ForkJoinPool的实现是这样的:

public class SumRecursiveTask extends RecursiveTask<Long> {

    private final long[] numbers;
    private final int start;
    private final int end;

    public static final long THRESHOLD = 10_000;

    public SumRecursiveTask(long[] numbers) {
        this(numbers, 0, numbers.length);
    }

    // 私有构造函数用于以递归方式为主任务创建子任务
    private SumRecursiveTask(long[] numbers, int start, int end) {
        this.numbers = numbers;
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        int length = end - start;
        if (length <= THRESHOLD) {
            return computeSequentially();
        }
        // 对任务分片,创建子任务
        SumRecursiveTask leftTask = new SumRecursiveTask(numbers, start, start + length / 2);
        leftTask.fork();
        SumRecursiveTask rightTask = new SumRecursiveTask(numbers, start + length / 2, end);
        Long rightResult = rightTask.compute();
        Long leftResult = leftTask.join();
        // 归并任务结果
        return leftResult + rightResult;
    }

    private long computeSequentially() {
        long sum = 0;
        for (int i = start; i < end; i++) {
            sum += numbers[i];
        }
        return sum;
    }
}

测试:

public class MultiThreadTest {
    @Test
    public void test2() throws Exception {
        long[] numbers = new long[1_000_000];
        for (int i = 0; i < 1_000_000; i++) {
            numbers[i] = i + 1;
        }
        RecursiveTask<Long> task = new SumRecursiveTask(numbers);
        // 默认创建和当前电脑CPU核心数相同的线程数
        Future<Long> future = ForkJoinPool.commonPool().submit(task);
        System.out.println(future.get());
    }
}

JDK1.8:使用Stream(流)

流是Java API的新成员,它允许你以声明性方式处理数据集合.此外,流还可以透明地并行处理,这样就无需写任何多线程代码了!上面计算从1到1百万的总和使用流来实现是这样的:

public class MultiThreadTest {
    @Test
    public void test3()  {
        long[] numbers = new long[1_000_000];
        for (int i = 0; i < 1_000_000; i++) {
            numbers[i] = i + 1;
        }
        System.out.println(LongStream.of(numbers).parallel().sum());
    }
}

在这里,并行流内部也是使用了默认的ForkJoinPool,只不过所有对任务进行分片及归并的逻辑都由JDK在幕后替我们完成.

接下来,我们来研究下线程安全的问题,如下是一个偶数生成器工厂EvenGeneratorFactory:

public class EvenGeneratorFactory {
    private static EvenGeneratorFactory evenGeneratorFactory = new EvenGeneratorFactory();
    /**
     * 损坏标志
     */
    private volatile boolean broken;
    private int currentValue = 0;

    private EvenGeneratorFactory() {
    }

    public static EvenGeneratorFactory newInstance() {
        return evenGeneratorFactory;
    }

    public int next() {
        currentValue++;
        currentValue++;
        return currentValue;
    }

    public boolean isBroken() {
        return broken;
    }

    public void setBroken(boolean broken) {
        this.broken = broken;
    }
}

这里EvenGeneratorFactory是一个单例对象,接着编写一个偶数检查器,在run方法里不断的尝试从EvenGeneratorFactory里取值并检查:

public class EvenChecker implements Runnable {

    @Override
    public void run() {
        EvenGeneratorFactory evenGeneratorFactory = EvenGeneratorFactory.newInstance();
        while (!evenGeneratorFactory.isBroken()) {
            int number = evenGeneratorFactory.next();
            if (number % 2 != 0) {
                System.err.println(number + "不是一个偶数!");
                evenGeneratorFactory.setBroken(true);
            }
        }
    }
    
}

测试:

public class MultiThreadTest {
    @Test
    public void test4() throws Exception {
        EvenGeneratorFactory evenGeneratorFactory = EvenGeneratorFactory.newInstance();
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 2; i++) {
            executorService.submit(new EvenChecker(evenGeneratorFactory));
        }
        executorService.awaitTermination(5, TimeUnit.SECONDS);
    }
}

不断运行这个测试类,可以看到基本每次打印的数字都不一样.这里的问题就在于EvenGeneratorFactory类的next方法里的变量递增操作不是原子性的,当有多个线程同时操纵同一个共享的EvenGeneratorFactory对象时就导致了意外的结果.那么要纠正这个问题,有两种方式,第一种,使用synchronized 关键字:

public class EvenGeneratorFactory {
    ......
    public synchronized int next() {
        currentValue++;
        currentValue++;
        return currentValue;
    }
    ......
}

第二种,使用ReentrantLock (可重入锁):

public class EvenGeneratorFactory {
    ......
    private ReentrantLock reentrantLock = new ReentrantLock();
    public int next() {
        reentrantLock.lock();
        try {
            currentValue++;
            currentValue++;
            return currentValue;
        }finally {
            reentrantLock.unlock();
        }
    }
    ......
}

那么synchronized 和 ReentrantLock有什么区别呢(大部分面试都会提到这个)?

  • 首先synchronized是java内置关键字,在jvm层面.ReentrantLock是个java类;
  • synchronized无法判断是否获取锁的状态,ReentrantLock可以判断是否获取到锁以及获取各种锁的信息;
  • synchronized无法设置获取锁超时,ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁 ;
  • synchronized的锁仅有单一的条件,而ReentrantLock可绑定多个条件实现多路通知;
  • synchronized会自动释放锁,ReentrantLock需在finally中手工释放锁,否则容易造成线程死锁;
  • synchronized的锁可重入、不可中断、非公平,而ReentrantLock锁可重入、可中断、可公平(两者皆可);
  • 二者的锁机制也是不一样的:ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的是对象头中mark word.

另外实现线程安全还可以使用ThreadLocal,例如我们很常用的SimpleDateFormat类不是线程安全的,与其在方法里每次使用的时候都new一个,不如使用ThreadLocal,让每个线程都拥有一个SimpleDateFormat对象,这样就不存在竞争问题了.看下面例子,共享同一个SimpleDateFormat对象:

public class Task4 implements Callable<String> {
    private SimpleDateFormat simpleDateFormat;

    public Task4(SimpleDateFormat simpleDateFormat) {
        this.simpleDateFormat = simpleDateFormat;
    }

    @Override
    public String call() throws ParseException {
        for (int i = 0; i < 10000; i++) {
            Date date = simpleDateFormat.parse("2019-12-12 12:12:12");
            if (date.getTime() != 1576123932000L) {
                System.err.println("解析日期错误:" + date);
            }
        }
        return getClass().getSimpleName() + " success";
    }
}

测试:

public class MultiThreadTest {
    @Test
    public void test5() throws Exception {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            executorService.submit(new Task4(simpleDateFormat));
        }
        executorService.awaitTermination(5, TimeUnit.SECONDS);
    }
}

运行测试类,就可以发现解析出了许多错误的日期,这也验证了SimpleDateFormat类不是线程安全的.改进版本,使用ThreadLocal:

public class Task4 implements Callable<String> {

    private static ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal
            .withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    @Override
    public String call() throws ParseException {
        // 某个线程首次调用get方法时,会先调用initialValue方法
        SimpleDateFormat simpleDateFormat = threadLocal.get();
        for (int i = 0; i < 10000; i++) {
            Date date = simpleDateFormat.parse("2019-12-12 12:12:12");
            if (date.getTime() != 1576123932000L) {
                System.err.println("解析日期错误" + date);
            }
        }
        return getClass().getSimpleName() + " success";
    }

}

测试:

public class MultiThreadTest {
    @Test
    public void test5() throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            executorService.submit(new Task4());
        }
        executorService.awaitTermination(5, TimeUnit.SECONDS);
    }
}

无论运行多少次,都不会出现解析错误的情况.

源码github地址:

jufeng98/java-master​github.com
50532f372d36a666e0b2e4157612c5f9.png
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值