笔记-Android中的线程使用
目录
Java中的线程
- Java中如何创建线程
- Java中的线程同步问题(synchronized关键字,lock, wait,notify,notifyall)
- Java中保证成员变量访问的同步和原子操作
- Java中如何终止线程
Android中的线程
- 线程间的通信Handler,Looper,MessageQueue
- ThreadLocal
- Asynctask引起内存泄漏
下面开始正文
Java中的线程
提起android中去使用线程,我们首先必须搞懂java中线程的一些基本概念.
1, Java中如何创建线程
在java中创建线程的几种方式
第一种: 直接new Thread
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.print("执行线程");
}
});
thread.start();
第二种: 线程工厂
ThreadFactory factory = new ThreadFactory() {
@Override
public Thread newThread(@NonNull Runnable r) {
return new Thread(r, "线程名字");
}
};
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " started!");
}
};
Thread thread1 = factory.newThread(runnable);
thread1.start();
第三种: 线程池
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Thread with Runnable started!");
}
};
Executor executor = Executors.newCachedThreadPool();
executor.execute(runnable);
2,Java中的线程同步问题
在使用线程过程,难免会遇到线程同步的问题,首先必须清楚线程不同步是如何产生的,然后再来看看解决方法.
下面看一段示例代码,它会出现一个情况就是线程不同步.
public static class ThreadTest{
private static int x,y;
public static void startThread1(){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100_00_00; i++){
x = i;
y = i;
}
if (x != y){
System.out.println("x != y x = "+x+" y = "+y);
}
}
});
thread.start();
}
public static void startThread2(){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100_00_00; i++){
x = i;
y = i;
}
if (x != y){
System.out.println("x != y x = "+x+" y = "+y);
}
}
});
thread.start();
}
}
我们运行startThread1()和startThread2()会输出
x != y x = 394076 y = 396613
这是因为出现了线程的不同步才产生的异常现象, 这是在线程1运行过程中,线程2也在运行导致多线程操作x,y,从而x y 不相等.
为了避免线程的不同步,java中引入了synchronized关键字. 下面对需要线程同步的代码块加入synchronized关键字.
public static class ThreadTest{
private static int x,y;
public static void startThread1(){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (ThreadDemoActivity.class){
for (int i = 0; i < 100_00_00; i++){
x = i;
y = i;
}
if (x != y){
System.out.println("x != y x = "+x+" y = "+y);
}
}
}
});
thread.start();
}
public static void startThread2(){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (ThreadDemoActivity.class){
for (int i = 0; i < 100_00_00; i++){
x = i;
y = i;
}
if (x != y){
System.out.println("x != y x = "+x+" y = "+y);
}
}
}
});
thread.start();
}
}
这下就不会有前面提到的线程不同步的问题了. synchronized关键字有几种不同的使用方法
synchronized关键字加到方法前面
public synchronized void demo(){
//...
}
synchronized定义代码块
private Object lock = new Object();
public void demo(){
synchronized (lock){
//...
}
}
synchronized都会去关联到一个锁对象,前者加到方法前面的synchronized,它的锁对象是类的对象,后者则是使用自定义的锁对象,前者更加方便,但是它只能指定一个类对象作为锁对象,后者更加灵活自由,可以定义同步代码块,自定义锁对象(就像刚开始的示例代码中使用了ThreadDemoActivity.class作为锁对象,当然也可以自定义一个对象作为锁对象)
synchronized除了解决上面提到的线程互斥访问问题,还会会解决另外一个问题数据的同步问题. 当我们在代码中有一个成员变量
x = 1
一个线程a要对x = 5赋值,它需要分三个步骤,一个是拷贝x成员变量到它线程所属的内存区域,二是把x的值赋成5,三是把x = 5放回原有的内存区域. 因为这样做会提高效率。synchronized在解决线程间同步问题的时候也顺带解决了这个数据的同步问题。
所以总结一下:
synchronized解决了两个问题,问题一数据访问的互斥,问题二是数据的同步问题
为了解决线程间的同步问题,除了使用synchronized关键字之外,还可以使用lock来解决,不过它用起来会比较麻烦。通常的代码格式是:
private Lock lock = new ReentrantLock();
public void demo(){
lock.lock();
//同步代码块
lock.unlock();
}
用得比较常见的地方的读写锁,在一个线程写的时候不允许别的线程写和读,但是在一个线程读的情况下,运行别的线程读,但是不能写。
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
private ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
private int x = 1;
public void write(){
writeLock.lock();
x = 5;
writeLock.unlock();
}
public void print(){
readLock.lock();
System.out.print("x = "+x);
readLock.unlock();
}
关于wait, notify和notifyAll的使用.
在程序设计中,我们会有一个线程必须满足一个条件才能继续执行的需求,而这个条件是另外一个线程去达成的,可以看下下面代码
private String shareMsg = null;
private synchronized void initshareMsg(){
shareMsg = "share msg";
}
private synchronized void printshareMsg(){
while (shareMsg != null){
System.out.print(shareMsg);
}
}
public void run(){
Thread thread_1 = new Thread(() -> {
initshareMsg();
});
Thread thread_2 = new Thread(() -> {
printshareMsg();
});
thread_2.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread_1.start();
}
很显然上面的代码会出现死锁的现象,printshareMsg会拿到锁一直循环下去,initshareMsg永远拿不到monitor,所以不能执行, 我们可以通过wait和notify的配合使用来达到我们想要的效果. 修改后代码如下.
private String shareMsg = null;
private synchronized void initshareMsg(){
shareMsg = "share msg";
notify(); //放弃monitor,通知之前wait的线程,继续执行
}
private synchronized void printshareMsg(){
while (shareMsg == null){
try {
wait(); //等待,放弃monitor,让别的线程获得monitor
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(shareMsg);
}
}
public void run(){
Thread thread_1 = new Thread(() -> {
initshareMsg();
});
Thread thread_2 = new Thread(() -> {
printshareMsg();
});
thread_2.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread_1.start();
}
那么notifyall又有什么用?如果有多个线程都处于wait状态,那么仅仅调用notify只能唤醒一个线程,而notifyall可以唤醒所有处于wait的线程来竞争monitor.
3, Java中保证成员变量访问的同步和原子操作
关键字:volatile, Atomic相关类
上面提到了,synchronized解决了数据访问的互斥和数据的同步问题,但是单独对一个成员变量来说不能加synchronized关键字,而我们仅仅对一个成员变量做数据访问的互斥和数据的同步加上synchronized关键字又会感觉太麻烦,目前就有一个很好解决这个问题的方法是使用
volatile关键字和Atomic相关类,它们的作用容易混淆.
volatile主要用来解决的是数据的同步问题
image.png
如上图,未加volatile的情况下线程b获取到x的值可能出现x = 1的情况,加上volatile关键字就可以避免这个问题.
Atomic相关类主要解决的问题是像a++这样的操作,a++这个操作其实分成了两步,一步是r = a+1, 第二步是 a = r. 这显然不是一个原子操作,使用AtomicInteger则把a++封装成为一个原子操作.
AtomicInteger a = new AtomicInteger();
a.incrementAndGet();
a++不能靠volatile保证原子操作,volatile解决的是同步问题Atomic相关的类解决的原子操作问题.
4, Java中如何终止线程
通常我们开启一个线程的后,会有终止一个线程的需求,那么在java中是如何去终止线程呢?可以通过调用
thread.stop()
它可以立即终止线程,但是它有个不好的地方是在于,立即终止是存在一定的风险,因为线程正常执行过程中立即终止会导致程序出现异常。所以stop这个方法是被弃用的,而正规终止线程的方式是使用interrupt去终止线程.
Thread thread = new Thread(){
@Override
public void run() {
super.run();
for (int i = 0; i < 100; i++){
if (isInterrupted()){
//线程终止,收尾操作
}
}
}
};
thread.interrupt();
通过isInterrupted判断外界是否调用了线程终止,从而进行一些收尾操作后终止线程,这样显然更加安全. 另外Thread还有一个判断终止的接口是Thread.interrupt(),它和isInterrupted()的区别是Thread.interrupt()调用后会把中断标志位重置,意思就是Thread.interrupt()第一次调用后是true,第二次调用后就是false了.
另外我们在使用Thread.sleep通常会抛出InterruptedException
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
这个InterruptedException,就是在线程休眠过程中调用interrupt就会抛出该异常.
Android中的线程
1,线程间的通信Handler,Looper,MessageQueue
Looper
其实Android中线程和java线程区别就是,Android提供了一种创建无限循环线程的模式, 就是looper机制,我们来看看如何在java中去创建无限循环的线程.
private void run(){ //执行入口
CustomizableThread customizableThread = new CustomizableThread();
//设置任务
customizableThread.setTask(new Runnable() {
@Override
public void run() {
System.out.print("执行任务");
}
});
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//退出循环
customizableThread.quit();
}
/**
* 创建一个无限循环的线程
*/
class CustomizableThread extends Thread {
private Runnable task;
private boolean quit;
synchronized void setTask(Runnable task){
this.task = task;
}
synchronized void quit(){
quit = true;
}
@Override
public void run() {
super.run();
while (!quit){
synchronized (this){
if (task != null){
task.run();
task = null;
}
}
}
}
}
上面这种无限循环的线程就是Android经常提到的Looper的原型了. 在Android中它把上面提到的无限循环的机制写成了一个 Looper对象,大致如下(当然实际代码肯定比这个复杂很多,这里只是为了说明Looper到底起到了什么作用)
class Looper {
private Runnable task;
private boolean quit;
synchronized void setTask(Runnable task){
this.task = task;
}
synchronized void quit(){
quit = true;
}
public void loop(){
while (!quit){
synchronized (this){
if (task != null){
task.run();
task = null;
}
}
}
}
}
另外looper会把这个task做成一个队列的形式,那就是MessageQueue了,那么Handler又是起到什么作用呢,Handler它其实一个关联一个looper的对象,用于像looper中MessageQueue发送消息,大致的模型如下:
image.png
ThreadLocal
接下来说下ThreadLocal,ThreadLocal是用来存放线程独立的对象,什么是线程独立的线程对象,我们知道线程之间是可以共享内存的,
public class ThreadLocalDemo {
private Integer mInteger = new Integer(1); //线程之间是共享的.
private void run(){
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
mInteger = 2;
}
});
thread1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.print(mInteger);
}
});
thread2.start();
}
}
那么如果使用ThreadLocal的话就可以做到内存的独立。
public class ThreadLocalDemo {
private Integer mInteger = new Integer(1);
static ThreadLocal<Integer> sThreadLocal1 = new ThreadLocal<>();
static ThreadLocal<Integer> sThreadLocal2 = new ThreadLocal<>();
public void run(){
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
sThreadLocal1.set(mInteger);
Integer integer = sThreadLocal1.get();
integer = 3;
System.out.println("ThreadLocal thread1 integer "+integer);
}
});
thread1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
sThreadLocal2.set(mInteger);
Integer integer = sThreadLocal2.get();
System.out.println("ThreadLocal thread2 integer "+integer);
}
});
thread2.start();
}
}
I/System.out: ThreadLocal thread1 integer 3
I/System.out: ThreadLocal thread2 integer 1
上面结果输出可以说明thread1对integer的赋值只对thread1生效.
实际上在android中looper就是用ThreadLocal进行存放的,这样可以做到每个线程之间looper是独立的.
public final class Looper {
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
//其余代码省略...
}
Asynctask引起内存泄漏
什么是内存泄漏,内存泄漏是GC Root引用链中有无用的对象。这里的GC Root有3种
1,正在运行的线程
2,static变量
3,native引用到的
Asynctask实际上是后台启动线程,返回到ui线程的一个过程,它常常会引用到外部的Activity引用,导致Activity的内存泄漏,但是这是暂时的,通常情况下Asynctask不会长期引起内存泄漏.