一、出现线程安全的原因
1.【根本原因】线程的调度执行时随机的(抢占式执行)->罪魁祸首
2.多个线程同时修改同一个变量
如果是一个线程修改一个变量 或者 多个线程读取同一个变量 或者 多个线程修改不同变量 这些都没事。
3.修改操作不是原子的!
像count++ 这样的修改,就是不是原子的修改。
但是像 = 这样的修改,就是属于原子的。(在Java针对内置类型进行 = ,或者针对引用 = 都是原子的)(*但是,在C++中就不一定了,还是不是原子的,就得具体问题具体分析了)
后序判定某个代码是否是线程安全的,需要结合这几点一起来判定。
*如果将全局变量count放入到main方法当中,就会出现报错。

把变量改成局部变量,编译直接过不了了:lambda表达式要想正确捕获变量,要求是final或者事实final(虽然没有加final关键字,但是代码中确实也没人修改)
写成成员变量在lambda中确实能够使用,此时不是走"变量捕获"语法,而是"内部类访问外部类成员",本身就ok。
lambda本质上就是匿名 内部类(函数式接口)

内部类访问外部类成员,本来就可以实现。
*扩展String
String是不可变对象,new好一个String对象本身就是不能修改的。设计成不可变对象,其中有一个理由,就是不可变对象天然就是线程安全的。
不可变对象,方便放到常量池中进行缓存。不可变对象,hasCode是固定值,也方便和哈希表进行结合,作为hash的Key
StringBuffer本身确实修改了,但是又通过其他路径(例如枷锁)解决线程安全问题。
StringBuilder是彻底的线程不安全。
*什么是原子的?一个事物是原子的,说明他就是不可拆分的最小单位。SQL中,事务就是把多个SQL打包成一个整体,执行的时候,要么全都执行完,要么一个都不执行。就不会出现执行一半的情况,这就成为原子性。
此处谈到的原子也是类似的含义,CPU执行指令的角度,如果是一条指令,对于CPU来说,要么就是执行完,要么就是不执行,不会出现"一个指令执行一般"这样的情况。CPU执行一条指令,这个行为就是原子的。
像count++这样的指令,对应到多条CPU指令,CPU执行过程中就可能执行一半,就调度走执行别人的指令了(这就不是原子的)
=这样的操作,也是对应到一条CPU指令(类似于MOVE)
那么如何将这些操作变为线程安全的呢?
核心思路:把修改操作变成原子的。
通过锁来实现。
关键字:synchronized通过这个关键字来实现使用锁。
对于锁这样的概念,涉及到两个核心操作:
1.加锁
2.解锁

Java就通过这一个关键字来表示这两种操作,进来就是加锁,出去就是解锁。sychronized()的()里面填写的是"锁对象",真正用来枷锁的锁是谁?------>在Java中,任何一个对象都可以用来作为锁对象(引用类型,不能是内置类型)
加锁,就是把若干个操作"打包成一个原子"。不是说把这count++的三个指令变成一个指令了。也不是说,这三个指令就必须要一口气在cpu上执行完,不会触发调度。加锁会影响到其他的加锁线程,而且是加同一个锁的线程。
当两个线程,尝试竞争同一把锁,才会产生"阻塞",如果是竞争不同的锁,就没有影响。
sychronized(锁对象),看锁对象是不是同一个对象。
锁竞争(Lock Contention)是指多个线程试图同时访问同一个临界区(即需要互斥访问的代码区域),因此它们之间产生了竞争。在任意时刻,只能有一个线程持有锁并进入临界区,其他试图进入临界区的线程必须等待。
如果是两个线程,一个加锁了另一个么有加锁,这样就不会产生阻塞。两个线程都加锁了,而且是同一个对象,才会产生锁竞争。

此处的加锁和解锁,也可以视为两个cpu指令。这两个操作使得这两个线程各自循环执行5w次。
整个程序按照如图所示的流程进行:

