java并发编程实战读书笔记:第三章 对象的共享

上一章讲的是怎么通过同步来避免让多个线程同时访问相同的数据。
而这一章要讲的是如何让多个线程同时安全的访问一个对象。(如何安全的共享和发布对象)

我们在上一章知道了可以使用同步代码块或者同步方法来确保以原子的方式来执行操作。
但有一个错误的认识是:认为synchronized只能用于实现原子性或者确定临界区在哪。(说来惭愧,我一直也是这么以为的)
但它还有一个重要的功能:内存可见性。我们有时候不仅希望一个对象的状态只能被一个线程同时修改。我们还希望当一个线程修改了对象的状态的时候,其他的线程可以看到状态的变化。这种就叫做对象被安全的发布。

可见性

当读和写在不同的线程中执行的时候,不能实时的看到其他线程写入的那个值,有时候是根本不可能的事。为了确保线程课件必须要使用同步机制。

public class Visbilityy {
    private boolean ready;
    private int number;
    
    private class ReaderThread extends Thread{
        /** (non-Javadoc)
         * @see java.lang.Thread#run()
         */
        @Override
        public void run() {
            while (!ready) {
                Thread.yield();
            }
            System.out.println(number);
        }
    }
   
    public void test() {
        new ReaderThread().start();
        number = 42;
        ready = true;
    }
}

这种在大多数情况下都会正常输出42。但是也有可能一直循环,或者输出0。这就是我们没有在代码中使用同步机制所导致的。无法确保主线程写入的ready值和number值对读线程是可见的。输出0是因为发生“重排序”的情况:线程看到了ready的值但是没有看到number的值。(ps:我自己的理解,就是jvm会对指令进行重排序,导致真实的情况不是代码里执行的情况)

这个我在我电脑上试了同时6000次,也没有出现过一直循环或者输出是0的情况。可能这是跟电脑有关系的。有些时候值存在了不同的地方。这个得了解了jvm的实现机制才能知道到底是怎么回事。

有一种简单的做法能避免这些复杂的问题:就是只要有数据字多个线程之间共享就应该正确使用同步。

失效数据

上面的例子已经展示了在缺少同步的程序中可能产生的一种情况:失效数据。当读线程查看ready变量时可能会得到一个失效的值。
失效数据会导致一些令人困惑的问题,比如数据结构被破坏了,就按的不精确,无限循环等等。。。

@NotThreadSafe
public class MutableInteger {
      private int value;
      
      public int get () {return value;}

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

这种是线程不安全的。get和set不在同一线程的会有可能看不到互相。

@ThreadSafe
public class SynchronizedInteger {
      @GuardeBy("this") private int value;

      public synchronized int get () {return value;}
      
      public synchronized void set ( int value){ this.value = value ;}
}
非原子的64位操作

这个很细节,不知道现在的jvm还是不是这样- - ,基础是多么重要。该好好看看关于jvm的书了

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

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

加锁和可见性

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

我的理解:为什么用锁了就可以保证内存的可见性?可能执行锁里的代码都是在一个特别的区域,读和写要是用的同一个锁这样会让两个线程对于同一个变量是可见的。

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

书中的图
Volatile 变量

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

理解volatile变量一种有效的方法就是,将他们想象成SynchronizedInteger的类似行为,也就是说volatile变量的读操作和写操作分别替换为get方法和set方法。然而,在访问volatile变量时实际上不会有加锁的操作,因此也就不会使线程阻塞了。
总的来说,volatile变量时一种比sychronized关键字更加轻量级的同步机制。

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

注意:加锁机制可以即确保可见性又可以确保原子性,然而volatile变量只能确保可见性。

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

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

“发布”是指对象能够在其他地方使用和访问。(就是可以随意的获得到这个对象的引用)。当不该发布的被发布了那么这种情况就是“逸出”。
如果发布某个对象的时候还是有可能会间接地发布其他对象。比如一个集合,可能会把集合里面的对象给暴露了出去。

安全的构造对象
有时候我们会在构造函数里不小心的引用了自己,就是不小心引用了this。
eg:

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

在这个ThisEscape的构造函数里,当new EventLisener这个类的的实例的时候就会顺带着把外部的ThisEscape这个实例也给发布了。这样就会造成这个对象还没有构造完就应经被使用了。

针对于上面那种在构造函数里注册一个监听事件的可以这么写:

public class SafeListener {
    private final EventListener listener;

    private SafeListener () {
          listener = new EventListener() {
                  public void onEvent (Event e) {
                        doSomthing (e);
                  }
          }
    }

    public static SafeListener newInstance (EventSource source) {
            SafeLisenter safe = new SafeListener() ;
            source.registerListener (safe.listener);
            return safe;
    }
}

就是使用私有的构造函数和一个公共的工厂方法来实现。(就是不要再没构造完就使用这个对象)

线程封闭

只在单线程里访问数据,这种被称为“线程封闭”。
例如我们在使用的jdbc连接中的Connection对象。这个对象并不是线程安全的。但我们在使用的时候,基本都是在单线程中采用同步的方式来处理的。这种就隐含的将Connection封闭在线程中了。

