分享

你的程序线程安全吗?

 郭恩 2018-05-25

线程安全一直是程序里面需要特别注意但又经常忽略的问题,这篇文章讲下怎么判断程序是否是线程安全的?至于如何写出高并发,高性能的程序,在接下来几篇会讲。

聊一聊线程的历史

一想到线程,总是觉得历史是那么惊人的相似,所谓希望,不过是命运,所谓未来,不过是往昔。进程和线程的诞生总是伴随着人的贪欲的增长而产生的。
当一台大型的、资源昂贵的计算机只能跑一个程序的时候,进程就诞生了。当进程之间通信、切换变得不能满足需求的时候,线程诞生了。
然后我就开始了循环往复的工作,提升性能->解决并发安全问题->再提升性能->再解决并发安全问题...循环往复,直到满意。

什么是线程安全?

当多个线程访问某个类时 ,不管运行时环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不许要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。


首先了解几个概念:

无状态

  1. public class MyClass  {  
  2.     public void service(){  
  3.         String str = "less";  
  4.         System.out.println(str);  
  5.     }  
  6. }  

上面的类是无状态的,即:它不包含任何域,也不包含对其他域的引用,在运行过程中临时的状态保存在线程站上的局部变量表里面。
所以无状态的对象一定是线程安全的。大多数servlet都是无状态的。

原子性

如果我们在上面的类加上一个状态回事什么情况?
  1. public class MyClass  {  
  2.     private int count = 0;  
  3.     public void service(){  
  4.         String str = "less";  
  5.         count++;  
  6.         System.out.println(str);  
  7.     }  
  8. }  
很不幸,这样MyClass就是非线程安全的,为什么?因为count++在表意上看似乎是一个操作,可是在计算机来看这样一个操作被分成三步,1:取出count ,2:count+1 ,3:存储count,所以在多线程的环境下会出现线程A取到count的值,同时线程B取到count的值,然后A自增,B也自增,最后A把值存回去,B也存回去,期待的结果是自增两次,其实只结果只加了1。这说明ount++并非是原子性的。

竞态条件

当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。换句话说,正确的结果靠运气。

可见性

看看以下代码会出现什么状况:
  1. public class NoVisibility {  
  2.     private static boolean ready;  
  3.     private static int number;  
  4.   
  5.     private static class ReaderThread extends Thread {  
  6.         public void run() {  
  7.             while (!ready)  
  8.                 Thread.yield();  
  9.             System.out.println(number);  
  10.         }  
  11.     }  
  12.   
  13.     public static void main(String[] args) {  
  14.         new ReaderThread().start();  
  15.         number = 42;  
  16.         ready = true;  
  17.     }  
  18. }  
以上代码在没有进行同步的情况下会出现两种我们预想不到的结果,一个是输出0,另一个是一直循环一直不结束。为什么会出现这两种情况那?
没有同步的情况下,我们不能保证主线程启动的读线程可以读到主线程写入的值。另一个出现0的情况是因为”重排序“的原因,没用同步的时候我们不知到编译器,处理器会对某些操作进行顺序上的优化,很有可能执行顺序颠倒。所以可以将ready设置为volatile类型的。

不变性

满足同步需求的另一种做法是不可变对象,前面说了原子性,和可见性的一些问题,都与多线程试图同时修改同一个可变的状态相关。如果对象的状态不会改变,那么这些问题就自然小时了,所以不可变的对象一定是线程安全的
  1.  public final class ThreeStooges {  
  2.     private final Set<String> stooges = new HashSet<String>();  
  3.     public ThreeStooges() {  
  4.         stooges.add("Moe");  
  5.         stooges.add("Larry");  
  6.         stooges.add("Curly");  
  7.     }  
  8.     public boolean isStooge(String name) {  
  9.         return stooges.contains(name);  
  10.     }  
  11. }  
虽然set对象是可以修改的,但是在上面代码的设计中可以看到,在对Set对象构造完成后无法对其进行修改。stooges是一个final类型的引用变量。所以上面的示例是线程安全的。

总结下:可变状态至关重要,所有并发问题都可以归结为如何协调对可变状态的反问,可变状态越少,就越容易保证线程安全。
尽量将域声明为final类型。
不可变对象一定是线程安全的。
如果多个线程中访问同一个可变变量是没有同步机制,那么程序会出现问题。
将同步策略文档化
这一篇总结了下线程安全的基础问题,接下几篇会对问题进行分析,处理,以及优化。



    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多