Java并发编程实战读书笔记-Chapter3

3.1 可见性

示例1:

public class NoVisibility {
    private static boolean ready;
    private static int number;

    private static class ReadrThread extends Thread {
        @Override
        public void run() {
            while (!ready) {
                Thread.yield();
                System.out.println(number);
            }
        }
    }

    public static void main(String[] args) {
        new ReadrThread().start();
        number = 10;
        ready = true;
    }
}

NoVisibility 可能会持续循环下去,因为线程可能永远看不到read的值,一种更奇怪的现象是,NoVisibility可能会输出0,因为读线程可能看到了写入ready的值,但却没有看到之后写入number的值,这种现象被称为重排序”。
有一种简单的方法能避免这些复杂的问题:只要有数据在多个线程之间共享,就使用正确的同步

3.1.1 失效数据

NoVisibility展示了在缺乏同步的程序中可能产生错误结果的一种情况:失效数据。当读线程查看ready变量时,可能会得到一个已经失效的值。除非在每次访问变量时都使用同步,否则很可能获得该变量的一个失效值。更糟糕的是,失效值可能不会同时出现:一个线程可能获得某个变量的最新值,而获得另一个变量的失效值。

示例2:非线程安全可变整数类

@NotThreadSafe
public class MutableInteger {
    private int value;

    public int get() {
        return value;
    }

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

MutableInteger不是线程安全的,因为get和set都是在没有同步的情况下访问value的。与其他问题相比,失效值问题更容易出现:如果某个线程线程调用了set,那么另一个正在调用get的线程可能会看到更新后的value值,也可能看不到。

示例3:线程安全的可变整数类

@ThreadSafe
public class MutableInteger {
    @GuardedBy("this") private int value;

    public synchronized int get() {
        return value;
    }

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

通过对get和set方法进行同步,可以使MutableInteger成为一个线程安全的类。仅对set方法进行同步是不够的,调用get的线程仍然会看见失效值

3.1.3 加锁与可见性

内置锁可以用于确保某个线程以一种可以预测的方式来查看另一个线程的执行结果,如图1所示。当线程A执行某个同步代码块时,线程B随后进入由同一个锁保护的同步代码块,在这种情况下可以保证在锁被释放前,A看到的变量值在B获得锁后同样可以由B看到。换句话说,当线程B执行由锁保护的同步代码块时,可以看到线程A之前在同一个同步代码块中的所有操作结果。如果没有同步,那么就无法实现上述保证。
在这里插入图片描述
现在我们就可以理解为什么在访问某个共享且可变的变量时要求所有线程在同一个锁上同步,就是为了确保某个线程写入该变量的值对于其他线程来说都是可见的。否则,如果一个线程在未持有正确锁的情况下读取某个变量,那么读到的可能是一个失效值。

3.1.4 Volatile

Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器不会将该变量上的操作与其他内存操作一起重排序。 同时volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞。所以volatile变量是一种比synchronized关键字更轻量级的同步机制

示例4:数绵羊

        volatile boolean asleep;
        ...
        while (!asleep) {
            countSomeSheep();
        }

volatile变量通常用做某个操作完成、发生中断或者状态的标志,例如示例4的asleep标志。在使用volatile时要非常小心,例如volatile的语义不足以确保递增操作(count++)的原子性,除非你能确保只有一个线程对变量执行写操作。
加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性

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

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

3.3 线程封闭

当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术被称为线程封闭,他是实现线程安全性的最简单方式之一。当某个对象封闭在一个线程中时,这种用法将自动实现线程安全性,即使被封闭的对象本身不是线程安全的。
线程封闭的一种常见应用是JDBC(Java Database Connectivity)的Connection对象。JDBC规范并不要求Connection对象是线程安全的。在典型的服务器应用程序中,线程从连接池中获得一个Connection对象,并且用该对象来处理请求,使用完后再将对象返还给连接池。由于大多数请求都是由单个线程采用同步的方式来处理,并且在Connection对象返回之前,连接池不会再将它分配给其他线程,因此,这种连接管理模式在处理请求时隐含地将Connection对象封闭在线程中。

3.3.3 ThreadLocal类

维持线程封闭性的一种更规范方法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。
通过将JDBC的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接。
示例5:使用·ThreadLocal来维持线程封闭性

    private static ThreadLocal<Connection> connectionHolder =
           new ThreadLocal<Connection>() {
       public Connection initialValue() {
           return DriverManager.getConnection(DB_URL);
       }
   };

   public static Connection getConnection() {
       return connectionHolder.get();
   }

3.4 不变性

满足同步需求的另一种方法是使用不可变对象。得到失效数据,丢失更新操作或者观察到某个对象处于不一致的状态等问题,都与多线程试图同时访问同一个可变的状态相关。如果对象的状态不会改变,那么这些不变性条件就能得以维持。

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

虽然在Java语言规范和Java内存模型中都没有给出不可变性的正式定义,但不可变性并不等于将对象中所有的域都声明为final类型(比如String),即使对象中所有的域都是final类型的,这个对象也仍然是可变的。因为在final类型的域中可以保存对可变对象的引用。

示例6:在不可变对象的内部仍可以使用可变对象来管理它们的状态。

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

尽管保存姓名的Set对象是可变的,但从ThreeStooges的设计可以看到,在Set对象构造完成后无法对其进行修改。

3.4.1 Final域

在Java内存模型中,fianl域还有着特殊的语义,final域能确保初始化过程的安全性,从而可以不受限制地访问不可变对象,并在共享这些对象时无须同步。通过将域声明为final类型,也相当于告诉维护人员这些域是不会变化的。

示例7:对数值及其因数分解结果进行缓存地不可变容器类(在某些情况下,不可变对象能提供一种弱形式的原子性)

public class OneValueCache {
    private final BigInteger lastNumber;
    private final BigInteger[] lastFactors;

    public OneValueCache(BigInteger i, BigInteger[] factors) {
        this.lastNumber = i;
        this.lastFactors = Arrays.copyOf(factors, factors.length);
    }

    public BigInteger[] getFactors(BigInteger i) {
        if (lastNumber == null || !lastNumber.equals(i)) {
            return null;
        } else {
            // 防止lastFactors被外部修改
            return Arrays.copyOf(lastFactors, lastFactors.length);
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值