Java并发

并发产生的原因:

在直观上,我们的机器是多任务并行执行的,但是在计算机内部的某个时刻时,只能有 一个进程在执行。
在单处理器中:由于进程的相对执行速度不可预测,它取决于其他进程的活动、操作系统处理中断的方式以及操作系统的高度策略。
在分布式环境下:对共享资源的依赖,就会有并发问题

并发问题的解决(系统层面):

操作系统解决并发问题一般是通过互斥,为了提供互斥的条件,需要满足以下的需求:

  • 一次只允许一个进程进入临界区
  • 一个非临界区停止的进程必须不干涉其他进程
  • 不允许出现一个需要访问临界区的进程被无限延迟
  • 一个进程驻留在临界区中的时间必须是有限的
  • 临界区空闲时,任何需要进入临界区的进程必须能够立即进入

这里有如下的一些解决方案:

  • 硬件支持
    • 中断禁用
    • 专用机器指令
  • 信号量
  • 管程
  • 消息传递

写线程安全的JAVA代码:

在写JAVA程序的时候,什么时候需要进行并发控制,关键在于判断这段程序或这个类是否是线程安全的。如果多个线程访问一个类时,不用考虑这些线程在运行时环境下的的调度与交替执行,并且也不需要进行额外的同步,那么称这个类为线程安全的,否则就要对其进行必要的设计以满足高并发的情况下的资源共享等问题。线程安全的类可以通过以下手段来满足:

  • 不跨线程共享变量
  • 使状态变量为不可变的
  • 在任何访问状态变量的时候使用同步。
  • 每个共享的可变变量都需要由唯一一个确定的锁保护。

满足线程安全的一些思路:

  • 从源头避免并发问题:很多开发者一想到有并发的可能就通过底层技术来解决问题,其实往往可以通过上层的架构设计和业务分析来避免并发场景。比如我们需要用多线程或分布式集群来计算一堆客户的相关统计值,由于客户的统计值是共享数据,因此会有并发潜在可能。但从业务上我们可以分析出客户与客户之间数据是不共享的,因此可以设计一个规则来保证一个客户的计算工作和数据访问只会被一个线程或一台工作机完成,而不是把一个客户的计算工作分配给多个线程去完成。这种规则很容易设计。当你从源头就避免了并发问题的可能,下面的工作就完全可以不用担心线程安全问题。

  • 无状态就是线程安全:多线程编程或者分布式编程最忌讳有状态,有状态不但限制了其横向扩展能力,也是产生并发问题的起源。当你设计的类是无状态的,那么它永远都是线程安全的。因此在设计阶段需要考虑如何用无状态的类来满足你的业务需求。

  • 锁的合理使用:大家都知道可以用锁来解决并发问题,但在具体使用上还有很多讲究。每个共享的可变变量都需要由一个个确定的锁保护。一旦使用了锁,就意味着这段代码的执行就丧失了操作系统多道程序的特性,会在一定程度上影响性能。锁不能解决在分布式环境共享变量的并发问题

  • 分清原子性操作和复合操作:所谓原子性,是说一个操作不会被其他线程打断,能保证其从开始到结束独享资源连续执行完这一操作。如果所有程序块都是原子性的,那么就不存在任何并发问题。而很多看上去像是原子性的操作正式并发问题高灾区。比如所熟知的计数器(count++)和check-then-act,这些都是很容易被忽视的,例如大家所常用的惰性初始化模式,以下代码就不是线程安全的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @NotThreadSafe  
    public class LazyInitRace {  
        private ExpensiveObject instance = null;  
        public ExpensiveObject getInstance() {  
            if (instance == null)  
                instance = new ExpensiveObject();  
            return instance;  
        }  
    }

这段代码具体问题在于没有认识到if(instance==null)和instance = new ExpensiveObject();是两条语句,放在一起就不是原子性的,就有可能当一个线程执行完if(instance==null)后会被中断,另一个线程也去执行if(instance==null),这次两个线程都会执行后面的instance = new ExpensiveObject();这也是这个程序所不希望发生的。
虽然check-then-act从表面上看很简单,但却普遍存在与我们日常的开发中,特别是在数据库存取这一块。比如我们需要在数据库里存一个客户的统计值,当统计值不存在时初始化,当存在时就去更新。如果不把这组逻辑设计为原子性的就很有可能产生出两条这个客户的统计值。
在单机环境下处理这个问题还算容易,通过锁或者同步来把这组复合操作变为原子操作,但在分布式环境下就不适用了。一般情况下是通过在数据库端做文章,比如通过唯一性索引或者悲观锁来保障其数据一致性。当然任何方案都是有代价的,这就需要具体情况下来权衡。
另外,java1.5以后提供了一套提供原子性操作的类,有兴趣的可以研究一下它是如何在软件层面保证原子性的。

Leo wechat
欢迎订阅公众号,建设中!