【Java并发编程实战-阅读笔记】02-对象的共享

        编写正确的并发程序需要在访问可变状态的时候进行正确的管理。前面说了如何通过同步避免多个线程在同一个时刻访问相同的数据,本章介绍如何共享和发布对象,才能让对象安全地被多个线程同时访问。
        synchronized只是实现了原子性和临界区。我们还希望某个线程修改对象状态后,其他线程能够立刻看到状态的变化。

3.1 可见性

        一般情况下,我们无法保证执行读操作的线程能够立刻看到其他线程写入的值,比如下面的例子:
public class NoVisibility {
    private static boolean ready;
    private static int number;

    public static class ReaderThread extends Thread {
        public void run() {
            while (!ready) {
                Thread.yield();
            }
            System.out.println(number);
        }
    }

    public static void main(String[] args) throws Exception {
        new ReaderThread().start();
        number = 42;
        ready = true;
        System.out.println("赋值结束");
    }
}
上面的代码虽然看起来没有问题,运行起来似乎也正确。但是,会存在如下可能性:
1、线程输出了0。(未测试出来)因为CPU会对指令编码进行重排序,导致“ready=true”先执行,“member=42”后执行。
2、死循环。虽然静态变量是公共的,子线程可能永远看不到主线程修改后的值。(因为子线程看到的是线程自身缓存的值,如果没有一个适当的触发机制让线程内的缓存重新触发更新,那么尽管主线程修改了静态变量,子线程仍然看不到修改后的值)
经过修改后的代码,就能出现死循环的情况:
public class NoVisibility {
    private static boolean ready;
    private static int number;

    public static class ReaderThread extends Thread {
        public void run() {
            int i = 0;
            while (!ready) {
                i++;
                /* 把下面这句print代码放开,就能触发内存更新,线程才能读取新的ready的值。 */
//                System.out.println("--进入循环体-");
                /*通知系统放弃执行该线程,转交其他线程,自己可能会由运行态-->可运行态 */
//                Thread.yield();
            }
            System.out.println(number + "," + i);
        }
    }

    public static void main(String[] args) throws Exception {
        new ReaderThread().start();
        Thread.sleep(5);
//        TimeUnit.MILLISECONDS.sleep(10);
        number = 42;
        ready = true;
        System.out.println("赋值结束");
    }
}

一、失效数据

        上面由于ready没有及时获取主线程更新到静态变量的值,还是用了之前的值做判断,称之为失效数据,这也是缺乏同步的表现。
        这种失效数据可能会导致意料之外的异常、被破坏的数据结构、不精确的计算以及无限循环等。
@NotThreadSafe
public class MutableInteger(){
    private int value;
    public int get(){return value;}
    public void set(int value){this.value = value;}
}
        上面这个代码,看起来没问题,但是不是线程安全的。如果某个线程调用了set,另一个线程正好在调用get,虽然set要比get早一点点(甚至1纳秒),但是却不能保证get的值是新值还是旧值。
        这里如果只对set同步是不行的,还要对get进行同步。
@ThreadSafe
public class MutableInteger(){
    private int value;
    public synchronized int get(){return value;}
    public synchronized void set(int value){this.value = value;}
}

二、非原子的64位操作

        一般情况下,就算是失效数据,至少也是曾经有效的,并不是一个随机的值。这个安全性保证称之为最低安全性。
        有一种特殊的情况不是最低安全性。普通的64位数值变量,比如double或者long,在Java内存模型里面,读和写都是非原子操作。因为JVM会将64位读写操作拆分为两个32位的操作。因此,在并发读写的时候,可能会读的到某个高32位的值和另一个低32位的值。这个就是一个奇怪的值。

三、加锁与可见性

        内置锁能够保证一个线程可以正确查看到另一个线程的执行结果。也就是说,对于某个锁M。线程A在unlock M之前的所有操作,在B线程 lock M的时候,都能够看到前一个同步代码块的操作的结果。
        加锁不仅仅在于互斥,而且还包括内存可见性。因此,为了确保所有的线程都能看到共享变量的最新纸,所有执行读操作和写操作的线程都必须在同一个锁上同步。

