多线程共享变量问题

来源:互联网 发布:2017淘宝现在还能刷吗 编辑:程序博客网 时间:2024/05/18 03:11

非线程安全代码举例

public class NoVisibility {    private static boolean ready;    private static int number;    private static class ReaderThread extends Thread {        public void run() {            while (!ready)                Thread.yield();            System.out.println(number);        }    }    public static void main(String[] args) {        new ReaderThread().start();        number = 42;        ready = true;    }}

NoVisibility可能会持续循环下去,因为读线程可能永远看不到ready的值。
另一种更奇怪的现象是,NoVisibility可能输出0,因为读线程看到了写入ready的值,但却没有看到之后写入number的值,这种现象称为“重排序”。

使用同步可以保证可见性

@NotThreadSafepublic class MutableInteger {    private int value;    public int get() {        return value;    }    public void set(int value) {        this.value = value;    }}
@ThreadSafepublic class SynchronizedInteger {    @GuardedBy("this")     private int value;    public synchronized int get() {        return value;    }    public synchronized void set(int value) {        this.value = value;    }}

对get()方法进行同步,保证了value的可见性。

非原子的64位操作

当线程没有同步的情况下读取变量,可能会得到一个失效的值,但是至少这个值是由之前某个线程设置的值,而不是一个随机值。这种安全性保证也被称为最低安全性。
最低安全性适合绝大多数变量,但是存在一个例外:非volatile类型的64位数值变量(double和long)。JVM允许64位的读操作或写操作分解为两个32位的操作。
因此,即使不考虑失效数据问题,在多线程程序中使用共享且可变的long和double等类型的变量也是不安全的,除非用关键字volatile来声明,或者用锁保护起来。

volatile关键字

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
  • 禁止进行指令重排序。

发布与溢出

发布:使对象能够在除当前作用域之外的地方使用。
溢出:指某个对象不应该发布却被发布了。

public class PublishAndEscape {      //发布status      public static String status = "status";      private Object[] objects;      // 内部的可变状态溢出,导致外部可以直接访问并修改object      public Object[] getObjects() {          return objects;      }  } 

改进代码

public class PublishAndEscape {      public static final String STATUS = "status";      private Object[] object;      //参见ArrayList、CopyOnWriteList    public Object[] getObject() {          return Arrays.copyOf(object, object.length);      }   } 

线程封闭

当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术叫做线程封闭(Thread Confinement)。

栈封闭

栈限制是线程限制的一种特例,在栈限制中,只能通过本地变量才可以触及对象。正如封装使不变约束更容易被保持,本地变量使对象更容易被限制在线程本地中。本地变量本身就被限制在执行线程中;它们存在于执行线程栈。其他线程无法访问这个栈。栈限制(也称线程内部或者线程本地用法,但是不要与核心库类的ThreadLocal混淆)

public int loadTheArk(Collection<Animal> candidates) {          SortedSet<Animal> animals;          int numPairs = 0;          Animal candidate = null;          //animals被封装在方法中,不要使它们溢出          animals = new TreeSet<Animal>(new SpeciesGenderComparator());          animals.addAll(candidates);          for(Animal a:animals){              if(candidate==null || !candidate.isPotentialMate(a)){                  candidate = a;              }else{                  ark.load(new AnimalPair(candidate,a));                  ++numPairs;                  candidate = null;              }          }          return numPairs;  }  

ThreadLocal

这个类能使线程中的某个值与保存值的对象关联起来。
ThreadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。
当某个线程终止后,与线程对应的对象会作为垃圾回收。
ThreadLocal对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享。

public class ConnectionDispenser {    static String DB_URL = "jdbc:mysql://localhost/mydatabase";    private ThreadLocal<Connection> connectionHolder            = new ThreadLocal<Connection>() {                public Connection initialValue() {                    try {                        return DriverManager.getConnection(DB_URL);                    } catch (SQLException e) {                        throw new RuntimeException("Unable to acquire Connection, e");                    }                };            };    public Connection getConnection() {        return connectionHolder.get();    }}
原创粉丝点击