我们知道,Java能够new一个Thread类或者一个有继承Thread类的子类,来创建一个线程。关于多线程编程,其中一个必须考虑的问题就是同步问题,比如,如果我有两个线程要操作同一个资源或者对象,如何能够保证这两个线程的操作是正确有效的、可以保持数据的一致性呢?
十分有用的一个解决方案是:使用synchronized关键字。
synchronized,字面意思是“同步的”。在探讨它的功能之前,我们最好先来认识一下“锁”这个东西。
想象一下,Java中每个对象,都有且只有一个叫做“锁”的东西。这个“锁”对于对象来说意义非凡,该对象的某些关联着这个“锁”的方法或者代码,只有获得对象这个“锁”的线程,才能访问执行。
举个例子来说,我是一个对象,我叫object1,我有一把“锁”,这把“锁”关联着我的其中一个方法objectMethod1();另外,有两个线程A和B,这一刻,线程A获得了我object1的“锁”,所以线程A有权访问我的方法objectMethod1(),与此同时线程B也想访问objectMethod1(),但是它没有“锁”,所以只能等待线程A访问完这个方法,释放了这个锁,B再去获得这个“锁”,才有资格访问objectMethod1()。
另一方面来说,没有关联着“锁”的方法和代码,不同的线程要访问它们,那都是随时随地的事情,没有什么制约也不用分先来后到。
从上面的描述来看,“锁”似乎是一个实实在在的东西,比较每个对象都有一个嘛。事实上,在代码当中,“锁”这个东西并不需要我们实际去声明(我们需要幻想它确实存在),我们需要做的仅仅是,声明那些与“锁”相关联的方法或者代码。这个时候,就要用到synchronized关键字了。
public class Run {
public static void main(String[] args) {
ObjectWithLock owl = new ObjectWithLock();
A a = new A(owl);
B b = new B(owl);
a.start();
b.start();
}
}
class ObjectWithLock {
private String sentence;
public void say(String sentence) {
this.sentence = sentence;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(this.sentence);
}
}
class A extends Thread {
private ObjectWithLock owl = null;
public A(ObjectWithLock owl) {
this.owl = owl;
}
public void run() {
owl.say("我是A");
}
}
class B extends Thread {
private ObjectWithLock owl = null;
public B(ObjectWithLock owl) {
this.owl = owl;
}
public void run() {
owl.say("我是B");
}
}
创建一个类,这个类记忆力很差,只能记住一句话,只能做一件事,修改自己的记忆,睡一秒,然后把记忆里的那句话说出来。
好了,问题来了,如果有两个线程A和B,有一个ObjectWithLock实例owl,两个线程分别调用owl.say("我是A")和owl.say(“我是B”),会发生什么呢?
按照寻常思路来说,线程A先把sentence改成了“我是A”,然后滚去睡了,切到线程B,它把sentence改成了“我是B”,也滚去睡了。接着A和B相继醒来,打印出记忆中的内容,那应该都是“我是B”。然而结果并非如此,A线程依然打印“我是A”,B线程依然打印“我是B”。
为什么呢?因为say()方法用了synchronized修饰。
关于synchronized的作用,可以总结为一句话:synchronized标示一个方法或者一段代码只能被拥有某个“锁”的线程访问,首先访问synchronized标识方法或代码的线程将得到“锁”。
回到例子,对象owl拥有一个锁,这个锁叫“对象锁”(因为它为一个特定对象所拥有)。它的say()方法与这把“对象锁”关联着,一旦A线程访问这个方法,A线程就得到了这把“对象锁”,即使去睡觉,依然抱着这把锁不放。这时,B线程也想访问owl的say()方法,但是这是做不到的,因为owl的“对象锁”被A线程持有,它只能等A线程执行完say(),放开这把锁,自己再去获得这把锁,它才有了访问say()方法的能力。
所谓“对象锁”,就是被一个对象持有的“锁”。声明一个方法或者一段代码需要获得“对象锁”才能执行,有以下3种写法:
1. 写在方法头部
class ObjectWithLock {
private String sentence;
// 方法头部加上synchronized
public synchronized void say(String sentence) {
this.sentence = sentence;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(this.sentence);
}
}
2. synchronized(this)用花括号包含特定代码段
class ObjectWithLock {
private String sentence;
public void say(String sentence) {
// synchronized(this)包含特定代码段
synchronized(this) {
this.sentence = sentence;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(this.sentence);
}
}
}
class ObjectWithLock {
private static Object lock;
private String sentence;
public void say(String sentence) {
// synchronized(variable)包含特定代码段
synchronized(lock) {
this.sentence = sentence;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(this.sentence);
}
}
}
上面3个片段,都是标识“对象锁”与代码关联的例子。需要强调的是,片段1和2都表明,方法或代码段与包含这个方法或者代码的当前对象的锁关联,而片段3则表明,那个代码片段与lock这个对象的锁关联。所以更进一步地说,下面例子中,如果两个线程分别调用say()和think(),两线程不必互相同步,它们因为分别得到不同的“对象锁”而顺利执行。
class ObjectWithLock {
private static Object lock;
private String sentence;
public void say(String sentence) {
// 关联lock对象的锁
synchronized(lock) {
this.sentence = sentence;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(this.sentence);
}
}
// 关联本对象的锁
public synchronized void think() {
System.out.println("我在思考");
}
}
有“对象锁”,也就有“类锁”。“类锁”是每个类有且只有一个的“锁”,用来标定一个静态方法或者一段代码必须持有这个锁才能被执行。
1. synchronized(class)包含特定代码段
class ObjectWithLock {
public void say(String sentence) {
// synchronized(class)包含特定代码段
synchronized(ObjectWithLock.class) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(sentence);
}
}
}
2. synchronized标识静态方法
class ObjectWithLock {
// synchronized标识静态方法
public static synchronized void say(String sentence) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(sentence);
}
}
“类锁”比“对象锁”虽然有差异,但是理解上仍遵循同一思路:“锁”是一个东西,某线程只有得到这个“锁”才能访问被这个“锁”关联的方法或者代码,无论“锁”是对象所有的还是类所有的。
另外,“对象锁”和“类锁”各自为政,互相没有干扰。