四、Volatile变量

        Volatile是一种稍微弱的同步机制,就是解决内存可见性的。通过这个volatile,可以确保将变量的更新操作通知到其他线程。把变量声明为volatile类型之后,编译器和运行时都会注意到这个变量是共享的:
(1)就不会把该变量上的操作和其他内存操作一起进行重排序;
(2)volatile变量不会被缓存再寄存器或者其他处理器不可见的地方。
        因此,读取volatile变量的时候,永远都会返回最新的值。上面的对象定义中,可以改成“private volatile int value;”这样,既不会使线程阻塞,也能够保证内存可见性。所以,volatile是轻量级的同步机制。(目前大多数处理器架构,读取volatile变量的开销,比读取非volatile变量的开销稍微高一点)
        从内存可见性来看,写入volatile变量,相当于unlock M,读取volatile相当于lock M。但是不建议过度依赖volatile。
        volatile一般推荐用于状态位标志,对于复合操作,volatile满足不了原子性。
        加锁机制既能保证原子性,又能保证可见性。volatile只能保存可见性。

3.2 发布与溢出

        发布就是指,对象能够在当前作用域之外的代码中使用。如果不该发布的对象发布出去,就会出现溢出。
(1)我们往往需要需要确保对象以及对象内部的状态不能被发布出去。
(2)如果需要发布对象,我们要保证发布时的线程安全,不能破坏线程安全性。
例1:公共静态变量的对象发布。看下面的例子:
public static Set<Secret> knownSecrets;
public void initialize(){
    knownSecrets = new HashSet<Secret>();
}
   这里,如果knownSecrets的Set发布的话,其内部的Secret对象就会被间接的发布出去。
例2:私有变量被发布,逃出了其本来的作用域。 看下面的例子:
class UnsafeStates {
    private String[] states = new String[]{"AK","AL"}
    public String[] getStates(){return states;}
}
如果按照这种方式发布states,就会出现问题,因为任何调用者都能修改这个数组内容,本来私有的变量,结果却被发布了。
        当发布一个对象的时候,该对象的非私有域中引用的所有对象都会被发布,包括通过非私有的变量或者方法到达的其他对象。
例3:发不了一个内部的类实例。再看下面的例子,问题更大。
public class ThisEscape {
    public ThisEscape(EventSource source){
        source.registerListener{
            new EventListener(){
                public void onEvent(Event e){
                    doSomething();
                }
            }
        }
    }
    public void doSomething(){
     //……
    }
}
上面的代码解释如下:在ThisEscape的构造函数中,通过EventSource注册了一个事件监听器。当执行“source.registerListener”的时候,等于开启了一个线程,当事件发生的时候,会执行ThisEscape对象的doSomething()方法。也就是“this.doSomething()”。主线程和线程B对“this”都是可见的。线程B本质上是拿到了ThisEscape对象的“this”,然后执行的doSomething()方法。
        如果在ThisEscape构造函数还没有初始化完成的时候,就发生了event事件,那么就会调用“this.doSomething”方法,但是,这个时候,ThisEscape还没有初始化完成,其他线程就已经使用了“this”,这里,就是this逸出。这会导致一些不可预料的现象发生。当且仅当对象的构造函数返回时,对象才会处于和预测的一致的状态。
        不要在构造函数中让this引用逸出。
        当构造函数启动一个新线程的时候,无论是new Thread(),还是通过Runnable接口,this都会被新创建的线程共享。
例4:上面的例子的加强版。只要线程被启动,该线程都能获取到“this”,然后使用this的时候,就会出问题。
public class ThisEscape {
    private String name;

    public ThisEscape(String name) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(ThisEscape.this.name);
            }
        }).start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.name = name;
    }

    public static void main(String[] args) {
        new ThisEscape("shenggang");
    }
}
打印“ThisEscape.this.name”会出现null。
例5:使用工厂方法,私有化构造函数,避免this逸出。
public class SafeListener {
    private final EventListener listener;
    private SafeListener(){
        listener = new EventListener() {
            public void onEvent(Event e){
                doSomething(e);
            }
        };
    }
    