本来load add save 在两个线程中是穿插执行的,但是在引入锁之后,就变成了"串行执行",不再穿插,最后输出结果也自然是1w次。
*要是两个锁对象不一样:不一样就不会产生阻塞,程序的执行也就不会出现上述的"禁止插队"这样的行为。

这里又一系列很复杂的逻辑
这里也有一系列很复杂的逻辑
日常工作中,一般都是让加锁范围尽量的小
这样的话,可以并发执行的逻辑就更多,此时外部的逻辑通常是更复杂的。
此时这张图中,只有count++是串行的。
引入多线程,就是为了并发执行,就是为了充分利用cpu多核心资源。
多进程编程 和 多线程编程 就是在利用多核心的编程手法。

t1如果加上锁,t1就会不停地执行循环,直到把5w次都执行完,才会去释放锁。
t2只能阻塞等待,一直等到t1释放锁(t1的5w次循环都执行完了),t2才能继续执行
此时,这两个线程的循环时完全串行的,(也就是和一个线程执行是类似的了),这种写法,两个代码是完全串行化。
此时,并没有把多核心利用起来。
这种写法,在当前代码下,执行速度反而更快,主要是因为当前的任务很简单。

*这种情况下,t1和t2谁先拿到锁?
结论没有唯一性,假设t1先拿到锁,当t1循环完毕一次,下一次加锁可能是t1继续加上,也可能是t2加上(这里体现了随机调度)。类似于"数据库隔离级别",隔离级别越高,并发程度越低,执行速度越慢。
synchronized使用方法
1.synchronized(锁对象){}
基础使用,常见使用。不是禁止调度,而是禁止其他线程插队。
2.修饰一个普通方法,就可以省略锁对象:

*此时,相当于针对this加锁,不是没有锁对象,而是把锁对象给省略掉了
等价于


上面这两种写法,实际上没有任何区别。锁对象,是啥对象不重要,重要的是,两个线程是否是针对同一个对象加锁。
3.synchronized修饰静态方法

此时认为synchronized是针对类对象进行加锁的
static修饰的方法,也叫做"类方法",不是针对"实例"的方法,而是针对类的。在这个方法里,没有this。
Counter.class------>反射 程序运行时,能够拿到类/对象的一些相关属性。(这个类有哪些成员,都叫啥名字,都是啥类型,都是private/public,有啥方法,都叫啥名,参数列表是啥,是private/public,这个类继承的父类是谁,上实现了哪些interface.......)
通过类对象拿到上述信息。(*不考虑运行,看下代码就行,强调"运行时"的意思就是,不让你看代码,也能够获取到这里的信息)
.java编译生成.class
.class被jvm加载到内存中,就得到了对应的"类对象"。
synchronized的特性
1.互斥性(前文已经提及)
2.可重入

如果一个线程,针对一把锁,连续加锁两次会发生死锁(deadlock)。(如图所示)
分析:初始情况下,假定locker是未加锁的状态,此时的synchronized就会加锁成功,继续向下执行。
第二次加锁的时候,这个锁已经是"被加锁"的状态了,如果这个锁已经被加锁了,尝试加锁操作,就会触发锁冲突/锁竞争,此时该线程就会阻塞等待,一直等到锁被释放。
走到第二个加锁的位置,触发阻塞,如何解除阻塞?得先释放第一个锁,如何释放第一个锁,得先把第二个锁加上,往下继续走。
BLOCKED状态不是"死锁",而是因为锁产生的阻塞,这里所说的死锁指的是这个锁再也解不开了。

有的时候,死锁的现象不是特别明显,稍有不慎就会发生死锁现象。
但是,java中引入了可重入机制,有效地避免了上述的死锁情况。(注意,死锁有很多种体现形式,可重入只是能解决一个线程一把锁,加锁两次的情况,解决不了其他情况)同一个线程,针对同一把锁,连续加锁多次,不会触发死锁,此时这个锁就可以称为"可重入锁"。