思维导图
1 线程安全性
当多个线程访问一个类,不用考虑这个线程所处环境,并且
不需要调用方额外的代码做出协调
,这个类仍然可以保持正确性,则称这个类为线程安全。
线程安全类如java中Vector许多涉及改变状态的方法都使用了内置锁synchronized
如下:
Vector作为线程安全类,封装了必要的同步,因此外部使用的客户不需要自己提供额外操作。
无状态对象是永远线程安全的
如图所示的代码,由于不包含状态也不包含对其它类的域,特定的计算状态会存储在线程栈中的局部变量表中,而虚拟机栈是线程私有的,不会影响其它线程。
2 原子性
编写如下的测试2-1demo:
public class AutomicClass {
/**
* 计数值
*/
private long count;
public long getCount() {
return count;
}
/**
* 服务方法
*/
public void service() {
// do something
System.out.println("do something");
// cala count++;
++count;
}
}
如果多个线程访问service,那么count将会发生改变,但是类似++count是原子性的吗?
分析++count,其实它并不是原子性的,它包括三个状态:
1. 读取操作:内存变量count->cpu寄存器。
2. 计算操作:cpu运算器中加法器获取寄存器count,计算+1.
3. 写回操作:cpu将计算结果写入指定的内存地址位置。
从上述分析可知count++是一个“读-改-写”的过程。每个状态依赖之前的状态,所以该操作不是原子性的。
2.1 竞争条件
上述2-1demo之所以会出现线程不安全,是由于存在竞争条件,也就是要想结果正确,只能依赖于正确的时序。
这里的竞争条件是“检查再运行”,也就是使用一个潜在的过期值,作为决定下一步操作的依据。
惰性初始化的竞争条件
以懒加载单例模式为例2-2demo:
public class LazyDemo {
//单例实例
private static LazyDemo single = null;
//构造私有
private LazyDemo(){};
//懒加载创建实例
public LazyDemo getSingle() {
if (single == null) {
return new LazyDemo();
}
return single;
}
}
比如说A、B线程同时执行getSingle,执行判断single==null
可能存在A和B都判断single为null导致两个线程获取两个不同的LazyDemo对象,而我们希望总是返回相同的实例。
2.2 原子操作
假设有操作A和B,如果从执行A的线程看,执行B操作的线程,要么B全部执行,要么一点没执行,这样A和B互为原子操作。
一个原子操作是指:该操作对于所有操作,都应该满足上述的描述。
3 锁
为了保护状态的一致性,应该在单一的原子操作中更新互相关联的状态变量。
3.1 内部锁
Java内部提供了强制原子性的内置锁机制:synchronized块。
synchronized包括锁对象的引用(监视器)和锁保护的代码块。
每个java对象都可以作为内部锁或者监视器锁。
synchronized可以用在:内部代码块、实例方法和静态方法:
1. 内部代码块:也就是方法内部加锁,可以使用任意对象作为锁对象:
public void demo() { synchronized (object) { getSingle(); } }
2. 实例方法:也就是方法上加锁,锁对象是当前对象实例:
public synchronized void demo() { getSingle(); }
3. 静态方法:也就是类方法static加锁:锁对象是Class对象本身:
public static synchronized void demo() {
//测试代码
single = new LazyDemo();
}
以字节码的形式看:对于方法加锁会添加flags:ACC_SYNCHRONIZED;对于内部代码块会使用指令: monitorenter和monitorexit
。
synchronized是互斥锁:不能实现多个线程的共同持有。
3.2 重进入
当一个线程获取了内部锁,再次获取所持有的锁,请求会成果也就是可重入的。
考虑如下代码2-3demo:
/**
* 演示锁的可重入——如果不是可重入,则子类调用super.doSomething会导致死锁。
* 执行结果:
* inside son doSomething
* 子类this:com.example.concurrent.threadsafeTwo.SychronizedRepeatableTest@49476842
* 子类super:com.example.concurrent.threadsafeTwo.SychronizedRepeatableTest@49476842
* inside parent doSomething
* 父类:com.example.concurrent.threadsafeTwo.SychronizedRepeatableTest@49476842
*
* 可见获取子类和父类的的锁其实都是同一个对象,所以可以演示可重入。
*/
public class SychronizedRepeatableTest extends Parent {
@Override
public synchronized void doSomething() {
System.out.println("inside son doSomething");
System.out.println("子类this:"+ this);
System.out.println("子类super:"+ super.toString());
super.doSomething();
}
public static void main(String[] args) {
new SychronizedRepeatableTest().doSomething();
}
}
class Parent {
public synchronized void doSomething() {
System.out.println("inside parent doSomething");
System.out.println("父类:"+ this);
}
}
运行结果:
可见此时可以成功运行,未发生死锁,也就是内部锁可重用。
上述代码子类覆写父类方法,并在代码中使用super调用父类加锁方法,其实此时的this和super都代表子类对象,所以进入父类方法也是获取的同一个锁。
4 用锁保护状态
锁可以使线程串行的访问它保护的代码路径。所以对于复合操作,需要在完整的运行期间占有锁。
一种常见的锁规则:在对象内部封装所以可变的状态,通过对象的内部锁来同步任何可导致状态变化的代码路径,这样就不需要在外部进行额外操作,许多线程安全类就是这个模式如Vector和StringBuffer等。