线程安全的意思是:在多线程各种随机的调度顺序下,代码没有bug,都能按照符合预期的方式来执行~~
如果在多线程随机调度下,代码出现bug,此时就是认为线程不安全!
导致线程不安全的几个因素:
典型线程不安全代码:
每个线程循环 5w 次,累加变量 count 的值(⭕预期结果10w):
/**
* @Author YuanYuan
* @Date 2022/9/11
* @Time 10:58
*/
class Counter {
public int count = 0;
public void increase() {
count++;
}
}
public class TestDemo {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread thread = new Thread(()-> {
for (int i = 0; i < 5_0000; i++) {
counter.increase();
}
});
Thread thread1 = new Thread(()-> {
for (int i = 0; i < 5_0000; i++) {
counter.increase();
}
});
thread.start();
thread1.start();
thread.join();
thread1.join();
System.out.println("counter :" + counter.count);
}
}

❓上述问题是怎么出现的呢?
进行的count++操作,底层是三条指令在CPU上完成的!!!
- 把内存的数据读取到CPU寄存器中
load- 把CPU的寄存器中的值进行 + 1
add- 把寄存器中的值,写回内存中
save
由于当前是两个线程修改一个变量,并且每次修改时三个步骤(不是原子的),还线程之间的调度顺序不确定,因此两个线程在真正执行这些操作的时候,就可能有多种执行的排列顺序!!


这只是举出来4个,其实还有非常多的情况…
在这些情况中,有些排列组合是没有问题的!
但还有些排列组合是存在问题的…
比如我举的例子,只有我画出来的前两种是没有问题的,其他情况都是有问题的!
就一个例子图解一下为什么其他情况有问题:

在形如这样的排列顺序下,此时多线程自增就会存在"线程安全问题"!!
💌
整个线程调度过程中,执行的顺序都是随机的~~ 由于在调度过程中,出现"串行执行"两种情况的次数,和其他情况的次数,不确定
因此得到的结果就是不确定的值~
虽然结果不确定,但可以知道结果的范围:
极端情况下:
如果两个线程之间的调度全是 串行执行,结果是10w
最多不会超过10w
解决上述线程不安全问题,就可以在count++之前先加锁,在count++之后再解锁
在加锁和解锁之间,别的线程无法修改!(别的线程只能阻塞等待,阻塞等待的线程状态,就是BLOCKED状态)

java代码中,进行加锁,使用synchronized关键字~
synchronized几种写法:
这是最基本的使用~
当进入方法的时候,就会加锁,方法执行完毕自然解锁~

锁具有独占特性!
如果当前锁没人来加,加锁操作就能成功!
如果当前锁已经被人加了,加锁操作就会阻塞等待~

本来线程调度是随机的过程
一旦两组load add save交织在一起就会产生线程安全问题!
现在使用锁,就可以使两组load add save 能够串行执行了
increase 里面涉及到锁竞争,这里的代码是串行执行的!
但是for循环在加锁外面!俩for仍然是并发的~~

因此这个代码仍然比两个循环串行执行要快,但是肯定比不加锁要慢
如果把for也写到加锁的代码里头,这个时候就和完全串行一样了!
锁的代码范围不一样,对代码执行结果会有很大的影响!
锁的代码越多,就叫做"锁的粒度越大/越粗"
锁的代码越小,就叫做"锁的粒度越小/越细"

1️⃣
synchronized里面写的锁对象是this,谁调用increase,就是针对谁进行加锁~
上面的代码就是在针对counter进行加锁! 并且两个线程针对同一个对象加锁,运行时会出现锁竞争(阻塞等待)
2️⃣
上面的这个代码是俩线程在针对不同对象加锁,就不会出现锁竞争
3️⃣
上面的代码是针对locker对象进行加锁~
locker是Counter的一个普通成员(非静态成员),每个Counter实例中,都有自己的locker实例~
上述代码中,counter对象是同一个,对应的counter里面的locker就是同一个对象,此时仍然是两个线程针对同一个对象加锁,仍然会存在锁竞争(阻塞等待)
那1️⃣和3️⃣有什么区别呢?
可以视为1️⃣和3️⃣从线程安全的角度来看待是没有任何区别的~
4️⃣
也不会产生锁竞争,因为counter和counter1不是同一个对象,里面的locker成员自然也不同,所以不会产生锁竞争
5️⃣
但如果把locker改成静态成员(类属性),那么类属性是唯一的(一个过程中,类对象只有一个,类属性也是只有一份),这时虽然counter和counter1是两个实例,但是这俩里面的locker其实是同一个locker,就会产生锁竞争!
6️⃣
此处的锁对象,变成了一个“类对象”, 类对象在JVM进程中只有一个,如果多个线程来针对类对象进行加锁,那势必就会产生锁竞争!!
上述代码实际上就是针对同一个Counter.class进行加锁,因此就会存在锁竞争
锁竞争的目的是为了保证线程安全~
锁的 SynchronizedDemo 类的对象
public class SynchronizedDemo {
public synchronized static void method() {
}
}
