引入问题
在初步接触多线程之后,我们知道多线程的一个问题是‘竞态条件’,导致程序变得不稳定或者不安全。我们看下面的例子(《Java编程的逻辑 15.2》)
public class Counter {
private int count;
public void incr(){
count ++;
}
public int getCount() {
return count;
}
}
public class CounterThread extends Thread {
Counter counter;
public CounterThread(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
for(int i = 0; i < 1000; i++) {
counter.incr();
}
}
public static void main(String[] args) throws InterruptedException {
int num = 1000;
Counter counter = new Counter();
Thread[] threads = new Thread[num];
for(int i = 0; i < num; i++) {
threads[i] = new CounterThread(counter);
threads[i].start();
}
for (int i = 0; i < num; i++) {
threads[i].join();
}
System.out.println(counter.getCount());
}
}
从main函数开始看:创建了‘一’个counter对象,创建了1000个CounterThread对象;然后调用start方法执行run方法体,执行了1000次incur;最后输出counter对象的count数值。
理想结果是1000*1000,但真实结果并非如此。
通过理论分析,问题出现在incur方法中的count++并非一个原子操作,它可以拆分为以下子操作:
- 先读取count的值
- count的值+1
- 赋值给count
这样就可能会出现一种情况,
- 某个线程A执行count++时,执行到第一个子操作,另一个线程B也抢占CPU执行count++。
- 导致线程A还未完成执行count++中的赋值操作,线程B就开始读取count的数值,这时线程B读取到的数值是线程A操作前的count数值,
- 最终导致了A的修改丢失了。
用synchronized解决
给incur方法加上synchronized关键字,再执行发现获得的结果与预期相同。
public class Counter {
private int count;
public synchronized void incr(){
count ++;
}
public int getCount() {
return count;
}
}
理解synchronized
这里我们最直观的理解是:
- synchronized保护了incr方法中的变量,让incr方法可以作为一个原子操作,
- 即incr方法执行完成后,才会让其它线程访问这个变量。
但这个理解是不准确的。
准确的理解是,synchronized保护的是‘对象’而非代码(方法):
- 只要访问的是‘同一个对象’的方法,即使是不同的方法(这些方法需要由synchronized修饰),也会被同步顺序访问。
- 访问‘不同对象’的同一synchronized方法,不能保证顺序访问。
对应下面两个例子(只展示了主要的部分):
//同一对象调用不同synchronized方法
public class Counter {
private int count = 0;
public synchronized void incr(){
count++;
}
public synchronized void decr(){
count--;
}
public synchronized int getCount() {
return count;
}
}
public void run() { // 总是输出0
for (int i = 0; i < 100; i++) {
counter.incr(); // incr执行完会释放锁,即使是其它线程获得了锁,再执行incr,也不会影响总共会执行1000*100次decr,且都是顺序执行。
counter.decr();
}
}
//不同对象调用同一synchronized方法
Counter counter1 = new Counter();
Counter counter2 = new Counter();
Thread t1 = new CounterThread(counter1);
Thread t2 = new CounterThread(counter2);
t1.start();
t2.start();
synchronized方法不能防止非synchronized方法被同时执行。一般在保护变量(某个对象的变量)时,需要在所有访问该变量的方法上加上synchronized。
- 例如,如果将上面的decr方法的synchronized去掉,则不能再保证decr会顺序执行。
执行synchronized实例方法的过程大致如下:
1)尝试获得‘锁’,如果能够获得锁,继续下一步,否则加入‘等待队列’,阻塞并等待唤醒。
2)执行实例方法体代码。
3)释放锁,如果等待队列上有等待的线程,从中取一个并唤醒,如果有多个等待的线程,唤醒哪一个是不一定的,不保证公平性。
上面所说的‘锁’和‘等待队列’的概念,对于任意一个对象都有一个锁和等待队列:
- 这里说的锁就是对象本身,不同的线程抢夺这个对象,抢到了就说获得了锁,执行完相应逻辑后就释放锁。
- 等待队列可以视为一个排队的队列,队列里面是等待需要这个对象的线程。
多种写法
掌握了上面的概念后,不同的写法万变不离其宗。
- 写在具体的类中
- 实例方法
- 静态方法
- 代码块
- 写在线程类的run方法中
- 代码块
// 1.写在具体类
// 实例方法
public class StaticCounter {
private int count = 0;
public synchronized void incr() {
count++;
}
}
// 静态方法
public class StaticCounter {
private static int count = 0;
public static synchronized void incr() {
count++;
}
}
//代码块
public class Counter {
private int count;
public void incr(){
synchronized(this){ // 这里的this就是具体的Counter类的实例对象
count ++;
}
}
}
public class StaticCounter {
private static int count = 0;
public static void incr() {
synchronized(StaticCounter.class){ // StaticCounter.class是StaticCounter类
count++;
}
}
}
// 2.写在线程类的run方法中
public void run() {
synchronized (counter) { // 这里counter是保护的对象。这里如果写this代表的是线程类的实例对象。
counter.incr();
counter.decr();
}
}
总结
- synchronized保护的是‘对象’而非代码(方法)
- 任意一个对象都有一个锁和等待队列
参考
《Java编程的逻辑》
欢迎讨论指正