共计 3235 个字符,预计需要花费 9 分钟才能阅读完成。
一个计数器
对于普通的变量,在涉及多线程操作时,会遇到经典的线程安全问题。考虑如下代码:
private static final int TEST_THREAD_COUNT = 100;
private static int counter = 0;
public static void main(String[] args) {final CountDownLatch latch = new CountDownLatch(TEST_THREAD_COUNT);
Thread[] threads = new Thread[TEST_THREAD_COUNT];
for (int i = 0; i < TEST_THREAD_COUNT; i++) {threads[i] = new Thread(new Runnable() {
@Override
public void run() {
++counter;
System.out.println("Thread" + Thread.currentThread().getId() + "/ Counter :" + counter);
latch.countDown();}
});
threads[i].start();}
try {latch.await();
System.out.println("Main Thread" + "/ Counter :" + counter);
} catch (InterruptedException e) {e.printStackTrace();
}
}
多次 执行这段程序,我们会发现最后 counter
的值会出现 98
,99
等值,而不是预想中的100
。
...
...
Thread 100 / Counter : 90
Thread 101 / Counter : 91
Thread 102 / Counter : 92
Thread 103 / Counter : 93
Thread 104 / Counter : 95
Thread 105 / Counter : 95
Thread 106 / Counter : 96
Thread 107 / Counter : 97
Thread 108 / Counter : 98
Thread 109 / Counter : 99
Main Thread / Counter : 99
这个问题发生的原因是 ++counter
不是一个 原子性 操作。当要对一个变量进行计算的时候,CPU 需要先从内存中将该变量的值读取到 高速缓存 中,再去计算,计算完毕后再将变量同步到主内存中。这在多线程环境中就会遇到问题,试想一下,线程 A 从主内存中复制了一个变量 a
= 3 到工作内存,并且对变量a
进行了加一操作,a
变成了 4, 此时线程 B 也从主内存中复制该变量到它自己的工作内存,它得到的 a 的值还是 3,a
的值不一致了(这里工作内存就是 高速缓存)。
同步
java 有个 sychronized
关键字,它能后保证同一个时刻只有一条线程能够执行被关键字修饰的代码,其他线程就会在队列中进行等待,等待这条线程执行完毕后,下一条线程才能对执行这段代码。
它的修饰对象有以下几种:
- 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号 {} 括起来的代码,作用的对象是调用这个代码块的对象;
- 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
- 修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
- 修饰一个类,其作用的范围是 synchronized 后面括号括起来的部分,作用主的对象是这个类的所有对象。
现在我们开始使用我们的新知识,调整以上代码,在 run()
上添加 sychronized
关键字。
private static final int TEST_THREAD_COUNT = 100;
private static int counter = 0;
public static void main(String[] args) {final CountDownLatch latch = new CountDownLatch(TEST_THREAD_COUNT);
Thread[] threads = new Thread[TEST_THREAD_COUNT];
for (int i = 0; i < TEST_THREAD_COUNT; i++) {threads[i] = new Thread(new Runnable() {
@Override
public synchronized void run() {
++counter;
System.out.println("Thread" + Thread.currentThread().getId() + "/ Counter :" + counter);
latch.countDown();}
});
threads[i].start();}
try {latch.await();
System.out.println("Main Thread" + "/ Counter :" + counter);
} catch (InterruptedException e) {e.printStackTrace();
}
}
多次 执行新代码,我们依旧发现结果不正确:
...
...
Thread 98 / Counter : 87
Thread 97 / Counter : 86
Thread 99 / Counter : 89
Thread 100 / Counter : 89
Thread 101 / Counter : 90
Thread 102 / Counter : 91
Thread 104 / Counter : 95
Thread 108 / Counter : 97
Thread 106 / Counter : 96
Thread 105 / Counter : 95
Thread 103 / Counter : 95
Thread 109 / Counter : 98
Thread 107 / Counter : 97
Main Thread / Counter : 98
这里的原因在于 synchronized
是锁定当前 实例对象 的代码块。也就是当多条线程操作同一个实例对象的同步方法是时,只有一条线程可以访问,其他线程都需要等待。这里 Runnable
实例有多个,所以锁就不起作用。
我们继续修改代码,使得 Runnable
实例只有一个:
private static final int TEST_THREAD_COUNT = 100;
private static int counter = 0;
private final static CountDownLatch latch = new CountDownLatch(TEST_THREAD_COUNT);
static class MyRunnable implements Runnable {
@Override
public synchronized void run() {
++counter;
System.out.println("Thread" + Thread.currentThread().getId() + "/ Counter :" + counter);
latch.countDown();}
}
public static void main(String[] args) {Thread[] threads = new Thread[TEST_THREAD_COUNT];
MyRunnable myRun = new MyRunnable();
for (int i = 0; i < TEST_THREAD_COUNT; i++) {threads[i] = new Thread(myRun);
threads[i].start();}
try {latch.await();
System.out.println("Main Thread" + "/ Counter :" + counter);
} catch (InterruptedException e) {e.printStackTrace();
}
}
现在我们发现多次执行代码后,最后结果都是 100
。
我们可以给 counter
变量添加 volatile
关键字(这里它对于结果没有影响)。
当一个变量被定义为 volatile 之后,它对所有的线程就具有了可见性,也就是说当一个线程修改了该变量的值,所有的其它线程都可以立即知道。