在工作中经常会遇到需要做线程同步处理的场景,但由于一直对线程同步是一知半解,没有很系统的去了解过,于是最近终于踩到了由于synchronized同步应用不当导致app频繁ANR无响应的坑了,下面就这次踩坑来对synchronized线程同步来做一个较全面的了解,以此说明细节也加深印象。
我们先直接看发生anr的示意函数:
//Tab.java
/**
* 该方法在主线程中调用,并且使用了synchronized同步关键字,且处理的逻辑较多。
*/
public synchronized boolean goBackOrForward(OffsetToPosInfo info) {
//...
//处理页面向前和回退时,一些状态的维护和线场的保存等
//...
}
/**
* 该方法主要工作是在特有的子线程中生成截图Bitmap,以便Tab任务管理器中展示时获取。
*/
void updateCaptureFromBlob(byte[] blob) {
Bitmap capture = mCapture;
synchronized (Tab.this) { //注意同步代码块
Options ops = new Options();
ops.inMutable = true;
ops.inPreferredConfig = Bitmap.Config.RGB_565;
//此处为耗时操作
Bitmap bitmap = BitmapFactory.decodeByteArray(blob, 0,
blob.length, ops);
//... 其他逻辑
}
}
分析原因并重现问题:
分析anr的trace文件堆栈信息时发现当时一直阻塞在函数goBackOrForward()处且没有更多的堆栈信息,但有如下提示:awaiting to lock <0x08308eb5> held by thread 54。结合函数一看便知是当前线程(主线程)在等待其他线程(子线程)对synchronized锁的释放,于是顺着找到了tid=54的子线程,并查看堆栈信息于是找到了该线程正在执行函数updateCaptureFromBlob()的同步代码块,阻塞在BitmapFactory.decodeByteArray()处。
总结原因为:主线程的ui操作依赖子线程耗时函数中的synchronized同步锁,最终发生了ANR无响应。
注意:此处均为网上查找的资料,以备后续查找。
1、内置锁
synchronized关键字涉及到锁的概念,需要了解一些相关锁的知识。
java的内置锁:每个java对象都可以用做一个实现同步的锁,这些锁成为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。
java内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。
**java的对象锁和类锁:**java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,两个锁实际是有很大的区别的,对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。
2、synchronized关键字的基本介绍
synchronized可以修饰方法,也可以修饰一个代码块。
对象锁的使用:
/**
* synchronized修饰方法名的对象锁
*/
private synchronized void test1() {
//do something...
}
/**
* synchronized修饰代码块的对象锁
*/
private void test2() {
//do something1...
synchronized (this) {
//do something2...
}
//do something3...
}
类锁的使用:
/**
* synchronized修饰方法名的类锁(静态方法)
*/
private synchronized static void test3() {
//do something...
}
/**
* synchronized修饰代码块的类锁(非静态方法)
*/
private static void test4() {
//do something1...
synchronized (MainActivity.class) {
//do something2...
}
//do something3...
}
3、对象锁和类锁修饰的方法之间线程同步的测试
对于在对象锁相互之间和类锁相互之间进行线程同步我们很好理解,但是对于在对象锁和类锁之间进行线程同步时,会是什么样的结果,表示不能确定,于是写demo验证了一下:
public class Test {
/**
* 使用synchronized修饰代码块来实现对象锁
*/
public void test1() {
synchronized (this) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
try {
Thread.sleep(1000);
} catch (Exception e) {
}
}
}
}
/**
* 使用synchronized修饰静态方法来实现类锁
*/
public synchronized static void test2() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
try {
Thread.sleep(1000);
} catch (Exception e) {
}
}
}
public static void main(String arg[]){
final Test test = new Test();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
test.test1();
}
}, "test1");
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
test.test2();
}
}, "test2");
thread1.start();
thread2.start();
}
}
输出结果:
test1: 0
test2: 0
test1: 1
test2: 1
test1: 2
test2: 2
test1: 3
test2: 3
test1: 4
test2: 4
结果表明,线程1和线程2在交替执行。对象锁和类锁的修饰的函数间进行线程同步时并不能达到我们预期的同步要求。这证明了类锁和对象锁是两个不一样的锁,控制着不同的区域,它们互不干扰。线程获得对象锁的同时,也可以获得该类锁。
既然有了synchronized修饰方法,为什么还需要synchronized来修饰代码块呢?java这样如此设计肯定是为了避免或解决什么问题。
1、便于降低同步代码的粒度,以此来提高性能。举例:
public synchronized void test3() {
/**
* 需要同步处理的关键代码
*/
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
/**
* 这里特别耗时,但不要求同步处理
*/
for (int i = 0; i < 1000000; i++) {
//...do something
}
}
public void test4() {
/**
* 需要同步处理的关键代码
*/
synchronized (this) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
/**
* 这里特别耗时,但不要求同步处理
*/
for (int i = 0; i < 1000000; i++) {
//...do something
}
}
方法test4()的效率明显高于方法test3(),因为test4的同步粒度更小,需要等待的时间更短。
2、便于实现局部功能的同步。举例:
private final Object mLock = new Object();
public synchronized void test5() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
public void test6() {
synchronized (mLock) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
public void test7() {
synchronized (mLock) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
方法test6()和test7()在线程间同步处理被阻塞时,函数test5()仍然可以进去。这是因为使用了不同的所对象,这对于仅仅实现类的局部同步时有着更高的处理效率。
回到最开始的ANR的例子这里由于两个方法goBackOrForward()和updateCaptureFromBlob同时使用了同一个同步对象锁所致,解决办法是使子线程处理函数updateCaptureFromBlob()使用一个局部锁来解耦主线程对其的依赖。如下所示:
private final Object mCaptureLock = new Object();
void updateCaptureFromBlob(byte[] blob) {
Bitmap capture = mCapture;
synchronized (mCaptureLock) { //使用一个仅处理该逻辑的才使用的一个对象锁
Options ops = new Options();
ops.inMutable = true;
ops.inPreferredConfig = Bitmap.Config.RGB_565;
//此处为耗时操作
Bitmap bitmap = BitmapFactory.decodeByteArray(blob, 0,
blob.length, ops);
//... 其他逻辑
}
}