自学内容网 自学内容网

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

一、什么是线程安全?

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

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

1.抢占式执行

   多线程调度的过程,可以是认为“随机”的,没有规律;

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

多个线程同时修改同一个变量,容易产生线程安全问题如果是修改不同变量,那么多个线程之间的寄存器数据修改对内存中的数据修改影响不大。

3.变量不保证原子性

原子性也指事务的不可分割性,一个事务的所有操作要么不间断地全部被执行,要么一个也没有执行。不保证原子性会给多线程带来的问题是:如果一个线程正对一个变量操作,由于线程调度是无序的(线程的抢占式调度现象),中途可能会让其他线程插进来了,如果这个操作被打断了,结果可能就是错误的。

4.指令重排序

在单线程环境下,指令重排序将代码的执行方式进行优化,而不会影响代码的结果。而在多线程环境下,由于多线程的代码执行复杂程度更高,编译器很难在编译阶段对代码的执行效果进行预测,因此很容易导致优化后的逻辑和之前的不等价。

三、解决线程的安全问题

加锁和解锁就是确保不是多个线程同时使用操作同一个数据,导致线程不安全。

1、synchronized(同步锁)

概念:

 针对某个修改操作进行加锁,让修改操作变成原子的.就像我们学校的公共澡堂一样,大家去洗澡都是抢占式洗,谁先占到位置就可以把门儿锁了然后洗澡,其它人就进不去了,想洗多久就洗多久,只有我洗完其它人才可以到我的这个位置洗.一旦一个线程先拿到锁对某个修改操作加锁,那么其它线程就无法对该修改操作加锁,需要拿到锁的线程释放锁之后才能加锁.这里不要将join()和加锁混淆哦!join()是将多个线程进行串行,而加锁只会让多个线程间的某个步骤进行串行,其它步骤还是并行.

代码实现:

public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        String s = "锁对象的外貌没有任何影响";
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (s) {
                    count++;
                }
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (s) {
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count="+count);
    }

count++操作就会被打包一个操作,也就是原子性,此时三条执行的执行顺序是一体的,也就是不会再产生上述的线程不安全问题了。

两个线程对同一个对象加锁之后,当t1先拿到锁,也就是先执行代码,即使t1线程执行到一半,被cpu调度走;此时t2线程也无法进行拿到锁。

加锁后,count++语句是串行执行的,而for循环语句是并行执行。

当t1释放锁之后,t1线程和t2线程还是会同时争夺这把锁,也就是说他们的拿到锁的顺序也是不确定的。

2、使用volatile关键字

  在多线程情况下,某线程A没有休眠就会执行非常快,编译器会对某个变量flag的重复加载进行优化,让flag只被加载一次,那么当另外的线程对flag进行修改后,在线程A中对flag的值还是初始加载的值,就会对变量flag的值产生误判,从而引起bug.这种情况我们就可以使用volatile关键字修饰变量flag,让编译器暂停优化,保证内存可见性(synchronized关键字是否能保证内存可见性目前是存在争议的).
代码实现:

    public static int flag = 0;
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (flag == 0) {
 
                }
                System.out.println("thread1结束");
            }
        });
 
        Thread thread2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入要修改flag的值:>");
            flag = scanner.nextInt();
        });
 
        thread1.start();
        thread2.start();

   

volvatile 关键字有如下两大作用:

禁止指令重排序:保证指令执行的顺序,防止 JVM 出于优化而修改指令执行顺序,引发线程安全问题。
保证内存可见性:也就是说,保证了我们读取到的数据是内存中的数据,而不是缓存,具体的,当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。

3.wait-notify

为了线程能按照规定的顺序执行,使用wait-notify。这两个都是Object提供的方法。

wait在执行时:

  1. 解锁;
  2. 阻塞等待;
  3. 当被其他线程唤醒之后,尝试重新加锁,加锁成功,wait执行完毕,继续往下执行其他逻辑。

故我们的 wait 方法和 notify 方法都要在 synchronized 内部使用,并且和synchronized的对象一致,如:

如果 wait 没有搭配synchronized 使用,会直接抛出异常

注意事项

  1.  要想让 notify 能顺利唤醒 wait ,需要确保 wait 和 notify 都是使用同一个对象调用的;
  2.  wait 和 notify 都需要在 synchronized 内部执行,notify 在 synchronized 内部执行是   Java强制要求的;
  3.  如果进行 notify 时,另一个线程没有处于 wait 状态不会有任何影响。

当 wait 引起线程阻塞时,可以使用 interrupt 方法打断当前线程的阻塞状态

4.wait 和 sleep 的区别

  1. wait 需要搭配synchronized 使用,sleep 不需要;
  2. wait 是 Object 的方法,sleep 是Thread 的静态方法。
  3.  wait 是用于线程之间的通信的,sleep是让线程阻塞一段时间。
  4. wait和sleep都可以让线程放弃执行一段时间

原文地址:https://blog.csdn.net/Brenda_Bestow/article/details/140253197

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!