线程安全问题

线程安全问题

线程安全指内存安全。(为什么这么说,因为和操作系统有关)
目前的主操作系统都是多任务的,即多个进程同时运行的。为了保证安全,每个进程只能访问分配给自己的内存空间,而不能访问别的进程,这是由操作系统保障的。
在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存),进程中所有的线程都可以访问到该区域,这就导致了问题的产生。
我们可以把进程看做是一栋房子,线程是住在这个房子里的人。如果这个房子里只住着一个人,那么房子里的所有资源就不会存在线程安全问题,如果住了多个人(例如唐僧师徒4人),那么公共厕所的使用就会出现问题,我们都知道一个时间点上只能有一个人使用,如果唐在使用,此时猪八戒想上厕所,那么他就要排队等待,或者放弃去找其他厕所或者干一架,打赢就可可使用。显然如果不事先制定规则,就会出现干一架的可能性。
对于这种情况有三种处理方式:

  1. 私有化(私有的东西就不该让别人知道)
    假使这个房子4个各住一个房间,每个房间都装一个厕所,那么就不会出现抢厕所的问题。在程序中也是这样的,每个线程都会有自己的私有内存空间(栈),其他线程无法访问,如果某些资源(数据)只被一个线程使用,那么就存在栈内存中,比较常见的就是局部变量。

  2. 大家不要抢,人人有份
    上面的解决方案是基于“位置”的,因为每个房间都只能自己进去,所以不存在大家抢厕所的问题,所以是安全的。假如厕所就是要放在公共区域,让大家都能使用,那么就不能私有化(在程序中即不能将类变量变成局部变量)。
    对于这种情况就应该是大家不要抢,人人有份,就是在公共区域建立4个以上的厕所,那么无论何时,每个人都能使用一个厕所,就不会出现干一架的情况,即是安全的。在程序里,要让公共区域的堆内存中的数据对于每个线程是安全的,就让每个线程拷贝一份,每个线程只处理拷贝的那一份,那么就不会影响其他线程了。即ThreadLocal类。
    直白些:就是把堆内存中的一个数据复制N份(N份是都在公共区域的),每个线程认领1份,同时规定好,每个线程只能玩自己的那份,不准影响别人的。

  3. 只能看,不能摸
    放在公共区域的资源,只是存在潜在安全风险,并不一定就不安全。有些资源放在公共区域就十分安全,例如挂在客厅的名贵画,写着只能看,不能摸。在程序里就是只能读取,不能修改。

    小节一下:以上三种解决方案,其实都是在“耍花招”。
    第一种,找个只有自己访问的地方建立一个厕所,当然安全了。
    第二种,每人复制1份,各玩各的,互不影响,当然也安全了。
    第三种,更狠了,直接规定,只能读取,禁止修改,当然也安全了。
    

    这三种都是建立在规则下的,有点“理想化”了,现实情况可能比较混乱,没有规则。

  4. 没有规则,就先入为主
    例如,餐桌和煤气灶,当唐发现餐桌和煤气灶都没有人用就去做饭,做到一半的时候,孙悟空也来做饭,就占着桌子,当唐做完,发现餐桌有人了,也只能等待,虽然之前没有人,但是不能规定之后也没有人。
    针对这种情况,可以找个人占着,然后唐去做饭,当孙过来时,发现餐桌有人了,就没办法占着了。程序里就是堆内存的资源,要被多个线程操作时,为了确保数据安全性,就在数据边放一把锁,要想操作数据,就得先获得锁。只要前一个人不释放锁,后一个人就不能操作该公共资源。即对应的互斥锁。
    互斥锁很影响性能。对于一些产生线程安全很小或者几乎不会有的情况,可以不使用互斥锁。即下面要说的

  5. 相信世界充满爱,即使被伤害
    栗子:当我们把1万块钱仍在热闹的大街上,机会百分之百会丢,如果仍在去人区,几乎不会丢。所以线程安全的产生也和公共区域的环境状况有关。程序里,如果把数据放到内存,但始终只有一个线程,即单线程模式,那么这个数据肯定是安全的。2个和2000个线程操作同一个数据,这个数据的安全概率是不一样的,线程越多,安全概率越小,越少越安全。取极限的情况,只有一个线程,那么就是百分百安全。即CAS
    CAS(Compare And Swap):一个线程操作一个数据,干了一半活,发现累了,于是记录这个数据的当前状态,就回去睡觉了,醒来后,打算继续干,但又担心数据可能被修改了,于是拿睡觉前保存的数据状态和当前的数据状态对比一下,如果一样,就继续干,不一样就放弃从头开始干。(如果被人改了,又改回来了,这是ABA情况),故CAS,不适用高并发的情况,因为只要被改了就要从头再来,很浪费资源。
    而对于ABA的情况,可以加一个版本号,例如睡觉前数据是5版本号是0,醒来后数据是5,版本号是0,表示没人改过;如果数据是5,版本号不为0,则可能被改了多次,最后又改回5。
    讲到这,我们已经知道互斥锁是悲观锁,CAS是乐观锁。

    小结:
    前两种属于隔离法,一个是位置隔离,一个是数据隔离。
    然后两种是标记法,一个是只读标记,一个是加锁标记。
    最后一种是大胆法,先来怼一把试试,若不行从头再来。
    

你可能感兴趣的:(线程)