java并发编程浅析

==> 学习汇总(持续更新)
==> 从零搭建后端基础设施系列(一)-- 背景介绍


一、继承 Thread

		/**
         * 通过调用start方法会通知jvm创建一个线程,然后调用run方法
         * 直接调用run方法就相当于调用了一个类的成员函数,并不会创建新的线程
         */
        for (int i = 0; i < 5; i++) {
            new MyThread().start();
            new MyThread().run();
        }
        ……
        class MyThread extends Thread {
	        @Override
	        public void run (){
	            System.out.println(Thread.currentThread().getName());
	        }
    	}

输出:

main
Thread-0
main
Thread-2
main
Thread-4
main
Thread-6
main
Thread-8

充分验证了"直接调用run方法就相当于调用了一个类的成员函数,并不会创建新的线程"这句话。
在实际开发中,一般不会使用这种方式,因为这样线程(Thread)和任务(Task)耦合了。

想一想:
为什么会有继承这种方式呢?我们都知道,继承是为了扩充父类的功能,也就是说,如果Thread的功能不能满足你,那你可以继承它,并且实现其它你需要的功能。

二、实现 Runnable

  		/**
         * 为什么我要命名为Task? 因为我想表达的就是,其实实现Runnable的类
         * 就相当于一个任务(Task),然后交给Thread(线程)去运行,分工明明白白
         */
        for (int i = 0; i < 5; i++) {
            new Thread(new Task()).start();
            new Thread(new Task()).run();
        }
        ……
        static class Task implements Runnable {
		       @Override
		       public void run() {
		           System.out.println(Thread.currentThread().getName());
		       }
    	}
      

Runnable只有一个run方法

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

Thread中的run方法,当构造参数是Runnable类型的,那么当jvm调用Thread的run方法的时候,会再调用target的run方法,也就是我们写的Task中的run方法。
和继承Thread对比,当jvm调用Thread的run方法的时候,实际上是调用被重写的run方法

    private Runnable target;
    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    ……
	@Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

三、实现 Callable

