JAVA并行编程-第三章 对象的共享-学习总结

本文探讨了并发编程中对象共享与发布的挑战,强调了正确管理共享可变状态的重要性。详细介绍了synchronized关键字的作用,volatile变量的使用,以及安全发布对象的策略,包括线程封闭、只读共享、线程安全共享和保护对象等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第三章 对象的共享

要编写正确的并发程序,关键问题在于:在访问共享的可变状态时需要进行正确的管理。本章学习重点就是如何共享和发布对象,从而使它们能够安全地由多个线程同时访问。

关键字synchronized 不仅可以实现原子性或者确定临界区(Critical Section),还有内存可见性(Memory Visibility):当一个线程修改了对象状态后,其他线程可以看到发生的状态变化。

可见性

为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。
在没有同步的情况下,编译器、处理器以及运行时都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中国,要想对内存操作的执行顺序进行判断,几乎无法得到正确的结论。

失效数据

缺乏同步的程序中可能得到产生错误结果的一种情况:失效数据。

非原子的64位操作

获得失效数据,这种安全性保证也称为最低安全性(out-fo-thin-airsafety)。
最低安全性适用于绝大多数变量,但是除了:非volatile类型的64位数值变量(double和long)。
原因:对于非volatile类型的long和double变量,JVM允许将64位的读操作或写操作分成两个32位的操作。当读取一个非volatile类型的long变量是,如果对该变量的读操作和写操作在不同线程中国执行,那么可能读取到某个值的高32位和另一个值的低32位。

加锁和可见性

在访问某个共享且可变的变量要求所有线程在同一锁上同步,就是为了确保某个线程写入该变量的值对其他线程都是可见的。

加锁的含义不仅是互斥行为,还有内存可见性。

Volatile变量

稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。把变量声明成voaltile类型后,编译器和运行都会注意到这个变量是共享的,因此不会对该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此读取volatile类型的变量总是返回最新写入的值。

从内存可见性角度看,写入volatile变量相当于退出同步代码块,读取volatile变量相当于进入同步代码块。

使用情况
仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。如果子啊验证正确性需要对可见性进行复杂的判断,那么就不要使用volatile变量。volatile变量的正确使用方式包括:确保它们自身状态的可见性,确保它们锁引用对象的状态的可见性,以及标识一些重要的程序生命周期事件的发生(如,初始化或关闭)。

//检查某个状态标志以判断是否退出循环
volatile boolean asleep;

    ~~~~    while (!asleep)
    ~~~~        ~~~~    countSomeSheep();

重排序

为了提高性能,编译器和处理器常常会对指令做重排序,重排序就会导致多线程执行的时候有数据不一致问题,导致程序结果不是理想结果。

重排序分为三类:

  • 编译器重排序:不改变单线程程序语义前提下,重新安排执行顺序
  • 指令级并行重排序:指令并行技术可以将多条指令重叠执行,如果不存在数据依赖性,处理器会改变语句对应的机器指令执行顺序
  • 内存系统重排序

发布与逸出(Publish and Escape)

发布一个对象:是对象能够在当前作用域之外的代码中使用。
逸出:某个不该发布的对象被发布。

假定有个类C,对C来说,外部(Alien)方法:行为并不完全由C来规定的方法,包括其他类中定义的方法以及类C中可以被改写的方法(既不是private方法,也不是final方法)。

安全的对象构造过程
不要在构造过程中使用this引用逸出。
在构造过程中使this引用逸出的一个常见错误是,在构造函数中启动一个线程。当对象在其构造函数中创建一个线程时,无论是显式创建(通过将它传给构造函数)还是隐式创建(由于Thread或Runnable是该对象的一个内部类),this引用都会被新创建的线程共享。

//隐式地使this引用逸出(错误)
> public class ThisEscape {
>  public ThisEscape(EventSource source) {
>    source.registerListener (
>      new EventListener() {
>        public void onEvenr(Event e) {
>          doSomething(e);
>        }
>      });
>  }
>}
// 使用工厂方法来防止this引用在构造过程中逸出 
public class SafeListener {
	private final EventListener listener;
		