    public static SafeListener newInstance(EventSource source){
        SafeListener safe = new SafeListener();
        source.registerListener(safe.listener);
        return safe;
    }
}
上面的代码,通过工厂方法,newInstance的时候,只是创建了一个safe对象。这个时候,并没有线程在用。等safe创建完成之后,其他线程可以随意的使用了也不会有影响。

3.3 线程封闭

        线程封闭(Thread Confinement),就是指不共享数据,数据仅仅在单线程里面访问。这是最简单的线程安全性的实现方式。
        典型使用:Swing(封闭到事件的分发线程)。JDBC的Connection对象(从连接池获取的connection对象,都是由单线程采用同步的方式处理)。

(1)Ad-hoc线程封闭

        完成通过程序去控制数据只能在某个线程中访问。很脆弱,不建议使用。

(2)栈封闭

        线程封闭的特例。只能通过局部变量才能访问对象。因为局部变量的特点是,如果它们位于执行线程的栈中,那么其他线程是无法访问到这个栈的。也就是说,方法内的变量,其他线程是看不到的。
 
    private int loadTheArk(Collection<Animal> candidates) {
        SortedSet<Animal> animals;
        int numPairs = 0;
        Animal candidate = null;

        /* animals被封闭在方法中 */
        animals = new TreeSet<Animal>();
        animals.addAll(candidates);
        for(Animal a : animals){
            if(candidate == null || !candidate.isGood()){
                candidate = a;
            } else {
                ++numPairs;
            }
        }

        return numPairs;
    }
上面的代码,“animals”被封闭在了方法中,也就是局部变量,那么其他线程是看不到的。

(3)ThreadLocal类

        维持线程封闭性,更好的规范方法是使用ThreadLocal。这个类可以试线程中的某个值和保存值的对象关联起来。ThreadLocal提供了get和set等方法接口
        场景1:
        ThreadLocal最典型的应用场景:connection数据库连接。一般情况下,为了避免每次调用方法都要传递一个Connection变量,因此一般Connection会创建为一个全局的数据库连接变量。如果多线程的情况下,大家都会取使用,而Connection本身不是线程安全的。那么,就可以将JDBC的连接保存到ThreadLocal对象中,每个线程都会有一个属于自己的连接。
        本质上,ThreadLocal对象用于放置可变的单例变量或者全局变量进行共享。
        场景2:
        某个频繁的操作需要一个临时对象,由不希望每次执行的时候,都去重新分配该临时对象,可以使用ThreadLocal。
        场景3:
        单线程的程序移植到多线程里,可以将共享的全局变量移动到ThreadLocal中,可以保持线程的安全性。
 
        关于ThreadLocal的理解
1、ThreadLocal不是控制并发访问同一个对象,而是给每个线程分配一个只属于该线程的“线程级别的局部变量”。
2、ThreadLocal本质上是ThreadLocalMap,在connection中,每次创建新线程,就会从连接池中取出一个conn连接,放到该线程的ThreadLocal中,这样可以保证线程内事务的统一。
3、ThreadLocal中的ThreadLocalMap,使用了弱引用(当没有外部强引用的时候,就会被GC掉)。在用完某个ThreadLocal之后,如果没有及时remove,会导致map中key为null的entry,这些对应的value永远无法被回收,造成内存泄漏。
4、ThreadLocal使用场景:ThreadLocal不是解决对象共享访问的问题的,而是一种避免频繁复杂的参数传递,而采用的一种方便的对象访问方式。最适合在多线程中,每个线程都需要一个实例的对象访问,而且这个对象会在该线程中频繁使用。

(4)不变性

        不变对象可以满足同步需求。不可变的对象一定是线程安全的。该对象只能通过构造函数创建。
        注意,虽然不变对象可以用final类型声明,但是,final类型的数据的域中,还是可以保存可变对象的引用的。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
原文地址:https://www.cnblogs.com/shenggang/p/8521911.html