Callable和Runnable的唯一区别在于,前者的call方法可以返回值,并且支持call方法抛异常
        //为什么Thread可以接受FutureTask参数? 因为FutureTask继承了Runnable和Future
        List<FutureTask> futures = new ArrayList<>();
        for (int i = 0; i < 2; i++) {
            futures.add(new FutureTask<String>(new TaskCall()));
            new Thread(futures.get(i)).start();
        }

        //也可以使用线程池中的submit
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 2; i++) {
            futures.add((FutureTask<String>)executorService.submit(new TaskCall()));
        }

        //submit也可以提交实现了Runnable的Task,只是返回的FutureTask中,获取不到线程返回点的结果
        //一般用submit提交Runnable的Task,只是为了使用FutureTask获取线程的状态或中断线程
        for (int i = 0; i < 2; i++) {
            futures.add((FutureTask) executorService.submit(new Task()));
        }

        executorService.shutdown();
        //遍历FutureTask,一个个的获取结果,需要注意的是,如果第一个线程还没执行完,但是后面的线程执行完了
        //也会阻塞,直到当前线程执行完并返回结果。
        for (int i = 0; i < futures.size(); i++) {
            try {
                String s = (String)futures.get(i).get();
                System.out.println(s);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }

输出:

Thread-0
Thread-1
pool-1-thread-1
pool-1-thread-2
null
null

很明显,经过FutureTask包装后,结果返回可以是顺序的可控的,执行可以是乱序。

再来解释一下Future和FutureTask

public interface Future<V> {
    /**
    *分三种情况
    *1.线程未开始执行,如果这时候取消,那么该线程就不会再执行
    *2.线程正在执行,如果这时候取消,不会影响当前正在执行的线程,只是影响获取线程返回结果(抛异常)。但是有个特殊的是,如果当前线程休眠的时候被取消,就真的会被中断,不会继续往下执行了
    *3.线程已经执行完成或者出了其它什么事,如果这时候取消,那么将返回false取消失败。
    *其实mayInterruptIfRunning这个参数并没有什么用,不能强制中断正在运行中的线程(有例外, 想一想是什么呢?)
    */
    boolean cancel(boolean mayInterruptIfRunning);
    //线程是否已取消
    boolean isCancelled();
    //线程是否是完成态,包括结束、取消、异常
    boolean isDone();
    //阻塞拿结果,直到获取到结果或抛异常
    V get() throws InterruptedException, ExecutionException;
    //阻塞给定时间,超时抛异常
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

针对cancel这个方法细说一下
下面是FutureTask的实现逻辑

     /* * Possible state transitions:
     * NEW -> COMPLETING -> NORMAL
     * NEW -> COMPLETING -> EXCEPTIONAL
     * NEW -> CANCELLED
     * NEW -> INTERRUPTING -> INTERRUPTED
     */
    private volatile int state;
    private static final int NEW          = 0;
    private static final int COMPLETING   = 1;
    private static final int NORMAL       = 2;
    private static final int EXCEPTIONAL  = 3;
    private static final int CANCELLED    = 4;
    private static final int INTERRUPTING = 5;
    private static final int INTERRUPTED  = 6;
     
	public boolean cancel(boolean mayInterruptIfRunning) {
	    //这里会将线程状态state置为INTERRUPTING(true)或者CANCELLED(false)
        if (!(state == NEW &&
              UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
                  mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
            return false;
        try {    // in case call to interrupt throws exception
            if (mayInterruptIfRunning) {
                try {
                    Thread t = runner;
                    if (t != null)
                        t.interrupt(); //这里还是会调用线程的interrupt方法,但是这个方法并不能强制中断当前线程,只是会通过标志位,告诉该线程有人想中断你,你要不要响应?
                } finally { // final state
                    UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
                }
            }
        } finally {
            finishCompletion();
        }
        return true;
    }
    
	public V get() throws InterruptedException, ExecutionException {
        int s = state;
        //只有NEW 和 COMPLETING两种状态会去阻塞获取结果
        if (s <= COMPLETING) 
            s = awaitDone(false, 0L);  
        return report(s); 
    }
    
    private V report(int s) throws ExecutionException {
        Object x = outcome;
        if (s == NORMAL)
            return (V)x;
        if (s >= CANCELLED)
            throw new CancellationException();
        throw new ExecutionException((Throwable)x);
    }

关于cancel的三种情况测试
1.还没开始就取消,此时不会打印System.out.println(Thread.currentThread().getName());

   	FutureTask<String> futureTask = new FutureTask<String>(new TaskCall());
    new Thread(futureTask).start();
    futureTask.cancel(false);
       ……
	static class TaskCall implements Callable<String> {
        @Override
        public String call() throws Exception {
            System.out.println(Thread.currentThread().getName());
            return Thread.currentThread().getName();
        }
    }

2.正在运行时取消
这种情况不论是false还是true,都是会抛异常,然后打印sum的值

	FutureTask<Long> futureTask = new FutureTask<Long>(new TaskCall());
    new Thread(futureTask).start();
    try {
        Thread.sleep(500);
        futureTask.cancel(false);
        //futureTask.cancel(true);
        futureTask.get();
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
	static class TaskCall implements Callable<Long> {
        @Override
        public Long call() throws Exception {
            //执行一些耗时操作
            long sum = 0;
            for (int i = 0; i < 100000; i++) {
                for (int j = 0; j < 100000; j++) {
                    sum += i;
                }
                sum /= i + 1;
            }
            System.out.println(sum);
            return sum;
        }
    }

输出:

Exception in thread "main" java.util.concurrent.CancellationException
	at java.util.concurrent.FutureTask.report(FutureTask.java:121)
	at java.util.concurrent.FutureTask.get(FutureTask.java:192)
	at com.example.demo.ThreadTest.main(ThreadTest.java:70)
99999

这种情况就得区分false和true了

	static class TaskCall implements Callable<Long> {
        @Override
        public Long call() throws Exception {
            Thread.sleep(2000);
            System.out.println(1111);
            return 1111L;
        }
    }

false的时候输出如图:
在这里插入图片描述
true的时候输出如图:
在这里插入图片描述
由此可以看出,如果当线程休眠的时候被取消,并且参数是true的时候,就会被中断。
3.运行完成后取消
如图
在这里插入图片描述
最后来一个简单的case,说明Callable的使用场景
场景1:求素数,现在想求1~1000000000的素数个数
思路1:一个for循环,慢慢算
思路2:多线程,分批算,但是如何拿到每个线程的运算结果呢?没错,就是它Callable

public static void main(String[] args) {
		ExecutorService executorService = Executors.newFixedThreadPool(50);
        List<FutureTask> futures = new ArrayList<>();
        long b = System.currentTimeMillis();
        int I = 0;
        while (I < 100000000) {
            futures.add((FutureTask) executorService.submit(new PrimeTask(I, I + 10000)));
            I += 10000;
        }

        int sum = 0;
        for (int i = 0; i < futures.size(); i++) {
            try {
                sum += (int)futures.get(i).get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
        long e = System.currentTimeMillis() - b;
        System.out.println(sum);
        System.out.println("多线程共耗时: " + e / 1000.0 + " s" );

        b = System.currentTimeMillis();
        sum = 0;
        for (int i = 1; i < 100000000; i++) {
            sum += isPrime(i) ? 1 : 0;
        }
        e = System.currentTimeMillis() - b;
        System.out.println(sum);
        System.out.println("单线程共耗时: " + e / 1000.0 + " s" );
        executorService.shutdown();
}

static class PrimeTask implements Callable<Integer> {
    private int start;
    private int end;

    public PrimeTask(int start, int end){
        this.start = start;
        this.end = end;
    }

    @Override
    public Integer call() {
        int sum = 0;
        for (int i = start; i < end ; i++) {
            sum += isPrime(i) ? 1 : 0;
        }
        return sum;
    }

    private boolean isPrime(int num){
        if(num <= 1){
            return false;
        }
        int n = (int)Math.sqrt(num);
        for (int i = 2; i <= n; i++) {
            if((num % i) == 0){
                return false;
            }
        }
        return true;
    }
}

输出:

5761455
多线程共耗时: 31.329 s
5761455
单线程共耗时: 139.329 s

场景2:从数百亿个数中找出topN
思路:分桶排序,之后归并,是不是联想到了map/reduce,多线程map,之后reduce汇总结果,那么就很适合用Callable,多线程跑,然后将结果汇总。
case略

四、ThreadPoolExecutor

		/**
         * corePoolSize: 核心线程数,就是常驻线程
         * maximumPoolSize: 最大线程数,如果核心线程都被占用了,还有多余的线程,那么就会创建新的线程,并且 核心线程数+新创建数 <= 最大线程数
         * keepAliveTime: 非核心线程存活时间,例如这个,如果一个非核心线程闲置了60秒,那么就会被回收
         * TimeUnit:时间单位
         * workQueue:工作队列,可根据业务,选择合适的工作队列,甚至可以自定义工作队列
         * threadFactory: 线程工程,相当于是对线程加工,大多数用法只是给线程池和线程起一个有意义的名称,方便到时候查问题
         * RejectedExecutionHandler: 拒绝策略处理器,就是队列满了,改如何处理,默认是直接抛出异常
         */
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1));
        //这个线程工程,相当于是对线程加工,大多数用法只是给线程池和线程起一个有意义的名称,方便到时候查问题
        //比如当发现线程异常、线程不断增长、线程死锁等问题,这时会导出线程栈信息,可以根据线程池和线程名称快速定位到属于自己代码里new的线程
        threadPoolExecutor.setThreadFactory(new ThreadFactory() {
            private final AtomicInteger poolNumber = new AtomicInteger(1);
            private final AtomicInteger threadNumber = new AtomicInteger(1);
            private final String namePrefix;

            {
                namePrefix = "MyPool-" +
                        poolNumber.getAndIncrement() +
                        "-Thread-";
            }

            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r, namePrefix + threadNumber.getAndIncrement());
                //当线程抛出未捕获的异常的时候的处理,一般自己写的线程不需要用这个,直接最外面套一个try catch完事了
                t.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
                    @Override
                    public void uncaughtException(Thread t, Throwable e) {
                        System.out.println(e);
                    }
                });
                return t;
            }
        });
        //设置当队列满时的拒绝策略,默认的话是直接抛异常
        threadPoolExecutor.setRejectedExecutionHandler(new RejectedExecutionHandler() {
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                System.out.println("当前任务数: " + executor.getTaskCount());
                System.out.println("当前活跃数:" + executor.getActiveCount());
                System.out.println("当前队列大小: " + executor.getQueue().size());
                try {
                    //如果当前队列容量不足,会等待任务执行释放后,才添加到队列末尾
                    executor.getQueue().put(r);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        });

ThreadPoolExecutor的execute和submit的区别就是前者不能返回FutureTask

五、Executors

1.newSingleThreadExecutor
	public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

可以看到,也是通过ThreadPoolExecutor来new的线程池,只是参数不一样,核心线程数和最大线程数都一样,并且当该线程挂掉的时候,会立刻new一个新的线程代替原来的核心线程。

		ExecutorService singleExecutorService = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 5; i++) {
            singleExecutorService.execute(new Task(i));
        }
        singleExecutorService.shutdown();
		……
		static class Task implements Runnable {
	        private int i = 0;
	        public Task(int i){
	            this.i = i;
	        }
	        @Override
	        public void run() {
	            System.out.println(Thread.currentThread().getName() + "->" + i);
	        }
    }

输出:

pool-1-thread-1->0
pool-1-thread-1->1
pool-1-thread-1->2
pool-1-thread-1->3
pool-1-thread-1->4

可以看出,这是串行执行的,因为就一个线程

2.newFixedThreadPool

	public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

由线程参数可以看出,这是一个核心线程等于最大线程数的线程池,说明了就算超过核心线程数的任务,也不会new新的线程,并且核心线程是不会被回收的。

		ExecutorService fixedExecutorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            fixedExecutorService.execute(new Task(i));
        }
        fixedExecutorService.shutdown();

