1)面对instance函数,synchronized锁定的是对象(object)而不是方法(method)或者代码(code)
对于instance函数,关键字其实并不锁定函数或者代码,它锁定的是对象!每个对象只有一个锁(lock)与之相连
当synchronized被当做函数修饰符时,它所取得的lock将被交给函数调用者(某个对象)。如果synchronized用于对象引用,则锁被交给该引用所指的对象
class Test {
public synchronized void method1(){
}
public void method2(){
synchronized (this) {
}
}
public void method3(SomeObject someObj){
synchronized (someObj) {
}
}
}
method1和method2在对象锁定的功能方面一致--尽管他们执行方式不同。二者都是对this进行同步控制。换句话说,获得的lock将给予调用此函数的对象(也就是this),由于这两个函数都隶属于Test class,因此lock都会被Test的某个对象获得。而method3则同步控制someObj所指的那个对象。
synchronized函数或者synchronized修饰的代码段在同一时刻下可以由多个线程执行---只要是不同的对象调用该函数。锁的概念只是相对于对象的,不同的对象可以在同一时刻调用synchronized修饰的方法!
注:Java中不允许将构造函数声明为synchronized。这是因为当两个线程并发调用同一个构造函数时,实际上各自操作的是同一个对象的两个实例的内存(换句话说synchronized是画蛇添足之举,因为这两个对象并不是同一个对象)然而如果在这些构造函数内包含了彼此竞争共享资源的代码,则必须同步控制那些资源以回避冲突。
2)synchronized修饰static的函数和普通的函数同步之间的区别:
synchronized static 获得的是Class Object lock,synchronized获得的是instance Object lock,两个锁,锁的并不是同一个对象。换句话说synchronized static 是Class 的锁,而synchronized获得的是new Class 对象的锁。
3)当两个函数共享一笔资源 -- 假设两个函数需要同时更新同一个对象。则代码必须实现同步控制:
i> 同步控制该对象
class Foo implements Runnable{
private SomeObj obj;
public void M1(){
synchronized(obj){}
}
public static void M2(Foo f){
synchronized(f.obj){}
}
}
ii> 同步控制一个特殊的instance变量,下例中两个同步的方法获得的锁是byte数组对象 lock 的锁
class Foo implements Runnable{
private byte[] lock = new byte[0];
public void M1(){
synchronized(lock){}
}
public static void M2(Foo f){
synchronized(f.lock){}
}
} // M1和M2不能同时刻执行,其锁会传给对象lock,从而达到锁住其中code的目的。该方法很经济,开销小
4)对于synchronized函数中可被修改的数据,需要用private修饰起来,并根据需要提供访问函数。如果函数返回的是可变对象,应首先克隆该对象。
class Foo implements Runnable{
public int[] intArray = new int[10];
private int[] intMArray = new int[10]; //为了没有漏洞,需要更改修饰符为private
public int[] getIntArray(){ //当添加一个public访问函数,该函数又出现了漏洞,返回一个对象的引用指向了intArray
return intArray; //可以通过 int[] tmp = foo.getIntArray(); tmp[5] = 1; 修改intArray的值。又出现了漏洞
}
public synchronized int[] getMIntArray(){ //该方法声明为synchronized,以确保在clone时不被改动
return intMArray.clone(); //该方法将返回的引用指向了另一个对象,而不是原来的intArray
}
public void run() {
addToArray();
}
public synchronized void addToArray(){
for (int i = 0; i < intArray.length; i++) {
intArray[i] = i;
}
}
public synchronized void subFromArray(){
for (int i = 0; i < intArray.length; i++) {
intArray[i] = i-10;
}
}
}
看似addToArray和subFromArray都有synchronized修饰,应该都是多线程安全的,实则不然:
Foo f = new Foo();
Thread t = new Thread(f);
t.start();
f.intArray = null;
程序在main线程和t.start()所起的线程中跳跃,在某个时间段main线程执行了f.intArray = null; 则程序出NullPointerExceptlon. 因此需要将intArray设置为private
5)避免无关紧要的同步控制
再次声明,当一个函数声明为synchronized,某个线程调用到该函数,所获得的锁是该对象的锁。因此如果某些方法中不需要锁住资源,则无需使用synchronized,或者说,需要控制同步的粒度,可以在方法内部使用synchronized块
6)访问共享变量时,需使用synchronized或者volatile
volatile关键字:用在多线程,同步变量。 线程为了提高效率,将某成员变量(如A)拷贝了一份(如B),线程中对A的访问其实访问的是B。只在某些动作时才进行A和B的同步。因此存在A和B不一致的情况。volatile就是用来避免这种情况的。volatile告诉jvm, 它所修饰的变量不保留拷贝,直接访问主内存中的(也就是上面说的A) 。一个变量经 volatile修饰后在所有线程中必须是同步的;任何线程中改变了它的值,所有其他线程立即获取到了相同的值。
注:既然volatile关键字已经实现了线程间数据同步,又要 synchronized干什么呢?呵呵,它们之间有两点不同。首先,synchronized获得并释放监视器——如果两个线程使用了同一个对象锁,监视器能强制保证代码块同时只被一个线程所执行——这是众所周知的事实。但是,synchronized也同步内存:事实上,synchronized在“ 主”内存区域同步整个线程的内存。因此,执行geti3()方法做了如下几步:
1. 线程请求获得监视this对象的对象锁(假设未被锁,否则线程等待直到锁释放)
2. 线程内存的数据被消除,从“主”内存区域中读入(Java虚拟机能优化此步。。。[后面的不知道怎么表达,汗])
3. 代码块被执行
4. 对于变量的任何改变现在可以安全地写到“主”内存区域中(不过geti3()方法不会改变变量值)
5. 线程释放监视this对象的对象锁
因此volatile只是在线程内存和“主”内存间同步某个变量的值,而synchronized通过锁定和解锁某个监视器同步所有变量的值。显然synchronized要比volatile消耗更多资源。
声明为volatile的简单变量如果当前值由该变量以前的值相关,那么volatile关键字不起作用。n++这种表达式无法满足volatile想要的效果
7)在单一操作(single operation)中锁定所有用到的对象
如果某个synchronized修饰的函数使用了两个普通的对象,并不表示该函数是线程安全的!
public synchronized int sumArray(int[] a, int[] b){
int value = 0; //尽管该方法被synchronized修饰,但该方法依然有可能不是线程安全的,因为在别的线程中可以修改a和b的值,改方法并没有锁定a和b两个对象
if (a.length == b.length) {
for (int i = 0; i < a.length; i++) {
value += a[i]+b[i];
}
}
return value; //永远要牢记,synchronized锁定的是它所属的对象而非函数或代码
}
8)以固定而全局性的顺序取得多个locks(机锁)以避免死锁
死锁:当两个或多个线程因[互相等待]而阻塞(blocked)时,就发生了死锁(deadlock).
9)优先使用notifyAll()而非notify()
notify()仅仅唤醒一个线程,如果jvm中有多个线程在wait的状态,唤醒的这个线程是未知的,可能不能唤醒你想唤醒的那个线程。线程的优先级在这里是无效的
notify()只有在如下两种情况下才是安全有效的:
i> 只有一个线程式正在等待,因此保证被除数唤醒的一定能够是它。
ii> 多个线程式正等待同一条件成立,哪个线程被唤醒无所谓。
notifyAll()唤醒所有等待中的线程,唤醒线程并不意味着他们都会获得锁,而是被唤醒,竞逐锁。
注:为了解决以特定顺序唤醒等待中的线程,需要自己来解决这个问题,最著名的解法Speciflc Notification Pattern,<Concurrent Programming in Java>可以让你掌握通知线程唤醒顺序
10)针对wait()和notifyAll()使用旋锁(spin locks)
package com.snow;
class Robot extends Thread{
private static byte[] commands;
private RobotController controller;
public Robot(RobotController c){
controller = c;
}
public static void storeCommands(byte[] b){
commands = b;
}
public void processCommand(byte b){
}
public void run(){
byte[] cmds;
while (true) {
synchronized (controller) { //1
if (commands == null) { //2
try {
controller.wait();
} catch (Exception e) { //3
// TODO: handle exception
}
}
cmds = new byte[commands.length];
for (int i = 0; i < commands.length; i++) {
cmds[i] = commands[i];
commands = null;
}
}
int size = cmds.length;
for (int i = 0; i < size; i++) {
processCommand(cmds[i]);
}
}
}
}
public class RobotController extends Thread{
private Robot rebot1;
private Robot rebot2;
public static void main(String[] args) {
RobotController rc = new RobotController();
rc.start();
}
public void run(){
rebot1 = new Robot(this);
rebot1.start();
rebot2 = new Robot(this);
rebot2.start();
}
public void loadCommands(byte[] b){
synchronized (this) { //4
Robot.storeCommands(b);
notifyAll();
}
}
}
这段代码看上去没有问题,实际上有一个bug,在某种情形下会导致失败:
1> 创建一个RobotController 对象,以及两个Robot 对象。所有这些对象都执行于各自的线程中。
2> 第一个Robot 线程在位置//2 查看commands 变量是否为null。
3> Commands 变量为null,于是第一个Robot 线程调用wait(),受用(blocked)于位置//3。
4> 第二个Robot 线程在位置//2 查看commands 变量是否为null。
5> Commands 变量为null,所以第二个Robot 线程调用wait(),同样受阻于位置//3。
6> RobotController 的loadCommands()被调用,接收一个命令表。
7> loadCommands()调用notifyAll()。
8> 两个阻塞线程被唤醒,二者都试图获得RobotController object lock。
9> 只有一个线程可获得lock,假设由第一个Robot 得到。在第一个Robot 线程释放lock 之前,第二个Robot 线程不会得到lock。
10> 第一个Robot 线程处理命令,然后设置commands 变量为null,释放lock。
11> 第二个Robot 线程获得lock,尝试处理命令。
12> 由于commands 变量为null,代码失败,产生NullPointerException 异常。
为了避免这个问题,需要在//2处修改为 while(commands == null)
11. 使用wait()t 和notifyAll()替换轮询循环(polling loops)
调用wait()时会释放同步对象锁,暂停(虚悬,suspend)此线程。被暂停的线程不会占用CPU时间,直到被唤醒。如:
public void run() {
int data;
while (true) {
synchronized (pipe) {
while ((data = pipe.getDate()) == 0) {
try {
pipe.waite(); //这里没有用pipe.sleep(200)每隔0.2秒轮询一次
} catch (InterruptedException e) {
}
}
}
// Process Data
}
}
12. 不要对locked object(上锁对象)之object reference 重新赋值
13. 不要调用stop()和suspend()
stop()中止一个线程时,会释放线程持有的所有locks,有搅乱内部数据的风险;suspend()暂时悬挂起一个线程,但不会释放持有的locks,可能带来死锁的风险。两种都会引发不可预测的行为和不正确的行为。