Android多线程开发总结

Android多线程

多线程开发在Android技术中非常重要,能否熟练掌握这些技术是衡量一个工程师技术水平能力的一个重要标准,也是决定能否开发出高效优质应用的前提条件。下面将分别展开描述以及对比,并结合实际工作场合分析优劣。主要有以下几种:

  • Thread
  • Handler
  • HandlerThread
  • IntentService
  • ThreadPool

1 Thread(线程)

1.1 定义

一个基本的CPU执行单元 & 程序执行流的最小单元

组成:线程ID + 程序计数器 + 寄存器集合 + 堆栈。比进程更小的可独立运行的基本单位,可理解为:轻量级进程。线程自己不拥有系统资源,与其他线程共享进程所拥有的全部资源。

1.2 与进程的区别

  • 进程是资源分配的最小单位,线程是程序执行的最小单位。
  • 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
  • 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。
  • 但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。

1.3 线程分类

线程主要分为:守护线程、非守护线程(用户线程)

1.3.1 守护线程

  • 定义:守护用户线程的线程,即在程序运行时为其他线程提供一种通用服务
  • 常见:如 垃圾回收线程
  • 设置方式:
//设置该线程为守护线程
thread.setDaemon(true);

1.3.2 非守护线程(用户线程)

主要包括:主线程 、子线程(工作线程)。

a. 主线程(UI线程)

  • 定义:Android系统在程序启动时会自动启动一条主线程
  • 作用:处理四大组件与用户进行交互的事情(如UI、界面交互相关)
  • 注:因为用户随时会与界面发生交互,因此主线程任何时候都必须保持很高的响应速度,所以主线程不允许进行耗时操作,否则会出现ANR

b. 子线程(工作线程)

  • 定义:手动创建的线程
  • 作用:耗时的操作(网络请求、I/O操作等)

1.3.3 守护线程 与 非守护线程的区别

  • 区别:虚拟机是否已退出:
  • 当所有用户线程结束时,因为没有守护的必要,所以守护线程也会终止,虚拟机也同样退出;
  • 反过来,只要任何用户线程还在运行,守护线程就不会终止,虚拟机就不会退出

1.3 线程优先级

1.3.1 表示

线程优先级分为10个级别,分别用Thread类常量表示。

// 譬如:
Thread.MIN_PRIORITY // 优先级1
Thread.MAX_PRIORITY // 优先级10

1.3.2 设置

通过方法setPriority(int grade)进行优先级设置
默认线程优先级是5,即 Thread.NORM_PRIORITY

1.4 多线程 - 介绍

多个线程同时进行,即多个任务同时进行

其实,计算机任何特定时刻只能执行一个任务;
多线程只是一种错觉:只是因为JVM快速调度资源来轮换线程,使得线程不断轮流执行,所以看起来好像在同时执行多个任务而已

扫描二维码关注公众号,回复: 9636312 查看本文章

1.5 线程调度

1.5.1 调度方式

  • 当系统存在大量线程时,系统会通过时间片轮转的方式调度线程,因此线程不可能做到绝对的并发
  • 处于就绪状态(Runnable)的线程都会进入到线程队列中等待CPU资源
    同一时刻在线程队列中可能有很多个

在采用时间片的系统中,每个线程都有机会获得CPU的资源以便进行自身的线程操作;当线程使用CPU资源的时间到后,即时线程没有完成自己的全部操作,JVM也会中断当前线程的执行,把CPU资源的使用权切换给下一个队列中等待的线程。
被中断的线程将等待CPU资源的下一次轮回,然后从中断处继续执行

1.5.2 调度优先级

Java虚拟机(JVM)中的线程调度器负责管理线程,并根据以下规则进行调度:

  • 根据线程优先级(高-低),将CPU资源分配给各线程
  • 具备相同优先级的线程以轮流的方式获取CPU资源
  • 示例

