1. 面对instance 函数,synchronized 锁定的是对象(objects)而非函数(methods)或代码(code)。
Synchronized既可以作用于方法修饰,也可以用于方法内的修饰。对于instance 函数,关键词synchronized 其实并不锁定方法或代码,它锁定的是对象(至于synchronized 对statics 的影响,请见下面)。记住,每个对象只有一个lock(机锁)与之相关联。当synchronized 被当作函数修饰符的时候,它所取得的lock 将被交给函数调用者(某对象)。如果synchronized 用于object reference,则取得的lock 将被交给该reference所指对象。分析如下:
public class Test {
public synchronized void method1() {//修饰方法
}
public void method2() {//修饰object reference
synchronized (this) {
}
}
public void method3(Object object) {//修饰object reference
synchronized (object) {
}
}
}
前两个函数method1()和method2()在[对象锁定]方面功能一致。 二者都对this进行同步控制。换句话说,获得的lock 将给予调用此函数的对象(也就是this)。由于这两个函数都隶属class Test,所以lock 由Test 的某个对象获得。method3()则同步控制object所指的那个对象。
对一个对象进行同步控制到底意味什么呢?它意味[调用该函数]之线程将会取得对象的lock。持有[对象A 之lock],意味另一个通过synchronized 函数或synchronized语句来申请[对象A 之lock]的线程,在该lock 被释放之前将无法获得满足。然而如果另一个线程对对象A 所属类之另一对象B 调用相同的synchronized 函数或synchronized 区块,可以获得[对象B 之lock]。因此,synchronized 函数或synchronized 区段内的代码在同一时刻下可由多个线程执行,只要是对不同的对象调用该函数。
记住,同步机制(synchronization)锁定的是对象,而不是函数或代码。函数或代码区段被声明为synchronization 并非意味它在同一时刻只能由一个线程执行。
最后一点说明是:Java 语言不允许你将构造函数声明为,synchronized(那么做会发生编译错误)。原因是当两个线程并发调用同一个构造函数时,它们各自操控的是同一个class 的两个不同实体(对象)的内存(也就是说synchronized 是画蛇添足之举)。然而如果在这些构造函数内包含了彼此竞争共享资源的代码(比如说静态变量),则必须同步控制那些资源以回避冲突。
2. 弄滇楚synchronized statics函数(同步静态函数)与synchronized instance函数(同步实例函数)之间的差异。
当调用synchronized statics方法时,获取到的lock是与定义该方法的Class对象相关,而不是与调用该方法的对象有关。当你对一个class literal(类名称字面常量)调用其synchronized 区段时,获得的也是同样那个lock,也就是[与特定Class 对象相关联]的lock。
如下:
public class Thread01 {
public static synchronized void method() {
}
public void method1() {
synchronized (Thread01.class) {
}
}
}
method()和method1()都是争取的同一个lock,也就是Thread01 Class object lock。method()通过synchronized的修饰符来获取lock,而method2()是通过class literal Thread01.class来获取lock的。
如果synchronized 施行于instance 函数和object references,得到的lock 就与前面的不一样了。对于instance 函数,取得的lock 隶属于其调用者(某个对象),至于同步控制一个(指名)对象,取得的当然是该对象的lock。
由于同步控制(1)instance 函数(2)static 函数(3)对象(object) (4)class literals 时得到的locks 不同,因此在决定互(mutual exclusion)行为时一定要小心谨慎。记住,同步控制[通过instance 函数或object reference 所取得的lock]。完全不同于同步控制[通过static 函数或class literal 所取得的lock]。两个函数被声明为synchronized 并不就意味它们具备多线程安全性。你必须小心识别和区分通过同步控制所取得的locks 之间的微妙差异。
如下:
class Thread02 implements Runnable {
public synchronized void printlnM1() {
while (true) {
System.out.println("printlnM1");
}
}
public static synchronized void printlnM2() {
while (true) {
System.out.println("printlnM2");
}
}
@Override
public void run() {
printlnM1();
}
}
class TestThread {
public static void main(String[] args) {
Thread02 t = new Thread02();
Thread f = new Thread(t);
f.start();
t.printlnM2();
}
}
尽管上述两个函数都声明为synchronized,它们并非[多线程安全](thread safe)。
其原因在于一个是synchronized static 函数,另一个是synchronized instance函数。因此它们争取的是不同的locks。instance 函数printlnM1()取得的是Thread02 object lock,static 函数printlnM2()取得的是Thread02 的Class object lock。这是不同的两个locks,彼此互不影响。当上述代码执行起来,两个字符串都打印在屏幕上。换句话讲,两个函数的执行交互穿插。如果需要同步控制这段代码,可以共享同一个资源。为了保护这笔资源,代码必须有正确的同步控制,以避免冲突。两种选择可以解决这个问题:
1.同步控制(synchronize)公用资源。
2. 同步控制(synchronize)—个特殊的instance 变量。
方案一实现:前提假设两个函数要更新同一个对象,它们就对其进行同步控制。
class Thread02 implements Runnable {
private Object o;
public synchronized void printlnM1() {
synchronized (o) {
while (true) {
System.out.println("printlnM1");
}
}
}
public static synchronized void printlnM2(Thread02 o) {
synchronized (o.o) {
while (true) {
System.out.println("printlnM2");
}
}
}
@Override
public void run() {
printlnM1();
}
}
方案二实现:声明一个local instance 变量,惟一的目的就是对它进行同步控制。
class Thread02 implements Runnable {
private byte[] lock = new byte[0];
public synchronized void printlnM1() {
synchronized (lock) {
while (true) {
System.out.println("printlnM1");
}
}
}
public static synchronized void printlnM2(Thread02 o) {
synchronized (o.lock) {
while (true) {
System.out.println("printlnM2");
}
}
}
@Override
public void run() {
printlnM1();
}
}由于只能锁定对象,你使用的local instance 变量必须是个对象。
3. 以[private数据+相应的访问函数(accessor)]替换[public/protected数据。这样做是为了封装。记住,对于[在synchronized 函数中可被修改的数据],应使之成为private,并根据需要提供访问函数(accessor)。如果访问函数返回是可变对象(mutable object),那么应该先cloned(克隆)该对象。
4. 要避免无所谓的同步控制。过度的同步控制估计会的导致死锁(deadlocks)或者并发(concurrency)度降低。同步机制对每个对象只提供一个lock。当一个函数声明为synchronized,所获得的lock 乃是隶属于调用此函数的那个对象。如果该对象要访问其他方法的话,就必须释放lock,这样的话性能就降低了,这时可以再添加一个变量来产生不同的lock。如下:
class Thread03{
private int[] a1;
private int[] a2;
private double[] d1;
private double[] d2;
private byte[] a = new byte[0];
private byte[] d = new byte[0];
public void ma1(){
synchronized (a) {
}
}
public void ma2(){
synchronized (a) {
}
}
public void md1(){
synchronized (d) {
}
}
public void md2(){
synchronized (d) {
}
}
}
5. 访问共享变量时请使用synchronized或volatile。可以确保变量与主内存完全保持一致,从而在任何时候得到正确数值。记住,一旦变量被声明为volatile,在每次访问它们时,它们就与主内存进行一致化。但如果使用synchronized,只有在取得lock 和释放lock 的时候,才会对变量和主内存进行一致化。
| 优点 | 缺点 |
synchronized | 取得和释放lock 时,进行私 有专用副本 与主内存正本的一致化 | 消除了并发性的可能 |
volatile | 允许并发 | 每次访问变量,就进行稀有专用内存与对 应之主内存的一致化 |
6. 在单一操作中锁定所有用到的对象。
7. 以固定而全局性的顺序取得多个locks(机锁)以避免死锁。
死锁:当两个或者多个线程因为互相等待而阻塞。
8. 优先使用notifyAll()而非notify()。
9. 针对wait()和notifyAll()使用旋锁(spin locks)。只在代码等待着某个特定条件,它就应当在一个循环内(或谓旋锁,spin lock)做那件事(那件事指的是[等待着某个特定条件])。尤其是在判断null之类的时候,因为不能够确保多个线程被唤醒的时候,其条件是否为空。
10. 使用wait()和notifyAll()替换轮询循环(polling loops)。
11. 不要对locked object(上锁对象)之object reference重新赋值。
12. 不要调用stop()或suspend()。
Stop()的本意是用来中止一个线程。中止线程的问题根源不在object locks,而在object 的状态。当stop()中止一个线程时,会释放线程持有的所有locks。但是你并不知道当时代码正在做什么。
Suspend()的本意时用来[暂时悬挂起一个线程](它由一个对应的resume()函数,用来恢复先前被悬挂起来的线程。Resume()也不再获得Java 2 SDK 支持)。Suspend()同样时不安全的,但其原因和stop()的故事不同。和stop()不同,suspend()并不释放[即将被悬挂支线程说持有的locks]。这些locks 在线程恢复执行前永远不会释放。
stop()带来[搅乱内部数据]的风险,suspend()带来死锁的风险。
13. 通过线程(threads)之间的协作来中止线程。
如何中止线程呢?在class 内提供一个变量,以及一个用来设置此变量值的函数。该变量用来表示线程何时应该被中止。
class Thread04 extends Thread {
private volatile boolean stop;
public void stopFlag() {
stop = true;
}
@Override
public void run() {
while (stop) {
super.run();
}
}
}