java并发编程笔记day1

来源:互联网 发布:linux启动jenkins 编辑:程序博客网 时间:2024/06/13 00:43

第三章 共享对象

3.1 可见性

  • 在没有同步的情况下共享变量,可能会导致一直循环,并且有可能发生重排序,打印结果为0。
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;    }}

3.1.1 过期数据

  • 过期数据可能会引发数据的脏读,错误的计算,无限循环
public class MutableInteger {    private int value;    public int get() {        return value;    }    public void set(int value) {        this.value = value;    }}

在set和get方法中我们都访问了value,却没有进行同步,在多线程中可能就导致数据无法及时更新。我们可以同步setter-getter方法来使之成为线程安全的。

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位的操作。这样就导致读或写如果发生在不同线程可能会导致一个值高的32位和另一个值底的32位。因此,多线程共享变量的double和long,我们要记得将它声明为volatile,或者用锁保护起来

3.1.3 锁和可见性

  • 内置锁
    用来确保一个线程以某种可预见的方式看见另一个线程的影响。如图所示:
    这里写图片描述
  • 锁不仅仅是关于同步互斥的,也是关于内存可见的。为了保证所有的线程都能够看到共享的,可变变量的最新纸,读取和写入线程必须使用公共的锁进行同步。

3.1.4 Volatile变量

  • 同步的若形式:volatile
    确保一个变量的更新以可预见的形式告知其他的线程。编译期与运行时,会监视这个变量:它是共享的。对它的操作不会与其他的内存操作进行重排序。volatile变量不会缓存在寄存器或者缓存在其他处理器的隐藏位置。

  • 对比synchronized,volatile只是轻量级的同步机制,加载volatile变量比加载非volatile变量的开销略高一点而已。

  • 写入volatile变量就相当于退出同步块
  • 不推荐过度依赖使用volatile变量,比锁机制更加脆弱,更加难以理解。
  • 正确使用volatile方式:1 确保它们所饮用对象的可见性 ; 2用于标识重要的生命周期事件(初始化或关闭)的发生。
  • 加锁能保证原子性和可见性,而volatile只能保证可见性
  • 只有满足以下条件,才去使用volatile变量:
    • 1 写入变量时并不依赖变量的当前值;或者能确保只有单一的线程修改变量的值
    • 2 变量不需要与其他的状态变量共同参与不变约束
    • 3 访问变量时,没有其他的原因需要加锁

3.2 发布和溢出

  • 发布
    发布一个对象是使它能被当前范围之外的代码所使用

  • 溢出
    一个对象在没有准备好构造就被发布出去这种情况称之为溢出

  • 最常见的对象发布
    将对象的引用存入公共静态域中,发布一个对象还会间接的发布其他对象,如下代码所示,我们发布set的时候,同时也将里面的对象发布出去了,因为任何代码都可以遍历获取新Student的引用。类似的我们可以从非私有方法返回一个对象,这也是发布了这个返回对象:

public class Initialize {    public static Set<Student> studentSets;    public void initailaze(){        knowSecretSets = new HashSet<>();    }}
  • 发布一个私有数组(不建议这么做),这直接将私有的数组发布出去了,而这个数组本应该是私有的。以这种形式发布的数组会出问题,因为任何一个调用者都可以修改它的内容,事实上已经等同于共有的了。
class UnsafeStates {    private String[] states = new String[]{        "AK", "AL" /*...*/    };    public String[] getStates() {        return states;    }}
  • 发布一个对象,同时也发布了该对象里所有非私有的对象。更可以说,发布一个对象,那些非私有的引用链,和方法调用链的可获得对象也都会被发布。
  • 将一个对象传递给外部方法,等同于发布了这个对象。这就是使用封装的强制原因:封装使得程序的正确性分析变得更加可行,而且更不易偶然的破坏涉及约束

  • 发布内部类,同时也会无条件的发布封装这个内部类的封装类。发布registerListener时同时将ThisEscape 也发布出去了,因为内部类的实例包含了对封装类实例的隐含引用。如下代码所示:

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

3.2.1 安全构建的实践

  • 对象只有通过构造函数返回后,才是可预见的,稳定的状态,所有从构造函数内部发布的对象只是一个未完成构造的对象。即使在构造函数的最后一行发布的引用也是如此。如果this引用在构造过程中发布,这样的对象被认为没有正确构建的。不要让this引用在构造中溢出
  • 不要在构造函数中启动一个线程。
    当对象在构造函数中启动了一个线程时,无论是显示的(通过将他传给构造函数)还是隐式的(因为Thread或Runnable是所属对象的内部类),this引用几乎会被新线程共享使用,于是新的线程在所属对象完成构建前就能看见它。

  • 在构造函数完成前创建一个线程没有错,但是不要启用它,发布一个start方法来启动对象拥有的线程。在构造函数中调用一个可覆盖的(既不是private和final)实例方法会导致this引用在构造期间溢出。

  • 更明确的说:this引用在构造函数完成前不会从线程溢出,只要构造函数完成前没有其他线程使用this引用,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;    }    void doSomething(Event e) {    }    interface EventSource {        void registerListener(EventListener e);    }    interface EventListener {        void onEvent(Event e);    }    interface Event {    }}

3.3 线程封闭

  • 不共享数据可以避免同步。线程封闭是实现线程安全最简单的方式之一。
  • Swing的可视化组件和数据模型对象并不是线程安全的,它们是通过将它们限制到swing的事件分发线程中,实现线程安全的。
  • 一种常用的使用线程限制的应用程序就是应用池化的JDBC Connection对象。
    线程总是从池中获得一个connection对象,然后用它处理单一的请求,最后归还给池,每个线程都会同步的处理大多请求,而且在connection对象归还给池之前,池不会再将该connection对象分配给其他线程,因此,这种连接管理模式隐式的将connection对象限制在处理请求处理期间的线程中。

3.3.1 AD-HOC线程限制

指的是维护线程限制性的任务全都落在了实现上的这种情况。
不建议使用,用单线程化子系统或者栈限制后者thread local取代