欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

多线程共享变量问题

程序员文章站 2022-05-02 13:12:19
...

非线程安全代码举例

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的值,这种现象称为“重排序”。

使用同步可以保证可见性

@NotThreadSafe
public class MutableInteger {
    private int value;

    public int get() {
        return value;
    }

    public void set(int value) {
        this.value = value;
    }
}
@ThreadSafe
public 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();
    }
}