存在A、B、C、D四个线程,其中:A和B的优先级高于C和D(A、B同级,C、D同级,那么JVM将先以轮流的方式调度A、B,直到A、B线程死亡,再以轮流的方式调度C、D

1.6 线程的使用

  • 继承Thread类
// 步骤1:创建线程类 (继承自Thread类)
class MyThread extends Thread{
// 步骤2:复写run(),内容 = 定义线程行为
    @Override
    public void run(){
    ... // 定义的线程行为
    }
}
// 步骤3:创建线程对象,即 实例化线程类
 MyThread mt=new MyThread(“线程名称”);
// 步骤4:通过 线程对象 控制线程的状态,如 运行、睡眠、挂起  / 停止
// 此处采用 start()开启线程
mt.start();
  • 配合Runnable接口创建线程
// 步骤1:创建线程辅助类,实现Runnable接口
class MyThread implements Runnable{
   ....
   @Override
// 步骤2:复写run(),定义线程行为
   public void run(){

   }
}
// 步骤3:创建线程辅助对象,即 实例化 线程辅助类
MyThread mt = new MyThread();
// 步骤4:创建线程对象,即 实例化线程类;线程类 = Thread类;
// 创建时通过Thread类的构造函数传入线程辅助类对象
// 原因:Runnable接口并没有任何对线程的支持,我们必须创建线程类(Thread类)的实例,从Thread类的一个实例内部运行
Thread td=new Thread(mt);
// 步骤5:通过 线程对象 控制线程的状态,如 运行、睡眠、挂起  / 停止
// 当调用start()方法时,线程对象会自动回调线程辅助类对象的run(),从而实现线程操作
td.start();
  • 匿名类创建线程

new Thread(): 在阿里开发手册中明确禁止使用这种方式开启新线程,主要是因为新线程这样开启之后无法主动停止,只适合执行耗时短的轻量级任务

// 步骤1:采用匿名类,直接 创建 线程类的实例
 new Thread("线程名称") {
 	 // 步骤2:复写run(),内容 = 定义线程行为
     @Override
     public void run() {       
  	 	// 步骤3:通过 线程对象 控制线程的状态,如 运行、睡眠、挂起  / 停止   
   }
 }.start();

1.7 new Thread()的缺点

在实际多线程开发中,一般不建议直接用new Thread(),性能开销达,不好管理,而要使用下面即将提到的其他多线程技术。

  • 每次new Thread()耗费性能
  • 调用new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制创建,之间相互竞争,会导致过多占用系统资源导致系统瘫痪。
  • 不利于扩展,比如如定时执行、定期执行、线程中断

2 Handler

2.1 定义

一套 Android 消息传递机制

2.2 作用

在多线程的应用场景中,将工作线程中需更新UI的操作信息 传递到 UI主线程,从而实现 工作线程对UI的更新处理,最终实现异步消息的处理
在这里插入图片描述

2.3 意义

多个线程并发更新UI的同时 保证线程安全

2.4 相关概念

Handler 、 Looper 、Message 这三者都与Android异步消息处理线程相关的概念。那么什么叫异步消息处理线程呢?
在这里插入图片描述

  • 异步消息处理线程启动后会进入一个无限的循环体之中,每循环一次,从其内部的消息队列中取出一个消息,然后回调相应的消息处理函数,执行完成一个消息后则继续循环。若消息队列为空,线程则会阻塞等待。
  • 那么Android消息机制主要是指Handler的运行机制,Handler运行需要底层的MessageQueue和Looper支撑。其中MessageQueue采用的是单链表的结构,Looper可以叫做消息循环。由于MessageQueue只是一个消息存储单元,不能去处理消息,而Looper就是专门来处理消息的,Looper会以无限循环的形式去查找是否有新消息,如果有的话,就处理,否则就一直等待着。
  • 我们知道,Handler创建的时候会采用当前线程的Looper来构造消息循环系统,需要注意的是,线程默认是没有Looper的,如果需要使用Handler就必须为线程创建Looper,因为默认的UI主线程,也就是ActivityThread,ActivityThread被创建的时候就会初始化Looper,这也是在主线程中默认可以使用Handler的原因。如果想让该线程具有消息队列和消息循环,并具有消息处理机制,就需要在线程中首先调用Looper.prepare()来创建消息队列,然后调用Looper.loop()进入消息循环。如以下代码所示:
public class LopperThread extends Thread{
    public Handler mHandler;
    
    @Override
    public void run(){
        Looper.prepare();
        mHandler = new Handler(){
        
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                //处理消息队列
            }
        };
        Looper.loop();
    }
}

2.5 使用方式

  • Handler.sendMessage

在该使用方式中,又分为2种:新建Handler子类(内部类)、匿名 Handler子类

/** 
  * 方式1:新建Handler子类(内部类)
*/

