Java并发编程实战 第3章 对象的共享

时间:2023-03-09 22:51:35
Java并发编程实战 第3章 对象的共享

可见性

可见性是由于java对于多线程处理的内存模型导致的。这似乎是一种失败的设计,但是JVM却能充分的利用多核处理器的强大性能,例如在缺乏同步的情况下,Java内存模型允许编译器对操作顺序进行重排序,并将数值缓存在寄存器中,同时,它还允许CPU对操作顺序进行重排序,并将数值缓存在处理器的特定缓存中。

可见性可以导致3个问题,失效数据,非原子的64位操作,重排序。

失效数据

如果一个共享的数据被多个线程读写,一个线程执行了写入,随后另外一个线程执行了读取,可能读取到的是未写入之前的数据。

也就是说,写入操作可能在寄存器缓存或者CPU缓存中,其他线程看不到。

非原子的64位操作

失效数据至少是之前某一个线程设置的值,只不过延迟了而已。而不是一个随机值,这种安全性叫做最低安全性。

最低安全性适用于大部分变量,但是存在一个例外,非volatile类型的64位数值变量(double和long)。

java内存模型要求,变量的读取操作和写入操作都必须是原子操作。但是对于非volatile类型的long和double类型,jvm允许将64位的度去操作或者写入操作分解成两个32位,

试想,当你读取或者写入long类型的时候,前32位是一个值的,后32位变成了另外一个值的了。那么这个值就是随机的。

重排序

如果在一个线程中执行下面的代码:

ready = true;

number = 42;

在另外一个线程中,有如下代码:

if(ready)

{

System.out.println(number);

}

这个时候即使是ready为true,number也可能不打印42.

一个线程不能依赖另外一个线程非同步的代码顺序。

因为java会对代码执行重排序,上面两句赋值代码的顺序可能会白java改变,因为这种改变在第一个线程本身中是不会有影响的。

加锁与可见性

加锁可以保证可见性,使用同一个锁加锁的两个代码块,他们是顺序执行的,所以不会有上面三种问题。

为了保证所有的线程都能看到共享数据的最新值,要保证所有的读写操作都要在一个共同的锁的管理下。

Volatile变量

一旦一个变量加入了volatile修饰符,那么在整个变量上,不会有可视性的三个问题:失效数据,非原子的64位操作,重排序。

编译器与运行时都会注意到volatile变量,它是共享的,所以不会被缓存在寄存器或者其他不会被其他线程看不到的地方,也不会对他进行重排序。读取volatile变量总能返回最新的 值。

理解volatile变量的的有效方法是:相当于在volatile变量上加入了synchronize的get和set方法。所有的读写都走这两个方法。

volatile变量的可见性影响比volatile变量本身更多。线程A写入了volatile变量,一旦线程B读取了这个volatile变量,在吸入volatile变量之前对A可见的所有变量的值,都会马山对B也可见。

不过不建议过度依赖volatile的这个作用,因为这种策略是脆弱的,也是难以理解的。

volatile的一个重要的用法是,检查状态标记,以判断是否退出循环。

while(!asleep)

{

//run

}

volatile只能保证可见性。若能用于i=3,但是不能用于i++;

对于i++这种:

如果只有一个线程在i++,也是没有问题的。++过程中没有其他写入操作与它冲突,一旦++完成,其他线程就能看到。也就是说,如果保证只有一个线程在做写操作,那么不论是多么复杂的写操作,只要写操作的中间过程不会对i赋予不合理的值,都是可以使用volatile的。

如果是多个线程进行写入,那么++操作是分为多步骤的,就可能会出现混乱。不过++逻辑比较简单,因为有可视性的保证,最坏的问题也就是,同时对一个i读取并且++了。还是可以保证最低安全性的。

发布与逸出

发布是对象能够在当前作用域之外的代码使用。

逸出:不应该发布的对象被发布了。

如get方法返回一个可变对象,如果这个对象不应该被更改,这个就叫逸出。

在构造器中发布对象的时候,如果包含了this的发布,那么在多线程环境中是有问题的,因为此时其他对象调用发布的对象,这是的this还没有构造完成。这是不正确的构造。

不要在构造器中使用this引用逸出。

线程封闭

线程封闭就是把数据封闭在县城内部,不共享数据。

有三种技术:

Ad-hoc线程封闭

Ad-hoc线程封闭是指,维护线程封闭型的职责完全由程序来承担。

很少使用。

栈封闭

栈封闭就是说在发布的时候,拷贝一个副本。将变量的作用域限制在在栈里。暴露的只是副本。

ThreadLocal类

该类提供了线程局部变量。这些变量不同于它们的普通对应物,因为访问一个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的私有静态字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

方法摘要

T

get()
返回此线程局部变量的当前线程副本中的值。

protected T

initialValue()
返回此线程局部变量的当前线程的初始值。

void

remove()
移除此线程局部变量的值。

void

set(T value)
将此线程局部变量的当前线程副本中的值设置为指定值。

可以将ThreadLocal<T>视为包含Map<Thread,T>的对象。其中保存了特定于该线程的值。

