Crab213's Blog.

Java并发编程实践笔记1:线程安全基础

2016/03/30

线程的优点

现代计算机通常都有多个处理器。线程作为大多数操作系统的调度单位可以有效提升计算机的处理效率。在处理阻塞IO的问题上线程更加占有优势,当IO阻塞时,被阻塞的线程等待,其他线程可以继续执行,相互不影响。

线程可以简化程序的模型。当我们将所有操作放在同一个进程中时,程序的模型会变得及其复杂。我们必须在同一个进程中处理所有的事务。这就意味着我们不能轻易的让进程阻塞,必须要使用非阻塞IO。同时,我们需要在进程中处理多个任务,而经验告诉我们,一个进程处理一个事情会使程序简化,不易出错。

线程允许我们写出相应速度更快的UI。我们可以将UI的事件处理放在同一个线程中,将其他任务放在其它线程中,使UI时间可以即时得到响应。

线程带来的风险

安全性问题。如果缺乏有效的同步机制,很多在单线程模型下正常工作的代码在多线程状况下会出现很多神奇的错误。因为线程共享一个内存空间,在内存共享时会发生冲突。如果没有同步机制的保证,那么一个被共享的内存地址上的数值可能在任何时间任何条件下被更改,甚至就在两条连续的语句内都会出错。而我们最大的敌人是不确定性,任何有问题的代码可能在百分之九十九的情况下不会出错,但是你永远不能保证下一秒不会出问题。

活性(liveness)问题。活性问题是在安全性问题之上的额外问题。在多线程环境下,有时你无法保证线程能正常结束。进程会进入一个无法结束的状态,比如死锁。像很多多线程bug一样,活性问题很难被直接发现,因为只有在某些特性的时序之下这个问题才会被暴露。

执行效率问题。在上面两个问题都得到解决之后,问题还没结束。进程调度和锁的开销可能会严重影响性能。

线程安全的定义

一个类是线程安全的,只有当它被多个线程个共享时,不论怎样的调度环境,它都能表现出正确的行为,不需要在线程中使用额外的同步机制。

保证线程安全的方式

无状态对象。一个对象如果不包含内部状态,那么线程在使用这个对象时,所有的信息都会被保存在线程的栈上。因此一个线程使用这个对象的时候不会改变这个对象的状态,因为这个对象没有状态,也就不会影响其他线程对这个对象的使用。因此没有状态的对象是线程安全的。

原子性质。原子被认为是不可分的,原子操作就是那些不可以继续分解的操作。一个原子操作只有未执行和已执行两种状态,不存在执行中这个状态。资源竞争(race condition)是一种多线程环境常见的问题,它指的是一个计算的正确性依赖于多个线程的特定时序。比如一个简单的自增操作,它分为三步,读取,自增,写入。如果两个线程同时执行这个操作,那么显而易见,这两个线程必须完全叉开才可以正常执行这个操作而不发生干扰。上面这个问题的根源是自增操作不适原子操作,他是一个组合操作(compound action)。

锁。锁是在多线程编程中最常用的元素。线程安全的本质是安全的同步机制,其他都是实现的手段。无状态对象,原子性质本身就是同步的,对于组合操作,我们可以用锁来使其行为不可分。java提供了内建的锁机制,synchronized块,这个块有两个组成部分:充当锁的对象,以及被锁保护的代码。每个java对象都可以当作锁,这个内建的锁叫做自有锁(intrisic lock)或者监视器锁(monitor lock)。synchronized关键字修饰的方法是一种简写,亦即这个方法都是被锁同步的代码,而锁对象就是这个方法所属的对象本身。自有锁是一种互斥锁(mutex, mutual exclusion lock),这个锁只允许一个人持有这个锁,其他的人会原地等待。同时自由锁是可重入(reentrancy)的,也就是说锁的保护对象是线程,同一个线程在多次请求同一个锁的时候,不会陷入死循环。jvm会保留这个锁当前的持有者和一个计数器,如果持有者请求这个锁,计数器会加一,释放这个锁时,计数器减一,计数器为零是,表示这个锁被完全释放。鉴于互斥锁的性质,我们可以使用互斥锁保护关键区域,使多线程环境下只有一个线程有权利执行关键区域的代码,从而保证一些关键的组合操作不会被其他线程干扰。但是需要注意的是,互斥锁会使其他线程进入等待状态,会使性能严重受到影响,所以粗粒度的互斥锁会严重影响性能,同时互斥锁同步的代码里坚决不能出现阻塞操作,这样会使所有线程进入停滞状态。

CATALOG
  1. 1. 线程的优点
  2. 2. 线程带来的风险
  3. 3. 线程安全的定义
  4. 4. 保证线程安全的方式