输出:

pool-1-thread-1->0
pool-1-thread-4->3
pool-1-thread-3->2
pool-1-thread-2->1
pool-1-thread-5->4

3 newCachedThreadPool

	public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

由参数可以看出,创建线程池的时候,是没有创建核心线程的,只有需要的时候,才会创建线程,并且线程闲置60s后会被回收

		ExecutorService cachedExecutorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            cachedExecutorService.execute(new Task3(i));
        }
        cachedExecutorService.shutdown();

输出:

pool-1-thread-2->1
pool-1-thread-4->3
pool-1-thread-3->2
pool-1-thread-4->7
pool-1-thread-1->0
pool-1-thread-7->6
pool-1-thread-3->8
pool-1-thread-6->5
pool-1-thread-5->4
pool-1-thread-2->9

由输出可以看出,按需创建的特点

4 newScheduledThreadPool

	public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

咋眼一看,是不是觉得这个线程池可以无限增长,那就大错特错了,DelayedWorkQueue这个队列是一个无界队列,也就是说,超过核心线程数的任务,就会被加入到该队列中排队,比如核心线程数50,有100000个任务,那么也只能慢慢排队,不能再创建新的线程

		/**
         * 可执行周期性任务的线程池
         * corePoolSize: 核心线程数,只能设置这个参数,最大线程数默认为Integer.MAX_VALUE
         * 工作队列使用DelayedWorkQueue
         */
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
        /**
         * initialDelay: 第一次执行的延迟时间,例如1000, TimeUnit.MILLISECONDS 那么就是1s后第一次执行
         * delay: 执行的间隔时间,要注意的是,不是执行开始时间算起的间隔,而是上一次执行结束算的间隔,例如1000,那么就是任务第一次执行完成(可能花了2s),再过1s才执行第二次
         */
        for (int i = 0; i < 5; i++) {
            scheduledExecutorService.scheduleWithFixedDelay(new Task4(i), 0, 1000, TimeUnit.MILLISECONDS);
        }

        /**
         * initialDelay: 第一次执行的延迟时间,例如1000, TimeUnit.MILLISECONDS 那么就是1s后第一次执行
         * period: 执行的周期,不管从任务执行开始算起,不管任务执行没执行完,例如1000,那么就是任务第一次执行(未完成可能花了2s),再过1s直接执行第二次
         */
        for (int i = 0; i < 5; i++) {
            scheduledExecutorService.scheduleAtFixedRate(new Task4(i), 1000, 1000, TimeUnit.MILLISECONDS);
        }

        //没有周期性执行的功能,单纯的就是一个延时执行功能
        for (int i = 0; i < 5; i++) {
            scheduledExecutorService.schedule(new Task4(i), 1000, TimeUnit.MILLISECONDS);
        }

可深入了解的点:
  • 各种阻塞队列的实现原理
  • 线程池中锁的应用
  • 线程安全
  • java.util.concurrent下各种并发工具类的使用及原理

六、总结

  • 建议使用Runnable,使Thread和Task解耦
  • 当需要汇总多线程的计算结果时,使用Callable
  • 取消线程需要注意时间节点,运行前取消则线程不再运行,运行中取消,若不是线程休眠中取消,则只是简单通过标志位通知线程是否响应该操作,运行完成取消,返回false
  • 线程池核心线程是不会被回收的,除非线程池关闭,非核心线程可以设置回收时间,但是只能是该线程空闲才会回收
  • 建议重写拒绝策略,防止抛异常影响其它业务逻辑
  • scheduleWithFixedDelay和scheduleAtFixedRate的区别,前者是线程执行结束后n 秒执行第二次,后者是执行开始后n秒执行第二次
原创文章 257 获赞 277 访问量 69万+

猜你喜欢

转载自blog.csdn.net/qq_18297675/article/details/90474252
今日推荐