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

线程安全问题是因为在多线程的调度下,存在随机性,也正是因为随机性导致多个线程共同争抢资源,从而出现BUG

一.什么是线程安全

        在操作系统中,因为线程的调度是随机的(抢占式执行),正是因为这种随机性,才会让代码中产生很多bug如果因为这样的线程调度才导致代码产生了bug,则认为线程是不安全的,如果这样的调度,并没有让代码产生ug。我们则认为线程是安全的。

本质上线程不安全是因为线程之间的调度顺序是不确定的。

二 线程不安全的原因

1.抢占式执行;

2.多个线程修改同一个变量;

3.修改操作,不是原子的;

4.内存可见性,引起的线程不安全;(volatile)

5.指令重排序,引起的线程不安全

三 解决线程安全问题

3.1 加锁sychronized

加锁能够起到保证“原子性”的效果。锁的核心操作有两个:加锁和解锁。一但某个线程加锁了之后,其他线程也想加锁就不能直接加上,就需要阻塞等待,一直等到拿到锁的线程释放了锁为止。当1号释放锁之后,等待锁的2号和3号线程,谁能抢先一步拿到锁,谁就成功加锁。

加锁本质上是把并发的变成了串行的

package asd;

/**
 * 使用两个线程来累加 count 的值
 * 每个线程循环 1w 次,累加变量 count 的值,count 默认值为 0,注意线程安全问题。
 */
class Counter{
    private int count = 0;
    public void add(){
       synchronized (this){
           count++;
       }
    }
    public int get(){
        return count;
    }
}
public class Test12 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter.add();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.get());
    }
}

在上述代码中,这两个线程是在竞争同一个锁对象,就会产生锁竞争(t1拿到锁,t2就会阻塞)此时就可以保证++操作就是原子的相互不会影响了.

()内的是锁对象:

        1.如果两个线程,针对同一个对象加锁,此时就会出现"锁竞争"(一个线程先拿到了锁,另一个线程阻塞等待)

        2.如果两个线程,针对不同对象加锁,此时不会存在锁竞争,各自获取各自的锁即可.

package Threading;

//线程不安全
class Counter{
    private int count = 0;
    private Object locker = new Object();
    public void add(){
        synchronized (locker){
            count++;
        }

    }

    public int get(){
        return count;
    }
}
public class ThreadDemo13 {
    public static void main(String[] args) throws InterruptedException{
        Counter counter = new Counter();

        //搞两个线程,两个线程分别对这个counter自增5w次
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        t1.start();
        t2.start();

        //等待两个线程执行结束,然后看结果
        t1.join();
        t2.join();
        System.out.println(counter.get());
    }
}

()里的锁对象,可以使任意一个Object对象(基本数据不行),

 这两种写法都一样,直接给对方使用synchronized修饰,此时就相当于一this为锁对象.

如果synchronized修饰静态方法(static)此时就不是给this加锁,而是给类对象加锁

 手动指定一个锁对象:

 如果多个线程尝试对同一个锁对象加锁,此时就会产生锁竞争针对不同对象加锁,就不会有锁竞争.

区别:join加锁

join只是让两个线程完整的进行串行

加锁是让两个线程的某个小部分串行了,大部分都是并发的

3.2 由于线程可见性,引起的线程不安全

        所谓的内存可见性,就是多线程环境下,编译器对于代码要求优化,产生了误判,从而引起bug,进一步导致代码的bug.

        代码编写完毕:

        预期结果:t1通过falg == 0作为条件进行循环,初始情况,将循环t2通过控制台输入一个整数.一旦用户输入了非0的值,此时t1的循环就会立即结束,从而t1线程退出

        实际结果:输入非0的值之后t1线程并没有退出,循环没有结束.通过jconsole可以看到t1线程仍然在执行,处在RUNNABLE状态.

 3.2.1  处理方式:让编译器针对这个场景暂停优化

        使用关键字volatile被volatile修饰的变量,此时编译器就会禁止上述优化,能够保证每次都是从内存重新读取数据.

package Threading;
import java.util.Scanner;
public class ThreadDemo14 {
    volatile public static int flag = 0;//解决
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (flag == 0){
                //空着
//                try {
//                    Thread.sleep(10);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
            }
            System.out.println("循环结束!t1结束!");
        });
        Thread t2 = new Thread(() ->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数:");
            flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

加上volatile关键词之后,此时编译器就能够保证每次都是从新从内存读取flag变量的值.此时t2修改falg,t1就可以立即感知到,t1就可以正确退出了.

volatile不保证原子性

volatile适用的场景:一个是线程读,一个线程写的情况.

synchronized:是多个线程写...

volatile的这个效果,称为"保证内存可见性",还有一个效果:禁止指令重排序

"synchronized也能保证内存可见性"存在争议

指令重排序:(也是编译器优化的策略),调整了代码执行的顺序,让程序更高效,前提也是保证整体逻辑不变

3.3 wait和notify

        线程的额度是无序的,随机的,但是也是有一定的需求场景,希望线程有序执行

        join:算是一种控制顺序的方式.(功效有限)

        wait():就是让某个现成先暂停下来等一等(条件不满足或时机不成熟,就先阻塞等待)

        notify():就是把该线程唤醒,能够继续执行(其他线程构造了一个成熟的条件,就可以唤醒1号线程)

        wait和notify是Object的方法:只要你是个类对象(不是内置类型),都是可以使用wait和notify

 IllegalMonitorStateException:不正确的  监视器/显示器(指的是synchronized监视器锁)

非法的锁状态异常:

wait()主要做三件事:1.解锁;2.阻塞等待;3.当收到通知的时候,就唤醒同时尝试重新获取锁

wait()必须写到synchronized代码块里面

 

 加锁的对象必须和wait的对象是同一个

notify也是要放到synchronized中使用

package Threading;

public class ThreadDemo16 {
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(() ->{
            
                    try {
                        System.out.println("wait开始");
                        synchronized (locker){
                        locker.wait();
                        }
                        System.out.println("wait结束");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
        });
        t1.start();
        Thread.sleep(1000);
        Thread t2 = new Thread(() ->{
           synchronized (locker){
               System.out.println("notify开始");
               locker.notify();
               System.out.println("notify结束");
           }
        });
        t2.start();
    }
}

 t1先执行,执行到了wait就阻塞,t1之后t2开始执行,执行到notify就会通知t1线程唤醒

        注意:notify是在synchronized内部,就需要t2释放锁t1才能继续往下走

使用wait阻塞状态会让线程进入WAITING状态,wait也提供了一个带参数的版本,参数指定的是最大等待时间

Object.notifyAll:会把所有进入阻塞状态的线程都唤醒

  • 19
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值