每个Java对象都有一个锁。线程可以调用同步方法获得这个锁。还有另外的一种机制获取锁:进入一个同步块;若线程进入如下这个块中:
synchronized (obj) {
critical section;
}
他就会得到obj的同步锁:
public class Bank {
private double[] accounts;
private Lock lock = new Object();
......
public void transefer(int from, int to, int amount) {
synchronized (lock) {
//an ad-hoc lock
accounts[from] -= amount;
accounts[to] += amount;
}
System.out.println(......);
}
}
在这里,创建了Lock对象只是为了使用每一个Java对象持有一个锁。
警告:使用同步代码块时,注意锁对象,例如下面的代码是有问题的:
private final String lock = "LOCK";
synchronized (lock) {.....] //dont lock on string
如果这个代码在同一个程序出现了两次,则锁将会是同一个对象,由于字符串字面量存在共享;这可能会导致死锁发生。
另外需要注意使用基本类型包装器作为一个锁;
private final Integer lock = new Integer(42);
构造器调用new Integer(8)已经被废弃了,若使用同一个魔法数两次,会导致出乎意外的共享锁。
若需要修改一个静态字段,会从特定的类上获取锁,而不是从getClass返回的数值获取:
synchronized (MyClass.class) {staticCounter++;} //OK
synchronized (getClass()) {staticCounter++;} //No
若一个子类调用包含这个代码块的方法,getClass会返回一个不同的Class对象!无法保证互斥!
在某些情况下,会使用一个对象的锁来实现额外的原子操作,这种做法叫做客户端锁定。
例如考虑Vector类,这是一个列表,方法是同步的。现在假设银行的余额都存储在Vector< Double>中。下面是transfer的一个方法原生实现:
public void transfer(Vector<Double> accounts, int from, int to, int account) {
accounts.set(from, accounts.get(from) - amount);
accounts.set(to, accounts.get(to) + amount);
System.out.println(....);
}
Vector类的get和set方法都是同步的,但是这对于我们没有什么帮助,一个线程完全可以在transfer方法中执行完第一个get调用之后被抢占,然后另一个线程可能会在相同的位置存储一个不同的数值。
不过我们可以截获这个锁:
public void transferI(Vector<Double> accounts, int from, int to, int amount) {
synchronized (accounts) {
accounts.set(from, accounts.get(from) - amount);
accounts.set(to, accounts.get(to) + amount);
}
}
该方法完全可行,但是完全依赖如下的事实:Vector类会对自己的所有更改器方法使用内部锁。
Java虚拟机对同步方法提供了内置支持;不过同步块会编译为很长的字节码序列来完成管理内部锁。
锁和条件是实现线程同步的强大工具,但是从严格意义上来说,他们并不是面向对象的。用Java语言描述监视器概念如下:
监视器的早期版本只有一个单一条件,使用一种很优雅的语法。可以调用await accounts并且不适用额外的显式条件变量。不过研究表明,盲目的重新测试条件及其低效。
最好的办法还是调用await/notifyAll/notify来访问条件变量。
注意,Java对象在以下3个方面不同于监视,削弱了线程安全性:
有时候,仅仅只是为了读写一两个实例字段而利用同步机制,所带来的性能开销有些得不偿失;
若使用锁机制来保护可能被多个线程访问的代码,就不会存在这些问题。因为编译器必须遵守锁的要求,在必要时刷新输出本地缓存,而且不能够不适合的重排指令顺序。
注意,有如下同步格言:若写一个变量,而且这个变量接下来可能会被另一个线程读取,或者,若读取一个变量可能已经被另一个线程写入了数值,就必须使用同步。
volatile关键字 给实例字段提供了一种免锁机制。若声明一个字段给volatile,则编译器和虚拟机会考虑这个字段可能会被另一个线程并发更新。
假设有一个对象有一个Boolean标记了done,其数值由另一个线程设置,而且由另一个线程负责查询:
private boolean done;
public synchronized boolean isDone() { return done; }
public synchronized void setDone() { doen = true; }
或许在对象内部使用锁机制不够优秀,若另一个线程已经给该对象加锁了,则方法可能存在阻塞。若是这个问题,可以只给这个变量使用一个单独的锁,这太麻烦了。
因此综上,考虑给字段声明为volatile 就很合适。
编译器会插入合适的代码,确保一个线程对done变量做了修改,这个修改对读取变量的所有其他线程都可见。
volatile变量无法提供原子性。例如如下方法:
public void flipDone() { done != done; }
无法确保字段的数值取反,无法保证读取、写入和取反不会被中断。
除非使用锁或者volatile修饰符,否则无法从多个线程安全的读取一个字段。
另一个情况可以安全的访问一个共享字段:
final var accounts = new HashMap<String, Double>();
其他的线程会在构造器完成构造之后才能看到accounts这个变量。
若不使用final,则无法保证其他线程看到的accounts更新之后的数值,可能看到的都是null。不是新构造的HashMap<>()。
注意;映射的操作并不是线程安全的。如果多个线程更改和读取这个映射,则仍然需要进行同步操作。