多线程编程时,同步是一个很重要的问题。如何保证代码不受并发访问问题的影响,Java语言提供了多种解决方法。其中一种便是使用synchronized关键字。 从JDK1.0开始,Java中每个对象都有一个内部锁,synchronized关键字就是使用对象的内部锁完成同步功能。它修饰的对象可以是方法、静态方法、代码块和类。
修饰代码块
synchronized修饰代码块时,被修饰的的代码块被称为同步语句块,其作用的范围是大括号{}中的代码,作用的对象是调用这个代码块的对象。
1.一个线程访问对象中的synchronized代码块时,其他试图访问该对象的synchronized代码块的线程将阻塞。
public class MyThread implements Runnable {
private static int count =0;
@Override
public void run() {
synchronized (this) {
for (int i = 0; i
try {
System.out.println(Thread.currentThread().getName() + " :" count++);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread,"t1");
Thread t2 = new Thread(myThread,"t2");
t1.start();
t2.start();
}
}
结果如下:
t1 :0
t1 :1
t1 :2
t2 :3
t2 :4
t2 :5
从结果来看,t1和t2是顺序执行。这也意味着,当并发的多线程访问同一个对象中的该synchronized代码块时,同一时刻只能有一个线程执行,其他线程都会阻塞,等待当前线程执行完毕。
上面的描述和小标题1略有不同,小标题1用黑体强调了“该对象的synchronized代码块”。这里有两个重点需要注意:(1)必须是同一个对象;(2)对象的所有synchronized代码块。分别用两个例子解释:
(1)不同对象的并发访问 现在把Example 1的主函数部分的线程创建如下修改:
public static void main(String[] args) {
Thread t1 = new Thread(new MyThread(),"t1");
Thread t2 = new Thread(new MyThread(),"t2");
t1.start();
t2.start();
}
结果如下:
t1 :0
t2 :1
t2 :2
t1 :3
t1 :4
t2 :4
注意,这里的结果是不唯一,多次运行会产生不同结果。从结果可以看出来,这里t1和t2是乱序执行,原因在于synchronized关键字内部锁锁定的是调用代码块的对象。这里产生了两个对象,分别有两个内部锁锁定t1和t2,两个线程互不干扰,同时执行各自的代码块。所以,当并发线程访问的是不同的对象,并不能解决并发访问问题。
(2)访问同一对象其他的synchronized代码块
public class MyThread implements Runnable {
private static int count =0;
private void increase(){
synchronized (this){
for (int i = 0; i
try {
System.out.println(Thread.currentThread().getName() + " :" + count++);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
private void printCount(){
synchronized (this){
for (int i = 0; i
try {
System.out.println(Thread.currentThread().getName() + " : "+ count);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
@Override
public void run() {
String name = Thread.currentThread().getName();
if (name.equals("t1")) {
increase();
} else {
printCount();
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread,"t1");
Thread t2 = new Thread(myThread,"t2");
t1.start();
t2.start();
}
}
结果如下:
t1 :0
t1 :1
t1 :2
t2 : 3
t2 : 3
t2 : 3
从结果看,t1和t2是顺序执行的。并发线程访问的是同一对象的不同synchronized代码块,同一时刻仍然只有一个线程能执行,其他线程会阻塞。原因仍然是synchronized代码锁锁定是调用代码块的对象。
2.一个线程访问对象中的synchronized代码块时,其他试图访问该对象的非synchronized代码块的线程可以正常访问。
由上面的例子,可以知道synchronized代码块只会阻塞那些访问同一对象的synchronized代码块的线程,不会影响访问其他代码的线程。将Example 3中,printCount()
改为非synchronized形式:
private void printCount(){
for (int i = 0; i
try {
System.out.println(Thread.currentThread().getName() + " : "+ count);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
结果如下:
t2 : 0
t1 :0
t2 : 1
t1 :1
t1 :2
t2 : 2
这里的运行结果也是不唯一的,因为t1和t2互不干扰,t2输出的count值会随时间变化(即count会在t1的increase()
执行后发生变化)。
总结:这里可以对synchronized关键字修饰代码块的用法,进行总结。synchronized代码块只会影响同一对象的所有synchronized代码块的同步访问,不影响不同对象的同步访问,不影响同一对象的非synchronized代码块的同步访问。
修饰方法
synchronized修饰方法时,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。
将Example 1的run()
方法如下修改:
@Override
public synchronized void run() {
for (int i = 0; i
try {
System.out.println(Thread.currentThread().getName() + " :" + count++);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果如下:
t1 :0
t1 :1
t1 :2
t2 :3
t2 :4
t2 :5
结果与Example 1完全一致。synchronized修饰方法和修饰代码块,只是写法的不同,效果是等价的。修饰代码块部分的总结规则也适用于修饰方法。
然而,synchronized可以修饰方法,但它不属于方法的一部分,因此,synchronized关键字不能被继承。如果父类方法使用synchronized关键字,而子类中覆盖了该方法,则子类这个方法默认是不同步的,必须显式地加上synchronized关键字才会同步。但是,若在子类中调用父类相应的同步方法,则子类的方法也就相当于同步了。
另外,定义接口方法时,不能使用synchronized关键字修饰。构造方法也不能使用synchronized关键字,但是可以使用synchronized修饰代码块来同步。
修饰静态方法
synchronized修饰静态方法时,作用范围是整个静态方法,作用的对象是这个类的所有对象。这是因为静态方法是属于类的而不属于对象,synchronized的内部锁锁定这个类所有对象。
public class MyThread implements Runnable {
private static int count = 0;
private synchronized static void increase() {
for (int i = 0; i < 3; i++) {
try {
System.out.println(Thread.currentThread().getName() + " :" + count++);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void run() {
increase();
}
public static void main(String[] args) {
Thread t1 = new Thread(new MyThread(), "t1");
Thread t2 = new Thread(new MyThread(), "t2");
t1.start();
t2.start();
}
}
运行结果如下:
t1 :0
t1 :1
t1 :2
t2 :3
t2 :4
t2 :5
从结果可以看到,尽管t1和t2是不同的线程,但在访问同一个静态方法时,发生了同步情况。t1先执行,t2被阻塞。所以,synchronized修饰静态方法,锁定的是整个类的所有对象。
修饰类
synchronized修饰类时,作用范围是synchronized后面{}内的部分,作用的对象也是这个类的所有对象。
public class MyThread implements Runnable {
private static int count = 0;
private synchronized static void increase() {
synchronized (MyThread.class) {
for (int i = 0; i < 3; i++) {
try {
System.out.println(Thread.currentThread().getName() + " :" + count++);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
@Override
public void run() {
increase();
}
public static void main(String[] args) {
Thread t1 = new Thread(new MyThread(), "t1");
Thread t2 = new Thread(new MyThread(), "t2");
t1.start();
t2.start();
}
}
运行结果如下:
t1 :0
t1 :1
t1 :2
t2 :3
t2 :4
t2 :5
Example 7的运行结果和Example 6完全一样,因为synchronized修饰类时,锁定的是整个类,该类的所有对象共用一把锁。
总结
只要明白synchronized关键字是通过对象的内部锁来实现同步的,再针对具体情况具体分析(静态方法、方法、类、代码块),很快便能搞明白synchronized关键字的含义和用法。