第二章-线程安全性

  • 线程安全核心:
对状态访问的控制,特别是共享(shared)和可变(Mutable)状态的访问
  • 共享:
由多个线程同时访问
  • 可变:
在变量的生命周期内可发生变化
  • 同步:
包括voliatile变量、synchronized、显示锁(Explicit Lock)、原子变量
  • 如何修复多个线程访问可变状态是发生的错误:
1.不在线程间共享该变量
2.将状态变量变为不可变的变量
3.在访问状态变量时使用同步

2.1 什么是线程安全性

  • 无状态对象一定是线程安全的
    • 线程访问无状态对象的行为不会影响其他线程中操作的正确性
@ThreadSafe
public class StatelessFactorizer implements Servlet{
    public void service(ServletRequest req,ServletResponse resp){
        BigInteger i=extractFromRequest(req);
        BigInteger[] factors=factor(i);
        encodeIntoResponse(factors,resp);
    }
}

这个例子中StatelessFactorizer就是无状态的,他既不包含任何域,也不包含对其他类中域的引用,。计算中的临时状态仅存于线程栈(线程独立)上的局部变量表中,并且只能由正在执行的线程访问

2.2 原子性

2.2.1 竞态条件
  • 竞态条件
当某个计算结果的正确性取决于多线程交替执行的时序时,就会发生竞态条件,简而言之就是靠运气。
最常见的就是“先检查后执行(Check-Then-Act)”
2.2.2 示例:延迟初始化时的竞态条件
@NotThreadSafe
public class LazyInitRace{
    private ExpenciveObject instance=null;
    
    public ExpenciveObject getInstance(){
        if(instance==null){
            instance=new ExpenciveObject(); 
        }
        return instance;
    }
}

假设有两个线程A和B同时访问getInstance,A看到instance对象为空创建一个ExpenciveObject对象,B线程同样需要判断instance对象是否为空,但是instance是否为空需要取决于不可预测的时序,包括线程的调度方式,A线程初始化ExpenciveObject并设置instance所耗费的时间。如果当B线程判断instance为空,则会新建一个ExpenciveObject对象,那么这两个线程调用getInstance时得到的就是两个不同的结果。

2.2.3 复合操作
  • 避免竞态条件
在某个线程修改变量时,通过某种方式阻止其他线程使用这个变量。从而确保其他线程只能在修改之前或者修改之后读取和修改状态,而不是在修改状态的过程中
  • 原子操作
对于访问同一个状态的所有操作(包括该操作本身),这个操作是以原子方式执行的操作。
假定线程A和B,如果从执行A线程的角度来说,当另一个线程B执行时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说都是原子的
@ThreadSafe
public class CountingFactorizer implements Servlet{
    private final AtomicLong count=new AtomicLong(0);
    
    public long getCount(){
        return count.get();
    }
    
    public void service(ServletRequest req,ServletResponse resp){
        BigInteger i=extractFromRequest(req);
        BigInteger[] factors=factor(i);
        count.incrementAndGet();
        encodeIntoResponse(factors,resp);
    }
}

AtomicLong是一个原子变量类,在java.concurrent.atomic包中,该包还含有其他一些原子变量类,用于实现在数值和对象引用上的原子状态转换,上面代码用例通过AtomicLong代替long类型计数器,能够确保所有对计数器的访问都是原子的,由于上面代码Servlet状态就是计数器的状态,并且计数器是线程安全的,因此Servlet也是线程安全的

2.3 加锁操作

  • 对多个线程安全状态变量操作,并不能保证线程安全。要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。
@NotThreadSafe
public class UnsafeCachingFactorizer implements Servlet{
    private final AtomicReference<BigInteger> lastNumber=new AtomicReference<BigInteger>();
    
    private final AtomicReference<BigInteger[]> lastFactors=new AtomicReference<>();
    
    public void service(ServletRequest req,ServletResponse resp){
        BigInteger i=extractFromRequest(req);
        if(i.equals(lastNumber.get())){
            encodeIntoResponse(factors,resp);
        }else{
            BigInteger[] factors=factor(i);
            lastNumber.set(i);
            lastFactors.set(factors);
            encodeIntoResponse(factors,resp);
        }
    }
}

