Java并发编程实战第三章笔记

第三章 对象的共享

3.1 可见性

当多个线程在没有同步的情况下共享数据时出现错误
程序清单3-1

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可能会持续循环下去,也可能会输出为0。因为读线程可能永远看不到ready的值,也可能看到了ready的值,但却没有看到之后写入number的值,这种现象被称为“重排序”。

在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得出正确的结论。

3.1.1 失效数据

image

当线程A对数据X进行了改变的时候,X的改变只存在于线程A的工作内存中,那么到线程A对X的修改被写入到主存之前的这段时间里,数据X对其他的线程来讲就是失效数据。更好理解的叫法我觉得是过时的数据。

程序清单3-2中的MutableInteger不是线程安全的,因为get和set都是在没有同步的情况下访问value的。

程序清单3-2

@NotThreadSafe
public class MutableInteger {
    private int value;

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}

在程序清单3-3中,通过的get和set等方法进行同步,可以使MutableInteger成为一个线程安全的类
程序清单3-3

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

3.1.2 非原子的64位操作

我们上面说的失效数据,虽然是“失效的”。但是得到的这个值不是一个随机的值,而是上一个有某个线程设置的值。这种称之为:最低安全性。

这里边存在一个例外,就是非volatile类型的64位数值变量:double,long。JVM允许将64位的读操作或者写操作分解成两个32位操作。当读取一个非volatile的long型,如果写操作在不同的线程中,那么很可能只会读到一个值的高32位或者低32位。即使不考虑失效性的问题,在多线程共享且可变的long和double类型的变量也是线程不安全的。这时候应该用volatile或者用锁保护起来。

加锁与可见性

用内置锁可以确保一个线程可以预测另外一线程的执行结果。

加锁的含义不仅仅局限于互斥行为,还包括内存可见性。

JMM关于synchronized的两条规定:

1)线程解锁前,必须把共享变量的最新值刷新到主内存中

2)线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新获取最新的值

(注意:加锁与解锁需要是同一把锁)

通过以上两点,可以看到synchronized能够实现可见性。同时,由于synchronized具有同步锁,所以它也具有原子性。

示例程序

/**
 * synchronized能够实现原子性(同步)、可见性
 * 
 * @author xuwenjin
 */
public class SynchronizedDemo {

    //共享变量
    private boolean ready = false;
    private int result = 0;
    private int number = 1;

    /**
     * 写操作
     */
    public synchronized void write() {
        ready = true; //1.1
        number = 2;   //1.2
    }

    /**
     * 读操作
     */
    public synchronized void read() {
        if (ready) {             //2.1
            result = number * 3; //2.2
        }
        System.out.println("result:" + result);
    }

    //内部线程类
    private class WriteReadThread extends Thread {
        
        private  boolean flag = false;
        
        public WriteReadThread(boolean flag){
            this.flag = flag;
        }
        
        @Override
        public void run() {
            if (flag) {
                write();
            }else {
                read();
            }
        }
    }
    
    public static void main(String[] args) {
        SynchronizedDemo demo = new SynchronizedDemo();
        //启动线程执行写操作
        demo.new WriteReadThread(true).start();
        //启动线程执行读操作
        demo.new WriteReadThread(false).start();
    }

}

3.1.4 Volatile变量

Volatile变量时一种java提供的稍弱的同步机制,这种机制用来确保将变量的更新操作通知到其他线程。

将变量声明为volatile类型后,在编译器运行的时候,会知道这个变量是要在线程之间共享的。所以不会将该变量的操作和其他内存操作一起重排序。也不会被缓存在寄存器或者其他处理器不可见的地方(失效性)。总之在读取volatile类型的变量总是会返回最新写人的值。

加锁机制既可以确保确保可见性又可以确保原子性,而volatile变量只能确保可见性

