来源: java-synchronization-and-thread-safety-tutorial-with-examples
Java通过Thread类支持多线程访问。我们知道,从同一对象创建的多个线程共享该对象的成员变量,当多个线程读写共享的成员变量时可能导致 data inconsistency (数据不一致)。
数据不一致的原因是更新任何一个成员变量都不是原子操作,它需要三步:首先从成员变量读取当前值,然后进行必要的操作得到新的值,最后将新的值赋给成员变量。
看下面这个简单的例子,多线程更新共享的数据:
java
package com.journaldev.threads;
public class ThreadSafety {
public static void main(String[] args) throws InterruptedException {
ProcessingThread pt = new ProcessingThread();
Thread t1 = new Thread(pt, "t1");
t1.start();
Thread t2 = new Thread(pt, "t2");
t2.start();
//wait for threads to finish processing
t1.join();
t2.join();
System.out.println("Processing count="+pt.getCount());
}
}
class ProcessingThread implements Runnable{
private int count;
@Override
public void run() {
for(int i=1; i< 5; i++){
processSomething(i);
count++;
}
}
public int getCount() {
return this.count;
}
private void processSomething(int i) {
// processing some job
try {
Thread.sleep(i*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上面程序中,count
在 for 循环中一共被执行4次自增操作,由于有两个线程,所以 count
在两个线程结束后应该为 8。但你运行几次后会发现,count
值并不总是8,而是在6、7和8之间变化。原因是,虽然 count++ 看起来像是原子操作,但实际上并不是,所以导致数据不一致。
Java线程安全
线程安全是指让程序安全地在多线程环境中执行,有不同的方式来实现线程安全。
- 同步(synchronization)是Java中最简单的,也是使用最广泛的线程安全方法
- 使用
java.util.concurrent.atomic
包中的原子包装类,比如AtomicInteger
- 使用
java.util.concurrent.locks
包中的锁 - 使用线程安全的集合类,可以参考关于 ConcurrentHashMap 另一篇文章
- 使用
volatile
关键字强制线程从主存中读数据,而不是从线程缓存中读数据
Java同步
同步(synchronization)用于保证线程安全,JVM保证同一时刻只有一个线程能执行同步的代码。Java中的 synchronized
关键字用于创建同步代码。synchronized 在内部给对象(Object)或类(Class)加锁来保证同一时刻只有一个线程执行同步代码。
- Java同步通过对资源加锁和解锁来工作,在任何线程进入同步代码前,它会要求锁定对象。当线程执行完代码后,解除对象上的锁定,允许其它线程锁定对象。而在对象已经被某个线程锁定期间,其他线程会处于wait状态等待锁定这个对象
synchronized
关键字有两种使用方法,一是让整个方法成为同步方法,二是仅创建同块代码块- 同步方法会锁定 对象,静态的同步方法会锁定 类,所以最佳实践是使用同步代码块锁定方法中的确需要同步的那部分代码
- 创建同步代码块时,需要提供需要被锁定的资源,它可以是 XYZ.class 或类中的任何成员对象
synchronized(this)
将在进入同步代码块中锁定当前对象- 应当使用最少的锁定(lowest level of locking),比如一个类中有多个同步块,其中一个同步块锁定对象,那么别的线程不能执行其他的同步块。锁定对象时,会锁定对象的所有成员变量
- Java的同步通过损失性能的方式来保证数据完整性,所以应当仅在绝对必要时使用同步
- Java的同步仅在同一JVM内起作用,所以如果要锁定跨多个JVM的资源,Java同步不起作用,你需要使用其它的全局锁定机制
- Java的同步可能引起死锁,请参考这篇文章 deadlock in java and how to avoid them
- Java的
synchronized
关键字不能用于构造方法或局部变量 - 更好的做法是创建私有的Object对象专门用于同步代码块,这个对象引用不会被修改。比如,你将一个有 setter、可能被修改的成员变量用于同步,这个成员可能被修改,结果导致同步代码块可以同时被不同线程执行(错误!)
- 不应使用常量池中的对象进行同步,比如千万不要使用String对象来进行同步,因为其他线程可能也锁定了同一个String对象,由于String对象可能来自常量池,结果两个完全不相关的线程却相互影响
通过如下修改可以保证前面的代码线程安全:
java
//dummy object variable for synchronization
private Object mutex=new Object();
...
//using synchronized block to read, increment and update count value synchronously
synchronized (mutex) {
count++;
}
来看一个同步的例子。
java
public class MyObject {
// Locks on the object's monitor
public synchronized void doSomething() {
// ...
}
}
// Hackers code
MyObject myObject = new MyObject();
synchronized (myObject) {
while (true) {
// Indefinitely delay myObject
Thread.sleep(Integer.MAX_VALUE);
}
}
Hackers code尝试锁定 myObject
实例,并且一旦锁定后就不同释放锁,导致 doSomething()
方法会等待锁并一直阻塞,结果系统进入死锁,引起拒绝服务(DoS)。
java
public class MyObject {
public Object lock = new Object();
public void doSomething() {
synchronized (lock) {
// ...
}
}
}
//untrusted code
MyObject myObject = new MyObject();
//change the lock Object reference
myObject.lock = new Object();
注意,上面的代码中 lock
对象是 public
的,通过修改 lock
,可以多线程执行同步代码。就算 lock
是 private
的,但如果可通过 setter 修改,仍然会有同样问题。
java
public class MyObject {
//locks on the class object's monitor
public static synchronized void doSomething() {
// ...
}
}
// hackers code
synchronized (MyObject.class) {
while (true) {
Thread.sleep(Integer.MAX_VALUE); // Indefinitely delay MyObject
}
}
Hackers code会得到MyObject类的锁,且不释放,同样会导致死锁和拒绝服务(DoS)。
下面是另一个例子,多个线程遍历字符串数组,每处理完一个字符串,将线程名添加到该字符串后面。
java
package com.journaldev.threads;
import java.util.Arrays;
public class SyncronizedMethod {
public static void main(String[] args) throws InterruptedException {
String[] arr = {"1","2","3","4","5","6"};
HashMapProcessor hmp = new HashMapProcessor(arr);
Thread t1=new Thread(hmp, "t1");
Thread t2=new Thread(hmp, "t2");
Thread t3=new Thread(hmp, "t3");
long start = System.currentTimeMillis();
//start all the threads
t1.start();t2.start();t3.start();
//wait for threads to finish
t1.join();t2.join();t3.join();
System.out.println("Time taken= "+(System.currentTimeMillis()-start));
//check the shared variable value now
System.out.println(Arrays.asList(hmp.getMap()));
}
}
class HashMapProcessor implements Runnable{
private String[] strArr = null;
public HashMapProcessor(String[] m){
this.strArr=m;
}
public String[] getMap() {
return strArr;
}
@Override
public void run() {
processArr(Thread.currentThread().getName());
}
private void processArr(String name) {
for(int i=0; i<strArr.length; i++){
//process data and append thread name
processSomething(i);
addThreadName(i, name);
}
}
private void addThreadName(int i, String name) {
strArr[i] = strArr[i] +":"+name;
}
private void processSomething(int index) {
// processing some job
try {
Thread.sleep(index*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上面程序的输出是
Time taken= 15005
[1:t2:t3, 2:t1, 3:t3, 4:t1:t3, 5:t2:t1, 6:t3]
字符串数组中的值不一致,原因是共享的数据未同步。修改 addThreadName()
方法保证线程安全,代码如下:
private Object lock = new Object();
private void addThreadName(int i, String name) {
synchronized(lock){
strArr[i] = strArr[i] +":"+name;
}
}
修改后,程序输出正常:
Time taken= 15004
[1:t1:t2:t3, 2:t2:t1:t3, 3:t2:t3:t1, 4:t3:t2:t1, 5:t2:t1:t3, 6:t2:t1:t3]