看一段错误示例
package com.bo.threadstudy.two;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SynchronizedTest {
static int num = 0;
public static void main(String[] args) throws InterruptedException {
//TODO 我在局部变量里写,为什么不行呢,原先写的Thread对象是可以调用的
// int num = 0;
Thread t1 = new Thread(()->{
for (int i = 0; i < 10000; i++) {
num++;
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 10000; i++) {
num--;
}
});
t1.start();
t2.start();
Thread.sleep(100);
//18:54:11.214 [main] DEBUG com.bo.threadstudy.two.SynchronizedTest - num==3883
log.debug("num=="+num);
}
}
在这里,我试了下结果不是0,而是各种值可能都会出现。原因是什么呢?
出现负数的场景
出现正数的场景
从图上可以看出来,因为共享变量存放在主存之中,而其它线程对这个数据进行操作时,会将其拷贝到高速缓存之中。线程首先在告诉缓存中完成对这个数据的修改,并且替换主存内容。
但如同上方场景,东西thread1都改完了,将0改成1,但是主存中的内容还没来得即写上去。此时另外一个线程thread2取了原先的数0,并进行修改后改为-1写入至主存。此时时间片又分配给了thread1,然后将主存中的内容-1替换成了1,最终就不是想要的效果。
针对这种场景,我们需要知道一下相关概念。
临界区 Critical Section
一个程序运行多个线程本身是没有问题的
问题出在多个线程访问共享资源
多个线程读共享资源其实也没有问题
在多个线程对共享资源读写操作时发生指令交错,就会出现问题
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区。
那么上方的代码临界区是哪里?就是count++以及count--这一块,对共享资源count发生了只能怪交错,可使用javap看一下。
竞态条件Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。
也就是说,临界区指的是共享资源发生读写操作的位置。而竞态条件指的是在临界区位置上由于执行指令顺序不同出现的问题。
那么在写程序时我们要考虑,临界区在哪,是否有竞态条件的发生。
解决方案
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
阻塞式的解决方案:synchronized,Lock
非阻塞式的解决方案:原子变量
本次课使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一
时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁
的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
package com.bo.threadstudy.two;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SynchronizedTest {
static int num = 0;
public static void main(String[] args) throws InterruptedException {
//TODO 我在局部变量里写,为什么不行呢,原先写的Thread对象是可以调用的
// int num = 0;
Object lock = new Object();
Thread t1 = new Thread(()->{
synchronized (lock){
for (int i = 0; i < 10000; i++) {
num++;
}
}
});
Thread t2 = new Thread(()->{
synchronized (lock){
for (int i = 0; i < 10000; i++) {
num--;
}
}
});
t1.start();
t2.start();
Thread.sleep(100);
//11:53:46.478 [main] DEBUG com.bo.threadstudy.two.SynchronizedTest - num==0
log.debug("num=="+num);
}
}
或者将synchronized放入for循环中也是可以的,不过频繁加锁解锁,性能不高,根据实际场景来吧。
你可以做这样的类比:
synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人进行计算,线程 t1,t2 想象成两个人
当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行count++ 代码
这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切换,阻塞住了
这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦),这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才能开门进入
当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
如果把 synchronized(obj) 放在 for 循环的外面,如何理解?-- 原子性
答案:这样的话,就相当于是一个串行任务了,只有在所有的count++任务或者count--任务结束完毕后,才会执行另一个线程之中的count--或count++任务。
如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?-- 锁对象
答案:锁资源都不是一个,会出现指令交错的问题。正常情况下,t1线程持有obj1这个锁资源,只要t1没有执行完成,t2就会得不到这个资源,导致一直没有办法执行任务。而这里用obj2,刚开始我就可以得到obj2这个资源直接执行,t1与t2并不满足互斥条件。
如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?-- 锁对象
和上面一样的道理。t1和t2不满足互斥条件,出现指令交错问题。
面向对象改进
就将方法改进了一下,没什么用。
package com.bo.threadstudy.two;
import lombok.extern.slf4j.Slf4j;
//针对SynchronizedTest类的方法做一个面向对象的改进
@Slf4j
public class UpdateObjectSynchronizedTest {
public static void main(String[] args) throws InterruptedException {
UpdateObject obj = new UpdateObject();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
obj.addNum();
}
},"t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
obj.subtractNum();
}
},"t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug(obj.getNum()+"");
}
}
class UpdateObject{
private volatile Integer num = 0;
/**
* 同步方法
*/
public synchronized void addNum(){
num++;
}
/**
* 同步方法
*/
public synchronized void subtractNum(){
num--;
}
public Integer getNum(){
return num;
}
}
synchronized可修饰方法,静态方法以及代码块。
class Test{
public synchronized void test() {
}
}
等价于
class Test{
public void test() {
synchronized(this) {
}
}
}
class Test{
public synchronized static void test() {
}
}
等价于
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}