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类型的数据的域中,还是可以保存可变对象的引用的。
     
     
     
     
     
     
     
     
     
     
     
     
     
     
  • 相关阅读:
    剑指 Offer 41. 数据流中的中位数
    剑指 Offer 19. 正则表达式匹配
    leetcode 75 颜色分类
    Deepin 添加 open as root
    window 下 无损进行其他文件系统(ext4) 到 ntfs 文件系统的转化
    Windows Teminal Preview Settings
    CentOS 7 容器内替换 apt-get 源为阿里源
    Ubuntu 20.04 安装 Consul
    elementary os 15 添加Open folder as root
    elementary os 15 gitbook install
  • 原文地址:https://www.cnblogs.com/shenggang/p/8521911.html
Copyright © 2011-2022 走看看