多线程共享同一储存空间,在带来方便的同时,也会造成访问冲突。Java语言提供了synchronized关键字已解决这种冲突,有效地避免了同一个数据对象被多个线程同时访问。使用synchronized关键字要注意一下几点:
- synchronized关键字可以作为函数的修饰符,也可以作为函数内的语句。synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)上。
- 无论synchronized关键字加在方法上还是加在对象上,它取得的锁都是对象,而不是一段代码或一个函数锁定,而且同步方法很可能还会被其他线程对象访问。
- 每个对象只有一把锁(lock)与之相关联。
- 实现同步是要以很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无所谓的同步控制。
synchronized关键字的作用域有两种。
- 某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法。如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其他线程不能同时访问这个对象中的任何一个synchronized方法。这时,不同的对象实例的synchronized方法是不相干扰的。也就是说,其他线程照样可以同时访问相同类的另一个对象实例中的synchronized方法。
- 某个类的范围中,synchronized static aStaticMethod(){}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。
synchronized方法控制对类成员变量的访问:每个类实例对应一把锁,每个synchronized方法都必须获得调用该类方法的类实例的锁方能执行,否则所属线程被阻塞,方法一旦执行,就独占该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类实例,其所有的声明为synchronized的成员函数中至少只有一个处于可执行状态(因为至多只有一个能够获得该类实例对应的锁),从而有效地避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方法均被声明为synchronized)。
下面举个例子。一电影院有20张电影票要卖,目前有3个售票员。我们用sleep()函数来模拟售票,假设不使用synchronized关键字。
/**
* 测试多线程情况下不使用synchronized关键字
* @author QuLei
*
*/
public class SellTest {
public static void main(String[] args) {
SellThread sellThread = new SellThread();
new Thread(sellThread,"售票员1").start();
new Thread(sellThread,"售票员2").start();
new Thread(sellThread,"售票员3").start();
}
}
/**
* 一电影院有20张电影票要卖,目前有3个售票员。我们用sleep()函数来模拟售票,
* 假设不使用synchronized关键字。
* @author QuLei
*
*/
class SellThread implements Runnable{
private int ticketCount = 20;
@Override
public void run() {
// TODO Auto-generated method stub
while(true) {
if(ticketCount > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"sell "+
ticketCount--);
}else {
break;
}
}
}
}
执行结果:
售票员2sell 2
售票员1sell 1
售票员3sell 0
售票员2sell -1
结果卖掉了22张票,如果使用synchronized关键字,就可以避免这种冲突,修改程序如下:
/**
* 测试多线程情况下不使用synchronized关键字
* @author QuLei
*
*/
public class SellTest {
public static void main(String[] args) {
//SellThread sellThread = new SellThread();
ThreadSell sellThread = new ThreadSell();
new Thread(sellThread,"售票员1").start();
new Thread(sellThread,"售票员2").start();
new Thread(sellThread,"售票员3").start();
}
}
/**
* 一电影院有20张电影票要卖,目前有3个售票员。我们用sleep()函数来模拟售票,
*使用synchronized关键字。
* @author QuLei
*
*/
class ThreadSell implements Runnable{
private int ticketCount = 20;
@Override
public void run() {
// TODO Auto-generated method stub
while(true) {
synchronized (this) {
if(ticketCount > 0 ) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+
"sell "+ticketCount--);
}else {
break;
}
}
}
}
}
执行的结果为:
售票员3sell 5
售票员2sell 4
售票员2sell 3
售票员3sell 2
售票员1sell 1
这样就卖掉了20张票,这里锁的this即为一个对象,也可以锁一个其它的对象,这个对象被当做是一个标志位,这个标志位可以被任何一个售票员使用,这样售票员1线程拿到了其它的售票员线程发现以后就会被搁置,一直等到售票员1释放掉标志对象。
这里有两个容易误解的地方:
- 一个线程拿到synchronized括号中的对象后,其它也需要拿到这个对象才能运行的线程不能执行了。其实其它线程也是可以执行的,但他们执行到了需要synchronized中对象的时候,发现对象的标志位不可用,只能又被搁置了,所以synchronized使线程同步的时候是以牺牲效率为代价的。
- 一个线程拿到了synchronized括号中的对象之后,其他任何线程都不能执行了,假如其他不需要synchronized的对象才能执行的线程,还是可以和拿到synchronized的括号中的对象的线程一起运行的。有的方法在前面被加上了synchronized,其实这个时候就是把这个方法的调用者即this的标志位置0了,这样就不能和其他需要this才能运行的线程一起执行,但可以和其他不需要这个this对象的线程一起运行。