在使用volatile的时候要注意的点:不要过度的使用volatile。如果你要对变量进行复杂地判断就不要使用volatile变量。(就不是不要对volatile修饰变量进行复杂的操作)正确的使用方法:确保自身状态的可见性,确保他们所引用对象的状态的可见性,以及标识一些重要的程序声明周期时间的发生(例如,初始化和关闭)。总之就是在一些不是那么复杂的变量的上面可以用volatile来修饰。要是很复杂,就还是用加锁的方式来确保线程安全。

当且仅当满足以下条件才应该使用volatile变量:

  1. 对变量的写入操作不依赖变量的当前值,或者保证只有一个线程会更新变量的值。
  2. 该变量不会与其他状态变量一起纳入不变性条件中。
  3. 在访问变量时不需要加锁

3.2 发布与逸出

发布:使对象能够在当前作用域之外的代码中使用。(就是可以随意的获得这个对象的引用)

逸出:某个不应该发布的对象被发布。

程序清单 3-5 发布一个对象

class Secrets {
    public static Set knownSecrets;

    public void initialize() {
        knownSecrets = new HashSet();
    }
}


class Secret {
}

当发布某个对象时,可能会间接地发布其他对象。如果将一个Secret对象添加到集合knowSecrets中,那么任何代码都可以遍历这个集合,并获得对象的引用。

同样,如果从非私有方法中返回一个引用,那么同样会发布返回的对象

程序清单3-6 使内部的可变状态逸出

class UnsafeStates {
    private String[] states = new String[]{
        "AK", "AL" /*...*/
    };

    public String[] getStates() {
        return states;
    }
}

最后一种发布对象或其内部状态的机制就是发布一个内部的类实例

程序清单3-7 隐式地使this引用逸出

public class ThisEscape {
    public ThisEscape(EventSource source) {
        source.registerListener(new EventListener() {
            public void onEvent(Event e) {
                doSomething(e);
            }
        });
    }

    void doSomething(Event e) {
    }


    interface EventSource {
        void registerListener(EventListener e);
    }

    interface EventListener {
        void onEvent(Event e);
    }

    interface Event {
    }
}

首先,看里面的 doSomething(e) 方法,这个方法是在 ThisEscape 中,通过 doSomething(e) 方法可以修改 ThisEscape 中的属性或者调用 ThisEscape 中的其他方法。

在多线程环境下,会出现这样一种情况:
线程 A 和线程 B 同时访问 ThisEscape 构造方法,这时线程 A 访问构造方法还未完成(可以理解为 ThisEscape 未初始化完全),此时由于 this 逸出,导致 this 在 A 和 B 中都具有可见性,线程 B 就可以通过 this 访问 doSomething(e) 方法,导致修改 ThisEscape 的属性。也就是在 ThisEscape 还未初始化完成,就被其他线程读取,导致出现一些奇怪的现象。
这也就是 this 逸出。

不要在构造过程中时this引用逸出

正确做法如下,使用私有的构造函数加公共的工厂方法

程序清单3-8

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;
    }

    void doSomething(Event e) {
    }


    interface EventSource {
        void registerListener(EventListener e);
    }

    interface EventListener {
        void onEvent(Event e);
    }

    interface Event {
    }
}

3.3 线程封闭

线程封闭: 仅在单线程内访问数据,就不需要同步

3.3.1 Ad-hoc线程封闭

Ad-hoc线程封闭:维护线程封闭性的职责完全由程序实现来承担。 --- 非常脆弱

3.3.2 栈封闭

就是使用局部变量。(引用是在栈中)局部变量就是封闭在执行线程中,他们在线程的栈中,其他线程无法访问到这个栈。(无法获取到) --- 比Ad-hoc易维护,更加健壮

程序清单3-9

public class Animals {
    Ark ark;
    Species species;
    Gender gender;