  1. Ad-hoc 线程封闭
    保证线程封闭完全由程序来实现。---非常脆弱
  2. 栈封闭
    就是使用局部变量。(引用是在栈中)局部变量就是封闭在执行线程中,他们在线程的栈中,其他线程无法访问到这个栈。(无法获取到)

但也需要一些需要注意的地方。比如我们在上下文context里使用非线程安全的,虽然context是线程安全的,但它里面的对象就不确定是否是线程安全的。

  1. ThreadLocal 类
    这个类会使得线程中的某个值和与保存值的对象关联起来(我的理解:是让线程和对象关联起来)。ThreadLocal提供了get与set等访问接口的方法,这些方法为每个使用该变量的线程都存有一份独立的副本,隐藏get总是返回当前执行线程在调用set时候的最新的值。
    ThreadLocal通常会用在对可变的单实例变量或者全局变量中使用的。当多线程的应用程序在没有协同的情况下使用全局变量的时候,这时候将这个变量放在ThreadLocal中,这样就会让每个线程使用的不影响。
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal <Connection>() {
      public Connection initalValue () {
              return DreiverManager.getConnection (DB_URL);
      }
};
public static Connection getConnection(0 {
       return connectionHolder.get();
}

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

不变性

如果共享的变量时不可变的,那么上面说的失效数据、丢失更新操作或者竞态条件这些问题都不会出现了。

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

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

  • 对象穿件以后状态不能被改变
  • 对象所有的域都是final类型的
  • 对象是正确创建的(创建期间,this引用没有逸出)

需要注意的是不可变不等于将对象所有的域都声明为final类型,即使所有的域都是final类型,但这个对象也有可能是可变的,因为在final类型的域中可以把村对可变对象的引用。

  1. Final域
    final 可以保证初始化过程的安全性。

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

  1. 使用Volatile类型来发布不可变对象
    保存在不可变对象中的程序状态仍然是可以更新的,即通过一个保存新状态的示例来“替换”原有的不可变对象。
    示例:
    因式分解Servlet将执行两个原子操作:更新缓存的结果,判断缓存中数值是否等于请求的数值来决定是否取缓存中的结果。这时候就可以考虑创建一个不可变的类来包含这些数据。
@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);
    }
}
public class VolatileCachedFactorizer implements Servlet {

    private volatile OneValueCache cache = new OneValueCache (null,null);
    
    @Override
    public void service(ServletRequest arg0, ServletResponse arg1) throws ServletException, IOException {
        BigInteger i = extractFormRequest (arg0);
        //调用不可变对象的方法来判断是否相同,即使准备个cache被别的线程修改也无所谓,这里返回的始终是这个i的因子
        BigInteger[] factors = cache.getFactors(i);
        if (factors == null) {
            factors = factor(i);
            cache = new OneValueCache(i, factors);
        }
        encodeIntoResponse (arg1,factors);
    }
}

上面的例子是用volatile类型来保证课件行,每次都创建新的不可变对象来确保不变性。

安全发布

对象怎么能安全的进行共享呢?(其实上面已经把需要注意的点都说了,这里就是总结下,说下方法)

  • 安全发布的常用模式

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

我的理解:

  1. 一般都写一个静态变量 eg: public static Holder holder = new Holder(42);
  2. 然后为了让多个线程可见使用volatile
  3. 然后保证这个引用不会去指向其他的对象,使用final
  4. 然后把这个对象放在一个锁里进行访问(自己写的也可以,也可以使用Hashtable,synchronizedMap,Vector这些线程安全的容器里)
  • 事实不可变对象
    上面说的是怎么安全发布可变的对象,还存在一种可变对象,叫“事实不可变对象”:再发布后不会被修改。虽然是可变的但是我们保证这个没有对这个对象写的方法。

在没有额外的同步的情况下,任何线程都可以安全的使用被安全发布的事实不可变对象。

eg:Date本身就是可变的,但是它就可以作为不可变对象来使用,咋多个线程之间共享Date对象时就可以省去对锁的使用。

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

synchronizedMap中的同步机制就足以保证Date被安全地发布,不需要在访问Date时做额外的同步

  • 可变对象
    如果对象在构造后可以修改,那么安全发布只能确保“发布当时”状态的可见性。对于可变对象,不仅要在发布对象时候需要使用同步,还要在每次的对象访问时候使用同步来确保安全。

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

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

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

上面说的是我们在编写一个多线程程序的时候所需要考虑的步骤。

  1. 先考虑是否可以不共享对象
  2. 必须要共享对象的时候,是否这个对象是只需要访问的,不需要在多个线程里进行写操作
  3. 如果这个共享的对象既需要读又需要写,那么我们可以考虑为这个对象封装些同步访问和修改的方法
  4. 如果不只是一个对象,需要很多共享对象互相操作比较复杂,这时候应该就使用特定的锁来进行访问这些对象

上面是我自己的理解,不知道我理解的对不对哈。。。如有不对请大家指正

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容