重温《并发编程实战》---对象的共享

1.同步另一个重要的方面:内存可见性。

2.结论:只要有数在多个线程之间共享,就使用正确的同步。

3.Java内存模型保证了简单的变量读取和写入操作都必须是原子操作,但对于非volatile类型的longdouble变量,JVM64位的读/写操作分解为两个32位的操作。如果变量的读操作和写操作在不同的线程中,那么很可能会读到某个值的高32位和另一个值的低32位置。java内存模型对基本类型数据的赋值操作都是单个原子操作,但是对64位的doublelong类型在并发程序中的使用就要小心了。

4.volatile:当把一个变量声明为volatile后,不会将该变量上的操作与其他内存操作重排序。

 并且volatile变量不会被缓存在寄存器或者其他对处理器不可见的地方,直接存储在主存中,因此其他线程读取volatile变量的时候总会返回最新的值。

5.volatile:最好不要把volatile变量至于复杂的逻辑判断程序中。

  volatile变量正确使用方式:a.)确保他们自身状态的可见性。

         b.)确保他们所引用的对象的状态的可见性。

      c.)标识一些重要的程序生命周期时间的发生。

  volatile变量通常用作某个操作完成、发生中断、或者状态的标志。

6.volatile只保证: (1)可见性(不存入缓存中,其他线程直接在主存中获得volatile变量的最新值)。

               (2)有序性(禁止重排序,volatile变量之前的代码一定在volatile变量之前执行,在volatile变量后面的一定在volatile变量之后执行)

关于Volatile变量的博客:

http://blog.csdn.net/michaelwubo/article/details/50905547

7.当某个不应该被发布的对象被发布的时候,就成为逸出。当某个对象被逸出的时候,你必须假设有某个类或者线程可能会误用该对象,这正是使用封装的最主要原因。

8.隐式this引用逸出:这种情况通常发生在匿名内部类的使用当中,代码如下:

public class ThisEscape {

private class EventListener{}

public ThisEscape(EventSourcesource){

source.registerListener(

new EventListener(){

dosomething()

});

}

}

每一个内部类对象都持有1个只想outer对象的this引用,当你在发布内部类对象的时候,也使outer对象逸出了,然而当且仅当构造函数返回的时候,对象才处于可预测的和一致的状态,因此当从构造函数中发布对象的时候,只发布了一个尚未构造完成的对象。

9.构造函数过程当中的this引用逸出问题是个很常见的错误:

例如在构造函数当中创建并启动一个线程(ThreadRunnable此时是内部类),新的线程都会持有这个this引用,如果在你的对象没有构造完毕但是直接启动了线程,即在构造函数中直接启动线程,那么这是不安全的,因为此时的构造函数没有返回,this对象不是安全的。

正确做法:可以在构造函数中创建线程,但是最好不要立即启动它,而是在其他地方通过1start或者initialize方法启动。

在构造函数中调用一个可改写的实例方法(即不是私有方法,也不是final方法),同样会导致this引用在构造过程中不恰当溢出。

从上得知,构造函数中处理一些东西的时候,可能会将this引用逸出,要是想将this引用安全的发布,应该保证构造函数的正确返回。

当你想在构造函数中注册事件监听器启动线程的时候,可以使用一个私有的构造函数(确保外部无法不正确的时候构造函数)和一个工厂方法(完成注册监听器或者线程启动的工作,也就是后续工作),代码示例如下:

 

 

public class Safe{

private final EventListner listener;

private Safe(){

listener = new EventListner{

//dosomething

}

}

public static Safe newInstance(EventRegistry registry){

Safe safe = new Safe();

registry.registerListener(safe.listener);

return safe;

}

}

 

 

 

 

 

public class Safe{

private final Threadthread;

private Safe(){

thread = new Thread(//省略);

}

public static Safe newInstance(EventRegistry registry){

Safe safe = new Safe();

safe.thread.start();//此时safe已经安全构建了

return safe;

}

}

10.线程封闭:

  线程封闭技术主要包含三大类线程封闭,分别是:

a.)Ad-hoc线程封闭(由程序完全实现线程封闭,非常脆弱,很少使用)

b.)栈封闭(局部变量在虚拟机栈上,栈是线程私有的,所以局部变量可以实现栈封闭。任何方法都无法获得对基本类型的引用,因此,基本类型的局部变量始终在线程内。在维持对象引用的栈封闭性的时候,你要避免逸出,才能实现栈封闭)