    public int loadTheArk(Collection candidates) {
        SortedSet animals;
        int numPairs = 0;
        Animal candidate = null;

        // animals confined to method, don't let them escape!
        animals = new TreeSet(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;
    }


    class Animal {
        Species species;
        Gender gender;

        public boolean isPotentialMate(Animal other) {
            return species == other.species && gender != other.gender;
        }
    }

    enum Species {
        AARDVARK, BENGAL_TIGER, CARIBOU, DINGO, ELEPHANT, FROG, GNU, HYENA,
        IGUANA, JAGUAR, KIWI, LEOPARD, MASTADON, NEWT, OCTOPUS,
        PIRANHA, QUETZAL, RHINOCEROS, SALAMANDER, THREE_TOED_SLOTH,
        UNICORN, VIPER, WEREWOLF, XANTHUS_HUMMINBIRD, YAK, ZEBRA
    }

    enum Gender {
        MALE, FEMALE
    }

    class AnimalPair {
        private final Animal one, two;

        public AnimalPair(Animal one, Animal two) {
            this.one = one;
            this.two = two;
        }
    }

    class SpeciesGenderComparator implements Comparator {
        public int compare(Animal one, Animal two) {
            int speciesCompare = one.species.compareTo(two.species);
            return (speciesCompare != 0)
                    ? speciesCompare
                    : one.gender.compareTo(two.gender);
        }
    }

    class Ark {
        private final Set loadedAnimals = new HashSet();

        public void load(AnimalPair pair) {
            loadedAnimals.add(pair);
        }
    }
}

3.3.3 ThreadLocal类

这个类会使得线程中的某个值和与保存值的对象关联起来(我的理解:是让线程和对象关联起来)。ThreadLocal提供了get与set等访问接口的方法,这些方法为每个使用该变量的线程都存有一份独立的副本,隐藏get总是返回当前执行线程在调用set时候的最新的值。

ThreadLocal通常会用在对可变的单实例变量或者全局变量中使用的。当多线程的应用程序在没有协同的情况下使用全局变量的时候,这时候将这个变量放在ThreadLocal中,这样就会让每个线程使用的不影响。

在单线程程序中会维持一个全局的数据库连接,并在程序启动时初始化这个连接对象,从而避免在调用每个方法时都要传递一个Connection对象。

程序清单3-10

public class ConnectionDispenser {
    static String DB_URL = "jdbc:mysql://localhost/mydatabase";

    private ThreadLocal connectionHolder
            = new ThreadLocal() {
                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();
    }
}

使用这种方法的缺点就是使得代码的耦合性更高了

3.4 不变性

不可变对象一定是线程安全的。

那什么是不可变的对象呢?

  1. 对象创建以后状态不能修改
  2. 对象所有的域都是final类型的
  3. 对象是正确创建的(创建期间,this引用没有逸出)

在不可变对象的内部可以使用可变对象来管理状态
程序清单3-11

@Immutable
 public final class ThreeStooges {
    private final Set stooges = new HashSet();

    public ThreeStooges() {
        stooges.add("Moe");
        stooges.add("Larry");
        stooges.add("Curly");
    }

    public boolean isStooge(String name) {
        return stooges.contains(name);
    }

    public String getStoogeNames() {
        List stooges = new Vector();
        stooges.add("Moe");
        stooges.add("Larry");
        stooges.add("Curly");
        return stooges.toString();
    }
}

通过构造方法对stooges进行赋值,由于被final修饰,所有之后无法再次修改

3.4.1 Final域

除非需要更高的可见性,否则应将所有的域都声明为私有域是一个良好的编程习惯,除非需要某个域是可变的,否则应将其声明为final域。

补充

  1. final修饰的变量称为常量,这些变量只能赋值一次,final 的变量不能被修改
  2. final修饰的引用类型变量,表示该变量的引用不能变,而不是该变量的值不能变;
  3. 内部类在局部时,只可以访问被final修饰的局部变量。

3.4.2 示例:使用Volatile类型来发布不可变对象

因式分解Servlet将执行两个原子操作:更新缓存的结果,判断缓存中数值是否等于请求的数值来决定是否取缓存中的结果。这时候就可以考虑创建一个不可变的类来包含这些数据。

程序清单3-12

@Immutable
public class OneValueCache {
    private final BigInteger lastNumber;
    private final BigInteger[] lastFactors;