上方代码中,在使用原子引用的情况下,尽管对set方法的每次调用都是原子的,但仍然无法同时更新lastNumber和lastFactors。如果只修改了其中一个变量,那么在这两次修改操作之间,其他线程将发现不变性条件被破坏了。同样,也不能保证会同时获取两个值:在线程A获取这两个值得过程中,线程B可能修改了它们,这样线程A发现不变性条件被破坏了

2.3.1 内置锁
  • 同步代码块(Synchronized Block称为内置锁或监视器锁)
    • 锁的对象引用
    • 由这个锁保护的代码块(静态的synchronized方法以class对象作为锁)
  • 进入同步代码块自动获得锁,退出同步代码块(无论是正常退出还是抛出异常)自动释放锁
  • JAVA内置锁相当于一种互斥锁,最多只有一个线程能持有这种锁,其他未持有锁的线程必须等待或阻塞,直到持有这个锁的线程释放锁,否则将一直等待下去。
  • 由这个锁保护的同步代码块会以原子方式执行
@ThreadSafe
public class UnsafeCachingFactorizer implements Servlet{
    private final AtomicReference<BigInteger> lastNumber=new AtomicReference<BigInteger>();
    
    private final AtomicReference<BigInteger[]> lastFactors=new AtomicReference<>();
    
    public synchronized void service(ServletRequest req,ServletResponse resp){
        BigInteger i=extractFromRequest(req);
        if(i.equals(lastNumber.get())){
            encodeIntoResponse(factors,resp);
        }else{
            BigInteger[] factors=factor(i);
            lastNumber.set(i);
            lastFactors.set(factors);
            encodeIntoResponse(factors,resp);
        }
    }
}

上面代码在service方法上加了synchronized后线程安全了,但是假如多个请求同时访问这个方法,由于同一时刻只有一个线程可以执行该方法,会导致性能很低。

2.3.2 重入
  • 内置锁是可重入的
当一个线程试图获得一个已经由他自己持有的锁,这个请求可以成功。
  • 获取锁的粒度是“线程”不是“调用”
  • 实现方式
为每个锁关联一个获取计数值和所有者线程,当计数值为0则认为该锁没有被任何线程持有。
当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并加计数值加设为1。
如果这个线程再次获得锁,计数值递增。
当线程退出同步代码块时,计数值递减,当计数值到0时,这个锁将被释放。

2.4 用锁来保护状态

  • 锁保护状态
对于可能被多个线程访问的可变状态变量,在访问它时都需要持有同一个锁。
  • 加锁约定
将所有的可变状态变量(前提是需要被多个线程同时访问)都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步,使得在该对象上不会发生并发访问
  • 对于每个包含多个变量的不变性条件,其中涉及的所有变量都要由同一把锁保护

2.5 活跃性与性能

  • 不良并发
可同时调用的数量,不仅受到可用处理资源的限制,还受到应用程序本身结构的限制
  • 避免不良并发
尽量将不影响共享状态且执行时间较长的操作从同步代码中分离出去,从而在这些操作的执行过程中,其他线程可以访问共享状态
  • 示例:
public class CachedFactorizer implements Servlet{
    private BigInteger lastNumber;
    private BigInteger[] lastFactors;
    private long hits;
    private long cacheHits;
    
    public synchronized long getHits(){return hits;}

    public synchronized double getCacheHitRatio(){
        return (double)cacheHits/(double)hits;
    }
    
    public void service(ServletRequest req,ServletResponse resp){
        BigInteger i=extractFromRequest(req);
        BigInteger[] factors=null;
        synchronized (this){
            ++hits;
            if(i.equals(lastNumber)){
                ++cacheHits;
                factors = lastFactors.clone();
            }
        }
        if(factors==null){
            factors=factor(i);
            synchronized (this){
                lastNumber=i;
                lastFactors=factors.clone();
            }
        }
        encodeIntoResponse(resp);
    }
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,335评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,895评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,766评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,918评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,042评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,169评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,219评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,976评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,393评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,711评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,876评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,562评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,193评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,903评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,699评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,764评论 2 351

推荐阅读更多精彩内容