// 步骤1:自定义Handler子类(继承Handler类) & 复写handleMessage()方法
class mHandler extends Handler {

    // 通过复写handlerMessage() 从而确定更新UI的操作
    @Override
    public void handleMessage(Message msg) {
     ...// 需执行的UI操作
        
    }
}

// 步骤2:在主线程中创建Handler实例
private Handler mhandler = new mHandler();

// 步骤3:创建所需的消息对象
Message msg = Message.obtain(); // 实例化消息对象
msg.what = 1; // 消息标识
msg.obj = "AA"; // 消息内容存放

// 步骤4:在工作线程中 通过Handler发送消息到消息队列中
// 可通过sendMessage() / post()
// 多线程可采用AsyncTask、继承Thread类、实现Runnable
mHandler.sendMessage(msg);

// 步骤5:开启工作线程(同时启动了Handler)
// 多线程可采用AsyncTask、继承Thread类、实现Runnable

/** 
* 方式2:匿名内部类
*/
// 步骤1:在主线程中 通过匿名内部类 创建Handler类对象
private Handler mhandler = new  Handler(){
    // 通过复写handlerMessage()从而确定更新UI的操作
	@Override
	public void handleMessage(Message msg) {
	    ...// 需执行的UI操作
	}
};

// 步骤2:创建消息对象
Message msg = Message.obtain(); // 实例化消息对象
msg.what = 1; // 消息标识
msg.obj = "AA"; // 消息内容存放

// 步骤3:在工作线程中 通过Handler发送消息到消息队列中
// 多线程可采用AsyncTask、继承Thread类、实现Runnable
mHandler.sendMessage(msg);

// 步骤4:开启工作线程(同时启动了Handler)
// 多线程可采用AsyncTask、继承Thread类、实现Runnable
  • Handler.post
// 步骤1:在主线程中创建Handler实例
private Handler mhandler = new mHandler();

// 步骤2:在工作线程中 发送消息到消息队列中 & 指定操作UI内容
// 需传入1个Runnable对象
mHandler.post(new Runnable() {
	@Override
	public void run() {
	   ... // 需执行的UI操作 
	}
});

// 步骤3:开启工作线程(同时启动了Handler)
// 多线程可采用AsyncTask、继承Thread类、实现Runnable

2.6 Handler内存泄漏

1、内存泄露的定义:本该被回收的对象不能被回收而停留在堆内存中
2、内存泄露出现的原因:当一个对象已经不再被使用时,本该被回收但却因为有另外一个正在使用的对象持有它的引用从而导致它不能被回收,这就导致了内存泄漏
3、主线程的Looper对象的生命周期 = 该应用程序的生命周期
4、在Java中,非静态内部类 & 匿名内部类都默认持有 外部类的引用

在Handler消息队列 还有未处理的消息 / 正在处理消息时,此时若需销毁外部类MainActivity,但由于引用关系,垃圾回收器(GC)无法回收MainActivity,从而造成内存泄漏。

2.6.1 解决方案

  • 解决方案1:静态内部类+弱引用

1、将Handler的子类设置成 静态内部类,同时,还可加上 使用WeakReference弱引用持有Activity实例。原因:弱引用的对象拥有短暂的生命周期。在垃圾回收器线程扫描时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
2、为了保证Handler中消息队列中的所有消息都能被执行,此处推荐使用解决方案1解决内存泄露问题,即静态内部类 + 弱引用的方式。

public class MainActivity extends AppCompatActivity {

    public static final String TAG = "carson:";
    private Handler showhandler;

    // 主线程创建时便自动创建Looper & 对应的MessageQueue
    // 之后执行Loop()进入消息循环
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //1. 实例化自定义的Handler类对象
       // a. 此处并无指定Looper,故自动绑定当前线程(主线程)的Looper、MessageQueue;
       // b. 定义时需传入持有的Activity实例(弱引用)
        showhandler = new FHandler(this);
        
