线程安全问题的原因和解决方案

一、线程安全问题概念:

指在多线程环境中线程无序调动,导致执行顺序不确定,则代码运行的结果不符合我们的预期。

二、线程安全问题产生的原因:

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 的静态方法. 
相同:都可以让线程放弃执行一段时间. 
 
 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值