2.1 什么是线程安全?
2.2 原子性
2.2.1 竞态条件
2.2.2 延迟初始化中的竟态条件
2.2.3 复合操作
2.3 加锁机制
2.3.1 内置锁
2.3.2 重入
2.4用锁来保护状态
2.5 活跃性与性能
Java中主要的同步机制有关键字synchronized,volatile变量,显示锁,原子变量。
/**
*因为访问此方法,不会影响另一个线程对其访问计算的结果,所以线程安全。
* 也就是多个线程去访问此方法结果是不互相干扰的,没有共享的变量。
* 它不包含共享的变量和其他域中的引用,计算过程中临时状态仅存在线程栈上的局部变量中。
*/
@ThreadSafe
public class StatelessFactorizer implements Servlet {
public void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);//获取数
BigInteger[] factors = factor(i);//对servlet中数进行因数分解
encodeIntResponse(resp,factors);//将因数进行返回
}
}
所谓的原子性,要不都一次执行,别的线程不能干扰,要不都不执行。
/**
* 统计已处理请求的个数
* 这个为线程不安全。假设count的初始值为5,当来多个线程去执行service的时候,都将其值改为了6,这就造成了
* 严重错误。
*/
@NotThreadSafe
public class UnsafeCountingFactorizer implements Servlet {
private long count = 0;
public long getCount(){
return count;
}
public void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractRequest(req);
BigInteger[] factors = factors(i);
++count;
encodeIntResponse(resp,factors);
}
}
上面代码分析原因:
在这个类中有了共享变量count,同时++count的操作不是原子性的,有三个独立的操作在进行“读入-修改-写入”的操作序列,并且最终结果依赖于之前count的状态。n(n>2)个线程到来都进行读入count之前状态,然后进行++操作,最终写入只是将count+1了,实际应该是count+n。
是对“先检查后执行给了一个示例” 。拿未加锁的懒汉式单例做例子。
/**
* 因为if-else的存在有了“先检查后执行”,存在竟态条件。会造成线程不安全。
*/
@NotThreadSafe
public class LazyInitRace {
//私有属性
private LazyInitRace instance =null;
//私有构造器
private LazyInitRace(){ };
//暴露方法
public LazyInitRace getInstance(){
if(instance == null){
instance = new LazyInitRace();
}
return instance;
}
}
/**
*使用了java中java.util.concurrent.atomic包中的一些原子变量类
*/
@ThreadSafe
public class CountingFactorizer implements Servlet {
private final AtomicLong count = new AtomicLong(0);//使用原子变量类,实现数值上的原子状态装换
public long getCount(){
return count.get();
}
@Override
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractRequest(req);
BigInteger[] factors = factors(i);
count.incrementAndGet();//原子递增当前值
encodeIntResponse(resp,factors);
}
}
对原子变量类的解释:
在 javautil. concurrentatomic包中包含了一些原子变量类,用于实现在数值和对象引用上的原子状态转换。通过用AtomicLong来代替long类型的计数器,能够确保所有对计数器状态的访问操作都是原子的。由于Servlet的状态就是计数器的状态,并且计数器是线程安全的,因此这里的Servlet也是线程安全的。
AtomicLong的底层实际使用了关键字volatile.
public class AtomicLong extends Number implements java.io.Serializable {
private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
private static final long VALUE = U.objectFieldOffset(java.util.concurrent.atomic.AtomicLong.class, "value");
private volatile long value;
public final long get() {
return value;
}
//原子递增当前值
public final long incrementAndGet() {
return U.getAndAddLong(this, VALUE, 1L) + 1L;
}
}
在无状态中添加一个状态由线程安全的对象来管理,这个类是线程安全的(2.2.3中的CountingFactorizer),但是存在多个状态变量就算每个都由变为原子的,该类依然不安全。
/**
* 缓存上一次的数和结果,如果下一次来的数相同,则直接返回。
* 下面将变量都包装成为了线程安全,但是其整体类却还是线程不安全的,因为变量和变量之间有依赖顺序
* 其实还存在竟态条件。需要将方法的操作也变为原子的(或加锁)
*/
@NotThreadSafe
public class UnsafeCachingFactorizer implements Servlet {
//AtomicReference是替代对象引用的线程安全类
private final AtomicReference lastNumber = new AtomicReference<>();//@1
private final AtomicReference lastFactors = new AtomicReference<>();//@2
public void service(ServletRequest req, ServletResponse resp){//@3
BigInteger i = extractFromRequest(req);
if(i.equals(lastNumber.get())){//@3.1
encodeIntoResponse(resp,lastFactors.get());
}else {//@3.2
BigInteger[] factors = factor(i);
lastNumber.set(i);
lastFactors.set(factors);
encodeIntoResponse(resp,lastFactors.get());
}
}
}
代码思考
/**
* 使用synchronized来给方法加锁
*/
@ThreadSafe
public class SynchronizedFactorizer implements Servlet {
//AtomicReference是替代对象引用的线程安全类
private final AtomicReference lastNumber = new AtomicReference<>();
private final AtomicReference lastFactors = new AtomicReference<>();
public synchronized void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
if(i.equals(lastNumber.get())){
encodeIntoResponse(resp,lastFactors.get());
}else {
BigInteger[] factors = factor(i);
lastNumber.set(i);
lastFactors.set(factors);
encodeIntoResponse(resp,lastFactors.get());
}
}
}
代码思考
/**
*如果内置锁不是可重入的话,这段代码就会发生死锁。
*/
public class Widget {
public synchronized void doSomething(){}
}
class LoggingWidget extends Widget{
public synchronized void dosomething(){
System.out.println(toString()+"calling doSomehing");
super.doSomething();
}
}
代码思考:
子类重写了父类的 synchronized方法,然后调用父类中的方法,此时如果没有可重入的锁,那么这段代码将产生死锁。由于 Widget和 LoggingWidget中 doSomething方法都是 synchronized方法,因此每个 dosomething方法在执行前都会获取Widget的锁,然而,如果内置锁不是可重入的,那么在调用 super doSomething时将无法获得 widget上的锁,因为这个锁已经被持有,从而线程将永远停顿下去,等待一个永远也无法获得的锁。重入则避免了这种死锁情况的发生。
问题:
1.线程获取的锁是谁的锁?假设new了一个LoggingWidget类型的对象A,是获取了当前对象A的锁吗?(自己理解Java中锁基于对象的,每个对象都有自己的锁。)
2.为神魔书中说是因此每个dosomething方法在执行前都会获取Widget的锁。其子类不应该获取LoggingWidget的锁吗?
@ThreadSafe
public class CachedFactorizer implements Servlet {
private BigInteger lastNumber;//缓存上一次的数
private BigInteger[] lastFactors;//缓存上一次数lastNumber的因数
private long hits;//执行请求的个数
private long cachehits;//所有请求中直接和lastNumber相同,使用lastFactors的请求的个数
//获取hits的个数
public synchronized long getHits(){
return hits;
}
//获取命中缓存的请求个数
public synchronized double getCachehits(){
return (double)cachehits/(double)hits;
}
//Servlet进行服务
public void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);//获取到计算的数
BigInteger[] factors = null;//存储i的因数,每一个请求到来都会刷新为null
//对代码块进行加锁
/*1.++hits是“读入-修改-写入”的符合操作,使得其变为原子操作
* 2.这里的if语句存在了一个“先检查后执行”的符合操作也是线程不安全的,所以加锁,也是其变为了原子操作
* 提示:this标识只有获得当前对象的锁才可以进入执行
*/
synchronized (this){
++hits;
if (i.equals(lastNumber)) {
++cachehits;
factors = lastFactors.clone();
}
}
if(factors==null){
factors = factor(i);//求i的因数
/**
* 如果本次没有命中缓存,则将lastNumber和lastFactors刷新
* 对其进行加锁,使其操作原子化
*/
synchronized (this){
lastNumber = i;
lastFactors = factors.clone();
}
}
encodeIntResponse(req,resp);
}
}
使用锁时应该考虑的:
重新构造后的CachedFactorizer实现了在简单性(对整个方法进行同步)与并发性(对尽可能短的代码路径进行同步)之间的平衡。在获取与释放锁等操作上都需要一定的开销,因此如果将同步代码块分解得过细(例如将++hits分解到它自己的同步代码块中),那么通常并不好,尽管这样做不会破坏原子性。当访问状态变量或者在复合操作的执行期间,CachedFactorizer 需要持有锁。但在执行时间较长的因数分解运算之前要释放锁。这样既确保了线程安全性,也不会过多地影响并发性,而且在每个同步代码块中的代码路径都“足够短”。要判断同步代码块的合理大小,需要在各种设计需求之间进行权衡。
通常,在简单性与性能之间存在着相互制约因素。当实现某个同步策略时,一定不要盲目地为了性能而牺牲简单性(这可能会破坏安全性)。
当执行时间较长的计算或者可能无法快速完成的操作时(例如,网络I/O或控制台I/O),一定不要持有锁。