目录
- Day 6:多线程(4)
- 1. 线程不安全的原因
- 2. 锁
- 3. synchronized
Day 6:多线程(4)
前序:针对Day 5结尾的count++
多线程的执行,是随机调度抢占式的执行模式,某个线程执行指令过程中,当它执行到任何一个指令的时候,都有可能被其他线程把它的CPU抢占走
实际并发执行,由于上述原因以及count++本质是CPU的三个指令,两个线程执行指令的相对顺序就可能会存在多种可能,不同的执行顺序,得到的结果就可能会存在差异
1. 线程不安全的原因
(1)线程在系统中是随即调度的,抢占式执行的,这是线程不安全的罪魁祸首,万恶之源
(2)当前代码中,多个线程同时修改同一个变量
(3)线程针对变量的修改操作,不是“原子”的,count++这种操作不是原子的,是包含了三个指令
(4)内存可见性问题(后续介绍)
(5)指令重排序(后续介绍)
针对上述原因进行问题解决
-
原因(1)无法干预,属于内核设计,无法改变
-
原因(2)是一个切入点,但是在Java中,并不普适,针对特定场景可以使用,例如String是不可变对象
- 一个线程修改同一个变量(ok)
- 多个线程读取同一个变量(ok)
- 多个线程修改不同的变量(ok)
String为不可变对象:很好的保证线程安全;有稳定的哈希值;方便在常量池中缓存
-
原因(3)是解决线程安全问题最普适的方案,可以通过一些操作,把“非原子”操作,打包成一个“原子”操作,例如:加锁
如果某个代码操作,对应到一个CPU指令,就是原子的,对应到多个就不是原子的,每个代码最终变成哪些指令,需要对芯片手册(CPU指令集)要有比较深入的理解
2. 锁
锁:本质上是操作系统提供的功能,内核提供的功能,同过api给应用程序了,Java(JVM)对于这样的系统api又进行了封装(其他的语言,同样也可以封装/调用这样的系统api来完成加锁操作)
锁的操作主要是两个方面
- 加锁:t1加锁之后,t2也尝试加锁,就会阻塞等待(系统内核控制的),在Java中就能看到BLOCKED状态
- 解锁:直到t1解锁之后,t2才有可能拿到锁(加锁成功),体现了锁的互斥
锁的主要特性:互斥,一个线程获取到锁之后,另一个线程也尝试加这个锁,就会阻塞等待,也叫做锁竞争/锁冲突
代码中可以创建多个锁,只有多个线程竞争同一把锁,才会产生互斥,针对不同的锁,则不会
3. synchronized
synchronized (locker){
.......
}
- synchronized是Java中的关键字,指的是同步的,此处谈到的同步,指的是互斥/独占,反义词可以理解为共享
-
synchronized (locker)
,()里面就是写的“锁对象”- 锁对象的用途,有且只有一个,就是用来区分两个线程是否是针对同一个对象加锁,如果是,就会出现锁竞争/锁冲突/锁互斥,就会引起阻塞等待
- 和对象具体是什么类型,有什么属性或者方法,没有任何关系
-
{}
中进入到代码块,就是给上述()锁对象进行了加锁操作,当出了代码块,就是给上述()锁对象进行了解锁操作
package thread;
public class Demo20 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(() ->{
for (int i = 0; i < 50000; i++) {
synchronized (locker){
count++;
}
}
});
Thread t2 = new Thread(() ->{
for (int i = 0; i < 50000; i++) {
synchronized (locker){
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + count);
}
}
这两个线程中,每次进行count++是存在锁竞争的,会变成串行执行,但是执行for循环中的条件以及i++,仍然是并发执行的
package thread;
class Counter {
private int count = 0;
//synchronized修饰普通方法,就相当于针对this加锁了
public void add() {
synchronized (this){
count++;
}
}
//上述方法也可以写成如下形式
synchronized public void add() {
count++;
}
public int get(){
return count;
}
//synchronized修饰static方法,相当于针对该类的类对象加锁
public static void func() {
synchronized(Counter.class){
//.....
}
}
//上述方法也可以写成如下形式
synchronized public static void func(){
//......
}
}
public class Demo20 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() ->{
for (int i = 0; i < 50000; i++) {
counter.add();
//counter.func();
}
});
Thread t2 = new Thread(() ->{
for (int i = 0; i < 50000; i++) {
counter.add();
//counter.func();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + counter.get());
}
}
synchronized(Counter.class)
中的Counter.class
是反射,即程序运行时,能够拿到类一些属性信息,包括不限于
- 类的名字,继承自哪个类,实现了哪些interface
- 类提供了哪些方法,每个方法叫什么,每个方法有什么参数,参数是什么类型
- 类提供了哪些属性,每个属性叫什么,每个属性是什么类型(public/private…)
上述信息,最初都是程序员自己写的.java源代码中提供的
- java编译之后,.java形成了.class字节码,上述信息转化为二进制
- java运行.class字节码,就会读取这里的内容, 加载到内存中,给后续使用这个类,提供基础
- 所以JVM中在内存里保存上述信息的对象,就是类对象,后续想创建这个类的实例,就需要依照上述信息
- 在Java中可以通过
类名.class
来拿到这个类对象,一个java进程中,某个类,只能有唯一一个类对象
所以,一旦多个线程调用func,则这些线程都会触发锁竞争