java并发编程笔记day1

xiaoxiao2021-02-28  115

第三章 共享对象

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取代

转载请注明原文地址: https://www.6miu.com/read-61177.html

最新回复(0)