Android线程管理之详解(线程创建、线程终止、线程同步、阻塞队列、线程池)

安卓 同时被 2 个专栏收录
58 篇文章 0 订阅
34 篇文章 1 订阅

前言

     总结一下Android上的线程管理,今天先来总结一下线程创建、线程终止、线程同步、阻塞队列、线程池使用。

一、线程创建

    线程的创建包括三种方法,分别是继承自Thread,实现Runnable接口、实现Callback接口,前两个在开发中最为常见。

①继承自Thread

    public class OneThread extends Thread {
        @Override
        public void run() {
            Log.e("TAG","OneThread:" + Thread.currentThread().getName());
        }
    }

②实现Runnable接口

    public class TwoThread implements Runnable {
        @Override
        public void run() {
            Log.e("TAG","TwoThread:" + Thread.currentThread().getName());
        }
    }

测试:

    public void create(View view) {
        OneThread oneThread = new OneThread();
        TwoThread twoThread = new TwoThread();
        //线程一
        oneThread.start();
        //线程二
        Thread thread = new Thread(twoThread);
        thread.start();
    }

 

1、线程的6种状态及切换

Thread主要函数

run()//包含线程运行时所执行的代码 

start()//用于启动线程

sleep()/sleep(long millis)//线程休眠,交出CPU,让CPU去执行其他的任务,然后线程进入阻塞状态,sleep方法不会释放锁

yield()//使当前线程交出CPU,让CPU去执行其他的任务,但不会是线程进入阻塞状态,而是重置为就绪状态,yield方法不会释放锁

join()/join(long millis)/join(long millis,int nanoseconds)//等待线程终止,直白的说 就是发起该子线程的线程 只有等待该子线程运行结束才能继续往下运行

wait()//交出cpu,让CPU去执行其他的任务,让线程进入阻塞状态,同时也会释放锁

interrupt()//中断线程,自stop函数过时之后,我们通过interrupt方法和isInterrupted()方法来停止正在运行的线程,注意只能中断已经处于阻塞的线程

getId()//获取当前线程的ID

getName()/setName()//获取和设置线程的名字

getPriority()/setPriority()//获取和这是线程的优先级 一般property用1-10的整数表示,默认优先级是5,优先级最高是10,优先级高的线程被执行的机率高

setDaemon()/isDaemo()//设置和判断是否是守护线程

currentThread()//静态函数获取当前线程

 

Thread线程主要状态

(1)初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
(2)运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
         线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程           调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态                    (running)。
(3)阻塞(BLOCKED):表示线程阻塞于锁。
(4)等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
(5)超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
(6)终止(TERMINATED):表示该线程已经执行完毕。

(1)初始状态
实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。

(2.1)就绪状态
就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
调用线程的start()方法,此线程进入就绪状态。
当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
锁池里的线程拿到对象锁后,进入就绪状态。
(2.2) 运行中状态
线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。

(3)阻塞状态
阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。

(4)等待
处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。

(5)超时等待
处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。

(6)终止状态
当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
 

2、使用回调接口解决匿名实现Runnable内存泄漏的问题

