线程安全
线程安全(风险)
线程不安全的原因:
解决线程不安全:
synchronized
内存刷新
可重入
volatile 关键字
wait 和 notify
wait()
notify ()
wait与sleep的区别:
某个代码在多线程的环境下执行,然后出现bug,其本质原因在于线程调度是不确定的。
比如:(代码有问题)
public class test3 {
static int count=0;
public static void sum(){
count++;
}
public static void main(String[] args) throws InterruptedException {
long time= System.nanoTime();
Thread t1=new Thread(()->{
for (int i = 0; i < 10000; i++) {
sum();
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 10000; i++) {
sum();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
我们会发现这个代码出现了一个问题,与我们想要的预期不一致。即出现bug
其本质是,count++操作,本质是有三个CPU指令构成
1.load,把内存中的数据读到cpu寄存器中。
2.add,就是把寄存器中的值,进行+1操作
3.save,把寄存器中值写回内存中。
大家肯定学过数学,那么对于组合,肯定是有过了解的。那么我问个问题,现在线程有两个,分别对count进行++操作。对于cpu来说有几种组合方式?3*3共有9种,那么问题来了,我们只要唯一的结果,不需要这么结果可能。
这个是我随便选择的两种情况,画出来的。箭头向下,表示时间的执行顺序。可以看到,第一个,t1的load执行后,t2的load开始执行了,然后执行t2的add操作,和save操作,再然后才执行t1的add操作和save操作。所以出现了不同的结果。
其实在进一步理解,可以理解为,两个线程对同一个变量,进行了相互作用。
1.抢占式执行(大部分的原因)
2.多个线程修改同一个变量(不安全)(而有几种情况是安全:
一个线程改同一个变量(安全),多个线程读同一个变量(安全),多个线程修改不同变量)
3.修改操作,不是原子性的。
4.内存可见性,引起的线程不安全。
5.指令重排序,引起的线程不安全。
那么如何是的让++操作不会被干扰呢?
主要思路是:诺是我们有一种操作,将++操作封装起来,让t1的++操作结束,再让t2的++操作开始。而这种操作就是加锁操作。
简单理解就是,目前有一个厕所,但是有很多要上厕所,怎么办,抢呗,总不可能等着膀胱爆炸吧,等待厕所里的人出来,并且打开厕所门,然后一群人看谁快,谁快谁就先如厕。
有时也把这个现象叫做同步互斥,表示操作是互相排斥的
java中提供了一个关键字:synchronized,监视器锁monitor lock
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到 同一个对象 synchronized 就会阻塞等待.
synchronized用的锁是存在Java对象头里的,底层是使用操作系统的mutex lock实现的.
工作流程:
synchronized 的工作过程:
1. 获得互斥锁
2. 从主内存拷贝变量的最新副本到工作的内存
3. 执行代码
4. 将更改后的共享变量的值刷新到主内存
5. 释放互斥锁
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题
这个问题呢,在synchronized中不会出现。而可重入其实就是,有人厕所上完了,但是他很缺德,将门锁了,然后呢,他突然发现他包忘哪了,回去后他在厕所门口等着,结果等了半天,里面的人死活不出来。但是厕所里没人。
static class Counter {
public int count = 0;
synchronized void increase() {
count++;
}
synchronized void increase2() {
increase();
}
}
可重入锁的内部, 包含了 "线程持有者" 和 "计数器" 两个信息:
synchronized 使用:
public class SynchronizedDemo {
public synchronized void methond() {
}
}
public class SynchronizedDemo {
public synchronized static void method() {
}
}
(锁当前对象)
public class SynchronizedDemo {
public void method() {
synchronized (this) {
}
}
}
(锁类对象 )
public class SynchronizedDemo {
public void method() {
synchronized (SynchronizedDemo.class) {
}
}
}
两个线程竞争同一把锁, 才会产生阻塞等待
Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.
还有一些是线程安全的. 使用了一些锁机制来控制.
volatile 能保证内存可见性
1.代码在写入 volatile 修饰的变量的时候,
2.代码在读取 volatile 修饰的变量的时候,
static class Counter {
public int flag = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
while (counter.flag == 0) {
// do nothing
}
System.out.println("循环结束!");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("输入一个整数:");
counter.flag = scanner.nextInt();
});
t1.start();
t2.start();
}
根据上面的代码,发现问题没有,无论我们在控制台中输入什么值,程序都不会结束。为什么呢,就像上面所说的那样,一个cup的寄存器中数据并没进行更新。另一个线程所拿到的数据没有进行跟换。其主要原因是计算机运算速度太快了。寄存器和缓存的速度都太快了,
使用特点:
1.volatile 不保证原子性
2.volatile 适用于一个线程读,一个线程写
3.synchronized 既能保证原子性, 也能保证内存可见性.
改正后:
static class Counter {
volatile public int flag = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
while (counter.flag == 0) {
// do nothing
}
System.out.println("循环结束!");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("输入一个整数:");
counter.flag = scanner.nextInt();
});
t1.start();
t2.start();
}
此时就可以结束进程了。
由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知. 但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.
完成这个协调工作, 主要涉及到三个方法
wait 做的事情:
wait 结束等待的条件:
notify 方法是唤醒等待的线程.
wait和notify的使用(只能一个结束,一个开始)
static class WaitTask implements Runnable {
private Object locker;
public WaitTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
while (true) {
try {
System.out.println("wait 开始");
locker.wait();
System.out.println("wait 结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
static class NotifyTask implements Runnable {
private Object locker;
public NotifyTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
System.out.println("notify 开始");
locker.notify();
System.out.println("notify 结束");
}
}
}
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(new WaitTask(locker));
Thread t2 = new Thread(new NotifyTask(locker));
t1.start();
Thread.sleep(1000);
t2.start();
}
notify方法只是唤醒某一个等待线程.。使用notifyAll方法可以一次唤醒所有的等待线程。虽然是同时唤醒多个线程, 但是这些线程需要竞争锁。 所以并不是同时执行, 而仍然是有先有后的执行。
其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间,唯一的相同点就是都可以让线程放弃执行一段时间。
1. wait 需要搭配 synchronized 使用.,sleep 不需要。
2. wait 是 Object 的方法 sleep 是 Thread 的静态方法。