学而实习之 不亦乐乎

Android 多线程:Thread理解和使用总结

2022-07-05 11:49:13

线程,可以看作是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。

一、Android 中的 Thread

1.1 Thread主要函数

run() 线程运行时所执行的代码
start() 启动线程
sleep()/sleep(long millis) 线程休眠,进入阻塞状态,sleep方法不会释放锁(其它线程不会进入synchronized方法体或方法块,不释放锁需要try/catch)
yield() 线程交出CPU,但是不会阻塞而是重置为就绪状态,不会释放锁
join()/join(long millis)/join(long millis,int nanoseconds) 线程插队,当该子线程执行完毕后接着执行其它
wait() 进入阻塞状态,释放锁(其它线程可以进入synchronized方法体或方法块,释放锁不需要try/catch)
interrupt() 中断线程,注意只能中断阻塞状态的线程
getId() 获取当前线程的id
getName()/setName() 获取和设置线程的name
getPriority()/setPriority() 获取和设置线程的优先级,范围1-10,默认是5
setDaemon()/isDaemo() 设置和获取是否守护线程
currentThread() 静态函数获取当前线程

1.2 Thread的几种状态

新建状态(new):实例化之后进入该状态;
就绪状态(Runnable):线程调用start()之后就绪状态等待cpu执行,注意这时只是表示可以运行并不代表已经运行;
运行状态(Running):线程获得cpu的执行,开始执行run()方法的代码;
阻塞状态(Blocked):线程由于各种原因进入阻塞状态:join()、sleep()、wait()、等待触发条件、等待由别的线程占用的锁;
死亡状态(Dead):线程运行完毕或异常退出,可使用isAlive()获取状态。

二、Android中Thread的使用

这里写出三种使用方式:

1. 继承Thread,重写run()方法。

使用时直接new并且start()。(不知道什么时候run()方法变成重写了,以前可是没有 @Override)

public class MyThread extends Thread{
    @Override
        public void run() {
            super.run();
            // do something
        }
    }

    // Thread使用
    public void goThread(){
        new MyThread().start();
    }
}

2. 实现Runnable,重写run()方法来执行任务。

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        // do something
    }
}

new Thread(new MyRunnable()).start();

另一种启动方式

new Thread(new Runnable() {
    @Override
    public void run() {
        ... ...
    }
}).start();

 3. 通过Handler启动线程。

首先定义好Handler和Runnable :

private int count = 0;
private Handler mHandler = new Handler();
private Runnable runnable = new Runnable() {
	@Override
	public void run() {
		Log.i("download",Thread.currentThread().getName()+":"+count);
		count ++;
		mHandler.postDelayed(runnable,1000); // 执行后延迟1000毫秒再次执行,count已++
	}
};

然后使用mHandler的post()方法执行线程。当上方的Runnable执行后里面定义了mHandler.postDelayed(runnable,1000);开启延迟1000毫秒后再次执行。

findViewById(R.id.btn_download).setOnClickListener(new View.OnClickListener() {
	@Override
	public void onClick(View v) {
		mHandler.post(runnable); // handler运行runnable
	}
});

三、Thread使用其它问题

1.如何终止线程

(1)使用boolean变量作为标记

如下,如果变量stop为true则跳出run()方法。关键字volatile表示这个变量随时都有可能发生变化,主要是表示同步,也就是同一时刻只能由一个线程来修改该变量的值。

public class ThreadStopTest extends Thread {
    public volatile boolean stop = false;
    @Override
    public void run() {
        super.run();
        while (!stop){
            // thread runing
        }
    }   
}

(2) 使用 interrupt()和isInterrupted()的区别

两个方法都是判断当前线程的中断标志位是否被置位,但调用interrupted()方法后,中断标志位将会重置,而isInterrupted()不会被重置。

interrupt()方法可以用来中断进程,然后就可以终止线程,使用该方法分两种情况,线程正常运行状态和阻塞状态。

我们一般会使用 interrupt() 方法中断线程的执行,但该方法并不会中断线程,它的作用只是设置一个中断标志位,我们还得在run()方法中判断这个标志位,并决定是否继续执行,通过这样的方式来达到中断的效果。 但该方法根据线程状态的不同,会有不同的结果。结果如下:
当线程由于调用wait()、join()、sleep()而阻塞时,中断标志位将会被清空,并接收到InterruptedException异常。
当线程由于正在进行InterruptibleChannel类型的I/O操作而阻塞时,中断标志位将会置位,并接收到ClosedByInterruptException异常(I/O流也会自动关闭)
当线程由于进行Selector操作而阻塞时,中断标志位将会置位,但不会接收到异常

[1] 线程正常运行状态
要注意的是interrupt()方法只是中断线程而不是结束线程,在线程正常运行状态下使用该方法是不能结束线程的,正确的做法是判断线程是否被中断来决定是否执行代码。这种方法类似于自定义标记。看一下示例:

... ...
// 定义开始和结束线程的方法,与按钮绑定
public void goThread(){
	if(null == myThread){
		myThread = new MyThread();
	}
	myThread.start();
}

