今天看啥  ›  专栏  ›  胖宅老鼠

Android基础知识:多线程基础总结

胖宅老鼠  · 掘金  ·  · 2019-07-25 02:09
阅读 51

Android基础知识:多线程基础总结

1、前言

Android中由于主线程不能进行耗时操作,所以耗时操作都要放到子线程中去做,所以多线程开发在实际中几乎无法避免。这篇文章就来总结一下与多线程有关的基础知识。

2、线程状态

一个线程有以下几种状态:
1. New: 新创建状态。线程被创建还没被调用start方法。在线程运行前还有些基础工作要做。
2. Runnable: 可运行状态。调用过start方法。一个可运行的线程可能正在运行也可能没在运行。
3. Blocked: 阻塞状态。表示线程被锁阻塞,暂时不活动。
4. Waiting: 等待状态。线程暂时不活动,直到线程调度器重新激活它。
5. Time waiting: 超时等待状态。和等待状态不用的是它是可以在指定的时间自行返回。
6. Terminated: 终止状态。表示当前线程已经执行完毕。

各个状态之间的转换关系如下图。

线程状态

3、线程的创建与终止

Java中创建线程的方法有三种:

3.1 继承Thread类,复写run方法。

    class MyThread extends Thread {
        @Override
        public void run() {
            Log.d(TAG, "MyThread线程名:" + Thread.currentThread().getName());
            Log.d(TAG, "MyThread is running");
        }
    }

    private void createThread() {
        MyThread myThread = new MyThread();
        myThread.start();
    }
复制代码

3.2 实现Runnable接口,实现run方法

    class MyRunnable implements Runnable {
        @Override
        public void run() {
            Log.d(TAG, "MyRunnable线程名:" + Thread.currentThread().getName());
            Log.d(TAG, "MyRunnable is running");
        }
    }
    
    private void createRunnable() {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
复制代码

3.3 实现Callable接口,实现call方法。

    class MyCallable implements Callable<String> {
        @Override
        public String call() throws Exception {
            Log.d(TAG, "MyCallable线程名:" + Thread.currentThread().getName());
            Log.d(TAG, "MyCallable is running");
            return "callable return";
        }
    }
    
    private void createCallable() throws ExecutionException, InterruptedException {
        MyCallable myCallable = new MyCallable();
        FutureTask<String> futureTask = new FutureTask<>(myCallable);
        Thread thread = new Thread(futureTask);
        thread.start();
        //获取返回值
        String result = futureTask.get();
        Log.d(TAG, result);
    }
复制代码

ThreadRunnable平时见得很多了,而关于Callable相对比较少,它与Runnnable接口类似,但是它有两点与Runnnable接口不同:

  1. Runnnable中的run方法无法抛出异常,而Callablecall方法可以。
  2. Runnnable中的run方法没有返回值,而Callablecall方法可以。Callable可以拿到一个Future对象,表示异步任务的返回结果,调用get方法可以获取结果,如果此时异步任务还没完成会阻塞当前进程直到返回结果。

下面举个Callable使用的例子,模拟一个买菜做菜的过程。先用Runnable实现,再用Callable实现,做一下对比。

  private void runnableCook() throws InterruptedException {
        //买食材线程
        Thread foodThread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    //买食材耗时
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.d(TAG, "五花肉已买");
                food = new String("五花肉");
            }
        });
        Log.d(TAG, "我要开始做菜了");
        Log.d(TAG, "我准备做红烧肉");
        Log.d(TAG, "我没有食材");
        //开启购买食材线程
        Log.d(TAG, "叫隔壁老王帮我出门去买食材");
        foodThread.start();
        //模拟清洗厨具准备的耗时操作
        Log.d(TAG, "清洗厨具准备调料,准备做菜");
        Thread.sleep(3000);
        //要等到食材买回来才能开始做菜,所以将食材线程join
        foodThread.join();
        Log.d(TAG, "开始做菜了");
        cook(food);
    }
    
    private void cook(String food) {
        if (TextUtils.isEmpty(food)) {
            //没调用foodThread.join就会走这里
            Log.d(TAG, "没有食材没法做菜");
            return;
        }
        Log.d(TAG, "菜做好了,开吃了");
    }
复制代码

代码很简单,就是定义一个买菜的线程做买菜的耗时任务,然后主线程打印做菜日志,因为Runnable没有返回值,在做完做菜准备之后需要调用foodThread.join()等待食材买回,再进行cook工作,如果不等到foodThread工作结束就调用cook方法,就会发现没有食材做菜。