c.)ThreadLocal类(http://blog.csdn.net/michaelwubo/article/details/50905555

首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。

  初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals

  然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

注意,使用ThreadLocalget()前必须使用set(),不然会报空指针异常,除非你重写initValue()方法使其返回正确的对象,默认initValue()返回null

ThreadLocal<Long> l1 = new ThreadLocal<Long>();

ThreadLocal<String> l2 = new ThreadLocal<String>();

public void set(){

l1.set(Thread.currentThread().getId());

l2.set(Thread.currentThread().getName());

}

public Long getLong(){

return l1.get();

}

public String getString(){

return l2.get();

}

public static void main(String[]args) {

Test t = new Test();

t.set();

System.out.println(t.getLong());

System.out.println(t.getString());

Thread t1 = new Thread(new Runnable() {

@Override

public void run() {

Test t = new Test();

t.set();

System.out.println(t.getLong());

System.out.println(t.getString());

}

});

t1.start();

11.不变性:

线程安全性是不可变对象的固有属性之一,不可变对象一定是线程安全的。不可变对象只有1种状态,并且这种状态由构造函数控制。

当满足以下条件后,对象才是不可变的:

a.)对象创建以后其状态不能修改。

b.)对象的所有域都是final类型。

c.)对象是正确创建的。

编程习惯:除非需要某个域是可变的,否则应将其声明为final域。

12.volatile类型+不可变对象的应用技巧:

每当我们需要一组数据以原子方式执行某个操作的时候,我们可以使用不可变对象构造一个变量容器,可以通过将这些变量全部保存在一个不可变对象中来消除“同时访问和更新多个相关变量时出现的静态问题”

不可变对象的原子性:

public class ValueContainer {

//当你需要更新和访问多个变量的时候,你可以使用不可变对象来提供一种稍微弱的原子性

private final Stringvalue1;

private final int value2;

//看,不可变对象的状态大多是由它的构造函数来设置的,一经设置,无法更改,当你需要不同的状态的对象的时候,sorry,你只能重新生成个不可变对象。

public ValueContainer(Stringvalue1,int value2){

this.value1 =value1;

this.value2 =value2;

}

public Object doSomething(){

//在这个用value1,value2可以做一些事情,随你心情。

return null;

}

}

从以上代码我们看出,不可变对象可以作为一个多值容器,并提供了一个稍微弱的原子性(状态同时由并只由构造函数更新)。

保证了原子性,还有可见性呢?答案当然是volatile

public class Test {

//volatile保证了状态一经更新,其他线程立马看到

private volatile ValueContainercache ;

public void ues(){

cache = new ValueContainer("value1", 2);

//dosomething;

}

public static void main(String[]args) {

}

}

13.对象的安全发布:

以上的知识点都是在让我们避免对象发布,例如线程封闭技术和将对象保存到一个对象的内部(不可变对象容器),当然我们在某些情况下更希望在多个线程间共享对象,我们此时必须要确保安全的共享。

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

b.)可变对象必须通过安全的方式来发布,这通常意味着发布和使用该对象的线程中必须使用同步。

c.)要安全的发布一个对象,对象的引用以及对象的状态都必须同时对其他线程可见,一个正确构造的对象可以通过以下方式来发布(以下方法主要是确保使用该对象的线程能看到对象处于已发布的状态):

c.a)在静态初始化函数中初始化一个对象引用。

c.b)将对象的引用保存到volatile类型的域或者AtomicReferance对象中。

c.c)将对象的引用保存到某个正确构造对象的final类型域中。

c.d)将对象的引用保存到一个由锁保护的域中。

当你把某个对象放到某个线程安全的容器内部的时候,符合上述最后一条。

关于第一条,最简单形式的静态初始化器:

public static Holder holder = new Holder(42);

静态初始化器由JVM在类的初始化阶段执行,由于在JVM内部存在着同步机制,因此通过这种方式初始化的任何对象都可以被安全的发布。

d.)事实不可变对象:如果独享从技术上来看是可变的,但在状态发布后不会再改变,那么这种对象成为“事实不可变对象”。

e.)可变对象:不仅在发布时要使用同步机制(状态的原子性和可见性),在每次对象的访问操作的时候也要使用同步机制来确保修改操作的可见性(修改操作负责新的状态,需要可见性)。

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

a.)线程封闭。

b.)共享的只读对象,包括:不可变对象和事实不可变对象。

c.)共享的线程安全对象。

d.)被保护对象:被保护对象只能通过持有特定的锁来访问,保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。

猜你喜欢

转载自blog.csdn.net/mypromise_tfs/article/details/72772700