private void stopThread() {
	if(null != myThread && myThread.isAlive()){
		myThread.interrupt();
		myThread = null;
	}
}

public class MyThread extends Thread{
	@Override
	public void run() {
		super.run();
		int i = 0;
		// 判断状态,如果被打断则跳出并将线程置空
		while (!isInterrupted()){
			i++;
			Log.i("thread",Thread.currentThread().getName()+":Running()_Count:"+i);
		}
	}
}

[2] 线程阻塞状态

先看下面的例子,重新写了一个SleepThread,开启和停止的代码省略。这个线程里每间隔1s循环增加i的值并打印。

public class SleepThread extends Thread{
	@Override
	public void run() {
		super.run();
		int i = 0;
		while(true){
			try {
				i++;
				Thread.sleep(1000);
				Log.i("thread",Thread.currentThread().getName()+":Running()_Count:"+i);
			} catch (InterruptedException e) {
				e.printStackTrace();
				Log.i("thread",Thread.currentThread().getName()+"异常抛出,停止线程");
				break;
			}
		}
	}
}


当我开始和终止该线程后Log如下:

10-14 19:46:57.562 32024-32227/com.sky.androidthreadapp I/thread: Thread-16142:Running()_Count:1
10-14 19:46:58.562 32024-32227/com.sky.androidthreadapp I/thread: Thread-16142:Running()_Count:2
...
10-14 19:47:01.564 32024-32227/com.sky.androidthreadapp I/thread: Thread-16142:Running()_Count:5
10-14 19:47:01.931 32024-32227/com.sky.androidthreadapp I/thread: Thread-16142异常抛出,停止线程
可以看到调用interrupt()方法后抛出InterruptedException异常,同时break跳出循环达到停止跑代码的作用。原理是当线程进入阻塞状态时,调用interrupt()方法会抛出异常,利用这个异常来跳出循环。

[3] 两种状态一起处理

此外,把两种状态的处理结合在一起是比较好的,这样既可以及时判断线程状态又可以捕获异常来跳出循环。

public class SleepThread extends Thread{
	@Override
	public void run() {
		super.run();
		int i = 0;
		while(!isInterrupted()){  // 判断线程是否被打断
			try {
				i++;
				Thread.sleep(1000);
				Log.i("thread",Thread.currentThread().getName()+":Running()_Count:"+i);
			} catch (InterruptedException e) {
				e.printStackTrace();
				Log.i("thread",Thread.currentThread().getName()+"异常抛出,停止线程");
				break;// 抛出异常跳出循环
			}
		}
	}
}


(3) 使用stop()方法终止线程

这种方法慎用!

(4) join()和sleep():两个方法都会让线程暂停执行

join()方法是让出执行资源(如:CPU时间片),使得其它线程可以获得执行的资源。所以调用join()方法会使进入阻塞状态,该线程被唤醒后会进入runable状态,等待下一个时间片的到来才能再次执行。
sleep()不会让出资源,只是处于睡眠状态(类似只执行空操作)。调用sleep()方法会使进入等待状态,当等待时间到后,如果还在时间片内,则直接进入运行状态,否则进入runable状态,等待下个时间片。

2. 线程安全与线程同步

(1) 什么是线程安全问题

简单地说,线程安全问题是指多个线程访问同一代码或数据,造成结果和数据的错乱或与期望的结果不同所产生的问题。

(2) 如何解决线程安全问题

基本上所有的并发模式在解决线程安全问题的问题上,都采用“序列化访问临界资源”的方案,即在同一时刻只能有一个线程访问临界资源(多个线程可能同时访问的数据或资源),也称同步互斥访问。

[1] synchronized 关键字,保证同时刻只有一个线程进入该方法或者代码块,使用方式(Tips:java中有很多方式来实现线程同步,我们常用的synchronized是效率最低的...但是它方便啊):

线程run()方法中要执行的代码方法添加synchronized关键字,注意要添加在方法的返回值前。

int count = 100;
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;
		}
	}
}

[2]特殊域变量volatile修饰变量:告诉虚拟机该变量随时可能更新,因此使用时每次都会重新计算,而不是使用寄存器的值。volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。(不能完全保证线程安全)

private volatile int count = 1000;

[3]使用重入锁实现线程同步。

ReentrantLock() : 创建一个ReentrantLock实例
lock() :获得锁
unlock() : 释放锁

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

[4] ThreadLocal

管理变量。如果一个变量使用ThreadLocal进行管理,每一个使用该变量的线程都会获得该变量的副本,副本之间相互独立,所以每个线程都可以修改变量而不会对其它线程造成影响。

private static ThreadLocal<Integer> number = new ThreadLocal<Integer>(){
	// 重写方法,设置默认值
	@Override
	protected Integer initialValue() {
		return 1;
	}
	// 自定义方法设置变量值
	public void saveNumber(int newNumber){
		number.set(number.get() + newNumber);
	}
	// 自定义方法获取变量值
	public int getNumber(){
		return number.get();
	}
};