【译】Java中的线程安全

原文www.journaldev.com/1061/thread…

Java中的线程安全

Java中的线程安全是一个非常重要的话题。Java使用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 synchronized

我们可以使用同步工具来实现线程安全,JVM保证同步代码一次只能由一个线程执行。 java关键字synchronized用于创建同步代码,内部它使用对象锁或类锁来确保只有一个线程正在执行同步代码。

  • Java同步在上锁和解锁资源时起作用,在任何线程进入同步代码之前,它获取对象锁,在代码执行结束后释放资源以便其他线程锁定。与此同时,其他线程处于等待状态以便锁定同步资源。
  • 我们有两种使用synchronized关键字的方式,一种是使整个完整的方法同步,另一种是创建同步代码块。
  • 当一个方法是同步方法时,它锁住的是当前对象,如果是静态方法,则锁住的这个类,因此最好使用同步代码块来锁住方法中需要同步的部分。
  • 在我们创建同步代码块时,我们需要提供获取锁的资源,它可以是XYZ.class或类的任何Object字段。
  • synchronized(this) 在进入同步代码块之前锁定对象。
  • 您应该使用最低级别的锁定,例如:如果类中有多个同步代码块且其中一个锁定了当前对象,则其他同步代码块都无法由其他线程执行。当我们锁定一个对象时,它会获取当前对象所有字段的锁定。
  • Java同步以降低性能为代价来保证数据的完整性,因此在绝对必要时才应使用它。
  • Java同步仅在同一个JVM里有效,所以如果你需要在多个JVM环境中锁定某些资源,Java同步将失效,您可能需要思考一些全局锁定机制。
  • Java同步可能会导致死锁,请查看有关java中死锁以及如何避免死锁的帖子。
  • Java synchronized关键字不能用于构造函数和变量。
  • 最好创建一个用于同步代码块的虚拟私有对象,这样它的引用就不能被任何其他代码更改。例如,如果Object的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))。

猜你喜欢

转载自juejin.im/post/5d53d96df265da03eb13c1d4