zoukankan      html  css  js  c++  java
  • java并发编程可见性与线程封闭

    可见性

      所谓可见性,指的是当一个线程修改了对象的状态后,其他线程能够看到该对象发生的变化。在单线程环境下,向某个变量写入值,然后在后面的操作再读取,在这个过程中该变量的值对该线程来说总是可见。但是,在多线程环境下,可见性就不一定等到保证,例如,对于一个共享变量 share = 0 来说,线程1和线程2都进行share++ 操作,但是最终share 的结果并不一定是2。先看看一段代码

    public class NoVisibility {
    
       private static boolean ready;
    
       private static int number;
    
       private static class ReaderThread extends Thread{
          public void run() {
             while (!ready) {
                Thread.yield(); //当前线程从运行态->就绪态,重新竞争cpu
             }
             System.out.println(number);
          }
       }
    
       public static void main(String[] args) {
          new ReaderThread().start();
          number = 42;
          ready = true;
       }
    }

      上面的 NoVisibility 可能会一直循环下去(虽然这种情况发生的概率很小),因为 ReaderThread 线程一直看不到主线程对ready的更新;还有另一种情况是输出结果可能是0,有人会问有输出说明 ready 已经被更新为 true,那么 number 也应该被更新了42,看起来是这样,但在jvm执行指令时会出现“指令重排序”的现象。

      “指令重排序”指的是处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。对于上面的代码,readynumber 谁先赋值对最终的程序结果并没有影响(对于main线程来说),故在实际执行可能先对 ready 赋值。但是要注意的是如果后面的语句对前面的语句存在依赖关系时,则不会发生“指令重排序”,例如

    int a = 2;    //语句1
    int r = 2;    //语句2
    a = a + 3;    //语句3
    r = a*a;     //语句4

    语句4 的 r 依赖于语句3的 a 的处理结果,故执行时语句4不能在语句3之前执行

      在上面 NoVisibility 产生错误的原因是缺乏同步使得 ReadTread 线程读取了失效数据。另外有一点,对于绝大多数基本变量的读取和写入都是原子操作,但是64位的数值变量除外(如long,double),这是因为对于64位的变量,jvm允许将64位的读或写操作分解为两个32位操作,当对该变量读和写操作在不同线程进行,有可能会读取到当前值的高32位和重新赋值后的低32位,如线程1去读 long num 这个变量,还未开始读取,这时线程2对num这个变量重新赋值,先对低32位进行更新,还未更新高32位,这时线程1继续执行读取操作,于是线程1就读取了原来的高32位和更新后的低32位。

      那么怎么使得不同线程不会读取到失效数据呢?一种简单的方式是加上内置锁,这样使得某一线程在尚未执行完同步代码块前,其他的线程无法执行同步代码块,这样就保证了每个线程都能看到共享变量的最新值;另外的一种方式是把共享变量用 volatile 修饰,线程在读取volatile 变量时总是会返回最新写入的值,关于volatile更多详细请看下面参考链接https://www.cnblogs.com/dolphin0520/p/3920373.html

           不过这里有一个点要注意,volatile 虽然保证了变量的可见性,但并不保证原子性,这也是为什么用 volatile 修饰的 int i 变量执行 i++ 操作仍不能保证线程安全,其实这要从i++ 这个操作的原理来讲,i++包括三个操作:1、取 i 值;2、将 i+1 存入 tmp;3、i = tmp。在多线程对 valotile 修饰的 i 进行++操作时,先假设线程1执行完第2步后堵塞这时线程2对 i 进行了更新,通知其他线程内存中的 i 已经被更新,你们应该重新取 i 值,但线程1在第3步并不需要取 i 值,而是将 tmp 值存入i。所以在使用volatile关键字是应该要记住它并不保证原子性。

    线程封闭

      什么是线程封闭?当我们访问共享变量时,通常要使用同步,一种避免使用同步的方式就是不共享数据。很显然仅在单线程内访问数据,就不需要同步,这种避免共享数据的技术就是线程封闭。在java中,较常用到的线程封闭技术是栈封闭和使用 ThreadLocal 类。

      栈封闭:我的理解就是使用线程内部的局部变量。

      ThreadLocal 类:这个要仔细讲讲,ThreadLocal 类为每个线程保存了一份独立的副本,每次线程执行 ThreadLocalgetset 方法都是每次以当前线程为参数去取当前线程对象里的 ThreadLocalMap,而 ThreadLocalMap 保存着以 ThreadLocal 对象为 key 的键值对,这样就使得每个线程访问 ThreadLocal 变量互不干扰。先来看看ThreadLocal类怎么使用。

    public class Test {
       private static ThreadLocal<Connection> conn
          = new ThreadLocal<Connection>() {
          // 重写 ThreadLocal类里的initialValue()方法
          public Connection initialValue() {
             try {
                return DriverManager.getConnection("DB_URL");//取得某一数据库连接
             } catch (SQLException e) {
                e.printStackTrace();
             }
             return null;
          }
       };
    
       public static Connection getConnection() {
          // 调用conn对象的get方法
          return conn.get();
       }
    }

    下面看看ThreadLocal类的源码

    先看看 get 方法

    public ThreadLocal() {
    
    }
    public T get() {
            Thread t = Thread.currentThread();
    
    /*
    * 查看当前线程t有没有相应的map,注意,该方法传入的参数为当前线程,
    * 返回的是线程t的静态变量 threadLocals,该变量初始值为null,故对不同
    * 线程来说,每个线程都有自己的threadLocals
    */ ThreadLocalMap map = getMap(t); /*
    *     ThreadLocalMap getMap(Thread t) { *      return t.threadLocals; *     } */ if (map != null) { // 获取当前线程下对象的value,注意,每个线程都存有当前对象的value ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } // 如果线程是第一次调用ThreadLocal的get方法,那么返回值从重写的初始化方法得到 return setInitialValue(); }

    去看看 setInitialValue() 方法

    private T setInitialValue() {
            // 初始化value
            T value = initialValue();
            Thread t = Thread.currentThread();
            // 取当前线程的map
            ThreadLocalMap map = getMap(t);
            if (map != null)
                // 不为空,更新当前线程下该对象的value
                map.set(this, value);
            else
             // map为空,创建map
                createMap(t, value);
            return value;
    }

    看看 ThreadLocal 怎么创建 ThreadLocalMap

    // 这个方法是使得ThreadLocal类保存线程本地变量的关键,它新建的ThreadLocalMap是以当前ThreadLocal对象为key,然后是存在该线程的静态变量threadLocals里。
    
    void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
                table = new Entry[INITIAL_CAPACITY];
                int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
                // 对于table里的引用,每次都是new出来了,故不会和其他线程指向同一个当前对象
                table[i] = new Entry(firstKey, firstValue);
                size = 1;
                setThreshold(INITIAL_CAPACITY);
     }

    看到这里,相信大家就已经大概明白ThreadLocal类线程封闭的具体原理了。

    以上内容如有不当之处,请指出。谢谢!

    参考链接:

    https://www.cnblogs.com/dolphin0520/p/3920407.html

    https://www.cnblogs.com/dolphin0520/p/3920373.html

  • 相关阅读:
    hdu2328 Corporate Identity
    hdu1238 Substrings
    hdu4300 Clairewd’s message
    hdu3336 Count the string
    hdu2597 Simpsons’ Hidden Talents
    poj3080 Blue Jeans
    poj2752 Seek the Name, Seek the Fame
    poj2406 Power Strings
    hust1010 The Minimum Length
    hdu1358 Period
  • 原文地址:https://www.cnblogs.com/X-huang/p/10725649.html
Copyright © 2011-2022 走看看