你的程序线程安全吗?

来源:互联网 发布:博优化纤招聘 编辑:程序博客网 时间:2024/05/21 09:11

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

聊一聊线程的历史

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

什么是线程安全?

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


首先了解几个概念:

无状态

public class MyClass  {public void service(){String str = "less";System.out.println(str);}}

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

原子性

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

竞态条件

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

可见性

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

不变性

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

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



0 0
原创粉丝点击