Android多线程
一般情况下,Android UI线程和主线程是一回事,文章的以后的内容中我都会用主线程代表UI线程。主线城主要是运行和UI页面相关的业务逻辑,动画绘制,页面布局以及响应屏幕的触摸事件。Android为了保证60fps,也就是每秒60帧的屏幕刷新率,每一帧的平均时间为16.67ms,这是一个相当短的时间。我们就必须保证主线程禁止执行耗时操作,比如说网络请求,数据库的读写,以及耗CPU的数据计算操作,否则引起页面刷新应该分得的时间被占用,导致丢帧表现出卡顿,甚至出现ANR(Application Not Response),导致用户体验欠佳,用户会关闭应用甚至卸载应用。
内存模型
先了解一下JMM(Java Memory Model)Java内存模型。我只讲和线程安全相关的内存模型,线程共用的部分是Heap,线程独占的部分是线程的栈帧,如下图所示:
VM创建线程之后,从Heap中读数据,然后对数据进行操作然后把数据写回Heap。
线程安全产生的原因
线程安全主要是数据安全。数据安全也就是数据的一致性,数据存在Heap中,同时线程也会从Heap中读取一份,多个线程对同一份数据进行读写,数据会有多份数据。如何保证保证多份数据的一致性,会是一个很有挑战性的问题。
线程安全的方法一(不可变)
数据不可变是能够保证线程安全的方法之一。例如像java.lang.String设计的那样,类被设计成public final(不能被继承), 类里面的相关属性设计成private final(属性不可变)。
线程安全的方法二(独占不分享)
如果数据只在线程内使用,不与Heap分享,这也能保证数据安全。ThreadLocal第一眼很容易让人误以为这是一个Thread,其实并不是,它是在JDK 1.2中引入,为每个线程提供一个独立的本地变量副本,用来解决变量并发访问的冲突问题。所有的线程可以共享一个ThreadLocal对象,但是每一个线程只能访问自己所存储的变量,线程之间互不影响。 例如,下面的类为每个线程生成唯一的本地标识符。线程的 ID 在第一次调用 ThreadId.get() 时分配,并在后续调用中保持不变。 示例代码如下:
java
import java. util. concurrent. atomic. AtomicInteger;
public class ThreadId {
// Atomic integer containing the next thread ID to be assigned
private static final AtomicInteger nextId = new AtomicInteger(0);
// Thread local variable containing each thread's ID
private static final ThreadLocal<Integer> threadId =
new ThreadLocal<Integer>() {
@Override protected Integer initialValue() {
return nextId. getAndIncrement();
}
};
// Returns the current thread's unique ID, assigning it if necessary
public static int get() {
return threadId. get();
}
}
线程安全的方法三(CPU cache无效)
volatile关键字主要有两个作用。1.内存可见性:volatile关键字确保当一个线程修改了该变量的值,其他线程能立即感知到这种变化。这是因为volatile变量在写操作时会立即同步到主内存中,而在读操作时,其他线程会从主内存中读取最新的值。这种机制保证了多线程环境下变量的值对所有线程都是可见的。2.禁止指令重排序:volatile关键字还确保变量的读写操作不会因为编译器或CPU的优化而被重排序。这意味着,即使编译器或CPU试图优化代码执行顺序,volatile变量的读写操作也会保持原有的顺序,从而避免了因指令重排序导致的并发问题。volatile修饰属性就是使用作用1,这样就能让CPU缓存cache中的数据无效,每次读数据都是从Heap读取;写数据时候,直接写到Heap。CPU cache读写速度是主内存千百倍的速度差异。
线程安全的方法三(synchronized)
Java中内置了语言级的同步原语--synchronized。Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。synchronized锁独占,互斥,请求并保持直到方法执行完毕。synchronized有两种类型的锁,一种是对象锁,另一种是类锁。对象锁的示例代码如下:
java
/**
* Returns the number of keys in this hashtable.
*
* @return the number of keys in this hashtable.
*/
public synchronized int size() {
return count;
}
/**
* Tests if this hashtable maps no keys to values.
*
* @return {@code true} if this hashtable maps no keys to values;
* {@code false} otherwise.
*/
public synchronized boolean isEmpty() {
return count == 0;
}
上述的代码是java.util.HashTable中的两个方法,两个方法都用synchronized关键字修饰,代表是对象锁。如果同一个对象在两个不同的线程A和B中同时执行size方法和isEmpty方法,这样就不能同时执行。比如线程A执行方法的时候拿到了对象锁,线程B就需要等到线程A释放对象锁,获取对象锁才能执行。但是synchronized锁是可重入的,线程A执行方法的时候获取到了对象B的对象锁,再调用对象B的相应的synchronized对象方法时候能够直接获取相应的对象锁,并执行相应的方法。
线程安全的方法四(Lock)
需要通过显式创建Lock对象,并调用lock()和unlock()方法来加锁和解锁,通常放在 finally块中以确保无论发生何种情况都能释放锁避免死锁。Lock提供了更大的灵活性,支持可中断的加锁lockInterruptibly(),尝试获取锁tryLock()和设置超时等操作。示例代码如下所示:
java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final Lock lock = new ReentrantLock();
private int counter = 0;
public void increment() {
lock.lock();
try {
counter++;
} finally {
lock.unlock();
}
}
}
总结
Android设备中CPU和内存资源是相对比较有限的,创建一个进程资源消耗的内存大概是2M,创建一个线程消耗内存大概是64K。创建线程的数量应该平衡,创建线程过多,线程之间切换次数过多导致效率低下;创建线程过少,线程不能跑满CPU,导致CPU不能物尽其用。多线程中的线程安全是一个很重要的话题。希望文章对您有所帮助,如有错误,请不吝指出。