您的位置:首页 > Web前端

Core Java Tutorial -- Thread Safety and Java Synchronization

2018-03-30 22:42 393 查看
Java 中的线程安全是一个重要的主题。Java 提供多线程环境支持 Java 线程,我们知道多线程由同一个 Object 创建共享对象变量,并且当线程用于读取和更新共享数据,可能会导致数据不一致。

Thread Safety

数据不一致的原因是因为更新任何字段的值不是一个原子操作,它需要三个步骤:首先获取当前值、其次要做必要的操作以获取更新的值,第三步将更新的值分配给字段引用。

让我们来用一个简单的程序来看这个问题,其中多个线程正在更新共享数据。

package Thread;

public class ThreadSafety {

public static void main(String[] args) {
ProcessingThread processingThread = new ProcessingThread();
Thread t1 = new Thread(processingThread);
t1.start();
Thread t2 = new Thread(processingThread);
t2.start();

// wait for threads to finish processing
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("Processing count = " + processingThread.getCount());
}

}

class ProcessingThread implements Runnable {
private int count;

@Override
public void run() {
for (int i = 0; i < 5; i++) {
processSomething(i);
count++;
}
}

public int getCount() {
return count;
}

private void processSomething(int i) {
// processing some job
try {
Thread.sleep(i * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}


上面的循环程序中,count 增加了 4 倍,并且由于我们由两个线程,所以线程完成执行后值应该是 8。但是当你多次运行程序时,你会发现计数值在 6 7 8 之间变化。发生这种情况是因为即使 count ++ 看起来是一个原子操作,其实并不是并且会导致数据损坏。

Thread Safety in Java

Java 中的线程安全,是让我们的程序在多线程环境中安全使用的过程,我们可以通过不同的方式使我们的程序线程安全。

Synchronization 是 Java 线程安全最简单并且最广泛使用的工具。

使用
java.util.concurrent.atomic
包的原子包装类。栗如
AtomicInteger


使用
java.util.concurrent.locks
包中的锁。

使用线程安全集合类,查看 ConcurrentHashMap 文章。

volatle 关键字与变量一起使用,使每个线程都从内存中读取数据,而不是从线程缓存中读取数据。

Java synchronized

同步是我们可以实现线程安全的工具,JVM 保证同步代码一次只能由一个线程执行。Java 关键字 synchonized 用于创建同步代码,并在内部使用 Object 或 Class 上的锁来确保只有一个线程正在执行同步代码。

Java 同步对资源的锁定和解锁起作用,在任何线程进入同步代码之前,必须获取对象的锁定,并且在代码执行结束时,解锁可被其他线程锁定的资源。同时其他线程处于等待状态以锁定同步资源。

我们可以通过两种方式使用 synchronized 关键字,一种是使完整的方法同步,另一种使创建同步块。

当一个方法是 synchronized,它锁定 Object,如果方法是静态的,它会锁定 Class,所以最好使用 synchronized 块来锁定需要同步的方法的唯一部分。

当创建同步块时,我们需要提供获取锁的资源,它可以是 XYZ.class 或类的任何对象字段。

synchronized(this)
将在进入同步块之前锁定对象。

你应该使用最低级别的锁定,栗如,如果某个类中由多个同步块,并且其中一个锁定了对象,则其他同步块也将不可供其他线程执行。当我们锁定一个对象时,它会获取该对象所有字段的锁定。

Java 同步损失性能来提供数据完整性,因此只有在绝对必要时才应使用它。

Java 同步只能在同一个 JVM 中工作,所以如果你需要在多个JVM环境中锁定一些资源,它将不起作用,你可能不得不照看一些全局锁定机制。

Java 同步可能导致死锁,请查看 (Java 死锁以及如何避免)[]https://www.journaldev.com/1058/deadlock-in-java-example]

Java synchronized 关键字不能被用于构造函数和变量。

最好创建一个用于同步块的虚拟私有对象,以便它的引用不能被任何其他代码修改。栗如:如果你有一个你正在同步的 Object 的 setter 方法,那么它的引用可以被一些其他的代码导致并行执行 synchronized 块。

我们不应该使用在常量池中维护的任何对象,栗如,字符串不应该用于同步,因为如果任何其他代码也锁定在同一个字符串上,它将尝试从字符串池获取相同引用对象的锁定,并且即使两个代码无关,他们也会互相锁定。

下面是我们在上面的程序中需要做的代码更改,以使它线程安全。

//dummy object variable for synchronization
private Object mutex=new Object();
...
//using synchronized block to read, increment and update count value synchronously
synchronized (mutex) {
count++;
}


让我们看看一些同步的例子,我们可以从中学到什么。

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() 方法在等待锁定时阻塞,这会导致系统进入死锁并导致 Denial of Service(DoS)。

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 对象是公共的,通过改变它的引用,我们可以在多个线程中执行并行块同步。类似的情况,如果你有私有对象,但有 setter 方法来改变它的引用。

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 正在获取类监视器上的锁定而不释放它,这将导致系统中的死锁和 DoS 。

这里是另一个例子,其中多个线程在相同的字符串数组上工作,并且一旦处理完毕,将线程名称附加到数组值。

package Thread;

import java.util.Arrays;

public class SynchronizedMethod {
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();
}
}

}


输出:

[1:t1, 2:t1:t2:t3, 3:t1:t2:t3, 4:t2:t3, 5:t1:t3, 6:t2:t1:t3]


字符串数组值因为共享数据而没有同步而被损坏。 以下是我们如何更改 addThreadName() 方法以使我们的程序线程安全。

private Object lock = new Object();
private void addThreadName(int i, String name) {
synchronized(lock){
strArr[i] = strArr[i] +":"+name;
}
}


改变后的输出:

[1:t1:t2:t3, 2:t1:t3:t2, 3:t1:t3:t2, 4:t2:t3:t1, 5:t2:t3:t1, 6:t1:t2:t3]
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: