JAVA并发编程实战笔记(第三章)

3.1 可见性
在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整,在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序的操作顺序进行判断,几乎无法得出正确的结论。

package chapter3;

/**
 * 在代码中没有使用足够的同步机制,因此无法保证主线程写入的ready值和number值对于线程来时是可见的。
 */
public class NoVisibility {
    private static boolean ready ;
    private static int number ;

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

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

1)非原子的64位操作
当线程在没有同步的情况下读取变量时,可能会得到一个失效值(或者是由之前某个线程设置的值),而不是一个随机值-- 最低线程安全性

非volatile类型的64位数值变量(double和long)
当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读到某个值的高32位和另一个值的低32位。因此,即使不考虑失效数据问题,在多线程程序中使用共享且可变的long和double等类型的变量也是不安全的,除非使用关键字volatile来声明它们,或者用锁保护起来

2)加锁与可见性
加锁的含义不仅仅局限于互斥行为,还包括内存可见性,为了确保所有线程都能看见共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。
在这里插入图片描述

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

注意:

  • 从内存可见性的角度来看,写入volatile变量相当于退出同步代码块,而读取volatile变量就相当于进入同步代码块
  • 如果在代码中依赖volatile变量来控制状态的可见性,通常比使用所的的代码更脆弱,也更难理解
  • volatile并不能提供原子的保证
  • 多线程下计数器必须使用锁保护。

应用场景:

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

3.2 发布与逸出
发布:使对象能够在当前作用域之外的代码中使用。
例如:

  • 发布对象的最简单方法是将对象的引用保存到一个公有的静态变量中,以便任何类和线程都可以看见。
  • 当发布一个对象时,在该对象的非私有域引用的所有对象同样会被发布。

逸出:当某个不应该发布的对象被发布时,这种情况就被称为逸出。
例如:

  • 在构造函数中启动一个线程。
    【当对象在某构造函数中创建一个线程时,无论是显示创建还是隐式创建,this引用都会被新创建的线程共享。】
  • 在构造函数中调用一个可改写的实例方法时(既不是私有方法,也不是终结方法),同样会导致this引用在构造过程中逸出。

3.3 线程封闭
当访问共享的可变数据时,通常需要使用同步。

  • 不共享数据
  • JDBC的Connection对象,JDBC规范并不要求Connection对象必须是线程安全的。【从连接池中获得一个Connection对象,并且用该对象来处理请求,使用完后再将对象返还给连接池】

3.3.1 Ad-hoc线程封闭
Ad-hoc线程封闭是指,维护线程封闭性的职责完全由程序实现来承担。由于Ad-hoc线程封闭技术的脆弱性,因此在程序中尽量少用它,在可能的情况下,应该使用更强的线程封闭技术。

3.3.2 栈封闭
栈封闭是线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。局部变量的固有属性之一就是封闭在执行线程中。

3.3.3 ThreadLocal类
维持线程封闭性的一种更规范方法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值
ThreadLocal对象通常用于防止对可变的单例变量(Singleton)或全局变量进行共享。

// 将JDBC的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){
	public Connection initialValue(){
		return DriverManager.getConnection(DB_URL);
	}
};
public static Connection getConnection(){
	return connectionHolder.get();
}

当某个频繁执行的操作需要一个临时对象,例如一个缓存区,而同时又希望避免在每次执行时都分配该临时对象,就可以使用这项技术。
ThreadLocal变量类似于全局变量,它能降低代码的可重用性,并在类之间引入隐含的耦合性,因此在使用时要倍加小心。

3.4 不变性
不可变对象一定是线程安全的。
当满足一下条件时,对象时不可变的:

  • 对象创建以后其状态就不能修改;
  • 对象的所有域都是final类型;
  • 对象时正确创建(在对象的创建期间,this引用没有逸出);
    3.4.1 Final域
  • final域是不能修改的;
  • final域能够保证初始化过程的安全性,从而可以不受限制地访问不可变对象,并在共享这些对象时无需同步;

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

3.5 安全发布
之前的重点是如何确保对象不被发布,例如让对象封闭在线程或另一个对象的内部。在某些情况下我们希望在多个线程间共享对象,此时必须确保安全的进行共享。

//不安全的发布
public Holder holder;

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

3.5.1 不正确的发布:正确的对象被破坏
除了发布对象的线程外,其它线程可以看到的Holder域是一个失效值,因此将看到一个空引用或者之前的旧值。
如果没有足够的同步,那么当在多个线程间共享数据时将发生一些非常奇怪的事情。

3.5.2 不可变对象与初始化安全性
任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。

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

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

但线程安全库中的容器类提供了以下的安全发布保证:

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

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

  1. 线程封闭 线程封闭的对象只能是一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。
  2. 只读共享 在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它,共享的只读对象包括不可变对象和事实不可变对象。
  3. 线程安全共享 线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。
  4. 保护对象 被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象,以及已经发布的并且由某个特定保护的对象。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值