Java中的线程安全

时间:2020-02-23 14:37:27  来源:igfitidea点击:

Java中的线程安全是一个非常重要的主题。
Java使用Java线程提供了多线程环境支持,我们知道从同一个对象创建的多个线程共享对象变量,当这些线程用于读取和更新共享数据时,这可能导致数据不一致。

线程安全

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

让我们用一个简单的程序检查一下,其中多个线程正在更新共享数据。

package com.theitroad.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增加1到4倍,并且由于我们有两个线程,因此两个线程执行完后其值应为8。
但是当您多次运行上述程序时,您会注意到计数值在6,7,8之间变化。
发生这种情况是因为,即使count ++似乎是原子操作,也不会产生NOT并导致数据损坏。

Java中的线程安全

java中的线程安全是使我们的程序在多线程环境中可以安全使用的过程,可以通过多种方法来使程序线程安全。

  • 同步是Java中线程安全最简单,使用最广泛的工具。

  • 从java.util.concurrent.atomic包使用Atomic Wrapper类。
    例如AtomicInteger

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

  • 使用线程安全收集类,请检查此帖子以了解ConcurrentHashMap的线程安全用法。

  • 使用带有变量的volatile关键字,可使每个线程从内存中读取数据,而不是从线程缓存中读取数据。

Java同步

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

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

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

  • 同步方法时,它会锁定对象;如果方法是静态的,则它会锁定类,因此,最佳做法始终是使用同步块来锁定方法中唯一需要同步的部分。

  • 在创建同步块时,我们需要提供将在其上获取锁的资源,它可以是XYZ.class或者该类的任何Object字段。

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

  • 您应该使用最低级别的锁定,例如,如果一个类中有多个同步块,而其中一个正在锁定Object,则其他同步块也将不可用于其他线程执行。
    当我们锁定一个对象时,它获得了该对象所有字段的锁定。

  • Java同步提供了性能方面的数据完整性,因此仅在绝对必要时才应使用它。

  • Java同步仅在同一个JVM中起作用,因此,如果您需要在多个JVM环境中锁定某些资源,则它将无法正常工作,因此您可能需要照顾一些全局锁定机制。

  • Java同步可能会导致死锁,请查看有关Java死锁以及如何避免死锁的文章。

  • Java同步关键字不能用于构造函数和变量。

  • 最好创建一个虚拟私有对象以用于同步块,以免其他任何代码更改其引用。
    例如,如果您有一个要在其上同步的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)。

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但有一个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。

这是另一个示例,其中多个线程正在同一String数组上工作,并且一旦被处理,就将线程名附加到数组值中。

package com.theitroad.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]