线程安全

内存模型

物理硬件和内存

一个计算器有多个CPU,每个CPU有多核,CPU在寄存器上执行速度远大于访问主存执行速度,所以有了高速缓存


物理内存.png

除了增加高速缓存,为了使处理器内部运算单元尽可能被充分利用,处理器还会对输入的代码进行乱序执行优化,但会保证结果与顺序执行的结果一致

Java内存模型

[1]是java虚拟机规范定义的,用来屏蔽掉java程序在各种不同的硬件和操作系统对内存的访问的差异,在各种不同的平台上都能达到内存访问的一致性。主要目标是定义程序中[2]的访问规则,即在虚拟机中将变量存储到主内存或者将变量从主内存取出这样的底层细节

内存映射.png

线程状态

线程运行状态.png

线程安全

当多个线程访问某个类时,这个类始终能表现出正确的行为

线程不安全原因

  • 可见性:一个变量修改了共享变量的值,其他线程能否立即知道这个修改
    • 原因:线程会拥有一份自己的从主内存复制的变量
  • 有序性:程序执行时,进行指令重排,重排后指令和原指令顺序未必一致
    • 原因:处理器会对代码执行乱序执行优化
  • 原子性:一个操作是不可中断的
    • 原因:代码中的一行代码,可能是分几步执行的,比如i++

编写线程安全的代码

其核心在于控制对共享、可变变量的访问。当多个线程访问可变的共享变量,特别有写操作时,必须采用同步机制协调对变量的访问

  • 不在线程间共享状态变量
  • 共享的状态变量为不可变
  • 访问可变共享变量时使用同步

同步机制

synchronized

内置锁(Intrinsic Lock)或监视锁(Minior Lock),他属于独占式的 [3],同时属于 [4]

作用范围

1.作用于实例方法:锁住的是对象实例
2.作用于静态方法:锁住的是Class实例(Class实例是共享的,相当于全局锁,会锁住所有调用该方法的线程)
3.指定加锁对象:锁住的是所有以该对象为锁的代码块

核心组件

1.Wait Set:调用wait方法阻塞的线程放置处
2.Contention List:竞争队列,所有通过synchronized请求锁的线程首先被放置在这里
3.Entry List:Contention List中有资格成为候选资源的移动到Entry List
4.OnDeck:竞争锁资源线程,只能有一个竞争锁线程
5.Owner:已经获取到锁资源的线程

实现

sync核心组件.png

1.JVM每次从队列尾部取一个用于OnDeck,但并发情况下Contention List会被大量CAS访问,为了降低对尾部元素竞争,JVM会将一部分线程移到Entry List作为候选竞争线程
2.在Owner unlock时,发现Entry List没有线程,会将Contention List部分线程移到Entry List,并制定Entry List中某个线程为OnDeck
3.处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态
4.Synchronized 是非公平锁。 Synchronized 在线程进入 ContentionList 时,会先尝试自旋获取锁,如果获取不到就进入 ContentionList

CAS

CAS(Compare And Swap/Set)比较并交换,它的算法过程是这样:CAS(V,E,N)包含三个参数

  • V:要更新的变量
  • E:旧值
  • N:新值
    仅当V=E的时候,将V更新为N,最后返回V的真实值

CAS抱着乐观的态度操作(乐观锁),总是认为自己能够成功。当多个线程操作一个变量时,只有一个会成功,其余均会失败,失败的线程可以进行下次操作也可以放弃。基于这样的操作,CAS即使没有锁也能排除其他线程影响,做出恰当的操作

AQS

AbstractQueuedSynchronizer抽象同步队列,它定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,比如CountDownLatch、Semaphore、ReentrantLock等

核心

aqs.png

它维护了volatile int state和一个FIFO的等待队列,其中对state的操作有三个

  • getState
  • setState
  • compareAndSetState

实现

AQS使用了模板方法模式,已在AQS实现了大部分功能,但具体资源的获取/释放方式需要由自定义同步器去实现
1.boolean isHeldExclusively():该线程是否在独占该资源,用到condition才需要去实现它
2.boolean tryAcquire(int arg):独占方式。尝试获取资源,如果成功返回true,失败返回false
3.boolean tryRelease(int arg):独占方式。尝试释放资源,如果成功返回true,失败返回false
4.int tryAcquireShared(int arg):共享方式。尝试获取资源,负数失败;0成功,但没有剩余资源;正数成功,且有剩余资源
5.boolean tryReleaseShared(int arg):共享方式。尝试释放资源,如果释放后允许唤醒后续等待节点true,否则false