	private SafeListener() {
		listener = new EventListener() {
			public void onEvent(Event e) {
				doSomething(e);
  			}
	   	}
	}

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

线程封闭

一种避免使用同步的方式就是不共享数据,如果仅在单线程内访问数据,就不需要同步。将对象封闭在一个线程中叫线程封闭。

常见应用:
Swing,JDBC(Java Database Connectivity)的Connection对象。

Ad-hoc 线程封闭

Ad-ho线程封闭:维护线程封闭性的职责完全由程序实现来承担。

栈封闭

在栈封闭中,只有局部变量才能访问对象。局部变量的固有属性之一就是封闭在执行线程中,它们位于执行线程的栈中,其他线程无法访问这个栈。栈封闭(也叫线程内部使用/线程局部使用)比AD-hoc线程封闭更易于维护,也更加健壮。
简单的说,多个线程访问同一个方法,此方法的局部变量都会拷贝一份到线程栈中。所以局部变量不会被多个线程所共享,也就不会出现并发问题。

//基本类型的局部变量与引用变量的线程封闭性
public int loadTHeArk(Collection<Animal> candidates) {
	SortedSet<Aniaml> animals;
	int numPairs = 0;
	Animal candidate = null;
	
	// animals 被封闭在方法中,不要使它们逸出
	animals = new TreeSet<Aniaml>(new SpeciesGenderComparator());
	animals.addAll(candidates);
	for (Aniaml a : animals) {
		if (candidate == null || !candidate.isPotentialMate(a))
			candidate = a;
		else {
			ark.load(new AnimalPair(candidate, a));
			++numPairs;
			candidiate = null;
	    }
	}
	return numPairs;
}

ThreadLocal类

使用ThreadLocal是实现线程封闭的最好方法。ThreadLocal内部维护了一个Map,Map的key是每个线程的名称,而Map的值就是我们要封闭的对象。每个线程中的对象都对应着Map中一个值,也就是ThreadLocal利用Map实现了对象的线程封闭。

不变性

不可变对象:某个对象在被创建后其状态不能被修改,它一定是线程安全的。

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

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

Final 域

final类型的域是不能修改的,当是如果final域所引用的对象是可变的,那么这些引用的对象是可以修改的。
在Java内存模型中,final域还有特殊的语义,final域能确保初始化过程中的安全性,从而可以不受限制地访问不可变对象,并在共享这些对象时无须同步。

除非需要更高的可见性,否则应将所有的域声明为私有域。
除非需要某个域是可变的,否则应将其声明为final域。

安全发布

不可变对象与初始化安全性

某个对象的引用对其他线程可见,不意味着对象状态对与使用该对象的线程来说一定是可见的。

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

安全发布的常用模式

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

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

线程安全库中的容器类提供了以下的安全发布保证:
-通过将一个键或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,可以安全的将它发布给任何从这些容器中访问它的线程(无论是直接访问还是通过迭代器访问)。

  • 通过将某个元素放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或synchronizedSet中,可以将该元素安全地发布到任何从这些容器中访问该元素的线程。
  • 通过将某个元素放入BlockQueue或者ConcurrentLinkedQueue中,可以将该元素安全地发布到任何从这些队列中访问该元素的线程。

事实不可变对象

如果对象在发布后,状态不再改变,那么这种对象就叫作事实不可变对象(Effectively Immutable Object)。

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

可变对象

对于可变对象,不仅在发布对象是需要使用同步,而且需要在每次对象访问时同样使用同步来确保后续修改操作的可见性。要安全地共享可变对象,这些对象就必须被安全地发布,并且必须是线程安全的或者由某个锁保护起来。

对象的发布需求取决与它的可变性:

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

安全地共享对象

在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:
线程封闭。线程封闭的对象只能由一个线程拥有,对象被封闭在给线程中国并且值能由这个线程修改。
只读共享。在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
线程安全共享。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来访问而不需要进一步访问。
保护对象。被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布并且由某个特定锁保护的对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值