        // 2. 启动子线程
        new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // a. 定义要发送的消息
                Message msg = Message.obtain();
                msg.what = 2;// 消息标识
                msg.obj = "BB";// 消息存放
                // b. 传入主线程的Handler & 向其MessageQueue发送消息
                showhandler.sendMessage(msg);
            }
        }.start();
    }

    // 分析1:自定义Handler子类
    // 设置为:静态内部类
    private static class FHandler extends Handler{
        // 定义 弱引用实例
        private WeakReference<Activity> reference;
        
        // 在构造方法中传入需持有的Activity实例
        public FHandler(Activity activity) {
            // 使用WeakReference弱引用持有Activity实例
            reference = new WeakReference<Activity>(activity); }

        // 通过复写handlerMessage() 从而确定更新UI的操作
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 1:
                    Log.d(TAG, "收到线程1的消息");
                    break;
                case 2:
                    Log.d(TAG, " 收到线程2的消息");
                    break;
            }
        }
    }
}
  • 解决方案2:当外部类结束生命周期时,清空Handler内消息队列

当 外部类(此处以Activity为例)结束生命周期时,清除 Handler消息队列里的所有消息。

@Override
protected void onDestroy() {
    super.onDestroy();
    // 外部类Activity生命周期结束时,同时清空消息队列 & 结束Handler生命周期
    mHandler.removeCallbacksAndMessages(null);
}

3 AsyncTask(Handler+线程池(默认串行))

1、在工作线程中执行任务,如 耗时任务
2、实现工作线程 & 主线程(UI线程)之间的通信,即:将工作线程的执行结果传递给主线程,从而在主线程中执行相关的UI操作
3、从而保证线程安全

3.1 优点

  • 方便实现异步通信

不需使用 “任务线程(如继承Thread类)+ Handler”的复杂组合

  • 节省资源

采用线程池的缓存线程 + 复用线程,避免了频繁创建 & 销毁线程所带来的系统资源开销

3.2 缺点

  • 实现比较繁琐,代码可读性差

实现一个AsyncTask比较繁琐,而且往往不用的业务需要实现不同的AsyncTask,导致代码可读性差一点,实际上项目用得不多,有其他更好的替代方案。

3.3 实例

public class MainActivity extends AppCompatActivity {
    // 线程变量
    MyTask mTask;
    // 主布局中的UI组件
    Button button,cancel; // 加载、取消按钮
    TextView text; // 更新的UI组件
    ProgressBar progressBar; // 进度条
 
    /**
     * 步骤1:创建AsyncTask子类
     * 注:
     *   a. 继承AsyncTask类
     *   b. 为3个泛型参数指定类型;若不使用,可用java.lang.Void类型代替
     *      此处指定为:输入参数 = String类型、执行进度 = Integer类型、执行结果 = String类型
     *   c. 根据需求,在AsyncTask子类内实现核心方法
     */
    private class MyTask extends AsyncTask<String, Integer, String> {
        // 方法1:onPreExecute()
        // 作用:执行 线程任务前的操作
        @Override
        protected void onPreExecute() {
            text.setText("加载中");
            // 执行前显示提示
        }
        
        // 方法2:doInBackground()
        // 作用:接收输入参数、执行任务中的耗时操作、返回 线程任务执行的结果
        // 此处通过计算从而模拟“加载进度”的情况
        @Override
        protected String doInBackground(String... params) {
            try {
                int count = 0;
                int length = 1;
                while (count<99) {
                    count += length;
                    // 可调用publishProgress()显示进度, 之后将执行onProgressUpdate()
                    publishProgress(count);
                    // 模拟耗时任务
                    Thread.sleep(50);
                }
            }catch (InterruptedException e) {
                e.printStackTrace();
            }
            return null;
        }

        // 方法3:onProgressUpdate()
        // 作用:在主线程 显示线程任务执行的进度
        @Override
        protected void onProgressUpdate(Integer... progresses) {
            progressBar.setProgress(progresses[0]);
            text.setText("loading..." + progresses[0] + "%");
        }

        // 方法4:onPostExecute()
        // 作用:接收线程任务执行结果、将执行结果显示到UI组件
        @Override
        protected void onPostExecute(String result) {
            // 执行完毕后,则更新UI
            text.setText("加载完毕");
        }

