在网络上看到关于线程安全的问题,在这里,我想结合操作系统中线程的概念,来说说这个问题!
进程是资源分配的最小单位,线程是程序执行的最小单位!也就是说对于进程来说,它的内存空间有一块特殊的公共区域,通常称为堆,这个区域是所有进程内的线程共享的区域。这个区域共享,好处是数据的传输非常方便,坏处是会造成数据的不安全,因为所有线程都能用,这就引出了我们说的线程安全的问题。
那么什么是线程安全?怎么去保证线程安全呢?
线程安全本质上是内存的安全!我们一步一步说明怎么去来实现线程的方法!
每个线程都有自己的线程上下文,包括唯一的整数线程ID、栈、栈指针、程序计数器、通用目的寄存器和条件码。既然是这个线程自己的,那么就可以是安全的了,那我们找到了第一个线程安全的地方,将数据放在栈内存中!在操作系统中不难过,这个地方的默认大小是8M。在代码中,我们线程的局部变量一般都是放在栈内存中的,也就是会说局部变量是线程安全的。
上面保证安全的方式是基于位置的,那么对于全局的变量,也就是常说的类的成员变量,可不可以保证安全呢?答案是可以的!我们给每个线程都拷贝一份呗,这样线程只处理自己的这一份拷贝,就可以了。在Java中,我们的ThreadLocal类就是这样实现的。这个属于数据上的隔离了。
还有没有别的方法?当然是有的,我们经常会遇到一种数据,只读数据!这名字一听就是线程安全的啊,“只可远观而不可亵玩焉”!在Java中,我们通过final关键字定义这样的数据。
那么除了上面这些数据,我们还有哪些保证线程安全的方式。
要想保证线程安全,我们同一时刻只让一个线程去访问,不就行了吗?这个时候我们引入了互斥锁!谁先进入这个内存区域读取数据,我们门口放一把锁,进入房间我们就把房门锁住,钥匙在这个线程手里,不用了就打开锁,把钥匙送还。这就是互斥锁实现的机制。
当然,这个也是有弊端的,网上有个非常好的解释,为了解决这个问题,就引入了条件变量的概念。下面我贴出网上关于互斥锁和条件变量的恩怨情仇!
两个线程操作同一临界区时,通过互斥锁保护,若A线程已经加锁,B线程再加锁时候会被阻塞,直到A释放锁,B再获得锁运行,进程B必须不停的主动获得锁、检查条件、释放锁、再获得锁、再检查、再释放,一直到满足运行的条件的时候才可以(而此过程中其他线程一直在等待该线程的结束),这种方式是比较消耗系统的资源的。而条件变量同样是阻塞,还需要通知才能唤醒,线程被唤醒后,它将重新检查判断条件是否满足,如果还不满足,该线程就休眠了,应该仍阻塞在这里,等待条件满足后被唤醒,节省了线程不断运行浪费的资源。这个过程一般用while语句实现。当线程B发现被锁定的变量不满足条件时会自动的释放锁并把自身置于等待状态,让出CPU的控制权给其它线程。其它线程 此时就有机会去进行操作,当修改完成后再通知那些由于条件不满足而陷入等待状态的线程。这是一种通知模型的同步方式,大大的节省了CPU的计算资源,减少了线程之间的竞争,而且提高了线程之间的系统工作的效率。这种同步方式就是条件变量。
而在Java中,其实就是线程之间的互斥和协作,条件变量就是用来协作的,对应java里的wait()和notify()函数。
上面这个样子能够保证线程安全,但是,锁的获取和释放是需要消耗资源的,如果线程的数量特别少的时候,可能根本没有别的线程来操作我们的数据,这个时候还这样就浪费了啊。
于是,我们引入了一种机制--CAS(compare and swap)。它其实也是一种锁,只是没有那么麻烦,属于乐观锁。所谓乐观锁就是秉持乐观向上的态度,我的数据是不会被意外修改的,修改了,就从头再来呗!
CAS翻译过来就是比较替换,它有3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
好了,我们的线程安全就说完了,其实这篇文章是基于Java来说的,由于本人太久没有弄Java的东西,忘掉了很多东西,本文也没有实例,但是对于线程安全的理解应该有一定的帮助,挖个坑,希望后面能补!
最后总结一下,就是以下这些了!
前两种属于隔离法,一个是位置隔离,一个是数据隔离。
然后两种是标记法,一个是只读标记,一个是加锁标记。
最后一种是大胆法,先来怼一把试试,若不行从头再来。
参考资料
https://zhuanlan.zhihu.com/p/67905621