线程安全
线程不安全的原因
如创建两个线程,进行++运算,使第一个线程自增500次,第二个线程也自增500次,预期的结果为1000,但实际自增的结果是无法预知的。这是为什么呢?
public class TestDemo {
static class Count{
public int count=0;
void increase() {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
final Count counter=new Count();
Thread t1=new Thread() {
@Override
public void run() {
for (int i = 0; i <500 ; i++) {
counter.increase();
}
}
};
Thread t2=new Thread() {
@Override
public void run() {
for (int i = 0; i <500 ; i++) {
counter.increase();
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
像这种无法预知的结果是我们在写代码时最害怕的,那么为什么会出现这样的情况,大概率与并发执行有关,由于多线程是并发执行的,导致代码结果不准确,我们称这样的情况为“线程不安全”。
下面我们分析上边代码具体的执行过程:
count++,可以分为三步骤:
- 把内存中的值读到CPU中 LOAD
- 执行++操作 ADD
- 把CPU的值写回到内存中 SAVE
由于线程的调度是随机的,下面我们以其中一种情况为例:
像上图执行的过程就存在线程安全,执行了两次++预期结果为3但结果是2。
操作系统调度线程的时候是“抢占式执行”某个线程什么时候上CPU什么时候切换出CPU完全不确定。因此两个线程执行的具体的顺序是完全不可预测的。
线程之间抢占式执行
抢占式执行导致两个线程里面的操作的先后顺序无法确定,这样的随机性是导致线程安全的根本原因。
多个线程修改同一个变量
一个线程修改同一个变量,不存在线程安全问题。
多个线程读取同一个变量,不存在线程安全问题。
多个线程修改不同的变量,不存在线程安全问题。
为避免线程安全问题,就可以尝试变换代码组织形式。
原子性
像++这样的操作,分为三个步骤,就是一个“非原子”的操作。
像=操作,本质上是一个步骤,就是“原子”操作。
内存可见性
一个线程修改一个线程读取,由于编译器的优化会把中间一些SAVE和LOAD操作省略掉此时读的线程可能就是未修改的结果。
指令重排序
与编译器的优化有关
synchronized关键字
synchronized的特性
1.互斥
线程执行到某个对象的synchronized中时,其他线程如果也执行到同一个对象synchronized就会阻塞等待。
进入synchronized修饰的代码块相对于加锁。
退出synchronized修饰的代码块相对于解锁。
synchronized需要指定一个具体的加锁对象。
- 修饰非静态方法,加锁对象是this。
- 修饰静态方法,加锁对象就是当前类对象
- 修饰代码块,加锁对象通过()来指定
2.刷新内存,保证内存可见性
在synchronized内部如果要访问变量,就会保证一定能操作内存(禁止优化)
3.可重入性
synchronized对同一个线程可重复加锁,不会出现把自己锁死的问题。
如上边代码加上synchronized后就不存在线程安全,结果就是正确的。
volatile关键字
volatile修饰变量保证“内存可见性”,不保证“原子性”
wait和notify
wait:等待 notify:通知
用来协同多个线程之间的执行顺序。
wait
- 让当前线程阻塞等待(让当前线程的PCB从就绪队列拿到等待队列中)并准备接受通知。
- 释放当前锁。使用wait/notify,必须搭配synchronized。需要先获取到锁。
- 满足一定的条件被唤醒时,重新尝试获取这个锁。
notify
- 在synchronized中使用
- notify一次唤醒一个线程(随机唤醒)
- notifyAll一次唤醒所有线程
wait和sleep的区别
- sleep操作是指一个固定的时间来阻塞等待。wait既可以指定时间也可以无线等待。
- wait唤醒可以通过notify或者interrupt或者时间到唤醒。sleep只能时间到唤醒。
- wait主要用途协调线程之间的先后顺序sleep不适合这个的场景,sleep只是单纯的让线程休眠不涉及多个线程配合。