        // 方法5:onCancelled()
        // 作用:将异步任务设置为:取消状态
        @Override
        protected void onCancelled() {
            text.setText("已取消");
            progressBar.setProgress(0);
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 绑定UI组件
        setContentView(R.layout.activity_main);
        button = (Button) findViewById(R.id.button);
        cancel = (Button) findViewById(R.id.cancel);
        text = (TextView) findViewById(R.id.text);
        progressBar = (ProgressBar) findViewById(R.id.progress_bar);
        /**
         * 步骤2:创建AsyncTask子类的实例对象(即 任务实例)
         * 注:AsyncTask子类的实例必须在UI线程中创建
         */
        mTask = new MyTask();

        // 加载按钮按按下时,则启动AsyncTask
        // 任务完成后更新TextView的文本
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                /**
                 * 步骤3:手动调用execute(Params... params) 从而执行异步线程任务
                 * 注:
                 *    a. 必须在UI线程中调用
                 *    b. 同一个AsyncTask实例对象只能执行1次,若执行第2次将会抛出异常
                 *    c. 执行任务中,系统会自动调用AsyncTask的一系列方法:onPreExecute() 、doInBackground()、onProgressUpdate() 、onPostExecute()
                 *    d. 不能手动调用上述方法
                 */
                mTask.execute();
            }
        });

        cancel = (Button) findViewById(R.id.cancel);
        cancel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 取消一个正在执行的任务,onCancelled方法将会被调用
                mTask.cancel(true);
            }
        });
    }
}

4 HandlerThread(Handler+Thread)

HandlerThread内部维护了一个消息队列,避免多次创建和销毁子线程来进行操作。

  • HandlerThread本质上是一个线程类,它继承了Thread;
  • HandlerThread有自己的内部Looper对象,可以进行looper循环;
  • 通过获取HandlerThread的looper对象传递给Handler对象,可以在handleMessage方法中执行异步任务。
  • 创建HandlerThread后必须先调用HandlerThread.start()方法,Thread会先调用run方法,创建Looper对象。

4.1 实例

// 步骤1:创建HandlerThread实例对象
// 传入参数 = 线程名字,作用 = 标记该线程
HandlerThread mHandlerThread = new HandlerThread("handlerThread");

// 步骤2:启动线程
mHandlerThread.start();

// 步骤3:创建工作线程Handler & 复写handleMessage()
// 作用:关联HandlerThread的Looper对象、实现消息处理操作 & 与其他线程进行通信
// 注:消息处理操作(HandlerMessage())的执行线程 = mHandlerThread所创建的工作线程中执行
Handler workHandler = new Handler( handlerThread.getLooper() ) {
	@Override
	public boolean handleMessage(Message msg) {
	    ...//消息处理。因为这里是线程中的Looper,是可以做异步延迟的
	    try {
             //延时操作
             Thread.sleep(1000);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         // 这里是不能直接更新UI的,如果想要更新UI必须获取到主线程的Handler,才能更新
	    return true;
	}
});

// 步骤4:使用工作线程Handler向工作线程的消息队列发送消息
// 在工作线程中,当消息循环时取出对应消息 & 在工作线程执行相关操作
  // a. 定义要发送的消息
  Message msg = Message.obtain();
  msg.what = 2; //消息的标识
  msg.obj = "B"; // 消息的存放
  // b. 通过Handler发送消息到其绑定的消息队列
  workHandler.sendMessage(msg);

// 步骤5:结束线程,即停止线程的消息循环
  mHandlerThread.quit();

4.2 注意点

  • 内存泄漏

关于Handler的内存泄露上面提到,使用HandlerThread要注意这块

In Android, Handler classes should be static or leaks might occur.
  • 连续发送消息

使用HandlerThread时只是开了一个工作线程,当你执行sendMessage n下后,只是将n个消息发送到消息队列MessageQueue里排队,等候派发消息给Handler再进行对应的操作。

5 IntentService

1、线程任务需按顺序在后台执行,比如离线下载
2、不符合多个数据同时请求的场景:所有的任务都在同一个Thread looper里执行

  • IntentService是Service的子类,根据需要处理异步请求(以intent表示)。客户端通过调用startService(Intent) 发送请求,该Service根据需要启动,使用工作线程处理依次每个Intent,并在停止工作时停止自身。
  • 它拥有较高的优先级,不易被系统杀死(继承自Service的缘故),因此比较适合执行一些高优先级的异步任务;
  • 它内部通过HandlerThread和Handler实现异步操作
  • 创建IntentService时,只需实现onHandleIntent和构造方法,onHandleIntent为异步方法,可以执行耗时操作;

5.1 使用步骤

  • 步骤1:定义 IntentService的子类,需复写onHandleIntent()方法
public class myIntentService extends IntentService {