ReentrantLock举例

重入锁类图.png

ReentrantLock为独占锁,他拥有一个Sync(继承AbstractQueuedSynchronizer)变量,而Sync又有两个实现,分别是FairSync(公平)、NonfairSync(非公平),下面就lock和unlock结合源码分析

lock
        //NonfairSync中lock实现,ReentrantLock中公平与非公平体现在lock的时候是否会先去CAS一次
        final void lock() {
            //CAS尝试获取资源,FairSync是直接调用acquire
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                //AQS中实现方法
                acquire(1);
        }

AbstractQueuedSynchronizer中方法acquire

    //获取资源
    public final void acquire(int arg) {
        //第一步尝试获取资源,由同步器自己定义
        if (!tryAcquire(arg) &&
            //addWaiter先加入等待队列,再从等待队列中拿线程去获取资源
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            //在等待的队列有可能被调用了中断操作,告知线程调用了中断操作
            selfInterrupt();
    }

ReentrantLock->FairSync中方法tryAcquire

        protected final boolean tryAcquire(int acquires) {
            //获取当前线程
            final Thread current = Thread.currentThread();
            //获取资源状态
            int c = getState();
            //状态为0,表明没有线程占有资源
            if (c == 0) {
                //
                if (!hasQueuedPredecessors() &&
                    //CAS设置值
                    compareAndSetState(0, acquires)) {
                    //设置拥有线程为当前线程
                    setExclusiveOwnerThread(current);
                    //返回获取资源成功
                    return true;
                }
            }
            //当前线程为拥有线程
            else if (current == getExclusiveOwnerThread()) {
                //重入锁加上值
                int nextc = c + acquires;
                //检查不是无效的值
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                //重入锁只有一个线程,直接设置值    
                setState(nextc);
                //返回获取资源成功
                return true;
            }
            //获取资源失败
            return false;
        }

AbstractQueuedSynchronizer中方法acquireQueued

    final boolean acquireQueued(final Node node, int arg) {
        //是否取消获取的标志位
        boolean failed = true;
        try {
            //是否有中断
            boolean interrupted = false;
            //循环尝试获取资源
            for (;;) {
                //获取前置节点
                final Node p = node.predecessor();
                //前置节点为头节点并且判断是否获取资源成功
                if (p == head && tryAcquire(arg)) {
                    //获取资源成功后,设置当前节点为头结点,当前线程已获取到资源,不需要在存在等待队列中
                    setHead(node);
                    p.next = null; // help GC
                    //标志位改为false
                    failed = false;
                    //返回是否有中断
                    return interrupted;
                }
                //判断是否应该park
                if (shouldParkAfterFailedAcquire(p, node) &&
                    //park阻塞,并且检查是否有其他线程中断当前线程
                    parkAndCheckInterrupt())
                    //有其他线程对当前线程发起中断
                    interrupted = true;
            }
        } finally {
            //取消
            if (failed)
                //取消获取资源
                cancelAcquire(node);
        }
    }
unlock

ReentrantLock解锁

    public void unlock() {
        //调用AQS的实现类的release
        sync.release(1);
    }

AbstractQueuedSynchronizer中方法release

    public final boolean release(int arg) {
        //同步器自己实现的tryRelease
        if (tryRelease(arg)) {
            //从等待队列中拿一个线程unpark
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

ReentrantLock->Sync中方法tryRelease

        protected final boolean tryRelease(int releases) {
            //减去release释放的资源,重入锁可以减多次
            int c = getState() - releases;
            //检测当前线程是占有资源线程
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            //是否已没有线程占有资源    
            boolean free = false;
            //为0时表示没有线程占有资源
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            //设置资源值为此次释放后的值
            setState(c);
            return free;
        }

参考

  • 多线程&线程安全

  1. JavaMemoryModel,JMM

  2. java程序中堆上的变量

  3. 总是假设会发生冲突

  4. 获得锁的线程可以再次获得锁,比如方法a、b都获得同一个锁,在方法a中调用b

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

推荐阅读更多精彩内容