zoukankan      html  css  js  c++  java
  • 【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类型的数据的域中,还是可以保存可变对象的引用的。
     
     
     
     
     
     
     
     
     
     
     
     
     
     
  • 相关阅读:
    Linux内核RPC请求过程
    二分图
    Java实现 蓝桥杯 算法提高 合并石子
    Java实现 蓝桥杯 算法提高 合并石子
    Java实现 蓝桥杯 算法提高 摩尔斯电码
    Java实现 蓝桥杯 算法提高 摩尔斯电码
    Java实现 蓝桥杯 算法提高 文本加密
    Java实现 蓝桥杯 算法提高 文本加密
    Java蓝桥杯 算法提高 九宫格
    Java蓝桥杯 算法提高 九宫格
  • 原文地址:https://www.cnblogs.com/shenggang/p/8521911.html
Copyright © 2011-2022 走看看