线程安全
我用了两个线程去调用Count类中的countAdd方法,来看看结果是否与预想的一样.
如果它是线程安全的,那么两个线程调用countAdd方法之后,count结果应该是10000.
class Count {
public int count = 0;
public void countAdd() {
this.count++;
}
}
public class Demo13 {
public static void main(String[] args) throws InterruptedException {
Count c1 = new Count();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
c1.countAdd();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
c1.countAdd();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("count = " + c1.count);
}
}
可是最终结果是6457,说明它是线程不安全的(每次运行结果可能都不一样).
那么,为什么呢?
我们在进行count++的时候,本质上是三个步骤.
- 把内存中的数据放到寄存器中(load).
- 把寄存器中的数据+1(add).
- 把寄存器中的数据写回到内存中(save).
所以,我们在进行count++操作时,线程并发执行就会出现问题.
如果在进行这三个操作的时候,顺序错乱了,就会出现不一样的结果.
线程安全的原因
-
- [根本原因]多个线程之间的调度是"随机"的,操作系统通过"抢占式"执行的策略来调度线程.
-
- 多个线程同时修改同一个变量,容易产生线程安全问题.
-
- 线程修改变量的时候,并不是"原子的",这也是我们解决线程安全问题的最主要的切入手段.
-
- 内存可见性问题.(放到后面)
-
- 指令重排序.(放到后面)
我们可以通过加锁来解决:
class Count {
public int count = 0;
public void countAdd() {
// 加锁,把三个步骤变成一个原子性问题
synchronized (this) {
this.count++;
}
}
}
public class Demo13 {
public static void main(String[] args) throws InterruptedException {
Count c1 = new Count();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
c1.countAdd();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
c1.countAdd();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("count = " + c1.count);
}
}
通过锁操作后,让多个线程在同一时刻,只有一个线程能够使用这个变量.
通过加锁操作后,把并发执行变成串行执行了,为什么还要使用多线程?
我们只是在count++的位置加锁了,在for循环中,我们并没有加锁.
因为在for循环中,变量i只是一个栈上的局部变量.
这两个for循环,也就是两个线程是有着独立的栈空间,是完全不同的变量,也就不涉及线程安全问题了.
在这两个线程中,有一部分是串行执行的,一部分是并行执行的,比纯粹的串行执行效率要高,所以我们要使用多线程.
synchronized
这是java中给我们提供的加锁的关键字.
1)进入代码块就加锁.
2)出了代码块就解锁.
是不是很方便!
synchronized进行加锁操作,是以"对象"为维度展开的.
我们在上述代码中,就根据this对象进行加锁.
此时两个线程针对用一个this对象加锁,就会出现锁竞争(锁冲突),即一个线程加锁成功,其它线程阻塞等待.
// 两个线程针对同一个locker对象进行加锁
class Count {
public int count = 0;
private Object locker = new Object();
public void countAdd1() {
// 加锁,把三个步骤变成一个原子性问题
synchronized (locker) {
this.count++;
}
}
public void countAdd2() {
// 加锁,把三个步骤变成一个原子性问题
synchronized (locker) {
this.count++;
}
}
}
// 两个对象针对不同对象加锁,就不会出现锁竞争,就会有线程安全问题.
class Count {
public int count = 0;
private Object locker = new Object();
public void countAdd1() {
// 加锁,把三个步骤变成一个原子性问题
synchronized (this) {
this.count++;
}
}
public void countAdd2() {
// 加锁,把三个步骤变成一个原子性问题
synchronized (locker) {
this.count++;
}
}
}
总结 : 只有两个线程针对同一个线程进行加锁,就会出现锁竞争(锁冲突),即一个线程加锁成功,其它线程阻塞等待.
互斥性
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.
刷新内存
synchronized 的工作过程:
1. 得到互斥锁.
2. 从主内存中拷贝变量的副本到工作内存中.
3. 执行代码逻辑.
4. 将更改后的变量刷新到主内存.
5. 释放互斥锁.
可重入性
// 两个线程针对同一个locker对象进行加锁
class Count {
public int count = 0;
private Object locker = new Object();
public void countAdd1() {
// 加锁,把三个步骤变成一个原子性问题
synchronized (locker) {
this.count++;
}
}
public void countAdd2() {
// 加锁,把三个步骤变成一个原子性问题
synchronized (locker) {
this.count++;
}
}
}
在这段代码中,假设countAdd1()已经加锁,当执行到countAdd2()时(是可以执行到的,因为是同一个 对象),又会再次加锁,这份代码是没有问题的,synchronized是可重入锁.
synchronized的使用
- 修饰普通方法:
public class Count {
public synchronized void countAdd() {
}
}
- 修饰静态方法:
public class SynchronizedDemo {
public synchronized static void methond() {
}
}
- 修饰代码块:
public void countAdd1() {
// 加锁,把三个步骤变成一个原子性问题
synchronized (locker) {
this.count++;
}
}
结语
在下篇博客中,依旧会总结线程安全问题,请大家多多支持.