Java中的线程安全
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]