一、线程安全
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,
或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象时线程安全的。
线程安全的代码都必须具备一个特征:代码本身封装了 所有毕业的正确性保障手段,令调用者无须关系多线程的问题,
更无须自己采用任何措施来保证多线程正确调用。
1、java语言中的线程安全
此处讨论线程安全,限定于多个线程之间存在数据访问的前提。
按照线程安全与强到弱排序,将java语言中各种操作共享数据分为5类:
- 不可变:不可变的对象一定是线程安全的,无论是对象的方式实现还是方法发调用者,都不需要采取任何线程安全保障措施。
如果共享数据是一个基本数据类型,那么只有在定义时使用final关键字修饰它,就可以保证他是不可变的。
如果共享数据是一个对象,那就要保证对象的行为不会对其自身状态产生任何影响。简单的方式就是把对象中带有状态的变量都声明为final,
这样在构造函数结束之后,他就是不可变的。
- 绝对线程安全:满足线程安全的定义“不管运行时环境如何,调用者都不需要任何额外的同步措施”
- 相对线程安全:就是通常意义上讲的线程安全,他需要保证对这个对象单独操作是线程安全的,在调用的时候不需要额外的保证措施,
对一些特定顺序的连续调用,就可以需要在调用端使用额外的同步手段来保证调用的正确性。
在java中大部分线程安全都属于此类,如Vector、HashTable、Collections的synchronizedCollection()方法包装的集合等。
- 线程兼容:指对象本身不是线程安全的,可以通过调用端正确的使用同步手段保证对象在并发环境中可以安全的使用。
平时常说的一个类不是线程安全的,绝大多数指的是这一种情况 - 线程对立:无论调用端是否采取了同步措施,都无法在多线程环境中并发使用代码。
2、线程安全的实现方法
2.1、互斥同步
互斥同步是常见的一种并发正确性保障手段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个线程使用。
互斥是实现同步的一种手段,临界区(Critical Selection)、互斥量(Mutex)、信号量(Semaphore) 都是主要的互斥实现方式。
因此互斥是因,同步是果,互斥是方法同步是目的。
java中最基本的互斥手段就是synchronized关键字,synchronized关键字经过编译后,会在同步块前后分拨形成monitorenter、monitorexit
两个字节码指令,,这两个字节码都需要一个reference类型参数来指明要锁定和解锁的对象。
如果synchronized明确指定了对象参数,那就是这个对象的reference;
如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法来作为所对象。
除了synchronized关键字外,还可以使用java.util.concurrent包中重入锁(ReentrantLock)来实现同步,ReentrantLock与synchronized
具备一样的线程重入特性。ReentrantLock高级特性:
- 等待可中断:指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情
- 可实现公平锁:指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来一次获得锁;而非公平锁不保证这一点。synchronized是非公平的
- 锁绑定多个条件:一个ReentrantLock对象可以同时绑定多个Condition对象。
2.2 非阻塞同步
互斥同步最主要问题就是进行线程阻塞和唤醒时带来的性能问题,这种同步也称为阻塞同步。
非阻塞同步:先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就采取其他补偿措施,
这种乐观的并发策略虚的实现都不需要讲线程挂起。
需要操作和冲突检测具备原子性,通过硬件来完成。语义上需要多次操作的行为只通过一条处理器指令就能完成,如:
- 测试并设置(Test-and-set)
- 获取并增加(Fetch-and-increment)
- 交换(swap)
- 比较并交换(Compare-and-swap,CAS)
- 加载连接/条件存储(Load-linked/Store-conditional,LL/SC)
CAS指令需要有3个操作数,分拨是内存地址(V)、旧的预期值(A)、新值(B)。CAS指令执行时,当且仅当V符合旧预期值A时,处理器
用新值B更新V的值,否则就不执行更新;无论是否更新了V,都会返回V的旧值,上述处理过程是一个原则操作。
2.3 无同步方案
如果一个方法本来就不涉及共享数据,就无需任何同步措施保证正确性。
- 可重入代码:也叫纯代码,可以在代码执行的任何时刻中断它,转而去执行另外一段代码,而在控制权返回后,原来程序不会出错。
可重入代码特征:不依赖存储在堆上的数据和公用的系统资源、用的状态量都由参数中传入、不调用非可重入的方法等。
判断代码是否具备可重入性:如果一个方法返回结果可预测,只有输入了相同的数据,都能返回相同的几个,那就满足可重入性的要求,也是线程安全的。
- 线程本地存储:
如果一段代码中所需的数据必须与其他代码共享,并且共享数据的可见范围限制在同一个线程内,那么无需同步也能保证线程之间不会出现数据争用。
如大部分消息队列架构模式:生产者-消费者模式,都将产品消费过程尽量在一个线程中消费完。
经典的Web交互模式中“的一个请求对应一个服务器线程”的处理方法。