未调用foodThread.join方法
调用foodThread.join方法

接下来使用Callable来完成这个过程。

 private void callableCook() throws InterruptedException, ExecutionException {
        Callable<String> callable = new Callable<String>() {
            @Override
            public String call() throws Exception {
                Thread.sleep(5000);
                Log.d(TAG, "五花肉已买");
                return "五花肉";
            }
        };
        FutureTask<String> futureTask = new FutureTask<String>(callable);
        Thread foodThread = new Thread(futureTask);
        Log.d(TAG, "我要开始做菜了");
        Log.d(TAG, "我准备做红烧肉");
        Log.d(TAG, "我没有食材");
        Log.d(TAG, "叫隔壁小王出门去买食材");
        foodThread.start();
        Log.d(TAG, "清洗厨具准备调料,准备做菜");
        Thread.sleep(3000);
        if (!futureTask.isDone()) {
            Log.d(TAG, "食材还没到啊,小王好慢啊");
        }
        food = futureTask.get();
        Log.d(TAG, "开始做菜了");
        cook(food);
    }
    
       private void cook(String food) {
        if (TextUtils.isEmpty(food)) {
            //没调用foodThread.join就会走这里
            Log.d(TAG, "没有食材没法做菜");
            return;
        }
        Log.d(TAG, "菜做好了,开吃了");
    }
复制代码

Callable接口的使用方法和Runnable类似。

第一步先实现Callable接口并为其指定一个泛型,这个泛型类型就是call方法要返回的类型,实现call方法在其中做耗时任务,并将任务结果通过return返回。

第二步创建一个FutrueTask,同样设置返回类型泛型,并将刚实现的Callable对象传入,这个FutrueTask后面就用来获得异步任务的结果。

最后一步和Runnable一样,新建一个Thread对象,将FutrueTask传入,然后开启线程即可。

Runnable不同的是FutrueTask提供一个isDone方法用来判断异步任务是否完成,并且提供futureTask.get方法来获取异步任务的返回结果,如果此时任务还没结束,则会阻塞当前线程直到任务完成。这样就不会出现食材还没买回来就开始做菜了。

callable实现

3.4 终止线程

一个线程的run方法运行完成或者方法中刚出现异常之后这个线程就会终止。除此之外Thread类提供了stopinterrupt方法终止线程,其中stop已过时被弃用,而调用interrupt方法,线程会将自己的终止标识为true,线程会一直检测这个标识位以判断是否被终止。

安全停止线程的方法:采用布尔值变量
  private volatile boolean flag = true;

  private void createFlagThread() {
        flag = true;
        count = 0;
        thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (flag) {
                    Log.d(TAG, "flag count:" + count++);
                }
            }
        });
        thread.start();
    }
    
    private void changeFlag() {
        flag = false;
    }
复制代码

定义布尔值flag调用createFlagThread方法创建线程,run方法中通过flag来判断是否结束循环从而结束线程,当需要停止线程时,调用changeFlag方法将布尔值设置为false即可。

3.5 volatile关键字

在3.4的例子中,用来停止循环的布尔值变量上加了一个volatile关键字,volatile被称为轻量级的synchronized,但volatile只能修饰变量不能修饰方法。在遇到变量需要被多个线程访问时可以用volatile关键字修饰它。例如在经典的双重检查的单例模式下就会用到volatile修饰。

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
        }  
    }  
    return singleton;  
    }  
}  
复制代码

那么volatile真的能替代synchronized吗?答案肯定是不能的。关于volatile的作用先要从并发编程的三个特性和Java的内存模型开始讲起。

3.5.1 原子性、可见性、有序性
  • 原子性: 原子性即满足不可拆分的操作,一个操作要么执行完成,要么没有执行。
    // 例如Java中的赋值操作
    int i = 1;//是原子行操作
    int j = i;//不是原子性操作,因为这个操作要先读取i的值,再将i的值赋给j
复制代码
  • 可见性: 可见性是指多线程之间的可见性,一个线程修改了某个状态对另一个线程是可见的即另一个线程马上就能看得见修改。
  • 有序性: 有序性是指Java内存模型中是允许编译器和处理器对指令进行重排序的,重排序不会影响单线程执行的正确性,但是在多线程情况下就会形象多线程并发的正确行。