  /** 
    * 在构造函数中传入线程名字
    **/  
    public myIntentService() {
        // 调用父类的构造函数
        // 参数 = 工作线程的名字
        super("myIntentService");
    }

   /** 
     * 复写onHandleIntent()方法
     * 根据 Intent实现 耗时任务 操作
     **/  
    @Override
    protected void onHandleIntent(Intent intent) {

        // 根据 Intent的不同,进行不同的事务处理
        String taskName = intent.getExtras().getString("taskName");
        switch (taskName) {
            case "task1":
                Log.i("myIntentService", "do task1");
                break;
            case "task2":
                Log.i("myIntentService", "do task2");
                break;
            default:
                break;
        }
    }

    @Override
    public void onCreate() {
        Log.i("myIntentService", "onCreate");
        super.onCreate();
    }
   /** 
     * 复写onStartCommand()方法
     * 默认实现 = 将请求的Intent添加到工作队列里
     **/  
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i("myIntentService", "onStartCommand");
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy() {
        Log.i("myIntentService", "onDestroy");
        super.onDestroy();
    }
}

  • 步骤2:在Manifest.xml中注册服务
<service android:name=".myIntentService">
    <intent-filter >
        <action android:name="cn.scu.finch"/>
    </intent-filter>
</service>
  • 步骤3:在Activity中开启Service服务
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

            // 同一服务只会开启1个工作线程
            // 在onHandleIntent()函数里,依次处理传入的Intent请求
            // 将请求通过Bundle对象传入到Intent,再传入到服务里

            // 请求1
            Intent i = new Intent("cn.scu.finch");
            Bundle bundle = new Bundle();
            bundle.putString("taskName", "task1");
            i.putExtras(bundle);
            startService(i);

            // 请求2
            Intent i2 = new Intent("cn.scu.finch");
            Bundle bundle2 = new Bundle();
            bundle2.putString("taskName", "task2");
            i2.putExtras(bundle2);
            startService(i2);

            startService(i);  //多次启动
        }
    }

6 ThreadPool(线程池)

1、重用存在的线程,减少对象创建、消亡的开销,性能佳
2、可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,

6.1 ThreadPoolExecutor构造参数解释

Executor threadPool = new ThreadPoolExecutor(
                                            CORE_POOL_SIZE,
                                            MAXIMUM_POOL_SIZE,
                                            KEEP_ALIVE,
                                            TimeUnit.SECONDS,
                                            sPoolWorkQueue,
                                            sThreadFactory
                                            );
  • corePoolSize: 线程池的核心线程数,默认情况下,核心线程数会一直在线程池中存活,即使它们处理闲置状态。如果将ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,那么闲置的核心线程在等待新任务到来时会执行超时策略,这个时间间隔由keepAliveTime所指定,当等待时间超过keepAliveTime所指定的时长后,核心线程就会被终止。
  • maximumPoolSize: 线程池所能容纳的最大线程数量,当活动线程数到达这个数值后,后续的新任务将会被阻塞。如果这个无限大永远不会阻塞,除非开辟的线程超过了CPU承受的最大范围。
  • keepAliveTime: 非核心线程的超时时长,当系统中非核心线程闲置时间超过keepAliveTime之后,则会被回收。如果ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,则该参数也表示核心线程的超时时长。
  • unit: keepAliveTime这个参数的单位,有纳秒、微秒、毫秒、秒、分、时、天等。常用的有TimeUnit.MILLISECONDS(毫秒),TimeUnit.SECONDS(秒)以及TimeUnit.MINUTES(分钟)等。
  • workQueue: 线程池中的任务队列,该队列主要用来存储已经被提交但是尚未执行的任务。存储在这里的任务是由ThreadPoolExecutor的execute方法提交来的。
  • threadFactory: 为线程池提供创建新线程的功能,这个我们一般使用默认即可。
  • handler: 拒绝策略,当线程无法执行新任务时(一般是由于线程池中的线程数量已经达到最大数或者线程池关闭导致的),默认情况下,当线程池无法处理新线程时,会抛出一个RejectedExecutionException。

6.2 实例

// 1. 创建线程池
// 创建时,通过配置线程池的参数,从而实现自己所需的线程池
 Executor threadPool = new ThreadPoolExecutor(...);
  // 注:在Java中,已内置4种常见线程池,下面会详细说明
  
