Java 中的线程安全
Java 中的线程安全是一个非常重要的主题。Java 使用 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();
}
}
}
在上面的 for 循环程序中,count增加了四次,由于我们有两个线程,所以在两个线程执行完成后,它的值应该是 8。但是当你多次运行上面的程序时,你会注意到 count 值在 6、7、8 之间变化。发生这种情况是因为即使count++似乎是一个原子操作,但它不是,并且会导致数据损坏。
Java 中的线程安全
Java 中的线程安全是使我们的程序在多线程环境中安全使用的过程,我们可以通过不同的方式使我们的程序线程安全。
- 同步是java中线程安全最简单、应用最广泛的工具。
- 使用java.util.concurrent.atomic包中的 Atomic Wrapper 类。例如 AtomicInteger
- 使用java.util.concurrent.locks包中的锁。
- 使用线程安全集合类,检查此帖子以了解ConcurrentHashMap的线程安全性用法。
- 使用带有变量的 volatile 关键字使得每个线程都从内存中读取数据,而不是从线程缓存中读取。
Java 同步
同步是我们可以用来实现线程安全的工具,JVM 保证同步代码一次只能由一个线程执行。java关键字 synchronized用于创建同步代码,并且在内部它使用对象或类上的锁来确保只有一个线程正在执行同步代码。
- Java 同步致力于锁定和解锁资源,在任何线程进入同步代码之前,它必须获取对象上的锁,当代码执行结束时,它会解锁其他线程可以锁定的资源。与此同时,其他线程处于等待状态以锁定同步资源。
- 我们可以通过两种方式使用synchronized关键字,一种是使整个方法同步,另一种方式是创建synchronized块。
- 当一个方法被同步时,它会锁定Object,如果方法是静态的,它会锁定Class,因此最佳做法是使用同步块来锁定需要同步的方法部分。
- 在创建同步块时,我们需要提供将获取锁的资源,它可以是 XYZ.class 或该类的任何对象字段。
synchronized(this)
将在进入同步块之前锁定对象。- 你应该使用最低级别的锁定,例如,如果一个类中有多个同步块,并且其中一个锁定了 Object,那么其他同步块也将无法被其他线程执行。当我们锁定一个 Object 时,它会获取 Object 所有字段的锁定。
- Java 同步以性能为代价来提供数据完整性,因此只有在绝对必要时才应使用它。
- Java 同步仅在同一个 JVM 中有效,因此如果您需要在多个 JVM 环境中锁定某些资源,它将不起作用,您可能需要注意某些全局锁定机制。
- Java 同步可能会导致死锁,请查看有关Java 中的死锁以及如何避免死锁的帖子。
- Java synchronized 关键字不能用于构造函数和变量。
- 最好创建一个虚拟私有对象用于同步块,这样它的引用就不能被任何其他代码更改。例如,如果您对要同步的对象有一个 setter 方法,它的引用可能会被其他一些代码更改,从而导致同步块的并行执行。
- 我们不应该使用任何维护在常量池中的对象,例如 String 不应该用于同步,因为如果任何其他代码也锁定同一个 String,它将尝试从String 池中获取对同一个引用对象的锁定,即使两个代码不相关,它们也会互相锁定。
以下是我们需要在上述程序中做的代码更改,以使其线程安全。
//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);
}
}
请注意,黑客的代码正在尝试锁定 myObject 实例,并且一旦获得锁定,它就永远不会释放它,从而导致 doSomething() 方法在等待锁定时阻塞,这将导致系统陷入死锁并导致拒绝服务(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();
请注意,锁定 Object 是公共的,通过更改其引用,我们可以在多个线程中并行执行同步块。如果您有私有 Object,但有一个 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
}
}
请注意,黑客代码正在获取类监视器上的锁,但不会释放它,这将导致系统中出现死锁和 DoS。这是另一个示例,其中多个线程正在处理同一个字符串数组,处理后,将线程名称附加到数组值。
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]
由于共享数据且没有同步,String 数组值被破坏。下面介绍如何更改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]
这就是关于 Java 中的线程安全的全部内容,我希望您了解线程安全编程和使用 synchronized 关键字。