再学Android之多线程

640 阅读8分钟

多线程

对于如何新建线程,join,yield,setDaemon,线程状态等这些简单知识就不做多介绍了,下面分享下我们日常开发中 常用的一些知识,多线程这块的知识很多,后续会慢慢完善,如有错误和不足,还请各位大佬指出。

线程池

好处:减小频繁创建线程带来的巨大的性能开销,同时设定了上线,可以防止创建线程数量过多导致系统崩溃的现象 先来看下java为我们提供的四种已经创建好的线程池:

  • newSingleThreadExecutor(使用阻塞队列:LinkedBlockingQueue) 创建单个线程的线程池,也就是只有一个线程,那么,所有提交上来的任务就是按顺序执行。如果这个线程发生异常的话,那么 会有一个新的线程来替代它
  • newFixedThreadPool(使用阻塞队列:LinkedBlockingQueue) 创建固定线程数量的线程池,每提交一个任务就会创建一个线程,直到达到线程池的最大大小。如果其中某个线程发生异常的话, 那么会有一个新的线程来替代它
  • newCachedThreadPool(使用阻塞队列:SynchronousQueue) 创建一个大小不受限制的,可缓存的,线程数量自动扩容,自动销毁的线程池,如果线程池的大小超过了处理任务所需要的线程,那么线程池会回收部分线程(时间是60s)
  • newScheduledThreadPool (使用阻塞队列:DelayedWorkQueue) 创建一个大小无限的线程池,支持定时任务和周期性执行的任务

怎么使用?

调用execute(runnable)或者submit(runnable)即可,二者有什么区别? sumit可以返回一个Future来获取执行的结果

上面四种线程池的构造函数如下:

    /**
    * @param corePoolSize 核心线程数量, 超出数量的线程会进入阻塞队列
    * @param maximumPoolSize 最大可创建线程数量
    * @param keepAliveTime 线程存活时间
    * @param unit 存活时间的单位
    * @param workQueue 线程溢出后的阻塞队列
    */
   public ThreadPoolExecutor(int corePoolSize,
                             int maximumPoolSize,
                             long keepAliveTime,
                             TimeUnit unit,
                             BlockingQueue<Runnable> workQueue) {
       this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, 
        Executors.defaultThreadFactory(), defaultHandler);
   }

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

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

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

   public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
       return new ScheduledThreadPoolExecutor(corePoolSize);
   }

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

上面的4种线程池已经满足我们日常开发中绝大部分的场景,但是有他们自己的弊端:

FixedThreadPool和SingleThreadPoolPool : 请求队列使用的是LinkedBlockingQueue,允许的请求队列的长度是Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM.

CachedThreadPool和ScheduledThreadPool : 允许创建的线程数量是Integer.MAX_VALUE,可能会创建大量的线程池, 从而导致 OOM.

以上也就是阿里建议我们自己配置线程池的原因,那么线程池的那些参数究竟该如何配置? 线程池大小的配置: 一般根据任务类型 和 CPU的核数(N)

CPU密集型的任务多的话,需要减少线程的数量,这样可以降低切换线程带来的开销,建议配置线程池的大小为: N+1 IO密集型的任务到的话,需要增加线程的数量,建议配置为 N*2

我们来看一下AsyncTask的线程池配置:

private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
   
    private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
    private static final int KEEP_ALIVE_SECONDS = 30;

    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        public Thread newThread(Runnable r) {
            return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
        }
    };

    private static final BlockingQueue<Runnable> sPoolWorkQueue =
            new LinkedBlockingQueue<Runnable>(128);

  
    public static final Executor THREAD_POOL_EXECUTOR;

    static {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
                sPoolWorkQueue, sThreadFactory);
        threadPoolExecutor.allowCoreThreadTimeOut(true);
        THREAD_POOL_EXECUTOR = threadPoolExecutor;
    }

从AsyncTask的源码中我们得知,核心线程的数量是至少有2线程,最多有4个线程,最好情况是有CPU的核心数-1,这样可以避免CPU在 后台工作的时候达到饱和。最大线程数量设置的是CPU_COUNT * 2 + 1,使用的阻塞队列是LinkedBlockingQueue,默认大小是 Integer.MAX_VALUE,但是AsyncTask使用时指定了其大小为128,下面说下阻塞队列:

1、ArrayBlockingQueue

是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。

2、LinkedBlockingQueue

一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()和newSingleThreadExecutor使用了这个队列

3、SynchronousQueue

一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。

4、PriorityBlockingQueue

一个具有优先级的无限阻塞队列。 5、DelayedWorkQueue 任务是按照目标执行时间进行排序。

ArchTaskExecutor

阅读LiveData源码的时候发现有这个API,看名字就是到该怎么使用,这里就不作多介绍了,下面看代码

    ArchTaskExecutor.getIOThreadExecutor().execute(new Runnable() {
            @Override
            public void run() {

            }
        });

        ArchTaskExecutor.getMainThreadExecutor().execute(new Runnable() {
            @Override
            public void run() {

            }
        });
        ArchTaskExecutor.getInstance().executeOnDiskIO(new Runnable() {
            @Override
            public void run() {

            }
        });
        ArchTaskExecutor.getInstance().postToMainThread(new Runnable() {
            @Override
            public void run() {

            }
        });

Collections.synchronizedList

ArrayList是非线程安全的,在多线程中,对ArrayList进行迭代的时候,会报ConcurrentModificationException错误, 为了保证同步,我们可以使用集合工具类来对ArrayList进行包装,即Collections.synchronizedList。同时我们也知道 Vector也是线程安全的,那么这两者有什么区别呢? Vector底层是数组实现的,使用同步方法来实现线程安全,Collections.synchronizedList并未修改list的底层实现,只是 对其进行了一层包装,使用同步代码块来实现线程安全。同步方法的锁是作用在方法上,所以锁的粒度会比同步代码块的大,同步代码的 执行效率是与锁的粒度成反比的,所以Collections.synchronizedList的执行效率是高于Vector,而且,Vector的所有对集合 的操作都加上了synchronized关键字,效率较低,已不推荐使用。 但是Collections.synchronizedList真的就线程安全了吗?

        List<Integer> integers = Collections.synchronizedList(new ArrayList<Integer>());
        Iterator<Integer> iterator = integers.iterator();
        while (iterator.hasNext()){
            doSth(iterator.next());
        }
        

多次运行后我们会发现依然会报ConcurrentModificationException错误,不是已经是线程安全了吗,为什么呢? 那有同学可能会说是不是迭代器的问题,我换成for-Each来遍历,for-Each只是简洁,数组索引的边界值只计算一次罢了, 试一下,同样会报ConcurrentModificationException的错误。那么究竟为什么? Collections.synchronizedList虽然每个操作已经使用静态代码块,但是在迭代的时候,整个的迭代过程不是原子性的, 想要结果正确,我们应该对整个迭代过程进行加锁处理:

        List<Integer> integers = Collections.synchronizedList(new ArrayList<Integer>());
        synchronized (integers){
            Iterator<Integer> iterator = integers.iterator();
            while (iterator.hasNext()){
                doSth(iterator.next());
            }
        }

看到上面的代码,我们不禁感到恐怖如斯,遍历一遍数据我们都要加锁,那不是很慢嘛,于是,CopyOnWriteArrayList登场

CopyOnWriteArrayList

我们看下JUC(java.util.concurrent)包下面的线程安全的类是怎样替代老一代的线程安全的类的: Hashtable -> ConcurrentHashMap Vector -> CopyOnWriteArrayList 其实他们之间最大的不同就是加锁粒度的不同,新一代的容器的加锁粒度更小,比如ConcurrentHashMap用了cas锁、volatile等方式来实现线程安全。 注:JUC下面的安全容器在遍历的时候不会抛出ConcurrentModificationException的异常的。 那么下面我们来学习下CopyOnWriteArrayList: 先来看下COW: 如果有多个调用者同时请求相同的资源的时候,他们会获取到相同的指针并且指向相同的资源,知道某一个调用者试图修改这个资源的时候,系统才会真正复制一份副本给到这个调用者,而其他的调用者看到的依然是之前的资源。 通过上面的概念我们就可以得出一个结论了:在读操作比较多的情况下,使用COW效率更高。

CopyOnWriteArrayList#add()在添加的时候就上锁,并复制一个新数组,增加操作在新数组上完成,将array指向到新数组中,最后解锁 写加锁,读不加锁 问题:为什么在遍历的时候,不用调用者显式的加锁呢? CopyOnWriteArrayList在使用迭代器遍历的时候,操作的都是原数组! 缺点: 1.如果CopyOnWriteArrayList经常要增删改里面的数据,经常要执行add()、set()、remove()的话,那是比较耗费内存的 2.CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性

ThreadLocal

跟同步机制类似,都是为了解决多线程访问同一变量造成的访问冲突问题,不同的的是: 同步机制是通过锁机制牺牲时间来达到多线程安全访问的目的; ThreadLocal为每个线程提供独立的变量副本,牺牲空间保证每个线程访问相同变量时可以得到自己专属的变量,达到多线程安全访问的目的。

volatile

对于过可见性、有序性及原子性问题,通常情况下我们可以通过Synchronized关键字来解决这些个问题,不过如果对Synchronized原理有了解的话,应该知道Synchronized是一个比较重量级的操作,对系统的性能有比较大的影响,所以,如果有其他解决方案,我们通常都避免使用Synchronized来解决问题。而volatile关键字就是Java中提供的另一种解决可见性和有序性问题的方案。对于原子性,需要强调一点,也是大家容易误解的一点:对volatile变量的单次读/写操作可以保证原子性的,如long和double类型变量,但是并不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作