多线程同步中的门道(一)
在涉及到多线程的开发时,线程同步的考虑是不可缺少的,否则很可能会造成各种超出预料的错误结果。以自己的学习经历来说,对于刚开始接触线程同步的人可能会感觉非常简单,在多线程操作可能会造成数据混乱的地方同步一下不就行了嘛,加个synchronized关键字,多简单!可是随着开发的深入,会渐渐的发现仅仅是一个synchronized关键字也不是那么简单,里面的门道和考虑到的情况还是不少。本系列就着循序渐进的程序和大家探讨一下synchronized关键字使用中的各种情形和会造成的各种意料之外和意料之中的结果,欢迎各位大神轻拍。
转载请注明本文地址:http://www.cnblogs.com/hellojava/p/3630395.html
synchronized涉及到同步方法、同步代码块、同步类、同步对象、静态方法等,本系列来挨个探讨。
注:因为考虑到文章篇幅和为了突出我们要分析的关键代码,所以下面程序有可能不会是最优写法。
未作线程同步
我们先来看看,在多线程运行下,未作线程同步的程序。
[测试程序1]
/**
* Test case 1.
* There is no thread synchronization.
*/
public class Test {
public static void main(String[] args) {
final TestCase test = new TestCase();
Thread thread1 = new Thread() {
@Override
public void run() {
test.function();
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
test.function();
}
};
thread1.start();
thread2.start();
}
}
class TestCase {
public void function() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " executed result: " + i);
}
}
}
上面的测试程序很简单,定义了一个测试用例类,类中有一个循环输出5次”线程名+输出次数”的方法。然后设置了两个线程,启动这两个线程跑这个测试用例对象的方法,查看会有什么样的输出结果。后面的测试程序基本都是在此程序上修改变化而出,用来测试不同情况。
运行程序,某次运行的结果可能如下:
Thread-0 executed result: 0
Thread-1 executed result: 0
Thread-1 executed result: 1
Thread-0 executed result: 1
Thread-1 executed result: 2
Thread-1 executed result: 3
Thread-1 executed result: 4
Thread-0 executed result: 2
Thread-0 executed result: 3
Thread-0 executed result: 4
从程序输出结果可以看出,Thread-0和Thread-1是无规则交叉输出的,也就意味着在未作线程同步的情况下,两个线程同时执行着TestCase的function方法,这种是属于线程不安全的。
同步方法
我们对上面的程序进行一下修改,加一个synchronized关键字用来同步方法。
[测试程序2.1]
/**
* Test case 2.1. synchronized method.
*/
public class Test {
public static void main(String[] args) {
final TestCase test = new TestCase();
Thread thread1 = new Thread() {
@Override
public void run() {
test.function();
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
test.function();
}
};
thread1.start();
thread2.start();
}
}
class TestCase {
public synchronized void function() {// add synchronized keyword.
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " executed result: " + i);
}
}
}
运行程序,得到的输出结果总是如下:
Thread-0 executed result: 0
Thread-0 executed result: 1
Thread-0 executed result: 2
Thread-0 executed result: 3
Thread-0 executed result: 4
Thread-1 executed result: 0
Thread-1 executed result: 1
Thread-1 executed result: 2
Thread-1 executed result: 3
Thread-1 executed result: 4
从输出结果可以看出,同步了方法之后,两个线程顺序输出,说明线程Thread-1进入function方法后,线程Thread-2在方法外等待,等Thread-1执行完后释放锁,Thread-2才进入方法执行。
但是我们对上面的代码稍作修改,看看同步方法,对于不同的对象情况下是否都有线程同步的效果。
[测试程序2.2]
/**
* Test case 2.2. synchronized method but different objects.
*/
public class Test {
public static void main(String[] args) {
// There are two objects.
final TestCase test1 = new TestCase();
final TestCase test2 = new TestCase();
Thread thread1 = new Thread() {
@Override
public void run() {
test1.function();
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
test2.function();
}
};
thread1.start();
thread2.start();
}
}
class TestCase {
public synchronized void function() {// add synchronized keyword.
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " executed result: " + i);
}
}
}
执行程序,某次的运行结果如下:
Thread-0 executed result: 0
Thread-0 executed result: 1
Thread-1 executed result: 0
Thread-1 executed result: 1
Thread-0 executed result: 2
Thread-0 executed result: 3
Thread-0 executed result: 4
Thread-1 executed result: 2
Thread-1 executed result: 3
Thread-1 executed result: 4
从以上结果可以看出,同步方法时,当一个线程进入一个对象的这个同步方法时,另一个线程可以进入这个类的别的对象的同一个同步方法。
同步方法小结
在多线程中,同步方法时:
- 同步方法,属于对象锁,只是对一个对象上锁;
- 一个线程进入这个对象的同步方法,其他线程则进不去这个对象所有被同步的方法,可以进入这个对象未被同步的其他方法;
- 一个线程进入这个对象的同步方法,其他线程可以进入同一个类的其他对象的所有方法(包括被同步的方法)。
- 同步方法只对单个对象有用。
对静态方法的同步
上面是对普通的方法进行同步,发现只能锁对象。那么这次我们试着同步静态方法看会有什么结果,与上面的[测试程序2.2]来进行对比。
[测试程序3.1]
/**
* Test case 3.1. synchronized static method.
*/
public class Test {
public static void main(String[] args) {
// There are two objects.
final TestCase test1 = new TestCase();
final TestCase test2 = new TestCase();
Thread thread1 = new Thread() {
@Override
public void run() {
test1.function();
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
test2.function();
}
};
thread1.start();
thread2.start();
}
}
class TestCase {
public synchronized static void function() {// add static keyword.
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " executed result: " + i);
}
}
}
执行程序,运行结果总是如下:
Thread-0 executed result: 0
Thread-0 executed result: 1
Thread-0 executed result: 2
Thread-0 executed result: 3
Thread-0 executed result: 4
Thread-1 executed result: 0
Thread-1 executed result: 1
Thread-1 executed result: 2
Thread-1 executed result: 3
Thread-1 executed result: 4
从结果可以看出,两个线程顺序执行。而上面的[测试程序2.2]两个线程是交叉输出的。为什么同步静态方法会起到两个对象也能同步的了呢?
因为被static修饰的方法是静态方法,也被称为类方法,表明这个方法是属于类的。与普通方法的区别是,在类被加载进内存时,就分配了方法的入口地址,而普通方法要实例化对象后才分配方法的入口地址。
所以对静态方法进行同步,相当于是锁住了类,当一个线程进入这个静态方法时,其他线程无论使用这个类的哪个对象都无法进入这个静态方法,直到上一个线程执行完毕释放锁。
同样的当一个线程进入这个静态方法时,其他线程也进不去这个类的其他被同步的静态方法,因为只要是静态方法都是属于类的嘛。但是可以进入其他未同步的方法(包括静态方法)。这些可以自己来测试,就不上例子了。
但是这种方式,与完全的同步类又有些区别,我们可以继续看下面的程序。
[测试程序3.2]
/**
* Test case 3.2. synchronized static method.
*/
public class Test {
public static void main(String[] args) {
// There are two objects.
final TestCase test = new TestCase();
Thread thread1 = new Thread() {
@Override
public void run() {
test.function1();
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
test.function2();
}
};
thread1.start();
thread2.start();
}
}
class TestCase {
public synchronized static void function1() {// add static keyword.
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " executed result: " + i);
}
}
public synchronized void function2() {// no static keyword.
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " executed result: " + i);
}
}
}
执行程序,某次的运行结果如下:
Thread-0 executed result: 0
Thread-1 executed result: 0
Thread-1 executed result: 1
Thread-1 executed result: 2
Thread-0 executed result: 1
Thread-0 executed result: 2
Thread-1 executed result: 3
Thread-1 executed result: 4
Thread-0 executed result: 3
Thread-0 executed result: 4
从以上结果可以看出,虽然静态方法和普通方法都被同步,虽然是对同一个对象,但是两个线程仍然交叉执行。说明当一个线程进入了类的静态同步方法,其他线程可以进入这个类的非静态的同步方法。
同步静态方法小结
在多线程中,同步静态方法时:
- 同步静态方法时,相当于对类所有的类方法上锁,但并不是完全的类同步;
- 一个线程进入这个类的静态同步方法时,其他线程无法进入这个类的其他静态同步方法;
- 但是其他线程可以进入这个类的非静态方法(无论是否同步)
- 至于未同步的方法,无论是否是静态方法,都是想进就进啦。