

家观察下是否适用多线程的现象是否⼀致?
同时尝试思考下为什么会有这样的现象发生呢?
// 此处定义⼀个 int 类型的变量
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
// 对 count 变量进⾏⾃增 5w 次
for (int i = 0; i < 50000; i++) {
count++;
}
});
Thread t2 = new Thread(() -> {
// 对 count 变量进⾏⾃增 5w 次
for (int i = 0; i < 50000; i++) {
count++;
}
});
t1.start();
t2.start();
// 如果没有这俩 join, 肯定不⾏的。 线程还没⾃增完, 就开始打印了, 很可能打印出来的 count 就是个 0
t1.join();
t2.join();
// 预期结果应该是10w
System.out.println("count: " + count);
}如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
随机调度使⼀个程序在多线程环境下,执行顺序存在很多的变数
程序猿必须保证在任意执行顺序下,代码都能正常工作

for循环的所有操作(包括i < 5w判断、i++、count++)外围加锁。for循环的所有步骤都串行执行(同一时间只有一个线程能执行循环内的所有操作)。i的判断和自增)被完全限制。count++处加锁,即题目中的写法)count++这一非原子操作单独加锁,而for循环的i < 5w判断、i++操作仍可并发执行。count++是串行的(保证数据一致性),循环的其他步骤(i的判断和自增)是并发的。count++的线程安全,又利用了循环的并发潜力,因此执行速度更快。count++是串行的?count++被包裹在加锁代码块内(如synchronized块),而锁具有互斥性—— 同一时间只有一个线程能获取锁并执行被保护的代码。count++的操作必须 “排队执行”:一个线程执行完count++并释放锁后,下一个线程才能获取锁执行count++,最终实现串行化,避免了多线程同时修改count导致的数据不一致。
从里面加锁到这个外边加锁进行简化

那这个还有一种特殊的情况:
static修饰的方法,是针对类这个本身的,static 修饰的方法,不存在this ,那这个Synchronized 修饰的就是针对类对象加锁

第一次执行加锁成功加锁(锁没有人使用)
第二次执行此时这个锁已经被占用了,此时就会阻塞
注意:想要解锁,需要等待上一个锁使用完解锁,但是这个会一直等待,等待第一次锁释放,但是释放不了,就称之为死锁
死锁是一个非常严重的bug ,使代码执行到这一块之后,就卡住不动了
locker进行了两次synchronized嵌套加锁。此时第一次加锁成功后,第二次加锁会因为锁已被当前线程持有而进入阻塞等待,这种重复加锁是不必要的,却容易在开发中因代码结构问题写出。counter对象加锁,而counter的add方法内部又对this(即counter对象)加锁。当方法调用层次较深时,开发者容易忽略这种间接的重复加锁情况,从而引发锁重入导致的阻塞问题。Java synchronized的引入了可重入概念,针对这个线程的锁,他已经被使用了,引入了可重入性就不会阻塞了,而是继续往下执行,因为这把锁就是被这个线程所持有的。
其他的线程加锁仍然会阻塞
public class demo20 {
public static void main(String[] args) throws InterruptedException {
Object locker1 = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(() -> {
synchronized (locker1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker2){
System.out.println("他 线程两个锁都捕获到了");
}
}
});
Thread t2 = new Thread(() -> {
synchronized(locker2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker1){
System.out.println("t2两个线程的两个锁都获取到了");
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}这个代码产生了死锁,显示blocked(竞争锁的缘故导致阻塞)

如果不加sleep是否还会出现上述的问题?
原因是:
locker1等locker2,t2 持locker2等locker1”,则会立即形成死锁;sleep强制延迟,线程可能快速执行完锁的获取与释放(例如 t1 获取locker1后,在 t2 获取locker2前就已拿到locker2并释放所有锁,或反之),此时不会死锁。Thread-1处于BLOCKED 状态,原因是它在等待获取java.lang.Object@6ae6a90b这把锁,而该锁当前被Thread-0持有。Thread-1在尝试进入某个synchronized代码块时,锁已被Thread-0占用。Thread-1已锁定java.lang.Object@28c30f8d,说明它在阻塞前已经持有了这把锁,现在又试图获取另一把锁(@6ae6a90b),这是锁竞争 + 多锁交互的典型表现,若处理不当可能引发死锁。多 个线程修改同⼀个变量 上面的线程不安全的代码中,涉及到多个线程针对 此时这个 count 变量进行修改. count 是⼀个多个线程都能访问到的"共享数据


我们把一段代码想象成⼀个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入 房间之后,还没有出来;B是不是也可以进⼊房间,打断A在房间里的隐私。这个就是不具备原子性的。 那我们应该如何解决这个问题呢?是不是只要给房间加⼀把锁,A进去就把门锁上,其他⼈是不是就进不来了。这样就保证了这段代码的原子性了。 有时也把这个现象叫做同步互斥,表示操作是互相排斥的。
⼀条java语句
不⼀定是原子的,也不一定只是一条指令 比如刚才我们看到的n++,其实是由三步操作组成的:
1. 从内存把数据读到CPU
2. 进行数据更新
3. 把数据写回到CPU 不保证原子性会给多线程带来什么问题 如果⼀个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。
这点也和线程的抢占式调度密切相关.如果线程不是"抢占"的,就算没有原子性,也问题不大
可见性指,⼀个线程对共享变量值的修改,能够及时地被其他线程看到.
Java内存模型(JMM):Java虚拟机规范中定义了Java内存模型.
目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果

.• 线程之间的共享变量存在主内存(MainMemory).
• 每⼀个线程都有自己的"工作内存"(WorkingMemory).
• 当线程要读取⼀个共享变量的时候,会先把变量从主内存拷贝到⼯作内存,再从⼯作内存读取数据.
• 当线程要修改⼀个共享变量的时候,也会先修改工作内存中的副本,再同步回主内存. 由于每个线程有自己的总顾总内存,这些工作内存中的内容相当于同⼀个共享变量的"副本".此时修改线 程1的⼯作内存中的值,线程2的工作内存不⼀定会及时变化.
1)初始情况下,两个线程的工作内存内容⼀致.

2) ⼀旦线程1修改了a的值,此时主内存不⼀定能及时同步.对应的线程2的⼯作内存的a的值也不⼀定 能及时同步

这个时候代码中就容易出现问题. 此时引入了两个问题:
• 为啥要整这么多内存?
• 为啥要这么麻烦的拷来拷去?
1) 为啥整这么多内存? 实际并没有这么多"内存".这只是Java规范中的⼀个术语,是属于"抽象"的叫法. 所谓的"主内存"才是真正硬件角度的"内存".而所谓的"工作内存",则是指CPU的寄存器和⾼速缓存.
2) 为啥要这么麻烦的拷来拷去? 因为CPU访问自身寄存器的速度以及高速缓存的速度,远远超过访问内存的速度(快了3-4个数量级, 也就是几千倍,上万倍). 比如某个代码中要连续10次读取某个变量的值,如果10次都从内存读,速度是很慢的.但是如果只是 第一次从内存读,读到的结果缓存到CPU的某个寄存器中,那么后9次读数据就不必直接访问内存了. 效率就大大提高了. 那么接下来问题又来了,既然访问寄存器速度这么快,还要内存干啥??
答案就是⼀个字:贵