3.5.2 Java内存模型

Java运行时数据区域如下图。

从图中可以看出主要分为以下几个部分:

1. 程序计数器

程序计数器是一块较小的内存空间。可看做当前线程所执行的字节码的行号指示器,线程私有。

2. Java虚拟机栈

线程私有。生命周期与线程相同。Java虚拟机栈描述的是Java方法执行的内存模型。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

3. 本地方法栈

与虚拟机栈作用相似,区别是虚拟机栈为虚拟机执行Java方法也就是字节码服务,而本地方法栈为虚拟机使用到的Native方法服务。

4. Java堆

所有线程共享的一块内存区域。在虚拟机启动时创建,存放对象实例。是垃圾收集器管理的主要区域。

5. 方法区

线程共享的一块内存区域。用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码的数据。

6. 运行时常量池

是方法区的一部分。用于存放编译期生成的各种字面量和符号引用。具有动态性。

7. 直接内存

并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存被频繁使用。

从以上可以看出Java堆内存是被所有线程共享的内存区域,于是就存在可见性的问题。Java内存模型定义了线程和主存之间的抽象关系:线程之间共享变量存储在主存中,每个线程有个私有的本地内存,本地内存中存储了共享变量的一个副本(本地内存是Java内存模型中一个抽象的概念,不真实存在)。抽象示意图如下。

如上图,线程A要与线程B通信要经过两个步骤:一是线程A把线程A本地内存中更新过的共享变量刷新到主内存中去;二是线程B要到主线程中读取线程A更新过去的共享变量。

回到之前讲的volatile关键字,volatile关键字修饰的变量只能保证并发三个特性中的两个:可见性和有序性。并不能保证原子性。所以在之前无论是单例还是中止线程的代码中都将多个线程访问的变量使用了volatile修饰,保证变量在一个线程修改了之后对所有线程可见且指令有序。又因为无法保证原子性,所以即使声明变量使用了volatile修饰,但是多个线程并发执行例如i++这种非原子性操作是还是会出现问题,这时就需要使用synchronized等方法来解决线程的同步问题。

4、线程同步

多线程访问同一个资源必然会产生线程同步问题,不解决线程同步问题就会造成资源数据的错误。关于线程同步问题还是通过那个经典的多窗口卖票问题来解释理解。

4.1 多窗口卖票

m个卖票窗口同时卖n张票,采用多线程实现。这里先看一下不做同步的错误示例。这里画了个简单的界面,两个输入框输入总票数和窗口数,按钮点击开始卖票,最后的结果由TextView展示。

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".TicketActivity">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <EditText
            android:id="@+id/et_ticket_count"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="输入总票数"
            android:inputType="number"
            android:text="100" />
        <EditText
            android:id="@+id/et_window_count"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="输入卖票窗口数"
            android:inputType="number"
            android:text="3" />
        <Button
            android:id="@+id/btn_begin"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="开卖" />

        <TextView
            android:id="@+id/tv_result"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textColor="@android:color/black"
            android:textSize="16sp" />
    </LinearLayout>
</ScrollView>
复制代码

卖票方法代码:

    //结果字符串
    private StringBuilder result = new StringBuilder();
    //总票数
    private  int  allTicketCount = 0;
    //卖票窗口数
    private int windowCount = 0;
    
    private void begin() {
        allTicketCount = Integer.parseInt(mEtTicketCount.getText().toString().trim());
        windowCount = Integer.parseInt(mEtWindowCount.getText().toString().trim());
        //每次调用清空之前结果
        result.delete(0, result.length());
        result.append("总票数:" + allTicketCount + ",共有" + windowCount + "个窗口卖票\n");
        mTvResult.setText(result.toString());
        //循环创建多个窗口线程并开启
        for (int count = 1; count <= windowCount; count++) {
            Thread window = new Thread(new TicketErrorRunnable(), "售票窗口" + count);
            window.start();
        }
    }
    
    /**
     * 无同步
     */
    class TicketErrorRunnable implements Runnable {

        @Override
        public void run() {
            while (allTicketCount > 0) {
                    // 总票数大于0就将票数减一
                    allTicketCount--;
                    // 输出添加卖出一张票和剩余票数
                    result.append(Thread.currentThread().getName() + ":卖出一张票,还剩" + allTicketCount + "张票。\n");
                    //通知刷新UI
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            mTvResult.setText(result.toString());
                        }
                    });
            
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
            //循环结束说明票卖完了,刷新UI
            result.append(Thread.currentThread().getName() + ":票卖完啦。\n");
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mTvResult.setText(result.toString());
                }
            });

        }
    }