我的理解:ThreadLocal本身是为了为每个线程提供独立拷贝,并不是为了解决锁争用的问题。不使用ThreadLocal,我们同样也可以在每个线程里都维护一个属性来实现拷贝,然后在主线程初始化新线程的时候通过新线程的构造函数传入。不过,如果只是线程在初始化一个属性的时候会访问到共享变量,根据这个共享变量初始化完自己的属性就不会再跟整个共享变量产生交互,那么是可以使用ThreadLocal来避免多个线程一起初始化自己的属性,同时访问共享变量产生的冲突的。

但是ThreadLocal还有一个作用,那就是一个线程终止之后,它的ThreadLocal就可以被垃圾回收。

  1. package com.zjf;
  2. import java.util.concurrent.ExecutorService;
  3. import java.util.concurrent.Executors;
  4. public class ThreadLocalTest {
  5.    public static void main(String[] args) {
  6.       ExecutorService es = Executors.newCachedThreadPool();
  7.       for(int i = 0; i < 10; i++)
  8.       {
  9.          es.execute(new Runnable() {
  10.             public void run() {
  11.                System.out.println(ThreadLocalTest.get());
  12.             }
  13.          });
  14.       }
  15.       es.shutdown();
  16.    }
  17.    //nextSerialNum要在多个线程之间共享的 用++生成多个线程的序列号
  18.    private static int nextSerialNum = 0;
  19.     private static ThreadLocal serialNum = new ThreadLocal() {
  20.        //initialValue方法用来初始化 在每个线程第一次调用serialNum.get()的时候触发
  21.         protected synchronized Object initialValue() {
  22.             return new Integer(nextSerialNum++);
  23.         }
  24.     };
  25.     public static int get() {
  26.        //serialNum.get()会返回initialValue或者set设置的值
  27.         return ((Integer) (serialNum.get())).intValue();
  28.     }
  29. }

结果:

出现了重复,这是为什么?

我们改变一下代码,将run中的内容改为:

  1. System.out.println(Thread.currentThread().getId() + ":" + ThreadLocalTest.get());

结果:

9:0

10:2

13:4

15:6

11:1

12:3

14:5

16:7

17:8

10:2

不变性

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

不可变对象:

  • 对象创建之后其状态不能被修改。
  • 对象的所有属性都是final。
  • 对象是正确创建的。在对象的创建期间,this没有逸出。

使用volatile来发布不可变对象

使用volatile和不可变对象来改写上一章的缓存程序:

  1. package com.zjf;
  2. import java.math.BigInteger;
  3. import java.util.concurrent.ExecutorService;
  4. import java.util.concurrent.Executors;
  5. import java.util.concurrent.TimeUnit;
  6. /**
  7.  * 一个获取用于计算整数的平方的类 实现了缓存
  8.  *
  9.  * @author hadoop
  10.  *
  11.  */
  12. //这是一个不可变的对象
  13. //通过构造器初始化两个final域
  14. //然后通过get方法返回
  15. class Cache {
  16.    private final BigInteger lastNumber;
  17.    private final BigInteger lastPower;
  18.    public Cache(BigInteger lastNumber,BigInteger lastPower) {
  19.       //这里要保证对象的状态在构造之后不能被修改
  20.       //因为lastNumber lastPower是外部传入的 其实如果传入之后外部修改了它的内容 这个对象就不是不可变对象了。
  21.       //但是因为我们传入的是BigInteger类型的 他是不可变对象 所以不需要担心这个问题
  22.       this.lastNumber = lastNumber;
  23.       this.lastPower = lastPower;
  24.    }
  25.    public BigInteger get(BigInteger i)
  26.    {
  27.       BigInteger result = null;
  28.       if(i.equals(lastNumber))
  29.       {
  30.          //返回的对象 也是返回了一个引用 但是由于BigInteger是不可变对象 所以我们也不需要创建拷贝
  31.          result = lastPower;
  32.       }
  33.       return result;
  34.    }
  35. }
  36. public class PowerCache2 {
  37.    //使用volatile加不可变对象
  38.    private volatile Cache cache = new Cache(null, null);
  39.    public BigInteger power(BigInteger i) {
  40.       BigInteger result;
  41.       //在执行get的过程中 其他线程不能修改cache的内容 因为它是不可变的 只能重新给cache赋值 但是我正在使用 jvm总要等我用完再赋值吧
  42.       result = cache.get(i);
  43.       if(result == null)
  44.       {
  45.          result = i.pow(2);
  46.          //因为是volatile的 所以这个改变立刻会被其他对象看到
  47.          cache = new Cache(i, result);
  48.       }
  49.       return result;
  50.    }
  51.    public static void main(String[] args) {
  52.       PowerCache2 pc = new PowerCache2();
  53.       ExecutorService es = Executors.newCachedThreadPool();
  54.       for(int i = 0; i < 100; i++)
  55.       {
  56.          es.execute(new Runnable() {
  57.             public void run() {
  58.                BigInteger bi = BigInteger.valueOf((long)(Math.random() * 10));
  59.                System.out.println(bi + ":" + PowerCache.pc.power(bi));
  60.             }
  61.          });
  62.       }
  63.       es.shutdown();
  64.    }
  65. }