值的⼀提的是,快和慢都是相对的.CPU访问寄存器速度远远快于内存,但是内存的访问速度⼜远远快 于硬盘. 对应的,CPU的价格最贵,内存次之,硬盘最便宜
指令重排序 什么是代码重排序 ⼀段代码是这样的:
1. 去前台取下U盘
2. 去教室写10分钟作业
3. 去前台取下快递 如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按1->3->2的方式执行,也是没问 题,可以少跑⼀次前台。这种叫做指令重排序
编译器对于指令重排序的前提是"保持逻辑发生变化".这⼀点在单线程环境下比较容易判断,但是 在多线程环境下就没那么容易了,多线程的代码执行复杂程度更高,编译器很难在编译阶段对代码的 执行效果进行预测,因此激进的重排序很容易导致优化后的逻辑和之前不等价.
重排序是⼀个比较复杂的话题,涉及到CPU以及编译器的⼀些底层工作原理,此处不做过多讨论
synchronized锁遵循此特性)
看下面这张图
一个滑稽代表一个线程,筷子是锁,俩筷子就可以继续执行不会产生死锁问题,一个的话就阻塞等待
这是五个滑稽,滑稽1 如果拿着一个筷子,他吃不到这个面条,需要两个筷子,那他拿着只有个筷子,只能等待,此时每一个线程都持有一个筷子,正在等待阻塞,这时候就会产生死锁,这就是我们刚刚所说的死锁问题。
解决办法如下:
我们不妨给他加一个约定,每个线程加锁的时候永远是先获得序号小的锁,在获取序号大的锁。
滑稽1 先不拿锁(筷子)
滑稽2 拿1筷子
滑稽3 拿2筷子
滑稽4 拿3筷子
滑稽5 拿4筷子
滑稽1 此时就在阻塞等待1号筷子
这时候筷子5就空闲了,滑稽5 ,这时候就可以拿走筷子5 ,进行吃面条,吃完就释放筷子
筷子4也空闲了,滑稽4就可以拿筷子4和筷子5,进行吃面条,然后释放筷子
.......滑稽2释放筷子1和筷子2,
此时滑稽1等待到了他的筷子1 就拿起筷子1,然后筷子也是闲置的,此时两个锁都执行,至此所有的滑稽都吃上面条了,没有产生死锁问题
