并发与共享的例子,深入理解java并发编程

  并发与共享的例子,深入理解java并发编程

  00-1010 1.可见性1.1失效数据1.2非原子64位操作1.3锁定和可见性1.4易变变量2。出版和披露。线程闭包3.1临时线程闭包3.2堆栈闭包3.3线程本地类4。不变性4.1最终域4.2使用volatile类型发布不可变对象5安全版本5.1不正确版本5.2不可变对象和初始化安全性5.3安全版本的通用模式5.4事实不可变对象5.5可变对象5.6安全共享对象

  00-1010通常我们不能保证执行读操作的线程能看到其他线程写的值,因为每个线程都有自己的缓存机制。为了确保多线程之间内存写操作的可见性,必须使用同步机制。

  public class novibility { private static boolean ready;私有静态int数;私有静态类ReaderThread扩展线程{ public void run() { while(!ready)thread . yield();System.out.println(数字);} } public static void main(String[]args){ new reader thread()。start();数字=42;就绪=真;}}上面的代码,看起来会输出42,但实际上很可能根本无法终止,因为读取线程永远看不到ready的值;输出0是可能的,因为读取器线程看到了写好的值,但没有看到后来写的值。这种现象被称为“重新排序”。没有同步,编译器、处理器、运行时等。可能会对操作的执行顺序进行一些意外的调整。

  因此,只要有数据在多个线程之间共享,就应该使用正确的同步。

  00-1010除非使用同步,否则很可能得到变量的无效值。无效值可能不会同时出现,一个线程可能会获得一个变量的最新值和另一个变量的无效值。数据的失败也可能导致一些令人困惑的失败,比如意外异常、数据结构损坏、计算不准确、无限循环等等。

  00-1010对于非易失性long和double变量,JVM允许将64位读或写操作拆分为两个32位操作。因此,很可能会读取最新值的高32位和无效值的低32位,从而产生随机值。除非用关键字volatile声明它们或者用锁保护它们。

  00-1010当一个线程执行一个被锁保护的同步代码块时,你可以看到同一个同步代码块中其他线程之前的所有操作结果。没有同步,上述保证就无法实现。锁定的含义不仅仅限于互斥行为,还包括可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读或写操作的线程必须在同一个锁上同步。

  00-1010当变量被声明为易变类型时,编译器和运行时都不会对变量上的操作以及其他内存操作进行重新排序。Volatile变量不缓存在寄存器或其他处理器不可见的地方,所以在读取volatile变量时总是会返回新写入的值。锁定机制可以同时保证可见性和原子性,而volatile变量只能保证可见性。

  只有在满足以下所有条件时,才应该使用volatile变量:

  写入变量不依赖于变量的当前值,也可以保证只有一个线程更新变量的值。该变量将不与其他状态变量一起包含在不变性条件中。访问变量时不需要锁。

  00-1010发布对象意味着可以在当前范围之外的代码中使用该对象。发布对象的方法有:对非私有变量的引用、方法调用返回的引用、内部类对象隐含的对外部类的引用等等。当一个不应该发布的对象被发布了,就叫泄漏。

  public class this escape { private int status;public this escape(Event source source){ source . register listener(new Event listener(){ public void on Event(Event e){ do something(e);} });状态=

  1; } void doSomething(Event e) { status = e.getStatus(); } interface EventSource { void registerListener(EventListener e); } interface EventListener { void onEvent(Event e); } interface Event { int getStatus(); }}由于内部类的实例包含了对外部类实例的隐含引用,当ThisEscape发布EventListener时,也隐含发布了ThisEscape实例本身。但在此时,变量status还没有被初始化,造成了this引用在构造函数中泄露。可以使用一个私有的构造函数和一个公共的工厂方法,避免不正确的构造过程:

  

public class SafeListener { private int status; private final EventListener listener; private SafeListener() { listener = new EventListener() { public void onEvent(Event e) { doSomething(e); } }; status = 1; } public static SafeListener newInstance(EventSource source) { SafeListener safe = new SafeListener(); source.registerListener(safe.listener); return safe; } void doSomething(Event e) { status = e.getStatus(); } interface EventSource { void registerListener(EventListener e); } interface EventListener { void onEvent(Event e); } interface Event { int getStatus(); }}

 

  

3. 线程封闭

一种避免使用同步的方式就是不共享。如果仅在单线程内访问数据,就不需要同步,这就被称为线程封闭。线程封闭是程序设计中的考虑因素,必须在程序中实现。Java也提供了一些机制帮助维护线程封闭,比如局部变量和ThreadLocal

 

  

 

  

3.1 Ad-hoc线程封闭

Ad-hoc线程封闭是指,维护线程封闭性的职责完全由程序实现来承担。使用volatile变量是实现Ad-hoc线程封闭的一种方式,只要能保证只有单个线程对共享volatile变量执行写入操作,那么就可以安全低在这些变量上进行读取-修改-写入操作,volatile变量的可见性又保证了其他线程能够看到最新的值。

 

  Ad-hoc线程封闭是非常脆弱的,因此在程序中尽量少使用。在可能的情况下,使用其他线程封闭技术,比如:栈封闭、ThreadLocal。

  

 

  

3.2 栈封闭

在栈封闭中,只能通过局部变量才能访问对象。它们位于执行线程的栈中,其他线程无法访问到。即使这些对象是非线程安全的对象,它们仍然是线程安全的。然而,值得注意的是,只要编写代码的人才知道哪些对象是栈封闭的。如果没有明确的说明,后续的维护人员很容易错误的泄露这些对象。

 

  

 

  

3.3 ThreadLocal类

