目录
📕 线程安全的概念
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
🎄 观察线程不安全
例子:
注意:当我们把两个for循环 i 的值分别改为累加到100,此时通过上述代码是可以得到两百的,但是不是没有线程安全问题,只是线程安全问题概率变小了,因为t2线程启动需要时间,当t1线程启动后,t1就开始计算了,与此同时主线程启动完t1之后,继续启动t2,很可能出现t2还没有开始启动,t1已经运行完了(计算机执行很快),此时相当于"串行"执行了。
典型的多线程并发导致的问题,如果让两个线程串行执行,就没有任何问题!!
上述问题,就是多线程在搞鬼!!!
🌳 线程不安全的原因
那么对于此图,就是可能二和可能三是能够正确执行的,因为他们是串行执行,其他情况就不行了
🚩 原因:
原子性:
原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要不都不执行
例子:我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。
那我们应该如何解决这个问题呢?
是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。
有时也把这个现象叫做同步互斥,表示操作是互相排斥的
注意:一条 java 语句不一定是原子的,也不一定只是一条指令
比如我们上述不安全的代码 count ++,其实是由三步操作组成的
- 从内存把数据读到 CPU
- 进行数据更新
- 把数据写回到 CPU
所以在多线程中,有可能一个线程还没自增完,可能才执行到第二步(进行数据更新),另一个线程就已经读取了值,导致结果错误。那如果我们能保证自增操作是一个原子性的操作,那么就能保证其他线程读取到的一定是自增后的数据。
🌲解决之前的线程不安全问题
结合上述整理的5条原因:
第一个原因,我们改变不了,因为内核已经是搞好了的,我们自己改也没用
第二个原因通过调整代码结构,尽量避免出现拿多个线程同时改同一个变量,这是一个切入点,但是在Java中,这种做法不是很普适,只是针对一些特定的场景是可以做到的,比如前面讲到过,String是不可变对象,StringBuffer加锁,是线程安全,StringBuilder不是线程安全
第三个原因,这是解决线程安全问题最普适的方案
针对锁进行小结:
🚩 synchronized 关键字
我们使用关键字synchronized进行加锁操作
代码:
这个代码就是两个线程针对同一个对象进行加锁!!!那么线程安全问题就解决了
如果说t1加锁了,t2没加锁,此时线程安全问题还是存在,意味着t2执行过程种没有任何阻塞,没有互斥,仍然会使t1++到一半的时候,被t2进来把结果覆盖掉。
此代码就是两个线程针对两个不同对象加锁,就不会产生"互斥"!!!
分析上述代码的执行过程:
首先,操作系统里面的加锁解锁功能,核心还是cpu提供的指令(硬件提供了这样的能力,软件才有对应的功能)。
代码一:
当我们把两个线程的synchronized加到for循环外面,这个情况就是t1把5w次循环循环完t2才开始算,此时for循环的条件和i++就不能并发执行了,这个代码是对的,但是好的选择(不如直接写单线程)。
关于synchronized的其他写法:
此代码本质上和上述产生线程安全的代码本质上是一样的,都是两个线程并发的针对同一个变量进行++,只不过此代码的变量用一个类包装起来了,通过add方法来进行++。结果也是一个不确定的值,此时也是刚才所谈到的线程安全问题!!!
对于此代码我们仍然也可以通过加锁来解决线程安全问题!!
还是可以搞一个Object类,通过这个对象作为锁:
观察上述代码,之前说过加锁操作,我们可以针对任意对象进行加锁,这里面已经有一个counter引用,就可以直接对这个counter进行加锁,对于这个代码,没必要在搞一个locker了。
即代码实现:
再次强调,加锁具体是针对哪个对象不重要,重要的是两个线程是否针对同一个对象进行加锁
那么既然是针对自己加锁,就可以把加锁操作在Counter类中的add方法中实现
他们之间并没有什么区别,只是上面代码针对counter加锁,下面代码synchronized括号中的this也是针对counter加锁,之前讲过,我们说this在哪个方法中,谁调用这个方法,this就指向谁,那么下面代码就是通过counter调用add,即this指向的是counter,也就是针对counter进行加锁!!!
代码:
在进一步讲,方法一进来就进行加锁,等到解锁之后,方法也就执行完了,意味着加锁的生命周期和方法的生命周期是一样的,这个时候就可以之间把synchronized写到方法上!两种写法是等价的
synchronized修饰普通方法相当于就是针对this加锁,还有个static方法,static修饰的方法没有this,synchronized修饰static放到,那么相当于就是针对该类的类对象加锁
上述方法代码具体例子:
this:(部分代码)
类对象:(部分代码)