复制代码

运行结果:

未做同步
点击按钮执行begin方法,根据运行结果可以看到不做线程同步,会出现这种两个窗口卖出同一张票的情况导致数据的错误。

4.2 同步代码块

将需要同步的代码放到同步代码块中可以保证线程同步,具体来看代码。

   /**
     * 同步代码块
     */
    class TicketSyncRunnable implements Runnable {
        @Override
        public void run() {
            while (allTicketCount > 0) {
                // ----------同步代码块----------------
                synchronized (TicketActivity.class) {
                    allTicketCount--;
                    result.append(Thread.currentThread().getName() + ":卖出一张票,还剩" + allTicketCount + "张票。\n");
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            mTvResult.setText(result.toString());
                        }
                    });
                }
               // ----------同步代码块----------------
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
            result.append(Thread.currentThread().getName() + ":票卖完啦。\n");
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mTvResult.setText(result.toString());
                }
            });
        }
    }
复制代码

这里其它的都没改变,只修改了Runnable中的内容,将对总票数的计算放到同步代码块中进行同步,保证同时只有一个线程能对总票数allTicketCount进行操作。

运行结果:

同步代码块

4.3 同步方法

将需要同步的代码抽出一个方法,再方法上加上synchronized关键字成为同步方法,也可以保证线程同步。

    /**
     * 同步方法
     */
    class TicketSyncMethodRunnable implements Runnable {
        @Override
        public void run() {
            ticket();
            result.append(Thread.currentThread().getName() + ":票卖完啦。\n");
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mTvResult.setText(result.toString());
                }
            });
        }
        // 同步方法
        private synchronized void ticket() {
            while (allTicketCount > 0) {
                allTicketCount--;
                result.append(Thread.currentThread().getName() + ":卖出一张票,还剩" + allTicketCount + "张票。\n");
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        mTvResult.setText(result.toString());
                    }
                });
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
复制代码

将对总票数的操作抽出到同步方法中,查看运行结果:

同步方法

4.4 重入锁ReentrantLock

自己给代码加锁当然也可以保证进程同步。

    private ReentrantLock reentrantLock = new ReentrantLock();
    
    /**
     * 重入锁
     */
    class TicketLockRunnable implements Runnable {
        @Override
        public void run() {
            while (allTicketCount > 0) {
                //上锁
                reentrantLock.lock();
                try {
                    allTicketCount--;
                    result.append(Thread.currentThread().getName() + ":卖出一张票,还剩" + allTicketCount + "张票。\n");
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            mTvResult.setText(result.toString());
                        }
                    });
                } finally {
                    //开锁
                    reentrantLock.unlock();
                }
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            result.append(Thread.currentThread().getName() + ":票卖完啦。\n");
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mTvResult.setText(result.toString());
                }
            });

        }
    }
复制代码

运行结果:

加锁

5、Android中实现多线程的几种方式

Android中当然可以使用上面提到的三种创建线程的方法实现多线程,但是我这里要说的是Android里为我们提供的几种封装过的多线程执行异步任务的使用方法,毕竟我们手动new Thread().start()不仅不便于管理,而且还会有意想不到的收获(内存泄漏)。

5.1 AsyncTask

AsyncTaskAndroid提供的执行异步任务的类。
AsyncTask的使用:首先定义一个类继承AsyncTask

 class MyAsyncTask extends AsyncTask<String, Integer, String> {
        int count = 0;
        @Override
        protected void onPreExecute() {
            //doInBackground执行之前
            Log.d(TAG, Thread.currentThread().getName() + " 异步任务准备开始 " + System.currentTimeMillis());
        }
        @Override
        protected void onProgressUpdate(Integer... values) {
            //doInBackground执行中
            Log.d(TAG, Thread.currentThread().getName() + " 任务进行中:" + values[0] + "% ");

        }
        @Override
        protected void onPostExecute(String result) {
            //doInBackground执行之后
            Log.d(TAG, Thread.currentThread().getName() + " " + result + " " + System.currentTimeMillis());
        }
        @Override
        protected String doInBackground(String... strings) {
            Log.d(TAG, Thread.currentThread().getName() + " 异步任务执行中 " + System.currentTimeMillis());
            while (count < 100) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count += 10;
                publishProgress(count);
            }
            Log.d(TAG, Thread.currentThread().getName() + " 异步任务完成!" + System.currentTimeMillis());
            return Thread.currentThread().getName() + ":任务完成";
        }
    }
