你的程序线程安全吗?

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

聊一聊线程的历史

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

什么是线程安全?

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


首先了解几个概念:

无状态

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
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要避免线程安全问题,可以采取以下几种方法: 1. 互斥锁(Mutex):使用互斥锁可以保证同一时间只有一个线程可以访问共享资源。在访问共享资源之前,线程需要先获取互斥锁,完成操作后再释放互斥锁。 2. 信号量(Semaphore):信号量可以控制对共享资源的访问数量。可以设置一个信号量来限制同时访问某个共享资源的线程数量。 3. 条件变量(Condition Variable):条件变量用于线程之间的通信和同步。一个线程可以等待某个条件发生,而另一个线程可以在某个条件满足时通知等待的线程。 4. 原子操作(Atomic Operation):原子操作是不可被中断的操作,可以保证在多线程环境下的数据一致性。原子操作可以通过使用特定的原子类型或者锁来实现。 5. 线程安全的数据结构:使用线程安全的数据结构,如线程安全的队列、哈希表等,可以避免多线程操作共享资源时的竞争问题。 6. 避免共享数据:尽量避免多个线程对同一份数据进行读写操作,可以通过将数据复制给每个线程或者使用局部变量来避免竞争。 7. 合理划分任务:合理划分任务可以减少线程之间的竞争,例如将一个大任务拆分成多个小任务,每个线程独立处理一个小任务。 8. 同步工具类:使用同步工具类,如读写锁、倒计时门等,可以帮助实现线程之间的同步和互斥。 通过以上方法,可以有效地避免线程安全问题,提高多线程程序的性能和稳定性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值