Java中的线程安全性介绍-创新互联

线程安全性概念

创新互联公司于2013年开始,先为岚皋等服务建站,岚皋等地企业,进行企业商务咨询服务。为岚皋企业网站制作PC+手机+微官网三网同步一站式服务解决您的所有建站问题。

当对一个复杂对象进行某种操作时,从操作开始到操作结束,被操作的对象往往会经历若干非法的中间状态。调用一个函数(假设该函数是正确的)操作某对象常常会使该对象暂时陷入不可用的状态(通常称为不稳定状态),等到操作完全结束,该对象才会重新回到完全可用的状态。如果其他线程企图访问一个处于不可用状态的对象,该对象将不能正确响应从而产生无法预料的结果,如何避免这种情况发生是线程安全性的核心问题。

原子概念

当方法调用似乎立即生效时,该方法就是原子的。 因此,其他线程在方法调用之前或之后只能看到状态,而没有中间状态。 让我们看一下非原子方法,看看原子方法如何使类具有线程安全性。

public class UniqueIdNotAtomic {
   private volatile long counter = 0;
   public  long nextId() { 
     return counter++;  
   }  
}

类UniqueIdNotAtomic通过使用易失性变量计数器创建唯一的ID。 我在第2行使用了volatile字段,以确保线程始终看到当前值,如此处更详细的说明。 要查看此类是否是线程安全的,我们使用以下测试:

public class TestUniqueIdNotAtomic {
   private final UniqueIdNotAtomic uniqueId = new UniqueIdNotAtomic();
   private long firstId;
   private long secondId;
   private void updateFirstId() {
     firstId  = uniqueId.nextId();
   }
   private void updateSecondId() {
     secondId = uniqueId.nextId();
   }
   @Test
   public void testUniqueId() throws InterruptedException {   
     try (AllInterleavings allInterleavings = 
         new AllInterleavings("TestUniqueIdNotAtomic");) {
     while(allInterleavings.hasNext()) { 
     Thread first = new Thread( () ->  { updateFirstId();  } ) ;
     Thread second = new Thread( () ->  { updateSecondId();  } ) ;
     first.start();
     second.start();
     first.join();
     second.join();  
     assertTrue(  firstId != secondId );
     }
     }
   }
}

为了测试计数器是否是线程安全的,我们需要在第16和17行中创建两个线程。我们启动这两个线程(第18和19行)。然后,我们等待直到两个线程都通过第20和21行结束。 在两个线程都停止之后,我们检查两个ID是否唯一,如第22行所示。

为了测试所有线程交织,我们使用来自vmlens第15行的AllInterleavings类,将完整的测试放在while循环中迭代所有线程交织。

运行测试,我们看到以下错误:

java.lang.AssertionError: 
   at org.junit.Assert.fail(Assert.java:91)
   at org.junit.Assert.assertTrue(Assert.java:43)

发生该错误的原因是,由于操作++不是原子操作,因此两个线程可以覆盖另一个线程的结果。 我们可以在vmlens的报告中看到这一点:

在发生错误的情况下,两个线程首先并行读取变量计数器。 然后,两个都创建相同的ID。 为了解决这个问题,我们通过使用同步块使方法原子化:

private final Object LOCK = new Object();
public  long nextId() {
  synchronized(LOCK) {
   return counter++;  
  } 
}

现在,该方法是原子的。 同步块可确保其他线程无法看到该方法的中间状态。

不访问共享状态的方法是自动原子的。 具有只读状态的类也是如此。 因此,无状态和不可变的类是实现线程安全类的简便方法。 他们所有的方法都是自动的。

并非原子方法的所有用法都是自动线程安全的。 将多个原子方法组合为相同的值通常会导致争用条件。 让我们看看从ConcurrentHashMap获取和放置的原子方法以了解原因。 当以前的映射不存在时,让我们使用这些方法在映射中插入一个值:

public class TestUpdateTwoAtomicMethods {
   public void update(ConcurrentHashMap  map)  {
       Integer result = map.get(1);     
       if( result == null )  {
         map.put(1, 1);
       }
       else   {
         map.put(1, result + 1 );
       }  
   }
   @Test
   public void testUpdate() throws InterruptedException   {
     try (AllInterleavings allInterleavings = 
      new AllInterleavings("TestUpdateTwoAtomicMethods");) {
     while(allInterleavings.hasNext()) { 
     final ConcurrentHashMap  map = 
      new  ConcurrentHashMap(); 
     Thread first = new Thread( () ->  { update(map);  } ) ;
     Thread second = new Thread( () ->  { update(map);  } ) ;
     first.start();
     second.start();
     first.join();
     second.join();  
     assertEquals( 2 , map.get(1).intValue() );
     }
     }
   }  
}

