Java学习笔记(十九)——线程安全问题

什么是线程安全

在多个线程并发执行的时候保证不会出现错误。因为多个线程共享进程(主线程)的数据。

举个栗子:

典型的抢票系统,在某个时间点有多个用户同时进行抢票操作,每个用户是一个线程。

假设系统内目前有10张票,线程抢到票就会执行总票数-1,但是如何保证每个线程看到的票数确实是当前真正的票数呢。

存在这样的可能:

最一开始A抢到票,但是还没来得及操作票数-1,B看到的票数就仍旧是10张票,继续抢票于是A和B抢到的是同一张票。如果不加以控制,可能就造成100个人都抢到票了,但实际上只有10张。

再糟糕一点的例子:

取钱如果也存在以上的线程安全问题,那我是不是可以同时在很多个点同时取出钱,尽管我只有10000元,但是第一个点取出10000的时候如果没有更新,第二个点看到的余额还有10000元,那么第二个点也取出10000。。。hjh可以发家致富了。无疑这样的线程安全问题极为严峻。必须解决。

为什么有线程安全问题存在

上面分析的时候看到在一个线程改变数据的时候,如果没有及时更新就切换到第二个线程就会造成一些错误的发生。

现在来仔细分析一下,明确其可能存在的原因和条件:

  • 多个线程并发执行(原本为了提高程序运行效率。)
  • 某个线程对数据的操作被打断。

当操作数据只有读的时候是安全的。当在写一个数据的时候就会出现安全问题。

写一个数据完整的操作可以分成3步:取出——修改——写回。

当一个线程修改数据100为99,在写回的时候被第二个线程打断:那么主进程的数据没有修改,在第二个线程看到的数据依旧是100,操作数据-1写回99。然后执行权回到第一个线程,继续写回,写回的也是99。这样就发生了错误。

线程安全问题如何解决

基本思想:

不拆解这个完整的数据操作。将这块代码“锁”起来,保证在执行的过程中不会被打断。只有某个线程的代码完整地执行完,才允许切换到另一个线程。

所谓将代码“锁”起来,就是在执行的时候有一个公共的监视器,确保任何时间内,只给某一个进程锁了这一段代码,只有这一个进程可以执行这段代码。

实现方法:

  • 同步代码块
synchronized(监视器){ 
    被锁起来的代码 
} 
1.监视器一定是唯一的,可以被所有线程看到的,否则不能实现执行这段代码时只有一个线程在执行不被打断(因为其他线程可能看不到锁)
2.任何一个对象都可以做监听器
  • 同步方法
provide synchronized void 函数名(){ 
    函数体
} 
1.可以在implement Runable接口中重写run实现使用
  • 手动加锁,释放锁
private Lock lock  = new ReentrantLock();   //创建一个lock对象,因为Lock是一个接口 注意new的是ReentrantLock,而不是Lock
……

lock.lock();

被锁起来的代码

lock.unlock

1.lock效率比较低
2.手动释放锁的时候很有可能会忘记释放建议使用try{ 上锁代码 }finall{ lock.unlock}
  try{
    lock.lock();
    ……
}
finally{
    lock.unlock();
}
3.如果忘记释放锁会导致上锁之后部分的代码一直处于单线程状态,别的线程无法执行,效率低下。

上锁机制

synchronized通过同步代码块和同步方法实现给某个时间段内只有某一个线程处于执行状态,并一直到这部分代码执行完才有可能被夺走执行权。那么synchronized又是通过什么实现的呢?

synchronized实现原理

synchronized——>监视器锁(Monitor)——>操作系统的互斥锁(Mutex Lock)

监视器锁(Monitor)

monitorenter:在编译后插入到同步代码块的开始位置,

monitorexit:在编译后插入到方法结束处和异常处。

任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。

虚拟机规范的要求:

在执行monitorenter指令时:首先尝试获取对象的锁,如果这个对象没被锁定(或者当前线程已经拥有了那个对象的锁)把锁的计数器加1;相应地,在执行monitorexit指令时会将锁计数器减1,当计数器被减到0时,锁就释放。如果获取对象锁失败,当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。

ps:

1、synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;

2、同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。

互斥锁(Mutex Lock)

每个对象都对应于一个可称为" 互斥锁" 的标记(用来保证在任一时刻,只能有一个线程访问该对象)

对共享资源进行上锁——访问共享资源的时候先检查是否上锁,如果已经上锁,调用这个操作的线程就会被阻塞,直到解锁之后才可以访问。如果没有上锁——上锁——访问操作——解锁。

如果要阻塞或唤醒一条线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间。所以synchronized是Java语言中的一个重量级操作。在JDK1.6中,虚拟机进行了一些优化(譬如在通知操作系统阻塞线程之前加入一段自旋等待过程,避免频繁地切入到核心态中,当然都是后话,不理解没关系,放到后面)

锁的发展历程

于是通过上面知道了上锁的大概机制,但是出现的问题是:如果一旦碰到已经上锁的线程就阻塞当前线程进行等待,这需要切换到操作系统的kernal来完成,如果发生频繁的切换会浪费很多时间。

于是改变策略,一碰到上锁我能不能等等看这个线程是不是很快就会结束,我就不用阻塞了;只要等待的时间短于我切换的时间岂不是就很有利。

于是锁发展出很多种类型的优化:

重量级锁<——轻量级锁 <—— 偏向锁 <—— 无锁状态。

一开始我们掌握的一碰到上锁就阻塞属于重量级锁。这是最强的锁不会出错,但是时间代价有点大,我们是最迫不得已才会使用这种方法。

于是当一个进程被执行的时候就会走下面的流程:

1.一个进程上锁会在线程的栈帧里创建lockRecord,在lockRecord里和锁对象的MarkWord里存储线程a的线程id。一个线程执行:测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,成功就是当前线程上锁;否则就是另一个线程锁着,检查这个原来持有该对象锁的线程是否依然存活,挂了,则可以将对象变为无锁状态,然后重新偏向新的线程;存活则锁升级到轻量级锁。

2.在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。如果,完成自旋策略还是发现线程没有释放锁,或者让别的线程占用,则线程试图将轻量级锁升级为重量级锁。

ps:升级为重量级锁有两种情况,

一个是自旋到了一定次数;

另一个是第一个线程上锁,第二个线程自旋,第三个线程来的时候就转化为重量级锁。

3.自旋的线程由用户态切换到内核态进行阻塞,等待之前线程执行完成并唤醒自己。

形象化理解:

你想上洗手间:

外边只有一个公共厕所:

你先去看看有没有人,没有人就进去,有人你问他你:“现在要出来吗?”;出来你就进去(偏向锁),否则你就等等(轻量级锁——自旋)

如果等了一段时间里面的人还没出来,你就先去写作业(重量级锁——阻塞)

或者你看到前面还有一个人等着也会写作业去(重量级锁——阻塞)

锁的其他优化策略

  • 锁剔除:一定不会发生线程安全问题,其他线程不会操作数据。——不需要锁
  • 锁粗化:对同一个对象反复加锁、释放锁。——将锁的范围扩展到整个操作序列外部。

还有一些其他概念:

例如

  • volatile变量的单个读/写操作
  • 互斥同步
  • 非阻塞同步
  • 无同步
  • 相对、绝对线程安全
  • ……

等等

大概都属于多线程并发的话题。后续慢慢补充。

最后学习参考的博客:

这个是查的所有资料讲的最通俗易懂,看的进去的↓

java-synchronized原理 - _星辰、 - 博客园   

这个是解释的比较深入的↓

Java synchronized原理总结 - 知乎

这个是比较全面的,但是框架关系对于我来讲不是那么清晰(但一看就是大佬),慢慢理顺叭↓

Java并发机制及锁的实现原理_void-CSDN博客_java 并发原理

非常简单的练习代码——银行系统:

BankSysterm: exercise for multiply threads

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值