1、什么是线程安全性
线程安全性——多线程访问时,一个类可持续进行正确的行为。
只有成员变量位置的参数才会涉及到线程安全问题。所以没有成员变量的类,永远是线程安全的。
2、原子性
原子操作:一个操作要么不执行,要么完全执行完成,过程中不受其他线程干扰。
如果我们定义一个成员变量int count用于基数,然后在成员方法中进行++count操作,由于++count紧凑的语法结构,我们很容易认为这是一个原子操作,但是其实是三步“读、改、写”,如果在三步中其他线程对count进行该操作,就会出现线程安全问题。
2.1、解决办法
- 同步/加锁就可理解为把一块代码做了个原子操作的处理。但是这么做特别影响效率
- 利用java.util.concurrent.atomic包中的原子变量类,如AtomicLong、AtomicInteger等
Atomic原子变量类,以CAS(compare and swap)原子操作原理,以不加锁的方式对数字对象做原子操作
2.2、补充CAS(compare and swap)
CAS操作包含三个操作数——内存位置(V)、预期原值(A)、和新值(B)。执行CAS操作时,将内存位置的值与预期原值进行比较,如果相同,处理器会自动将该位置的值更新为新值。否则不做任何操作,(继续循环上面操作)。
这里面有个隐含前提:比较操作和赋新值的操作,在计算机底层命令中是个“原子命令”(一条命令)
AtomicInteger源码为例来看看CAS操作:
public final int getAndAdd(int delta) {
for (; ; ) {
int current = get();
int next = current + delta;
if (compareAndSet(current, next))
return current;
}
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
CAS原子操作目前常用的例子:
- 数据库表中通过添加版本号字段,配合一句sql(原子指令)来实现乐观锁
- Atomic包中对内存中数据的修改,依赖底层机器指令的一条原子操作(cmpxchg比较后修改)来实现
CAS的问题:
- ABA问题:因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有变化则
更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值
-
循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销
-
只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
3、锁
原子变量类在某些特殊场景使用时,可以实现原子操作。但是,更普遍的场景是,对一系列操作变成一个“原子操作”!
3.1、内部锁
java提供了强制原子性的内置锁机制:synchronized块,包括两部分:锁对象的引用和锁保护的代码块。synchronized修饰成员方法,锁就是对象本身;修饰静态方法,锁就是Class对象。
每个java对象都可以隐式扮演一个同步锁的角色,这些内置的锁被称为内部锁或者监视器锁。内部锁在java中同时也扮演了一些其他角色:
- 互斥锁的角色——一个锁最多可以有一个线程获得锁,其他线程必须阻塞或者等待
- 可重入——线程在获得锁的同时可以继续获取同类型锁并正常执行代码块中方法
对于可被多个线程访问的可变状态变量,如果所有访问他的线程在执行同一个锁,这种情况下,我们称这个变量是由这个锁保护的。
4、活跃度与性能
保证线程安全提供了简单性(同步整个方法)与并发性(同步尽可能短的代码路径)之间的平衡。请求和释放锁都需要开销,所以将synchronized块分解得过于琐碎是不合理的。
有些耗时操作,比如网络或者控制台I/O,难以快速完成,执行这些操作时尽量不要占用锁。