一、线程安全问题概念:
指在多线程环境中线程无序调动,导致执行顺序不确定,则代码运行的结果不符合我们的预期。
二、线程安全问题产生的原因:
1.修改共享数据
变量在堆上,可以被多个线程共享访问,一旦涉及到多个线程对同一个变量进行修改 ,就会触发线程安全问题。
2.变量不保证原子性
原子性也指事务的不可分割性,一个事务的所有操作要么不间断地全部被执行,要么一个也没有执行。不保证原子性会给多线程带来的问题是:如果一个线程正对一个变量操作,由于线程调度是无序的(线程的抢占式调度现象),中途可能会让其他线程插进来了,如果这个操作被打断了,结果可能就是错误的。
3.内存可见性
可见性是指 一个线程对共享变量的值的修改,能够及时的被其他线程看到。这里我们先来讲讲java的内存模型。
Java的内存模型:
~线程之间的共享变量存在于主内存中;
~每一个线程都有自己的"工作内存"(cpu的寄存器和高速缓存);
~当线程要读取一个共享变量的时候,他先将共享变量从主内存拷贝到工作内存(以此访问寄存器中load过的值),再从工作内存中读取数据;【为什么访问寄存器中load过的值,而不直接访问内存中的变量值?/为什么要进行拷贝?答:因为cpu访问自身寄存器和访问高速缓存的速度,远远超过访问内存的速度,能大大提高效率】
~当线程要修改一个共享变量的时候,也会先修改工作内存中的副本,再同步回主内存中(不能及时)。
总结以上,内存可见性带来线程不安全问题的最主要原因是:每个线程都有自己的工作内存,这些"工作内存"中的内容相当于同一个主内存中共享变量的副本,在多线程中修改线程1的工作内存中的值,线程2的工作内存不一定及时变化。
4.多线程中的指令重排序
在单线程环境下,指令重排序将代码的执行方式进行优化,而不会影响代码的结果。而在多线程环境下,由于多线程的代码执行复杂程度更高,编译器很难在编译阶段对代码的执行效果进行预测,因此很容易导致优化后的逻辑和之前的不等价。
什么是指令重排序?
编译器对于指令重排序的前提是"保持重排序前后代码逻辑不发生变化",目的是提高代码的执行效率。举一个例子:
实例化new一个对象【s=new Student()】/【或者举买房的例子】一般分为三步:
①申请一块内存空间;(买房先交钱)
②调用构造方法------即初始化内存中的数据;(给房间的空间进行装修)
③把对象的引用赋值给s----给申请到的内存地址赋值;(拿到房子钥匙)
在多线程中指令重排序难度很高,而以上例子可以适用于单线程中②③会发生指令重排序:
排序后的结果有两种、①②③(精装修) ①③②(毛胚房)
三、线程安全问题的解决方案
1.引入synchronized关键字(监视器锁)
1.1synchronized的特性:
①是互斥锁。synchronized会起到互斥效果,某个线程执行到某个对象的synchronized中时,其他线程如果执行到同一个对象synchronized就会产生阻塞等待。(等待上一个线程解锁了,等待操作系统唤醒此线程后,才能对此对象加锁使用)
synchronized void add(){//前花括号相当于针对当前对象加锁
count++;
}//后花括号相当于在此方法中针对当前对象解锁
②刷新内存,保证内存可见性。synchronized的工作过程如下:
a.获得互斥锁
b.从主内存拷贝变量的最新副本到工作内存中
c.执行代码
d.将修改后的共享变量的值刷新到主内存中
e.释放互斥锁
③是可重入锁。synchronized同步块对于同一条线程来说是可重入的,通俗的说,如果一个线程对某个对象加锁了,由于某种原因未释放锁出来了,再次加锁进去时,此过程不会产生阻塞等待(不用等待第一次加的锁释放),即不会出现把自己死锁的情况。
static class counter{
public int count=0;
synchronized void increase(){
count++;
}
synchronized void increase2(){//在这个方法中,调用increase2 的时候先加了一次锁,执行到increase的时候,又加了一次锁【上一个锁还没释放,相当于连续加锁两次】
increase();
}
}
以上代码是没有问题的,因为synchronized是可重入锁。
2.引入volatile关键字
2.1.volatile修饰的变量,能够保证“内存可见性”
代码在写入volatile修饰的变量时:
①当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改后的结果。
②当一个线程向被volatile修饰的变量写入数据的时候,虚拟机会强制他被刷新到主内存中,保证每次都是从主内存中读取变量的值。
代码在读取volatile修饰的变量时:
①从主内存中读取volatile变量的最新值到线程的工作内存(cpu寄存器或cpu缓存),缓存的读取速度介于寄存器和内存之间。
②当一个线程被volatile修饰时,修改后也可直接在主内存中读取。(cpu每次都需要从主内存中读取,性能会减弱)
//1.创建两个线程t1和t2
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){//2.t2中包含一个循环,这个循环以flag==0为循环条件
//do things
}
System.out.println("循环结束了");
});
Thread t2=new Thread(()->{
Scanner scanner=new Scanner(System.in);//t2从键盘读入一个整数,并把这个整数值赋予flag
counter.flag=scanner.nextInt();
});
t1.start;
t2.start;
}
上述代码当用户输入非0时,t1线程循环不会结束~当t2对flag变量进行修改,此t1感知不到flag变化;这是就要修改代码的flag加上volatile;
static class Counter{
public volatile int flag=0;
{
2.2.volatile修饰的变量,不保证“原子性”
volatile强制读写内存,适用于一个线程读,一个线程写~(速度是变慢了,但是数据变得更准确)
//给count加上volatile关键字
static class Counter{
volatile public int count=0;
void increase(){
count++;
}
}
public static void main(String[] args) throws InterruptedException{
final Counter counter=new Counter();
Thread t1=new Thread(()->{
for(int i=0;i<5000;i++){
counter.increase();
}
});
Thread t2=new Thread(()->{
for(int i=0;i<5000;i++){
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(Counter.count);
}
可以看到,count被volatile修饰,最终的count值无法保证是10000
2.3.volatile修饰的变量,能够防止指令重排序
指令重排序是编译器和处理器为了高校对程序优化的手段,只能保证程序的执行结果正确,但无法保证程序的操作执行顺序和代码顺序一致。指令重排序在单线程中不会出现问题,在多线程中会出现问题。
3.引入wait和notify
3.1.引入wait和notify的目的
由于线程之间的先后执行顺序难以预知(抢占式执行),希望合理的协调多个线程之间的执行先后顺序。完成这个协调工作,主要用到两个方法:
①wait()/wait(long timeout):让线程进入等待状态。
②notify()/notifyAll():唤醒在当前对象上等待的线程。
注意:wait,notify,notifyAll都是Object类方法
3.2 wait()方法
wait要做的事情:
①把线程放到等待队列中;
②释放当前的锁;
③满足一定条件时被唤醒,队列中的线程尝试重新获取这把锁。
wait要搭配synchronized来使用,单独使用wait会直接抛出异常。
不带参数的wait是死等,带参数的wait会等待到最大时间后,没有notify就自己唤醒。
wait结束等待的条件:
①其他线程调用该对象的notify方法;
②wait等待超时;
③其他线程调用该等待线程的interrupted方法,导致wait抛出interruptedExpection异常。
wait()方法使用示例:
public static void main(String[] args) throws InterruptedException{
Object object=new Pbject();
synchronized(object){
System.out.println("正在等待");
object.wait();
System.out.println("等待结束");
}
}
3.3 notify()方法
notify方法的介绍:
①方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的
其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
②如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到")
③在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行
完,也就是退出同步代码块之后才会释放对象锁。
notify()方法使用示例<3步>:
第一步:创建WaitTask类,对应一个线程,run内部循环调用wait;
static class WaitTask implements Runnable{
private Object locker;
public WaitTask(Object locker){
this.locker=locker;
}
public void run(){
synchronized (locker){
while(true){
try{
System.out.println("wait 开始");
locker.wait();
System.out.println("wait 结束");
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
}
第二步:创建 NotifyTask 类, 对应另一个线程, 在 run 内部调用一次 notify
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 结束");
}
}
}
第三步:写main方法
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();
}
3.4 notifyAll()方法
notifyAll方法的介绍:
notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程.
notifyAll()方法使用示例
创建 3 个 WaitTask 实例. 1 个 NotifyTask 实例.
第一步: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.notifyAll();
System.out.println("notify 结束");
}
}
}
//第三步:
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(new WaitTask(locker));
Thread t3 = new Thread(new WaitTask(locker));
Thread t4 = new Thread(new WaitTask(locker));
Thread t2 = new Thread(new NotifyTask(locker));
t1.start();
t3.start();
t4.start();
Thread.sleep(1000);
t2.start();
}
四、wait 和 sleep 的对比
不同: 1.wait 是用于线程之间的通信的,sleep是让线程阻塞一段时间
2.wait 需要搭配 synchronized 使用. sleep 不需要.
3. wait 是 Object 的方法 sleep 是 Thread 的静态方法.
相同:都可以让线程放弃执行一段时间.