全干工程师

Java-Logo

Java线程安全的几种方法

Java多线程并发是什么教程中,我们了解到,如果多个线程试图同时访问和更改相同的数据,那么在多线程环境中就会出现问题。这是一个严重的问题,因为它会导致执行死锁和数据损坏。允许多个线程同时更改相同数据结构或对象的程序被称为非线程安全程序。反之,如果一个程序是线程安全的,那么当一个线程已经在处理同一个对象时,其他线程就无法处理该对象。在本编程教程中,我们将探讨在 Java 程序中实现线程安全的四种相对简单的方法。

如果您错过了,我们建议您阅读本系列中关于 Java 线程的上一部分:Java多线程并发是什么

使用synchronized关键字

使程序线程安全的第一种方法是使用同步。简单地说,同步就是一次只允许一个线程完成特定操作的过程。它通过防止多个线程同时访问同一资源来解决不一致问题。同步使用synchronized关键字,它是一个特殊的修饰符,可以创建一个称为关键部分的代码块。

下面是一个 Java 程序示例,它在两个独立的线程上将一个值递增:

package com.developer;

public class Maths {
  void add5(int num) {
    // Create a thread instance
    Thread t = Thread.currentThread();
    for (int i = 1; i <= 5; i++) {
      System.out.println(t.getName() + " : " + (num + i));
    }
  }
}
package com.developer;
public class Maths2 extends Thread {
  Maths maths = new Maths();
  public void run() {
    maths.add5(10);
  }
}
package com.developer;
public class SynchronizationDemo {

  public static void main(String[] args) {
    Maths2 maths = new Maths2();
    
    Thread t1 = new Thread(maths);
    Thread t2 = new Thread(maths);
    
    t1.setName("Thread 1");
    t2.setName("Thread 2");
    
    t1.start();
    t2.start();
  }
}

不出所料,当每个线程同时访问同一个变量时,增量值会到处跳动:

Thread 1 : 11
Thread 1 : 12
Thread 1 : 13
Thread 2 : 11
Thread 1 : 14
Thread 2 : 12
Thread 2 : 13
Thread 1 : 15
Thread 2 : 14
Thread 2 : 15

add5()方法中添加同步(synchronized)关键字可以解决这个问题:

public class Maths {
  synchronized void add5(int num) {
    // Create a thread instance
    Thread t = Thread.currentThread();
    for (int i = 1; i <= 5; i++) {
      System.out.println(t.getName() + " : " + (num + i));
    }
  }
}

现在,每个线程依次完成其工作:

Thread 1 : 11
Thread 1 : 12
Thread 1 : 13
Thread 1 : 14
Thread 1 : 15
Thread 2 : 11
Thread 2 : 12
Thread 2 : 13
Thread 2 : 14
Thread 2 : 15

使用volatile关键字

在 Java 中实现线程安全的另一种方法是使用volatile关键字。它是一个字段修饰符,可确保对象可同时被多个线程使用,而不会导致上述问题行为。

下面的示例代码使用volatile关键字声明并实例化了两个整数:

package com.developer;

public class VolatileKeywordDemo {
  static volatile int int1 = 0, int2 = 0;
  
  static void methodOne() {
    int1++;
    int2++;
  }
  
  static void methodTwo() {
    System.out.println("int1=" + int1 + " int2=" + int2);
  }
  
  public static void main(String[] args) {
  
    Thread t1 = new Thread() {
      public void run() {
        for (int i = 0; i < 5; i++) {
          methodOne();
        }
      }
    };
    
    Thread t2 = new Thread() {
      public void run() {
        for (int i = 0; i < 5; i++) {
          methodTwo();
        }
      }
    };
    
    t1.start();
    t2.start();
  }
}

从程序输出中我们可以看到,在第二个线程输出变量值之前,第一个线程已经将这两个变量完全递增:

int1=5 int2=5
int1=5 int2=5
int1=5 int2=5
int1=5 int2=5
int1=5 int2=5

使用原子变量

在 Java 中实现线程安全的另一种方法是使用原子变量。顾名思义,原子变量允许开发人员对变量执行原子操作。原子变量可最大限度地减少同步,并有助于避免内存一致性错误。最常用的原子变量是AtomicIntegerAtomicLongAtomicBooleanAtomicReference。下面是一些使用AtomicInteger对两个独立计数器进行递增的 Java 示例代码,然后再输出它们的组合值:

package com.developer;
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
  AtomicInteger counter = new AtomicInteger();
  
  public void increment() {
    counter.incrementAndGet();
  }
}
package com.developer;

public class AtomicIntegerDemo {
  public static void main(String[] args) throws Exception {
  
    Counter c = new Counter();
    
    Thread t1 = new Thread(new Runnable() {
      public void run() {
        for (int i = 1; i <= 5000; i++) {
          c.increment();
        }
      }
    });
    
    Thread t2 = new Thread(new Runnable() {
        public void run() {
          for (int i = 1; i <= 5000; i++) {
            c.increment();
          }
        }
    });
    
    t1.start();
    t2.start();
    
    t1.join();
    t2.join();
    
    System.out.println(c.counter);
  }
}

运行上述程序,输出结果为 “10000”。

使用final关键字

在 Java 中,final变量始终是线程安全的,因为一个对象的引用一旦分配,就不能指向另一个对象。下面是一个演示如何在 Java 中使用final关键字的简短程序:

package com.developer;

public class FinalKeywordDemo {
  final String aString = new String("Immutable");
  
  void someMethod() {
    aString = "new value";
  }
}

集成开发环境(IDE)甚至不会让您运行上述代码,并会显示编译器错误,提示您试图为最终 aString 变量重新赋值:

java-final-keyword

总结

本教程介绍了在Java程序中实现线程安全的四种方法,即:使用同步(Synchronization)、volatile关键字、原子变量和final关键字。在 Java 中还有其他实现线程安全的方法,但这些方法需要开发人员付出稍多的努力。这些方法包括使用 java.util.concurrent.locks包中的锁,以及使用线程安全的集合类(如 ConcurrentHashMap)。

本文章翻译于Thread Safety in Java

留言