该测试与先前的测试相似。 再次,我们使用两个线程来测试我们的方法是否是线程安全的(第18行和第19行)。再次,我们在两个线程完成之后测试结果是否正确(第24行)。运行测试,我们看到以下错误:

java.lang.AssertionError: expected:<2> but was:<1>
   at org.junit.Assert.fail(Assert.java:91)
   at org.junit.Assert.failNotEquals(Assert.java:645)

该错误的原因是,两种原子方法get和put的组合不是原子的。 因此,两个线程可以覆盖另一个线程的结果。 我们可以在vmlens的报告中看到这一点:

在发生错误的情况下,两个线程首先并行获取值。 然后,两个都创建相同的值并将其放入地图中。 要解决这种竞争状况,我们需要使用一种方法而不是两种方法。 在我们的例子中,我们可以使用单个方法而不是两个方法get和put来进行计算:

public void update() {
  map.compute(1, (key, value) -> {
   if (value == null) {
     return 1;
   } 
   return value + 1;
  });
}

因为方法计算是原子的,所以这解决了竞争条件。 虽然对ConcurrentHashMap的相同元素进行的所有操作都是原子操作,但对整个地图(如大小)进行操作的操作都是静态的。 因此,让我们看看静态意味着什么。

“静止”是什么意思?

静态意味着当我们调用静态方法时,我们需要确保当前没有其他方法在运行。 下面的示例显示如何使用ConcurrentHashMap的静态方法大小:

ConcurrentHashMap  map = 
   new  ConcurrentHashMap();
Thread first  = new Thread(() -> { map.put(1,1);});
Thread second = new Thread(() -> { map.put(2,2);});
first.start();
second.start();
first.join();
second.join();  
assertEquals( 2 ,  map.size());

通过等待直到所有线程都使用线程连接完成为止,当我们调用方法大小时,我们确保没有其他线程正在访问ConcurrentHashMap。

方法大小使用在java.util.concurrent.atomic.LongAdder,LongAccumulator,DoubleAdder和DoubleAccumulator类中也使用的一种机制来避免争用。 与其使用单个变量存储当前大小,不如使用数组。 不同的线程更新数组的不同部分,从而避免争用。 该算法在Striped64的Java文档中有更详细的说明。

静态类和静态方法对于收集竞争激烈的统计数据很有用。 收集数据后,可以使用一个线程来评估收集的统计信息。

为什么在Java中没有其他线程安全方法?

在理论计算机科学中,线程安全性意味着数据结构满足正确性标准。 最常用的正确性标准是可线性化的,这意味着数据结构的方法是原子的。

对于常见的数据结构,存在可证明的线性化并发数据结构,请参见Maurice Herlihy和Nir Shavit撰写的《多处理器编程的艺术》一书。 但是要使数据结构线性化,需要使用比较和交换之类的昂贵同步机制,请参阅论文《定律:无法消除并发算法中的昂贵同步》以了解更多信息。

因此,研究了其他正确性标准(例如静态)。 因此,我认为问题不在于“为什么Java中没有其他类型的线程安全方法?” 但是,Java何时将提供其他类型的线程安全性?
结论

Java中的线程安全性意味着类的方法是原子的或静态的。 当方法调用似乎立即生效时,该方法就是原子的。 静态意味着当我们调用静态方法时,我们需要确保当前没有其他方法在运行。

目前,静态方法仅用于收集统计信息,例如ConcurrentHashMap的大小。 对于所有其他用例,使用原子方法。 让我们拭目以待,未来是否会带来更多类型的线程安全方法。

另外有需要云服务器可以了解下创新互联scvps.cn,海内外云服务器15元起步,三天无理由+7*72小时售后在线,公司持有idc许可证,提供“云服务器、裸金属服务器、高防服务器、香港服务器、美国服务器、虚拟主机、免备案服务器”等云主机租用服务以及企业上云的综合解决方案,具有“安全稳定、简单易用、服务可用性高、性价比高”等特点与优势,专为企业上云打造定制,能够满足用户丰富、多元化的应用场景需求。


网站栏目:Java中的线程安全性介绍-创新互联
浏览路径:http://scyanting.com/article/hidjg.html