    public OneValueCache(BigInteger i,
                         BigInteger[] factors) {
        lastNumber = i;
        lastFactors = Arrays.copyOf(factors, factors.length);
    }

    public BigInteger[] getFactors(BigInteger i) {
        if (lastNumber == null || !lastNumber.equals(i))
            return null;
        else
            return Arrays.copyOf(lastFactors, lastFactors.length);
    }
}

程序清单3-13

@ThreadSafe
public class VolatileCachedFactorizer extends GenericServlet implements Servlet {
    private volatile OneValueCache cache = new OneValueCache(null, null);

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = cache.getFactors(i);
        if (factors == null) {
            factors = factor(i);
            cache = new OneValueCache(i, factors);
        }
        encodeIntoResponse(resp, factors);
    }

    void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
    }

    BigInteger extractFromRequest(ServletRequest req) {
        return new BigInteger("7");
    }

    BigInteger[] factor(BigInteger i) {
        // Doesn't really factor
        return new BigInteger[]{i};
    }
}

3.5 安全发布

任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。

3.5.3 安全发布的常用模式

  1. 在静态初始化函数中初始化一个对象引用
  2. 将对象的引用保存到volatile类型的域或AtomicReferance对象中
  3. 将对象的引用保存到某个正确构造对象的final类型中
  4. 将对象的引用保存到一个由锁保护的域中

在线程安全容器内部的同步意味着,在将对象放入到某个容器,例如Vector或synchronizedList时,将满足上述最后一条需求。

线程安全库中的容器类提供了以下的安全发布保证

  1. 通过讲一个键或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程
  2. 通过将某个元素放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或synchronizedSet中,可以将该元素安全地发布到任何从这些容器中访问该元素的线程
  3. 通过将某个元素放入BlockingQueue或者ConcurrentLinkedQueue中,可以将该元素安全地发布到任何从这些队列中访问该元素的线程

通常,要发布一个静态构造的对象,最简单和最安全的方式是使用静态的初始化器

public static Holder holder = new Holder(42)

3.5.4 事实不可变对象

事实不可变对象:对象从技术上来看是可变的,但其状态在发布后不会再改变。

例如,Date本身是可变的,但是可以Date对象的值在被放入Map后就不会改变,那么synchronizedMap中的同步机制就可以使Date值被安全地发布。

public Map lastLogin =
    Collections.synchronizedMap(new HashMap());

3.5.5 可变对象

如果对象在构造后可以修改,那么安全发布只能确保"发布当时"状态的可见性。对于可变对象,不仅发布时要同步,而且在每次访问时同样需要使用同步来确保后续修改操作的可见性。要安全地共享可变对象,必须安全地发布,并且必须是线程安全的或者由某个锁保护起来。

对象的发布需求取决于它的可变性:

  1. 不可变对象可以通过任意机制来发布。
  2. 事实不可变对象必须通过安全方式来发布。
  3. 可变对象必须通过安全方式来发布,并且必须 是线程安全的或者由某个锁保护起来。

3.5.6 安全地共享对象

在我们获得一个对象的引用的时候,你需要知道和了解我们的会在这个引用上执行哪些操作。是否需要获得一个锁?是否可以修改它状态,或者只能读取它?许多并发错误都是由于没有理解共享对象的这些规则锁导致的。

在并发程序中使用和共享对象时, 一些使用策略:

  1. 线程封闭:线程封闭的对象只能由一个线程所拥有
  2. 只读共享:只能被多个线程并发访问,不能修改它
  3. 线程安全共享:线程安全的对象在起内部实现同步,因此多个线程统一通过对象的公用方法来访问,不需要进一步同步。
  4. 保护对象:使用特定的锁来访问。保护对象包括封装在其他线程安全中的对象,已经已发布的并且有某个特定锁保护的对象。

你可能感兴趣的:(Java并发编程实战第三章笔记)