(该处转载至原文:https://blog.csdn.net/xwx617/article/details/81193102 )

在Android开发中经常会看到这样的代码:

public class MyActivity extends Activity {
    ...
    new Thread(new Runnable() {
        @Override
        public void run() {
            //具体实现
        }
    }).start();
}

开辟一个线程去执行一个耗时操作是常见的做法,但是在Android开发中,诸如Activity、Fragment这种生命周期不确定的类(周期长的情况就是一直不关闭当前Activity/Fragment,周期短的情况就是很快的关闭了当前Activity/Fragment),在生命周期短的情况下,由于匿名内部类会隐式持有外部类的引用,因此会导致外部类无法被回收,最终导致内存泄漏。

我们知道只有匿名内部类和非静态内部类会隐式持有外部类的引用,所以我们第一个想法就是使用外部类来解决这个问题,比如我们可以在外部创建一个类实现Runnable接口,再在Activity中实例化这个类:
 

public class MyRunnable extends Runnable {

    @Override
    public void run() {
        //具体实现
    ]
}

public class MyActivity extends Activity {
    ...
    new Thread(new MyRunnable()).start();
}

这样确实解决了内存泄漏的问题,但是这种做法有一个弊端,就是在有非常多的需求时,那就必须要创建非常多的实现了Runnable的外部类来满足各个需求,并且如果我需要使用到Activity中的变量时,还需要将变量传递到这些类中,非常的麻烦,所以这种做法在大型项目中根本不可取。

既要能有一个外部类解决内存泄漏的问题,又要能在Activity中去写我具体的逻辑而不用将变量再传递到外部类中,仔细一想,这不正是回调接口吗?
 

public interface MyInterface {
    void doSomething();
}

public class MyRunable implements Runnable {

    private WeakReference<MyInterface> mInterface;

    public MyRunnable(MyInterface mInterface) {
        this.mInterface = new WeakReference<>(mInterface);
    }

    @Override
    public void run() {
        mInterface.get().doSomething();
    ]
}

public class MyActivity extends Activity {
    ...
    new Thread(new MyRunnable(new MyInterface() {
        @Override
        public void doSomething() {
            //具体实现
        }
    })).start();
}

通过回调接口,我们将具体实现放在了Activity中编写,但是我们注意到,这个接口在MyRunnable中是以弱引用的方式被持有的,所以在GC时,它会被立刻回收,所以即使最终我们还是使用了匿名内部类,但是却不会再发生内存泄漏了。

 

二、线程终止


    在早期的jdk中有一个stop方法可以终止线程,但是已经被弃用了,提供了interrupt方法来中断线程。

1、destroy() 为何被废弃

destroy() 只是抛出了一个 NoSuchMethodError,所以该方法无法终止线程:

@Deprecated  
public void destroy() {  
    throw new NoSuchMethodError();  
}  

2、stop() 为何被废弃

调用 stop() 方法是不安全的,这是因为当调用 stop() 方法时,会发生下面两件事:

  1. 即刻抛出 ThreadDeath 异常,在线程的 run() 方法内,任何一点都有可能抛出 ThreadDeath Error,包括在 catch 或 finally 语句中
  2. 会释放该线程所持有的所有的锁,而这种释放是不可控制的,非预期的
  3. 一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止
public class SynchronizedObject {
    private String name = "a";         // 省略 getter、setter
    private String password = "aa";    // 省略 getter、setter

    public synchronized void printString(String name, String password) {
        try {
            this.name = name;
            Thread.sleep(100000);
            this.password = password;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

public class MyThread extends Thread {
    private SynchronizedObject synchronizedObject;
    public MyThread(SynchronizedObject synchronizedObject) {
        this.synchronizedObject = synchronizedObject;
    }

    public void run() {
        synchronizedObject.printString("b", "bb");
    }
}

public class Run {
    public static void main(String args[]) throws InterruptedException {
        SynchronizedObject synchronizedObject = new SynchronizedObject();
        Thread thread = new MyThread(synchronizedObject);
        thread.start();
        Thread.sleep(500);
        thread.stop();
        System.out.println(synchronizedObject.getName() + "  " + synchronizedObject.getPassword());
    }
}

输出结果:

b aa

从上面的程序验证结果来看,thread.stop() 会释放该线程所持有的所有的锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用 thread.stop() 后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。

Java 中多线程锁释放的条件:

  1. 执行完同步代码块,就会释放锁(synchronized)
  2. 在执行同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放(exception)
  3. 在执行同步代码块的过程中,执行了锁所属对象的 wait() 方法,这个线程会释放锁,进入对象的等待池(wait)

从上面的三点就可以看到 stop() 释放锁是在第二点的,通过抛出异常来释放锁,通过证明,这种方式是不安全的,不可靠的。

 

3、如何正确地停止一个线程

该处部分内容转载至链接:https://www.jianshu.com/p/5f368f9a5734

以下 3种方法可以终止正在运行的线程:

  1. 使用退出标志,使线程正常退出,也就是当 run 方法完成后线程终止
  2. 使用 interrupt 方法中断线程
  3. AsyncTask自带线程停止方法

(1)退出标志法

需要 while() 循环在某以特定条件下退出,最直接的办法就是设一个 boolean 标志,并通过设置这个标志来控制循环是否退出:

public class FiveThread implements Runnable {
    private volatile boolean isCancelled;
    
    public void run() {
        Log.e("TAG", "FiveThread:" + Thread.currentThread().getName());
        while (!isCancelled) {
            //do something
                Log.e("TAG", "线程没有中断");
                SystemClock.sleep(500);
        }
        Log.e("TAG", "线程FiveThread终止了");
    }
    
    public void cancel() { isCancelled=true; }
}

注意,isCancelled 需要为 volatile,保证线程读取时 isCancelled 是最新数据

测试:

    public void stop(View view) {
        FiveThread runnable = new FiveThread();
        Thread fiveThread = new Thread(runnable);
        fiveThread.start();
        SystemClock.sleep(1000);
        runnable.cancel();
    }

结果:

(2)使用interrupt终止

如果线程是阻塞的,则不能使用退出标志法来终止线程。这时就只能使用 Java 提供的中断机制:

  • public void interrupt()

       是一个实例方法,该方法用于设置当前线程对象的中断标识位。

       如果线程处于被阻塞状态(例如处于 sleep, wait, join 等状态),那么线程将立即退出被阻塞状态,并抛出一个 InterruptedException 异常。
       如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,被设置中断标志的线程将继续正常运行不受影响。   

  • public static boolean interrupted()

        是一个静态的方法,测试当前线程(正在执行这一命令的线程)是否被中断。该方法调用结束的时候会清空中断标识位即将当前线程的中断状态重置为 false。

public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

private native boolean isInterrupted(boolean ClearInterrupted);
  • public boolean isInterrupted()

       测试线程是否被终止不像静态的中断方法,这一调用不改变线程的中断状态。它是一个实例方法,主要用于判断当前线程对象的中断标志位是否被标记了,如果被标记了则返回true表示当前已经被中断,否则返回false。我们也可以看看它的实现源码:

public boolean isInterrupted() {
        return isInterrupted(false);
}

private native boolean isInterrupted(boolean ClearInterrupted);

底层调用的本地方法isInterrupted,传入一个boolean类型的参数,用于指定调用该方法之后是否需要清除该线程对象的中断标识位。从这里我们也可以看出来,调用isInterrupted并不会清除线程对象的中断标识位。

 

中断原则:如果一个线程处于了阻塞状态,如线程调用了thread.sleep、thread.join、thread.wait等,再检查时候发现中断标识位为true,则会在阻塞方法处抛出InterruptedExcept异常,并且在抛出异常前将线程中的标识位置为false。这个异常应该是需要处理的。

interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。也就是说,一个线程如果有被中断的需求,那么就可以这样做:

① 在正常运行任务时,经常检查本线程的中断标志位,这里可以调用Thread.currentThread.isInterrupted判断。如果被设置了中断标志就自行停止线程或者抛出 InterruptedException,使得线程停止的事件得以向上传播

Thread thread = new Thread(() -> {
    while (!Thread.interrupted()) {
        // do more work
    }
    // return or throw InterruptedException
});
thread.start();

// 一段时间以后
thread.interrupt();

thread.interrupted() 清除标志位是为了下次继续检测标志位。如果一个线程被设置中断标志后,选择结束线程那么自然不存在下次的问题,而如果一个线程被设置中断标识后,进行了一些处理后选择继续进行任务,而且这个任务也是需要被中断的,那么当然需要清除标志位了

② 在调用阻塞方法时正确处理 InterruptedException 异常(例如,catch 异常后就结束线程)

    public class ThreeThread implements Runnable {
 
        @Override
        public void run() {
            Log.e("TAG", "ThreeThread:" + Thread.currentThread().getName());
            while (!Thread.currentThread().isInterrupted()) {
                Log.e("TAG", "线程没有中断");
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
        // 抛出 InterruptedException 后中断标志被清除,标准做法是再次调用 interrupt 恢复中断
                    Thread.currentThread().interrupt();
                }
            }
            Log.e("TAG", "线程ThreeThread终止了");
        }
    }

测试:

    public void stop(View view) {
        Thread threeThread = new Thread(new ThreeThread());
        threeThread.start();
        SystemClock.sleep(1000);
        threeThread.interrupt();
    }

结果:

Thread.sleep 这个阻塞方法,接收到中断请求,会抛出 InterruptedException,让上层代码处理。这时,可以什么都不做,但这等于吞掉了中断。因为抛出 InterruptedException 后,中断标记会被重新设置为 false!看 sleep() 的注释,也强调了这点:

@throws InterruptedException
     if any thread has interrupted the current thread. 
     The interrupted status of the current thread is 
     cleared when this exception is thrown.
public static native void sleep(long millis) throws InterruptedException;

记得这个规则:什么时候都不应该吞掉中断!每个线程都应该有合适的方法响应中断!

在接收到中断请求时,标准做法是执行 Thread.currentThread().interrupt() 恢复中断,让线程退出

从另一方面谈起,你不能吞掉中断,也不能中断你不熟悉的线程。如果线程没有响应中断的方法,你无论调用多少次 interrupt() 方法,也像泥牛入海

 

线程在不同状态下对于中断所产生的反应

该处部分转载至https://www.cnblogs.com/yangming1996/p/7612653.html

线程一共6种状态,分别是NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED(Thread类中有一个State枚举类型列举了线程的所有状态)。下面我们就将把线程分别置于上述的不同种状态,然后看看我们的中断操作对它们的影响。

①NEW和TERMINATED
     线程的new状态表示还未调用start方法,还未真正启动。线程的terminated状态表示线程已经运行终止。这两个状态下调用中断方法来中断线程的时候,Java认为毫无意义,所以并不会设置线程的中断标识位,什么事也不会发生。例如:

public static void main(String[] args) throws InterruptedException {

    Thread thread = new MyThread();
    System.out.println(thread.getState());

    thread.interrupt();

    System.out.println(thread.isInterrupted());
}

结果:

terminated状态:

public static void main(String[] args) throws InterruptedException {

     Thread thread = new MyThread();
     thread.start();

     thread.join();
     System.out.println(thread.getState());

     thread.interrupt();

    System.out.println(thread.isInterrupted());

}

结果:

从上述的两个例子来看,对于处于new和terminated状态的线程对于中断是屏蔽的,也就是说中断操作对这两种状态下的线程是无效的。

②RUNNABLE
     如果线程处于运行状态,那么该线程的状态就是RUNNABLE,但是不一定所有处于RUNNABLE状态的线程都能获得CPU运行,在某个时间段,只能由一个线程占用CPU,那么其余的线程虽然状态是RUNNABLE,但是都没有处于运行状态。而我们处于RUNNABLE状态的线程在遭遇中断操作的时候只会设置该线程的中断标志位,并不会让线程实际中断,想要发现本线程已经被要求中断了则需要用程序去判断。例如:

/*先定义一个线程类*/
public class MyThread extends Thread{

    @Override
    public void run(){
        while(true){
            //do something
        }
    }
}


/*main函数启动线程*/
public static void main(String[] args) throws InterruptedException {

    Thread thread = new MyThread();
    thread.start();

    System.out.println(thread.getState());

    thread.interrupt();
    Thread.sleep(1000);//等到thread线程被中断之后
    System.out.println(thread.isInterrupted());

    System.out.println(thread.getState());
}

我们定义的线程始终循环做一些事情,主线程启动该线程并输出该线程的状态,然后调用中断方法中断该线程并再次输出该线程的状态。总的输出结果如下:

可以看到在我们启动线程之后,线程状态变为RUNNABLE,中断之后输出中断标志,显然中断位已经被标记,但是当我们再次输出线程状态的时候发现,线程仍然处于RUNNABLE状态。很显然,处于RUNNBALE状态下的线程即便遇到中断操作,也只会设置中断标志位并不会实际中断线程运行。那么问题是,既然不能直接中断线程,我要中断标志有何用处?
这里其实Java将这种权力交给了我们的程序,Java给我们提供了一个中断标志位,我们的程序可以通过if判断中断标志位是否被设置来中断我们的程序而不是系统强制的中断。例如:

/*修改MyThread类的run方法*/
public void run(){
    while(true){
        if (Thread.currentThread().isInterrupted()){
            System.out.println("exit MyThread");
            break;
        }
    }
}

线程一旦发现自己的中断标志为被设置了,立马跳出死循环。这样的设计好处就在于给了我们程序更大的灵活性。

③BLOCKED
     当线程处于BLOCKED状态说明该线程由于竞争某个对象的锁失败而被挂在了该对象的阻塞队列上了。那么此时发起中断操作不会对该线程产生任何影响,依然只是设置中断标志位。例如:

/*自定义线程类*/
public class MyThread extends Thread{

    public synchronized static void doSomething(){
        while(true){
            //do something
        }
    }
    @Override
    public void run(){
        doSomething();
    }
}

public static void main(String[] args) throws InterruptedException {

    Thread thread1 = new MyThread();
    thread1.start();

    Thread thread2 = new MyThread();
    thread2.start();

    Thread.sleep(1000);
    System.out.println(thread1.getState());
    System.out.println(thread2.getState());

    thread2.interrupt();
    System.out.println(thread2.isInterrupted());
    System.out.println(thread2.getState());
}

这里我们自定义了一个线程类,run方法中主要就做一件事情,调用一个有锁的静态方法,该方法内部是一个死循环(占用该锁让其他线程阻塞)。在我们的主线程中,我们定义了两个线程并按照定义顺序启动他们,显然thread1启动后便占用MyThread类锁,此后thread2在获取锁的时候一定失败,自然被阻塞在阻塞队列上,而我们对thread2进行中断,输出结果如下:

从输出结果看来,thread2处于BLOCKED状态,执行中断操作之后,该线程仍然处于BLOCKED状态,但是中断标志位却已被修改。这种状态下的线程和处于RUNNABLE状态下的线程是类似的,给了我们程序更大的灵活性去判断和处理中断。

④WAITING/TIMED_WAITING
     这两种状态本质上是同一种状态,只不过TIMED_WAITING在等待一段时间后会自动释放自己,而WAITING则是无限期等待,需要其他线程调用notify方法释放自己。但是他们都是线程在运行的过程中由于缺少某些条件而被挂起在某个对象的等待队列上。当这些线程遇到中断操作的时候,会抛出一个InterruptedException异常,并清空中断标志位。例如:

/*定义一个线程类*/
public class MyThread extends Thread{

    @Override
    public void run(){
        synchronized (this){
            try {
                wait();
            } catch (InterruptedException e) {
                System.out.println("i am waiting but facing interruptexception now");
            }
        }
    }
}

/*main函数启动该线程*/
public static void main(String[] args) throws InterruptedException {

    Thread thread = new MyThread();
    thread.start();

    Thread.sleep(500);
    System.out.println(thread.getState());
    thread.interrupt();
    Thread.sleep(1000);
    System.out.println(thread.isInterrupted());
}

我们定义了一个线程类,其中run方法让当前线程阻塞到条件队列上,并且针对InterruptedException 进行捕获,如果遇到InterruptedException 异常则输出一行信息。在main线程中我们启动一个MyThread线程,然后对其进行中断操作。

从运行结果看,当前程thread启动之后就被挂起到该线程对象的条件队列上,然后我们调用interrupt方法对该线程进行中断,输出了我们在catch中的输出语句,显然是捕获了InterruptedException异常,接着就看到该线程的中断标志位被清空。

综上所述,我们分别介绍了不同种线程的不同状态下对于中断请求的反应。NEW和TERMINATED对于中断操作几乎是屏蔽的,RUNNABLE和BLOCKED类似,对于中断操作只是设置中断标志位并没有强制终止线程,对于线程的终止权利依然在程序手中。WAITING/TIMED_WAITING状态下的线程对于中断操作是敏感的,他们会抛出异常并清空中断标志位。

 

(3)AsyncTask自带线程停止方法(推荐)

       该处部分转载至https://blog.csdn.net/libeyond_/article/details/79544791,我在前几个月的项目中遇到查询下载多个文件未结束时重新查询下载因为列表清空原先的下载线程没有终止获取数据会异常,使用中断等方法都无法解决,还好看到了这篇博文真的好庆幸,此处直接引用该博文的范例进行介绍。

        Thread.interrupt()方法不会中断一个正在运行的线程。这一方法实际上完成的是,在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态。更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,那么,它将接收到一个中断异常(InterruptedException),从而提早地终结被阻塞状态。 
        因此,如果线程被上述几种方法阻塞,正确的停止线程方式是设置共享变量,并调用interrupt()(注意变量应该先设置)。如果线程没有被阻塞,这时调用interrupt()将不起作用;否则,线程就将得到异常(该线程必须事先预备好处理此状况),接着逃离阻塞状态。在任何一种情况中,最后线程都将检查共享变量然后再停止。

       在处理多线程任务时,比如倒计时接单(通过子线程实现倒计时),新来订单15s后自动消失。而业务需求中又要允许用户在倒计时未结束时手动划掉消失,则此时需要停止倒计时线程。或者可能有时有多单依次叠加,此时最上面的一单消失时,也需要先停止上一单倒计时再自动开启下一单的倒计时。
如果是new Thread方式的interrupt()方法来停止线程,打印时输出会发现倒计时中累加的变量并未停止累加。万般无奈中想起AsyncTask不是自带线程停止方法么,即taskProgress.cancel(true)(taskProgress是TaskProgress继承AsyncTask后新new的对象),并且自带判断线程是否正在执行的方法taskProgress.getStatus() == AsyncTask.Status.RUNNING,果断解决问题。代码如下:

/*
     * 新来订单时开始计时
     */
    public static TaskProgress taskProgress;
    public void startCountdown(final int mProgress, final int maxtime) {
        if (maxtime > 0) {
            roundProgressBar.setVisibility(View.VISIBLE);

            if (taskProgress != null && taskProgress.getStatus() == AsyncTask.Status.RUNNING)  
            {  
                taskProgress.cancel(true); // 如果Task还在运行,则先取消它,然后执行关闭activity代码  
                taskProgress = null;
            }  
            taskProgress = new TaskProgress(maxtime);
            taskProgress.execute(0);

        }
    }
    /*
     * 开启倒计时,倒计时任务需要能手动停止,故而用AsyncTask,Thread无法在左右滑动卡片时停止线程
     */
    public class TaskProgress extends AsyncTask<Integer, Integer, Integer> {// 继承AsyncTask
        int maxtime;
        int mProgress;

        public TaskProgress(int maxtime) {
            this.maxtime = maxtime;
        }

        @Override
        protected Integer doInBackground(Integer... params) {// 处理后台执行的任务,在后台线程执行
            int flag = 1;
            if (isCancelled())
                return null;
            mProgress = params[0];
            while (mProgress <= maxtime && !isCancelled()) {
                try {
                    publishProgress(mProgress);//将会调用onProgressUpdate(Integer... progress)方法
                    mProgress += 1;
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                LogUtils.e("-----------------------", String.valueOf(mProgress));
                LogUtils.e("-----------------------", String.valueOf(mProgress));
                LogUtils.e("-----------------------", String.valueOf(mProgress));

            }
            return flag;
        }

        protected void onProgressUpdate(Integer... progress) {// 在调用publishProgress之后被调用,在ui线程执行
            // roundProgressBar.setProgress(progress[0]);//更新进度条的进度
            if (isCancelled()) {
                return;
            } else {
                roundProgressBar.setProgress(progress[0], maxtime);
            }
        }

        protected void onPostExecute(Integer result) {// 后台任务执行完之后被调用,在ui线程执行
            if (result != null) {
                //自动消失
                MainActivity.getInstance().vanishOnDisappear();
            } 
        }

        protected void onPreExecute() {// 在doInBackground(Params...)之前被调用,在ui线程执行
            roundProgressBar.setProgress(0, maxtime);// 进度条复位
        }

        protected void onCancelled() {// 在ui线程执行
//          roundProgressBar.setProgress(0, maxtime);// 进度条复位
        }

    }

三、线程同步

 线程的同步是为了防止多个线程访问一个数据对象时,造成数据不一致的问题。


    Lock和Condition接口为开发人员提供了高度的程序锁定控制,但是大多数情况下并不需要使用Lock和Condition进行复杂的次序控制,在Java语言内部,每一个对象都有一个内部锁。如果一个方法被synchronized声明那么对象锁将锁住整个方法或者同步代码块,要调用该方法或者执行或者执行该代码块,必须拿到该所锁对象,否则只能等待无法访问。也就是说任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行synchronized。

    同时Java也提供了wait、notify、notifyAll三个方法进行线程同步,wait方法是将一个线程添加到等待集合中,notify、notifyAll是解除等待线程的阻状态。二者的区别是notify表示唤醒一个线程,notifyAll也表示唤醒一个线程,但它会notify所有的线程,具体唤醒哪一个线程,由jvm来决定。主要的效果区别是notify用得不好容易导致死锁,锁池中的队列空了,等待池中有一堆线程,但不会再被唤醒永远等待。

    再借用知乎中一个通俗的说法来进行说明:比如说,你是你家挣钱的,儿子和女儿是花钱的。儿子给家里要100,女儿要30。可是家里没钱,他们只能在外面等待池里面等。后来你出去打工,赚钱了,赚了50,这时你要在儿子和女儿之间选择一个人叫醒。如果使用notify不凑巧,你把儿子叫醒了,儿子发现钱还是不够,又去等。因为你只能叫一次,女儿就错过了使用这50块钱的机会。如果下一次也叫醒了儿子或者女儿不等了,那么就会进入死锁。你决定使用notifyAll把所有的人都叫醒,虽然费劲一点。这样一来,儿子发现不够,接着等,女儿发现够了,就用了。

    现在以生产者消费者模式来进行详细说明,生产者生产商品供消费者进行消费,因此需要创建两个线程,一个是生产者线程一个是消费者线程,这里面需要协调好生产者和消费者的关系,生产者的生产要供得上消费者的使用,同时应该满足生产者不至于每次生产的太多产品,够消费者使用即可。不能供大于求或者供过于求现象。
1、商品类

    public class Goodes {
        public int num;
        public String name;
 
        @Override
        public String toString() {
            return "Goodes{" +
                    "num=" + num +
                    ", name='" + name + '\'' +
                    '}';
        }
    }

2、生产者

    public class SixThread1 implements Runnable {
 
        private Goodes mGoodes;
 
        public Goodes getGoodes() {
            return mGoodes;
        }
 
        public void setGoodes(Goodes goodes) {
            this.mGoodes = goodes;
        }
 
        @Override
        public void run() {
            //模拟生产30个商品
            for (int i = 0; i < 30; i++) {
                //使用synchronized加锁,以保证次序
                synchronized (mGoodes) {
                    if (mGoodes.num > 0) {
                        try {
                            //如果有商品就不进行生产,本次商品生产进入等待池
                            mGoodes.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    //没有商品这进行生产
                    mGoodes.name = "android进阶之光";
                    mGoodes.num = mGoodes.num + 1;
                    Log.e("TAG", "生产了" + mGoodes.name + "---------" + mGoodes.num);
                    //本次商品生产完毕,唤醒消费者线程开始消费
                    mGoodes.notify();
                }
            }
        }
    }

3、消费者

    public class SixThread2 implements Runnable {
 
        private Goodes mGoodes;
 
        public Goodes getGoodes() {
            return mGoodes;
        }
 
        public void setGoodes(Goodes goodes) {
            this.mGoodes = goodes;
        }
 
        @Override
        public void run() {
            //模拟消费30个商品
            for (int i = 0; i < 30; i++) {
                //加锁保证每次只能走一个消费流程
                synchronized (mGoodes) {
                    if (mGoodes.num <= 0) {
                        try {
                            //如果本次没有东西消费了进入等待池,等待生产
                            mGoodes.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    //模拟耗时,生产和消费不同步
                    SystemClock.sleep(1000);
                    Log.e("TAG", "取走了" + mGoodes.name + "---------" + mGoodes.num);
                    //消费了一个goodes
                    mGoodes.num = mGoodes.num - 1;
                    //唤醒生产者线程开始生产
                    mGoodes.notify();
                }
            }
        }
    }

这里生产者和消费者必须是同一个Goodes对象,以保证,生产和消费的同一个。同时两个线程的对象锁也应该保持一致,否则出现异常。

结果:

可能有点复杂,重新举例说明下。

举例说明:比如多线程操作一个全局变量
private int count = 100;    private boolean isRunning = false;    private void test1() {
        isRunning=true;
        SyncThread syncThread1 = new SyncThread("线程一");
        SyncThread syncThread2 = new SyncThread("线程二");
        SyncThread syncThread3 = new SyncThread("线程三");

        syncThread1.start();
        syncThread2.start();
        syncThread3.start();

    }    /**
     * 继承Thread方式     */
    private class SyncThread extends Thread {

        SyncThread(String name) {            super(name);
        }

        @Override        public void run() {            while (isRunning) {
                count();
            }
        }
    }
未加同步的count函数
private void count() {        if (count > 0) {
            Log.e(TAG, Thread.currentThread().getName() + "--->" + count--);
        } else {
            isRunning = false;
        }
    }

执行结果:仔细观察会发现有数据错乱的现象

添加同步的count函数

private void count() {        synchronized (this) {            if (count > 0) {
                Log.e(TAG, Thread.currentThread().getName() + "--->" + count--);
            } else {
                isRunning = false;
            }
        }
    }

线程同步的几种方式

同样还是以上面的为例

①同步函数

private synchronized void count() {        if (count > 0) {
            Log.e(TAG, Thread.currentThread().getName() + "--->" + count--);
        } else {
            isRunning = false;
        }
    }

②同步代码块 
private void count() {        synchronized (this) {            if (count > 0) {
                Log.e(TAG, Thread.currentThread().getName() + "--->" + count--);
            } else {
                isRunning = false;
            }
        }
    }

③使用特殊域变量(volatile)实现线程同步
private volatile int count = 1000;

       a.volatile关键字为域变量的访问提供了一种免锁机制, 
  b.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新, 
  c.因此每次使用该域就要重新计算,而不是使用寄存器中的值 
  d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量

④使用重入锁实现线程同步

  ReentrantLock() : 创建一个ReentrantLock实例 

  lock() : 获得锁 

  unlock() : 释放锁 

private void count() {
        lock.lock();        if (count > 0) {
            Log.e(TAG, Thread.currentThread().getName() + "--->" + count--);
        } else {
            isRunning = false;
        }
        lock.unlock();
    }

四、阻塞队列

       在新增的Concurrent包中,队列阻塞很好的解决了多线程中如何高效安全传输数据的问题,通过队列可以很容易实现数据共享,比如上述的生产者和消费者模型,就不需要使用wait和nofity在生产者和消费者之间通信了,从而简化了很多。

(1)BlockingQueue的核心方法:

    ①offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.(本方法不阻塞当前执行方法的线程);      
    ②offer(E o, long timeout, TimeUnit unit):可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败。
    ③put(anObject):把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续.
    ④poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null;
    ⑤poll(long timeout, TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则知道时间超时还没有数据可取,返回失败。
    ⑥take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入;

    ⑦drainTo():一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。

(2)常见的阻塞队列

①ArrayBlockingQueue

他是用数组实现的有界的阻塞队列,,按照先进先出的原则对元素进行排序,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,同时ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。默认情况下不保证线程公平地访问队列。

②LinkedBlockingQueue

    基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回,只有当队列缓冲区达到最大值缓存容量时才阻塞;当消费者从队列中消费掉一份数据,生产者线程会被唤醒。如果构造一个LinkedBlockingQueue而没有指定容量的大小,他会默认采取Integer的最大值,这样一来肯能没有扥到队列阻塞产生就产生OOM了。一般情况下才处理多线程生产者消费者问题的时候使用这两个类足够了,其他的类不再介绍。

(3)队列阻塞案例 —— 生产者消费者模式

    在上面使用了同步方法来实现生产者消费者模式,现在使用队列阻塞来实现。

①生产者
 

    public class SevenThread1 implements Runnable {
 
        private ArrayBlockingQueue<Goodes> mQueue;
 
        public void setGueue(ArrayBlockingQueue<Goodes> queue) {
            this.mQueue = queue;
        }
 
        @Override
        public void run() {
            //模拟生产5个商品
            for (int i = 0; i < 5; i++) {
                Goodes goodes = new Goodes();
                goodes.name = "android进阶之光" + (i + 1);
                goodes.num = goodes.num + 1;
                try {
                    //将商品加入队列
                    mQueue.put(goodes);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.e("TAG", "生产者队列里面目前有:" + mQueue.size() + "个数据,为:" + goodes.toString());
            }
        }
    }

②消费者

    public class SevenThread2 implements Runnable {
 
        private ArrayBlockingQueue<Goodes> mQueue;
 
        public void setGueue(ArrayBlockingQueue<Goodes> queue) {
            this.mQueue = queue;
        }
 
        @Override
        public void run() {
            while (true) {
                //模拟耗时,生产和消费不同步
                SystemClock.sleep(1000);
                Goodes take = null;
                try {
                    //从队列里面取出商品,按照先存先取的顺序
                    take = mQueue.take();
                    Log.e("TAG", "消费者队列里面目前有:" + mQueue.size() + "个数据,为:" + take.toString());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

测试:

    public void blockingQueue(View view) {
        ArrayBlockingQueue<Goodes> queue = new ArrayBlockingQueue<>(10, false);
 
        //生产者生产商品
        SevenThread1 sevenThread1 = new SevenThread1();
        sevenThread1.setGueue(queue);
        //消费者1取走商品
        SevenThread2 sevenThread2 = new SevenThread2();
        sevenThread2.setGueue(queue);
 
        new Thread(sevenThread1).start();
        new Thread(sevenThread2).start();
    }

结果:


五、线程池

new Thread的弊端

执行异步任务只是如下new Thread吗?

new Thread(new Runnable() {
    @Override
    public void run() {
        // TODO Auto-generated method stub
    }
}).start();

那就太out了,new Thread的弊端如下:

1)每次通过new Thread()创建对象性能不佳。

2)线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。

3)缺乏更多功能,如定时执行、定期执行、线程中断。

相比new Thread,Java提供的四种线程池的好处在于: 

1)重用存在的线程,减少对象创建、消亡的开销,提升性能。

2)可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。

3)提供定时执行、定期执行、单线程、并发数控制等功能。

Java 线程池

Java通过Executors提供四种线程池,分别为: 

newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 

newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 

newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。 

newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。下面这张图完整描述了线程池的类体系结构:

1. newCachedThreadPool

创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。

public static ExecutorService newCachedThreadPool()

示例代码:

public class ThreadPoolExecutorTest {
   public static void main(String[] args ) {
    ExecutorService cacheThreadPool =Executors.newCachedThreadPool();
     for(int i =1;i<=5;i++){
       final int index=i ;
       try{
        Thread.sleep(1000);
      }catch(InterruptedException e ) {
         e.printStackTrace();
      }
       cacheThreadPool.execute(new Runnable(){
         @Override
         public void run() {
          System.out.println("第" +index +"个线程" +Thread.currentThread().getName());  
        }  
      });
    }
  }
}

//输出结果

第1个线程pool-1-thread-1

第2个线程pool-1-thread-1

第3个线程pool-1-thread-1

第4个线程pool-1-thread-1 第5个线程pool-1-thread-1  

由结果可看出 当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。

 

2. newFixedThreadPool

创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。

public static ExecutorService newFixedThreadPool(int nThreads)

nThreads - 池中的线程数

示例代码:

public class ThreadPoolExecutorTest {
   public static void main(String[] args) {
    ExecutorService fixedThreadPool =Executors. newFixedThreadPool(3);
     for (int i =1; i<=5;i++){
       final int index=i ;
       fixedThreadPool.execute(new Runnable(){
         @Override
         public void run() {
           try {
            System.out.println("第" +index + "个线程" +Thread.currentThread().getName());
            Thread.sleep(1000);
          } catch(InterruptedException e ) {
             e .printStackTrace();
          }
        }
 
      });
    }
  }
}

由于设置最大线程数为3,所以在输出三个数后等待2秒后才继续输出。

2. newScheduledThreadPool

创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

corePoolSize - 池中所保存的线程数,即使线程是空闲的也包括在内。

延迟执行示例代码:

public class ThreadPoolExecutorTest {  
  public static void main(String[] args) {
    ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);   
    scheduledThreadPool.schedule(newRunnable(){     
      @Override
      public void run() {
       System.out.println("延迟三秒");
       }
   }, 3, TimeUnit.SECONDS);
  }
}

表示延迟3秒执行。

定期执行示例代码:

public class ThreadPoolExecutorTest {  
  public static void main(String[] args) {
 
    ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);    
  scheduledThreadPool.scheduleAtFixedRate(newRunnable(){    
    @Override      
    public void run() {
       System.out.println("延迟1秒后每三秒执行一次");
     }
   },1,3,TimeUnit.SECONDS);
 }
 
}

表示延迟1秒后每3秒执行一次。

4.newSingleThreadExecutor

创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。(注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程将代替它执行后续的任务)。可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。与其他等效的 newFixedThreadPool(1)不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。

public static ExecutorService newSingleThreadExecutor()

示例代码:

public class ThreadPoolExecutorTest {  
  public static void main(String[] args) {
    ExecutorService singleThreadPool= Executors.newSingleThreadExecutor();    
    for(int i=1;i<=5;i++){      
      int index=i;      
    singleThreadPool.execute(new Runnable(){
       @Override
       public void run() {         
        try{
         System.out.println("第"+index+"个线程");
        Thread.sleep(2000);
         }catch(InterruptedException e) {            
          e.printStackTrace();
        }
      } });
    }
  }
}

最后欢迎关注我的公众号,互相学习探讨,谢谢!

android小咖秀

  • 1
    点赞
  • 1
    评论
  • 3
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页

打赏作者

peihp_

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值