zoukankan      html  css  js  c++  java
  • 线程不安全

    众所周知,多线程访问同一公共资源会带来线程的不安全,本文探讨一下这个问题的若干细节。

    关于线程安全的基本问题

    有关线程安全常涉及两个概念:

    竞态条件:当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。
    临界区:导致竞态条件发生的代码区称作临界区(线程不安全的代码区)。

    线程安全与竞态条件:线程安全的代码区不存在竞态条件,线程不安全的代码区(临界区)存在竞态条件。

    为什么会出现线程安全问题?
    道理很简单,多个线程同时访问(写操作)同一个公共资源必然带来问题,我们举2个例子:
    例1:线程A,线程B同时拿到全局变量i(值为0)并存储在自己的本地栈中,线程A对i加1,线程B也对i加1,那么线程A,线程B提交后,i的结果为1,而不是我们期望的结果2。
    例2:线程A,线程B同时拿到全局变量i(值为0)并存储在自己的本地栈中,线程A对i加1,线程B随后读取i,那么线程B读取的结果依然为0,而不是我们期望的结果1。这就是多线程并发导致的可见性问题。

    如何解决线程不安全?
    临界区进行同步,从而避免竞态条件。如:使用synchronized或JUC中的Lock对临界区加锁。给临界区加锁好比给公园的一个厕所加了一把锁,避免了人们共同进入厕所的问题,而必须是只有拿到钥匙的人才能进入(这个例子有点那个,但是我总会联想到这个例子)。

    举例说明

    既然多线程引发的安全问题是因为同时访问同一个公共资源导致的,相应的,如果多线程访问的不是公共资源也就不会发生线程安全问题。下面按资源是否公共举几个例子来说明问题。

    公共资源:对象的成员变量

    对象的成员变量:成员变量存储在共享堆上,如果两个线程同时更新同一个对象的同一个成员变量,那这个代码就不是线程安全的。
    示例代码:

    public class ThreadSafe_ {
        public static void main(String[] args) {
            Obj obj = new Obj();
            MyRunnable task = new MyRunnable(obj);
            new Thread(task,"t1").start();
            new Thread(task,"t2").start();
        }
    }
    class Obj {
        StringBuilder noSafeBuilder = new StringBuilder();// 对象的成员变量:成员变量存储在共享堆上,如果两个线程同时更新同一个对象的同一个成员变量,那这个代码就不是线程安全的。
    
        void add(String text){
            noSafeBuilder.append(text);
        }
    }
    class MyRunnable implements Runnable{
        private Obj obj = null;
    
        MyRunnable(Obj obj){
            this.obj = obj;
        }
    
        @Override
        public void run() {// 临界区(线程不安全的代码区,因为多个线程可能同时修改成员变量noSafeBulider,所以会带来问题)
            this.obj.add(" "+Thread.currentThread().getName());
            System.out.println(Thread.currentThread().getName()+"=>"+this.obj.noSafeBuilder.toString());
        }
    }
    

     打印结果:

    t2=> t1 t2
    t1=> t1 t2

    当然,上面run方法因为线程不安全,无法保证线程的执行顺序,所以上面的代码运行多次可能带来多个结果:在这段代码中,我们期望的是,Obj对象的noSafeBuilder属性先追加1个线程名然后打印这个线程名,然后noSafeBuilder再追加第2个线程名并打印追加的2个线程名,但从打印结果看,第1个线程在执行完run方法后就已经打印了2个线程名——这就是线程不安全所带来的问题。

    从上图可以知道,线程不安全会带来多少问题。

    解决办法:

    class MyRunnable implements Runnable{
        private Obj obj = null;
    
        MyRunnable(Obj obj){
            this.obj = obj;
        }
    
        private final Object lock = new Object();
    
        @Override
        public void run() {
            synchronized (lock) {//临界区 加锁同步
                this.obj.add(" "+Thread.currentThread().getName());
                System.out.println(Thread.currentThread().getName()+"=>"+this.obj.noSafeBuilder.toString());
            }
        }
    }
    

    打印结果:

    t1=> t1
    t2=> t1 t2

    由于t2可能先执行,所以上面代码的打印结果也可能为:

    t2=> t2
    t1=> t2 t1

    非公共资源:局部基本类型变量

    局部变量存储在线程自己的栈中,也就是说,局部变量永远也不会被多个线程共享。如:

    public class ThreadTest {
        public static void main(String[]args){
            MyThread share = new MyThread();
            for (int i=0;i<50;i++){
                new Thread(share,"线程"+i).start();
            }
        }
    }
    
    class MyThread implements Runnable{
        public void run() {
            int a =0;
            ++a;
            System.out.println(Thread.currentThread().getName()+":"+a);
        }
    }

    无论多少个线程对run()方法中的基本类型a执行++a操作,只是更新当前线程栈的值,不会影响其他线程,也就是不共享数据。

    特殊资源:局部的对象引用

    为什么这个是特殊示例呢?因为对象的局部引用和基础类型的局部变量不太一样,尽管引用本身没有被共享,但引用所指的对象并没有存储在线程的栈内。所有的对象都存在共享堆中。这就意味着访问对象的引用的代码可能是线程安全的,也有可能是线程不安全的——这主要取决于:在某个方法中创建的对象是否会逃逸(即该对象不会被其它方法获得,也不会被非局部变量引用到),如果不会,那么这个方法就是线程安全的,反之就是不安全的。

    实际上,哪怕将这个对象作为参数传给其它方法,只要别的线程获取不到这个对象,那它仍是线程安全的。如:

        public void method1(){
            LocalObject localObject = new LocalObject();
            localObject.callMethod();
            method2(localObject);
        }
    
        public void method2(LocalObject localObject){
            localObject.setValue("value");
        }
  • 相关阅读:
    Codeforces 834E The Bakery【枚举+数位dp】
    Codeforces 834D The Bakery【dp+线段树维护+lazy】
    Codeforces Round #426 (Div. 2)【A.枚举,B.思维,C,二分+数学】
    暑期英语学习(词组积累)【持续更新中】
    “玲珑杯”ACM比赛 Round #19题解&源码【A,规律,B,二分,C,牛顿迭代法,D,平衡树,E,概率dp】
    关于前端的photoshop初探的学习笔记
    2017 Multi-University Training Contest
    BZOJ 1041: [HAOI2008]圆上的整点【数论,解方程】
    微信公众平台教程,注册申请、认证、开发、推广营销,教你怎么用微信公众号
    微信电视2.0版将新增语音搜索、节目单分享推荐自定义等
  • 原文地址:https://www.cnblogs.com/wql025/p/14391704.html
Copyright © 2011-2022 走看看