Java中的线程安全
Java中的线程安全是一个非常重要的主题。Java通过Java线程提供多线程环境支持,我们知道从同一个对象创建的多个线程共享对象变量,当线程用于读取和更新共享数据时,这可能导致数据不一致性。
线程安全
数据不一致的原因是因为更新任何字段的值不是原子性操作,它需要三个步骤:首先读取当前值,然后执行必要的操作以获取更新后的值,并将更新后的值赋给字段引用。让我们通过一个简单的程序来检查这个情况,其中多个线程正在更新共享数据。
package com.Olivia.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循环,计数器递增了四次。由于我们有两个线程,当这两个线程都执行完后,计数器的值应该是8。但是当你多次运行上述程序时,你会发现计数器的值在6、7、8之间变化。这是因为即使count++看起来是一个原子操作,实际上它并不是,导致数据出现错误。
Java中的线程安全
Java中的线程安全是指使我们的程序在多线程环境中可以安全地使用的过程,有多种方法可以使我们的程序线程安全。
- Synchronization is the easiest and most widely used tool for thread safety in java.
- Use of Atomic Wrapper classes from java.util.concurrent.atomic package. For example AtomicInteger
- Use of locks from java.util.concurrent.locks package.
- Using thread safe collection classes, check this post for usage of ConcurrentHashMap for thread safety.
- Using volatile keyword with variables to make every thread read the data from memory, not read from thread cache.
Java同步机制
同步是我们实现线程安全的工具,JVM保证同步代码只会被一个线程执行。Java关键字synchronized用于创建同步代码,在内部它使用对象或类的锁来确保只有一个线程执行同步代码。
- Java synchronization works on locking and unlocking of the resource before any thread enters into synchronized code, it has to acquire the lock on the Object and when code execution ends, it unlocks the resource that can be locked by other threads. In the meantime, other threads are in wait state to lock the synchronized resource.
- We can use synchronized keyword in two ways, one is to make a complete method synchronized and another way is to create synchronized block.
- When a method is synchronized, it locks the Object, if method is static it locks the Class, so it’s always best practice to use synchronized block to lock the only sections of method that needs synchronization.
- While creating a synchronized block, we need to provide the resource on which lock will be acquired, it can be XYZ.class or any Object field of the class.
- synchronized(this) will lock the Object before entering into the synchronized block.
- You should use the lowest level of locking, for example, if there are multiple synchronized block in a class and one of them is locking the Object, then other synchronized blocks will also be not available for execution by other threads. When we lock an Object, it acquires a lock on all the fields of the Object.
- Java Synchronization provides data integrity on the cost of performance, so it should be used only when it’s absolutely necessary.
- Java Synchronization works only in the same JVM, so if you need to lock some resource in multiple JVM environment, it will not work and you might have to look after some global locking mechanism.
- Java Synchronization could result in deadlocks, check this post about deadlock in java and how to avoid them.
- Java synchronized keyword cannot be used for constructors and variables.
- It is preferable to create a dummy private Object to use for the synchronized block so that it’s reference can’t be changed by any other code. For example, if you have a setter method for Object on which you are synchronizing, it’s reference can be changed by some other code leads to the parallel execution of the synchronized block.
- We should not use any object that is maintained in a constant pool, for example String should not be used for synchronization because if any other code is also locking on same String, it will try to acquire lock on the same reference object from String pool and even though both the codes are unrelated, they will lock each other.
以下是我们需要在上述程序中进行的代码更改,以使其具备线程安全性。
//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();
请注意,锁定对象是公共的,通过更改其引用,我们可以在多个线程中并行执行同步块。如果您拥有私有对象但具有设置器方法来更改其引用,情况类似。
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
}
}
请注意,黑客代码正在锁定班级监视器并且未释放,这将导致系统出现死锁和拒绝服务攻击。以下是另一个例子,多个线程正在处理同一字符串数组,一旦处理完成,会将线程名称附加到数组值上。
package com.Olivia.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关键字的方法。