使用ThreadLocal是一种更规范的线程封闭方式,它能是线程中的某个值与保存值的对象关联起来。如下代码,通过将JDBC的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接:

 

  

public class ConnectionDispenser { static String DB_URL = "jdbc:mysql://localhost/mydatabase"; private ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { 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(); }}

从概念上看,你可以将ThreadLocal<T>视为包含了Map<Thread,T>对象,其中保存了特定于改线程的值,但ThreadLocal的实现并非如此。这些特定于线程的值保存在Thread对象中,当线程终止后,这些值会作为垃圾被回收。

 

  

 

  

4. 不变性

如果某个对象在被创建后其状态就不能被修改,那么这个对象就被称为不可变对象。满足同步需求的另一种方法就是使用不可变对象。不可变对象一定是线程安全的。当满足以下条件时,对象才是不可变的:

 

  对象创建以后其状态就不能改变对象的所有域都是final类型对象是正确创建的,在对象创建期间,this引用没有泄露

public final class ThreeStooges { private final Set<String> stooges = new HashSet<String>(); public ThreeStooges() { stooges.add("Moe"); stooges.add("Larry"); stooges.add("Curly"); } public boolean isStooge(String name) { return stooges.contains(name); }}

上述代码中,尽管stooges对象是可变的,但在它构造完成后无法对其修改。stooges是一个final类型的引用变量,因此所有的对象状态都通过一个final域访问。在构造函数中,this引用不能被除了构造函数之外的代码访问到。

 

  

 

  

4.1 final域

final类型的域是不能修改的,但如果final域所引用的对象是可变的,那么这些被引用的对象是可以修改的。final域的对象在构造函数中不会被重排序,所以final域也能保证初始化过程的安全性。和除非需要更高的可见性,否则应将所有的域都声明为私用域一样,除非需要某个域是可变的,否则应将其声明为final域也是一个良好的编程习惯。

 

  

 

  

4.2 使用volatile类型来发布不可变对象

因式分解Sevlet将执行两个原子操作:

 

  更新缓存通过判断缓存中的数值是否等于请求的数值来决定是否直接读取缓存中的结果每当需要一组相关数据以原子方式执行某个操作时,就可以考虑创建一个不可变的类来包含这些数据:

  

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

当线程获取了不可变对象的引用后,不必担心另一个线程会修改对象的状态。如果要更新这些变量,可以创建一个新的容器对象,但其他使用原有对象的线程仍然会看到对象处于一致的状态。当一个线程将volatile类型的cache设置为引用一个新的OneValueCache时,其他线程就会立即看到新缓存的数据:

 

  

public class VolatileCachedFactorizer 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); }}

 

  

5 安全发布

 

  

5.1 不正确的发布

像这样将对象引用保存到公有域中就是不安全的:

 

  

public Holder holder;public void initialize(){ holder = new Holder(42);}

由于存在可见性问题,其他线程看到的Holder对象将处于不一致的状态。除了发布对象的线程外,其他线程可以看到Holder域是一个失效值,因此将看到一个空引用或者之前的旧值。

 

  

public class Holder { private int n; public Holder(int n) { this.n = n; } public void assertSanity() { if (n != n) throw new AssertionError("This statement is false."); }}

上述代码,即使Holder对象被正确的发布,assertSanity也有可能抛出AssertionError。因为线程看到Holder引用的值是最新的,但由于重排序Holder状态的值却是时效的。

 

  

 

  

5.2 不可变对象与初始化安全性

即使在发布不可变对象的引用时没有使用同步,也仍然可以安全地访问该对象。任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。在没有额外同步的情况下,也可以安全地访问final类型的域。然而,如果final类型的域所指向的是可变对象,那么在访问这些域所指向的对象的状态时仍然需要同步。

 

  

 

  

5.3 安全发布的常用模式

要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全发布:

 

  在静态初始化函数里初始化一个对象引用。将对象的引用保存到volatile类型的域或者AtomicReference对象中。将对象的引用保存到某个正确构造对象的final类型域中。将对象的引用保存到一个由锁保护的域中。线程安全库中的容器类提供了以下的安全发布保证:

  通过将一个键或者值放入HashtablesynchronizedMap或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程。通过将某个对象放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或者synchronizedSet中,可以将该对象安全地发布到任何从这些容器中访问该对象的线程。通过将某个对象放入BlockingQueue或者ConcurrentLinkedQueue中,可以将该对象安全地发布到任何从这些队列中访问该对象的线程。

 

  

5.4 事实不可变对象

如果对象从技术上来看是可变的,但其状态在发布后不会再改变,那么把这种对象称为事实不可变对象。在没有额外的同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。例如维护一个Map对象,其中保存了每位用户的最新登录时间:

 

  

public Map<String, Date> lastLogin =Collections.synchronizedMap(new HashMap<String, Date());

如果Date对象的值在被放入Map后就不会改变,那么synchronizedMap中的同步机制就足以使Date值被安全地发布,并且在访问这些Date值时不需要额外的同步。

 

  

 

  

5.5 可变对象

对于可变对象,不仅在发布对象是需要使用同步,而且在每次对象访问时同样需要使用同步来确保后续修改操作的可见性。对象的发布需求取决于它的可变性:

 

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

 

  

5.6 安全的共享对象

在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:

 

  线程封闭。线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。只读共享。在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。线程安全共享。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公共接口来进行访问而不需要进一步的同步。保护对象。被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。到此这篇关于Java并发编程之对象的共享的文章就介绍到这了,更多相关Java对象的共享内容请搜索盛行IT以前的文章或继续浏览下面的相关文章希望大家以后多多支持盛行IT!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: