本节书摘来自异步社区《深入解析Android 5.0系统》一书中的第6章,第6.3节Android Java层的同步机制,作者 刘超,更多章节内容可以访问云栖社区“异步社区”公众号查看
6.3 Android Java层的同步机制
深入解析Android 5.0系统
Java语言和C/C++语言不一样,Java语言中提供了同步关键字synchronized来支持线程间的同步操作。
6.3.1 同步关键字synchronized
synchronized关键字最常见的用法是保护一段代码,如下所示:
class Foo implements Runnable
{
private String mLock;
public void lockedMethod() {
......
synchronized(**mL**ock) { ...... }
}
......
}
synchronized还可以用在类的方法前面,如下所示:
class FooA implements Runnable
{
public **synchronized******void lockedMethod() { ..... }
public **synchronized******void unlockedockedMethod() { ..... }
}
synchronize的第三种用法是修饰一个静态方法,如下所示:
class FooB implements Runnable
{
public **synchronized**** static **void lockedMethod() { ...... }
}
这3种使用方法的含义有很大的区别。首先要理解,synchronize锁住的是对象,而不是一段代码。在Java中,每个对象都可以是一把锁,这意味着每个Java对象都有类似native层的Mutex对象的功能。而synchronize关键字则和native层的Autolock类似,起到的是自动加锁和解锁的作用。
对于上面介绍的第一种使用方法:使用synchronize保护一段代码,假设类Foo有两个实例对象foo1和foo2,如果在两个线程中分别调用fool.lockedMethod()方法,在同一时间,只有一个线程能执行被synchronize括起来的代码,也就起到了互斥作用。但是如果一个线程调用的是fool.lockedMethod(),另一个线程调用的是foo2.lockedMethod(),这样是不会互斥的。原因是对于前者,上锁的对象是foo1.mLock,后者上锁的对象是foo2.mLock,它们其实是两把完全不同的锁。
synchronize关键字的第二种用法:“用synchronize修饰一个非静态方法”,语法上等价于下面这种写法:
synchronized (this) { ...... }
它表明上锁的对象是类的实例对象本身,而第一种用法中上锁的对象只是实例对象的成员变量,锁的范围扩大了。这样导致的结果是两个线程对于同一个实例对象的任何访问都将会互斥。具体而言:对于方法二中的例子,假定类FooA有一个实例对象foo_a,在两个线程中都访问foo_a.lockMethod()一定会互斥,一个线程访问foo_a.lockMethod(),另一个线程访问不同的函数foo_a.unlockedMethod()同样会互斥。当然如果是两个类实例对象foo_a和foo_b,分别调用它们的函数都不会互斥。
synchronize关键字的第三种用法:“用synchronize修饰一个静态的方法”,语法上等价于下面的写法:
synchronized (FooB.class) { ...... }
它表明上锁的对象是类本身,这样锁的作用范围进一步扩大了。因此,导致的结果是一旦有一个线程调用的FooB类的任何实例对象的lockedMethod()方法,另一个线程对于FooB类的任何对象的lockedMethod()方法都将会将产生互斥。这种用法导致的结果是最严厉的。
从上面的介绍可以看到synchronized并不像表面看上去的那么简单,在一个函数内部使用synchronized关键字,有可能会导致对整个类的访问都被锁定(当然这样会显得比较晦涩)。可能也是这个原因,Java在语法上创造了用synchronize来修饰一个方法,这样至少看上去会更明显些。
6.3.2 Object类在同步中的作用
synchronized关键字只能提供互斥访问的同步机制,如果需要完成一些更复杂的同步操作,就需要Object类来配合完成。Object类是所有Java对象的基类,在Object类中提供了一些与同步相关的方法,如下所示:
public final native void notify();
public final native void notifyAll();
public final void wait();
public final void wait(long millis);
ublic final native void wait(long millis, int nanos);
同步相关的接口分为notify和wait两种类型,notify类型的函数用来唤醒挂起的线程,wait类型的函数用来挂起调用者所在的线程。
要注意的是,这些方法并不是随时随地都可以调用的,对它们的调用必须是在本线程取得对“Object对象”的控制权以后才能进行。更准确地说,应该在synchronized的作用域内调用。这可能有点不太好理解,下面来看一个使用Object对象做同步控制的例子,代码如下:
public class ObjectSync implements Runnable {
private final static String Tag = "ObjectSync";
private String name;
private Object lock;
private ObjectSync(String name, Object lock) {
this.name = name;
this.lock = lock;
}
@Override
public void run() {
int count = 10;
while (count > 0) {
synchronized (lock) {
Log.i(Tag, name);
lock.notify();
try {
if(--count > 0) {
lock.wait();
}
} catch (InterruptedException e) {
Log.e(Tag, e.getMessage());
}
}
}
}
public static void output() {
Object lock = new Object();
Thread threadA = new Thread(new ObjectSync("A", lock));
Thread threadB = new Thread(new ObjectSync("B", lock));
threadA.start();
threadB.start();
try {
threadA.join();
threadB.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.i(Tag, "thread A & B are exited.");
}
}
上面的例子中notify()和wait()都是在对lock对象操作,因此,它们必须在synchronized(lock)的代码块内调用,否则运行时会报异常。当然也可以把对notfiy()和wait()的调用放到一个方法内,然后在synchronized块内调用这个方法。
这个例子的目的是在两个线程中分别输出文字“A”和“B”,但是输出结果要是交替的,也是就“ABABAB”的形式。例子中output()方法是入口,它创建了一个Object对象lock作为锁,然后又创建了两个线程。两个线程用的是同一个线程类,但是用参数加以区别,并通过参数把lock对象传给了两个线程中,这样两个线程中将使用同一个对象锁。在线程的运行方法run()中,首先用synchronized关键字形成了一个临界区,进入临界区后,先打印输出,然后调用notify()方法,恢复对方线程的运行,再调用wait()使自己挂起,接着对方线程运行,打印结果后,再重复上面的过程。这样两个线程就交替运行起来了。