// 2. 向线程池提交任务:execute()
// 说明:传入 Runnable对象
   threadPool.execute(new Runnable() {
        @Override
        public void run() {
            ... // 线程执行任务
        }
    });

// 3. 关闭线程池shutdown() 
threadPool.shutdown();

// 关闭线程的原理
// a. 遍历线程池中的所有工作线程
// b. 逐个调用线程的interrupt()中断线程(注:无法响应中断的任务可能永远无法终止)

// 也可调用shutdownNow()关闭线程:threadPool.shutdownNow()
// 二者区别:
// shutdown:设置 线程池的状态 为 SHUTDOWN,然后中断所有没有正在执行任务的线程
// shutdownNow:设置 线程池的状态 为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表
// 使用建议:一般调用shutdown()关闭线程池;若任务不一定要执行完,则调用shutdownNow()

6.2 四种线程池

6.2.1 定长线程池(FixedThreadPool)

特点:只有核心线程而且不会被回收、线程数量固定、任务队列无大小限制(超出的线程任务会在队列中等待阻塞)
应用场景:控制线程最大并发数

// 1. 创建定长线程池对象 & 设置线程池线程数量固定为3
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);

// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
	public void run(){
		System.out.println("执行任务啦");
	}
};
        
// 3. 向线程池提交任务:execute()
fixedThreadPool.execute(task);
        
// 4. 关闭线程池
fixedThreadPool.shutdown();

6.2.2 定时线程池(ScheduledThreadPool )

核心线程数量固定、非核心线程数量无限制,不会阻塞(闲置时马上回收)
应用场景:执行定时以及周期性任务

为什么不用Timer做定时和延时任务?

  • Timer的特点
    1.Timer是单线程模式;
    2.如果在执行任务期间某个TimerTask耗时较久,那么就会影响其它任务的调度;
    3.Timer的任务调度是基于绝对时间的,对系统时间敏感;
    4.Timer不会捕获执行TimerTask时所抛出的异常,由于Timer是单线程,所以一旦出现异常,则线程就会终止,其他任务也得不到执行。
  • ScheduledThreadPoolExecutor的特点
    1.ScheduledThreadPoolExecutor是多线程
    2.多线程,单个线程耗时操作不会影响影响其它任务的调度
    3.基于相对时间,对系统时间不敏感
    4.多线程,单个任务的执行异常不会影响其他线程
// 1. 创建 定时线程池对象 & 设置线程池线程数量固定为5
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);

// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
   public void run(){
          System.out.println("执行任务啦");
      }
};
// 3. 向线程池提交任务:schedule()
scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS); // 延迟1s后执行任务
scheduledThreadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);// 延迟10ms后、每隔1000ms执行任务

// 4. 关闭线程池
scheduledThreadPool.shutdown();

6.2.3 可缓存线程池(CachedThreadPool)

特点:只有非核心线程、线程数量不固定(可无限大)、灵活回收空闲线程(具备超时机制,全部回收时几乎不占系统资源)、新建线程(无线程可用时),不会阻塞
应用场景:执行大量、耗时少的线程任务

// 1. 创建可缓存线程池对象
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run(){
    System.out.println("执行任务啦");
        }
};

// 3. 向线程池提交任务:execute()
cachedThreadPool.execute(task);

// 4. 关闭线程池
cachedThreadPool.shutdown();

//当执行第二个任务时第一个任务已经完成
//那么会复用执行第一个任务的线程,而不用每次新建线程。

6.2.4 单线程化线程池(SingleThreadExecutor)

特点:只有一个核心线程(超过一个任务会阻塞,保证所有任务按照指定顺序在一个线程中执行,不需要处理线程同步的问题)
应用场景:不适合并发但可能引起IO阻塞性及影响UI线程响应的操作,如数据库操作,文件操作等

// 1. 创建单线程化线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run(){
        System.out.println("执行任务啦");
            }
 };

// 3. 向线程池提交任务:execute()
singleThreadExecutor.execute(task);

// 4. 关闭线程池
singleThreadExecutor.shutdown();

6.3 线程池的注意点

虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。用线程池构建的应用程序容易遭受任何其它多线程应用程序容易遭受的所有并发风险,诸如同步错误和死锁,它还容易遭受特定于线程池的少数其它风险,诸如与池有关的死锁、资源不足和线程泄漏。
1.死锁
任何多线程应用程序都有死锁风险。当一组进程或线程中的每一个都在等待一个只有该组中另一个进程才能引起的事件时,我们就说这组进程或线程 死锁了。死锁的最简单情形是:线程 A 持有对象 X 的独占锁,并且在等待对象 Y 的锁,而线程 B 持有对象 Y 的独占锁,却在等待对象 X 的锁。除非有某种方法来打破对锁的等待(Java 锁定不支持这种方法),否则死锁的线程将永远等下去。
虽然任何多线程程序中都有死锁的风险,但线程池却引入了另一种死锁可能,在那种情况下,所有池线程都在执行已阻塞的等待队列中另一任务的执行结果的任务,但这一任务却因为没有未被占用的线程而不能运行。当线程池被用来实现涉及许多交互对象的模拟,被模拟的对象可以相互发送查询,这些查询接下来作为排队的任务执行,查询对象又同步等待着响应时,会发生这种情况。
2.资源不足
线程池的一个优点在于:相对于其它替代调度机制而言,它们通常执行得很好。但只有恰当地调整了线程池大小时才是这样的。线程消耗包括内存和其它系统资源在内的大量资源。除了 Thread 对象所需的内存之外,每个线程都需要两个可能很大的执行调用堆栈。除此以外,JVM 可能会为每个 Java 线程创建一个本机线程,这些本机线程将消耗额外的系统资源。最后,虽然线程之间切换的调度开销很小,但如果有很多线程,环境切换也可能严重地影响程序的性能。
如果线程池太大,那么被那些线程消耗的资源可能严重地影响系统性能。在线程之间进行切换将会浪费时间,而且使用超出比您实际需要的线程可能会引起资源匮乏问题,因为池线程正在消耗一些资源,而这些资源可能会被其它任务更有效地利用。除了线程自身所使用的资源以外,服务请求时所做的工作可能需要其它资源,例如 JDBC 连接、套接字或文件。这些也都是有限资源,有太多的并发请求也可能引起失效,例如不能分配 JDBC 连接。
3.并发错误
线程池和其它排队机制依靠使用 wait() 和 notify() 方法,这两个方法都难于使用。如果编码不正确,那么可能丢失通知,导致线程保持空闲状态,尽管队列中有工作要处理。使用这些方法时,必须格外小心;即便是专家也可能在它们上面出错。而最好使用现有的、已经知道能工作的实现,例如 util.concurrent 包。
4.线程泄漏
各种类型的线程池中一个严重的风险是线程泄漏,当从池中除去一个线程以执行一项任务,而在任务完成后该线程却没有返回池时,会发生这种情况。发生线程泄漏的一种情形出现在任务抛出一个 RuntimeException 或一个 Error 时。如果池类没有捕捉到它们,那么线程只会退出而线程池的大小将会永久减少一个。当这种情况发生的次数足够多时,线程池最终就为空,而且系统将停止,因为没有可用的线程来处理任务。
有些任务可能会永远等待某些资源或来自用户的输入,而这些资源又不能保证变得可用,用户可能也已经回家了,诸如此类的任务会永久停止,而这些停止的任务也会引起和线程泄漏同样的问题。如果某个线程被这样一个任务永久地消耗着,那么它实际上就被从池除去了。对于这样的任务,应该要么只给予它们自己的线程,要么只让它们等待有限的时间。
5.请求过载
仅仅是请求就压垮了服务器,这种情况是可能的。在这种情形下,我们可能不想将每个到来的请求都排队到我们的工作队列,因为排在队列中等待执行的任务可能会消耗太多的系统资源并引起资源缺乏。在这种情形下决定如何做取决于您自己;在某些情况下,您可以简单地抛弃请求,依靠更高级别的协议稍后重试请求,您也可以用一个指出服务器暂时很忙的响应来拒绝请求。

7 结尾

有句话说的不错,好记忆不如烂笔头。Android学习过程中,最好的方式就是记录,记多了看多了不知不觉就成专家了。而写博客就是帮自己梳理知识点的最好的方式,你可以尝试去多读几篇类似的文章,然后自己梳理记下来,写出自己的博客,你会受益匪浅而且极易深刻,不信你试试!!共勉吧~

发布了17 篇原创文章 · 获赞 100 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/yinhaide/article/details/103984277