复制代码

AsyncTask三个泛型参数ParamsProgressResult分别对应传入的参数类型、中间进度的类型、返回结果的类型。AsyncTask主要有四个回调方法复写。

  • doInBackground:必须复写,异步执行后台线程,要完成的耗时任务在这里处理,运行在子线程。
  • onPreExecute:执行doInBackground前被调用,进行耗时操作前的初始化或者准备工作,运行在主线程。
  • onProgressUpdate:doInBackground执行完成后调用,返回处理结果,更新UI等,运行在主线程。
  • onProgressUpdate:在doInBackground方法中调用publishProgress方法后调用更新耗时任务进度,运行在主线程。

调用AsyncTask

   MyAsyncTask myAsyncTask = new MyAsyncTask();
   myAsyncTask.execute();
复制代码

运行结果日志:

5.2 HandlerThread

HandlerThread是将HandlerThread的封装,本质上还是一个Thread
HandlerThread的使用如下:

        // 创建一个HandlerThread
        HandlerThread handlerThread = new HandlerThread("myHandlerThread");
        //调用start方法开启线程
        handlerThread.start();
        //创建一个Handler传入handlerThread的Looper
        handler = new Handler(handlerThread.getLooper()) {
            //复写handleMessage方法处理消息
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case 1:
                        Log.d(TAG, Thread.currentThread().getName() + " receive one message");
                        break;
                }
            }
        };
        
        //在调用处使用handler发送消息
        Message message = Message.obtain();
        message.what = 1;
        handler.sendMessage(message);
        
        // 使用完退出HandlerThread
        handlerThread.quit();
复制代码

运行结果日志:

5.3 IntentService

IntentService是将ServiceHandlerThread封装,与普通Service不同的是,普通Service运行在主线程,IntentService创建一个工作子线程执行任务,并且IntentService在执行完任务后自动关闭服务,而普通Service需要手动调用stopService方法。 IntentService使用如下:

//继承实现IntentService
public class MyIntentService  extends IntentService {

    private static final String TAG = "MyIntentService";

    public MyIntentService() {
        super("MyIntentService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        //复写onHandleIntent方法根据intent传递参数实现处理任务
        String type = intent.getStringExtra("type");
        switch (type){
            case "type1":
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.d(TAG,Thread.currentThread().getName()+" type1 doing");
                break;
        }
    }
}

//因为是服务所以要在AndroidManifest中注册
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.thread.threaddemo">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
          ......
        <service android:name=".MyIntentService"/>
    </application>
</manifest>

//启动服务
Intent intent = new Intent(this, MyIntentService.class);
intent.putExtra("type", "type1");
startService(intent);
复制代码

运行日志结果:

5.4 Handler

对于Android开发者来说,Handler再熟悉不过了,HandlerAndroid提供的一种多线程间通信方式。Android关于多线程肯定避不开Handler。这里对Handler的使用就不举例了,想要了解使用及原理的可以看我写的这篇Android进阶知识:Handler相关

6、线程池

线程池是用来管理线程的工具,在开发中如果每次进行异步任务都手动创建一个线程,不仅浪费资源而且不容易管理控制,线程池的使用就能很好的解决这些问题。

6.1 阻塞队列

在了解线程池之前先来了解一下阻塞队列,阻塞队列的理解有助于我们对线程池的工作原理的学习。阻塞队列是并发编程中“生产者-消费者”模式里用来存放元素的容器,生产者生产元素放入阻塞队列,消费者从阻塞队列中取出元素,当队列中没有元素或者队列中元素已满时会发生阻塞,直到队列中再次加入元素或者去除元素为止。阻塞队列实现了元素添加取出时的锁操作,所以使用时无需单独考虑线程同步问题。

Java中提供了七种阻塞队列:

  • ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
  • LinkedBlockingQueue:由链表结构组成的有界阻塞队列。
  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
  • DelayQueue:使用优先级队列实现的无界阻塞队列。
  • SynchronousQueue:不存储元素的阻塞队列。
  • LinkedTransferQueue:由链表结构足层的无界阻塞队列。
  • LinkedBlockingDeque:由链表结构组成的双向阻塞队列。

下面通过使用阻塞队列实现一个“生产者-消费者”模式的例子。

package com.thread.threaddemo;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import java.util.concurrent.ArrayBlockingQueue;

public class BlockingQueueActivity extends AppCompatActivity implements View.OnClickListener {

    private static final String TAG = "BlockingQueueActivity";
    private Button mBtnMain;
    private Button mBtnProduce;
    private Button mBtnConsumer;
    // 阻塞队列
    private ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(5);
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_blocking_queue);
        initView();
    }
    private void initView() {
        mBtnMain = (Button) findViewById(R.id.btn_main);
        mBtnProduce = (Button) findViewById(R.id.btn_produce);
        mBtnConsumer = (Button) findViewById(R.id.btn_consumer);
        mBtnMain.setOnClickListener(this);
        mBtnProduce.setOnClickListener(this);
        mBtnConsumer.setOnClickListener(this);
    }
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_main:
                mainStart();
                break;
            case R.id.btn_produce:
                addData();
                break;
            case R.id.btn_consumer:
                removeData();
                break;
        }
    }
    /*
     * 手动从阻塞队列取出一个元素
     */
    private void removeData() {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    queue.take();
                    printData();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "remove");
        thread.start();
    }
    /*
     * 手动从阻塞队列添加一个元素
     */
    private void addData() {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    queue.put("data");
                    printData();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "add");
        thread.start();
    }
     /*
      * 打印日志
      */
    private void printData() {
        Log.d(TAG, Thread.currentThread().getName() + ":" + queue.toString());
    }
     /*
      * 开启生产者消费者线程
      */
    private void mainStart() {
        ProducerThread producerThread = new ProducerThread("Producer");
        producerThread.start();
        ConsumerThread consumerThread = new ConsumerThread("Consumer");
        consumerThread.start();
    }
     /*
      * 消费者线程
      */
    class ConsumerThread extends Thread {
        public ConsumerThread(String name) {
            super(name);
        }
        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(2000);
                    queue.take();
                    printData();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
     /*
      * 生产者线程
      */
    class ProducerThread extends Thread {
        public ProducerThread(String name) {
            super(name);
        }
        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(1000);
                    queue.put("data");
                    printData();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
复制代码

运行打印日志:

从这段代码可以看到首先创建阻塞队列queue容量为5,点击按钮调用mainStart方法,开启生产者线程和消费者线程,生产者线程每隔1秒生产一个元素加入队列,消费者每隔2秒消费一个元素取出队列。可以看到由于生产比消费快,所以容器逐渐被加满,最后保证队列中充满五个元素,再想添加只能等到消费者线程消费了元素才行。当然也可以通过手动调用addDataremoveData方法来向队列里添加和取出元素,同样阻塞队列满了或者空了就会发生阻塞,直到队列中空出位置或者加入新元素为止。

6.2 线程池

6.2.1 构造函数

Java中线程池的核心实现类是ThreadPoolExecutor,无论是Runnable还是Callable都可以交给线程池来统一管理,要使用线程池就要先创建一个线程池对象。

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 20, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(100), threadFactory, new ThreadPoolExecutor.AbortPolicy());
复制代码

这样就创建了一个线程池对象,接着就可以调用executesubmit提交任务,线程池会分配线程处理任务。不过ThreadPoolExecutor的构造方法中这几个参数到底代表了什么,我们来看一下它的构造方法。

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
复制代码

构造函数中就是对这几个成员参数的一个初始化赋值,来看这几个参数代表的意思。

  • corePoolSize: 核心线程数。默认情况下线程池是空的,在有任务提交时,当前运行线程少于corePoolSize数,就会创建新线程来处理任务。
  • maximumPoolSize: 线程池允许创建的最大线程数。如果任务队列已满并且线程数小于maximumPoolSize时,线程池仍旧会创建新的线程来处理任务。
  • keepAliveTime: 非核心线程空闲的超时时间。超过这个时间则会被回收。
  • TimeUnit:keepAliveTime的时间单位。
  • workQueue: 任务队列。是一个阻塞队列,如果当前线程数大核心线程数,则将任务添加到此任务队列中。
  • ThreadFactory: 线程工厂。
  • RejectedExecutionHandler: 饱和策略。当任务队列和线程池都满了时所采取的策略。
6.2.2 执行流程

线程执行流程源码从ThreadPoolExecutorexecute方法开始来看。

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        //判断工作线程数是否小于核心线程数
        if (workerCountOf(c) < corePoolSize) {
            //小于就调用addWorker方法创建核心线程
            //这里第二个参数true表示是核心线程
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        //否则调用workQueue.offer方法将任务放入任务队列
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //放入任务队列失败则再调用addWorker方法创建非核心线程执行任务
        //这里第二参数传入false表示非核心线程
        else if (!addWorker(command, false))
           //如果addWorker返回false
           //说明线程数超过最大线程数maximumPoolSize
           //调用reject方法执行饱和策略
            reject(command);
    }
复制代码

由上述代码和注释可以看出具体执行流程如下:

  1. 判断线程池中核心线程数量是否已到达设置的最大核心线程数corePoolSize,若没有到达核心线程数就创建核心线程处理任务。
  2. 若已到达核心线程数,再判断任务队列是否已满,若未满就将任务加入到任务队列中。
  3. 若任务队列已满,再判断是否已达到线程池最大线程数maxmumPoolSize,若未达到最大线程数,则创建非核心线程处理任务。
  4. 若到达最大线程数,就执行饱和策略。

这里再继续看一下reject方法:

  final void reject(Runnable command) {
        handler.rejectedExecution(command, this);
    }
复制代码

调用了饱和策略中的rejectedExecution方法。这里饱和策略有4种,分别是:

  • AbordPolicy: 默认是这种策略,表示无法处理新任务,并抛出RejectedExecutionException异常。
  • CallerRunsPolicy: 用调用者所在的线程来处理任务。
  • DiscardPolicy: 不能执行的任务,并将该任务删除。
  • DiscardOldestPolicy: 丢弃队列最近的任务,并执行当前的任务。

这里看下默认的AbordPolicy的实现。

public static class AbortPolicy implements RejectedExecutionHandler {
        /**
         * Creates an {@code AbortPolicy}.
         */
        public AbortPolicy() { }

        /**
         * Always throws RejectedExecutionException.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         * @throws RejectedExecutionException always
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }
复制代码

可以看到实现很简单就是实现了RejectedExecutionHandler接口,复写rejectedExecution方法,抛出RejectedExecutionException异常。其它的饱和策略的实现也是类似。

线程池执行流程

6.2.3 常用线程池

Java中提供了几种默认常用线程池。

  • FixedThreadPool: 可重用固定线程数的线程池。
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }
复制代码

从创建方法中可以看出FixedThreadPool线程池的核心线程数和最大线程数是相同的,所以它是没有非核心线程的。并且它的线程空闲超时时长设置为0,所以一旦线程任务结束空闲下来就会被回收,因此不会有空闲线程存在。

  • CachedThreadPool: 可缓存线程池。
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
复制代码

CachedThreadPool它的核心线程数为0,不会创建核心线程,而是直接将任务加入任务队列,而它的最大线程数为Integer.MAX_VALUE,也就是说可以无限创建非核心线程的来处理任务。并且它的线程空闲超时时间为60秒,空闲线程可以缓存60秒后才会被回收。

  • SingleThreadExecutor: 单线程工作的线程池。
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
复制代码

SingleThreadExecutor的核心线程数为1,最大线程数为1,所以SingleThreadExecutor线程池中只有一个核心线程在工作,空闲超时时间为0,任务结束立即回收,在唯一的核心线程在工作时,提交的任务会放入LinkedBlockingQueue任务队列中等待一个一个执行。

  • ScheduledThreadPool: 实现定时和周期性任务的线程池。
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    
    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE,
              DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
              new DelayedWorkQueue());
    }
复制代码

ScheduledThreadPool的核心线程数是固定传入的,而最大线程数是Integer.MAX_VALUE,任务队列为DelayedWorkQueue,本身是无界队列,所以线程池中不会创建非核心线程,当工作线程数到达核心线程数时,线程池只会一直往任务队列中添加任务,空闲线程的超时时间为10秒。

7、总结

以上就是Android中多线程相关的基础知识。关于多线程在使用的时候需要多加注意,不仅要注意保证线程同步在Android中还要注意内存泄漏的发生。需要频繁创建子线程操作最好使用线程池进行管理。

参考资料:

Android进阶之光
深入理解Java虚拟机




原文地址:访问原文地址
快照地址: 访问文章快照