Java并发编程实战-学习笔记

第3章 对象的共享

3.1、可见性

可见性概念:在多个线程对一个变更操作时, 其中一个线程将变量修改后,其他线程在读取该变量的时候,得到的是修改后的值(即最新的值)。因此为确保多个线程之间对内存写入操作的可见性,必须使用同步机制。

 

3.1.1、失效数据

在多线程中,当没有同步机制时,某一线程取的数据可能是失效的数据(即该线程先取出变量,但后来被其他线程修改了,但是该线程没有被通知,所以该线程取的变量的数据是失效的)

 

3.1.2、32位基本基本类型数据的读取操作是线程安全的,但是非volatile的64位操作(如long、double类型数据变量)不是线程安全的,这类数据线程安全的方法:使用volatile关键字,或者使用锁

 

3.1.3、内置锁可以保证数据可见性

加锁的含义不仅局限于互斥行为, 还包括内存可见性。为确保所有线程都能看到共享变量的最新值,所有执行读取操作或者写操作的线程必须在同一个锁上同步。

 

3.1.4、Volatile变量

稍弱的同步机制,用来确保变量的更新操作通知到其他线程。比sychronized关键字更轻量级的同步机制。

volatile的使用条件(满足下面所有条件):

a、对变量的写操作不依赖于变量的当前值(如a++,a=a+10),才能使用volatile变量

b、该变量不会与其他状态变量一起纳入不变性条件

c、在访问变量时不需要加锁

 

3.2 发布与逸出

发布一个对象:使对象能够在当前作用域之外的代码中使用。如,将一个指向该对象的引用保存到其他代码可以访问的地方,或者在某一个非私有的方法中返回该引用,或者将引用传递到其他类的方法中。

在多数情况下, 确保对象及内部状态不被发布。发布内部状态可能会破坏封装性, 并使得程序难以维持不变性条件。如,如果在对象构造完成之前就发布对象,就会破坏线程安全性。

逸出:当某个不应该发布的对象被发布时,这种情况被称为逸出。如:将对象的引用保存到一个公有的静态变量中, 则其他任何类和线程都可以看到该对象,所以不利于统一做同步的控制。

安全的对象构造过程:不要在构造过程中使用this引用逸出。在构造函数中创建线程,this引用都会被新创建的线程共享,所以在构造函数时, 不要启动线程,而是通过start或initialize方法来启动。

 

3.3 线程封闭

在单线程中,不需要线程同步,这种技术称为线程封闭,实现线程安全最简单的方式之一。当某个对象封闭在一个线程中时, 这个用法将自动实现线程安全性,即使被封闭的对象本身不是线程安全的。

 

3.3.1 Ad-hoc 线程封闭:完全由程序来实现,由于这种技术的脆弱性,程序中尽量少用。(不太理解这种技术)

3.3.2 栈封闭:只能通过局部变量才能访问对象。当执行某一方法时,JVM会给每个线程都分配各自的栈,所以方法中的局部变量是封闭的。

对于基本类型的局部变量,因为任何方法都无法获得对基本类型的引用,所以不会破坏栈封闭性。

对于对象引用对象的栈封闭性,程序员需要多做一些工作以确保被引用的对象不会逸出

 

3.3.3 ThreadLocal类

维持线程封闭性的一种更规范方法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal有get,set方法,这些方法为每一个线程创建独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。

ThreadLocal可以避免在调用每个方法时都要传递该ThreadLocal类变量。

缺点:ThreadLocal变量类似于全局变量,它能降低代码的可重用性,并在类之间引入隐含的耦合性,因此使用要小心。

 

3.4 不变性

对象的状态(成员变量)都是不可变的

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

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

a、对象创建以后其状态变不能修改

b、对象的所有域都是final类型

c、对象是正确创建的(在对象的创建期间,this没有逸出)

 

安全共享对象的策略:

a、线程封闭

b、只读共享

c、线程安全共享:线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。

e、保护对象:被保护的对象只能通过持有锁来访问

 

 

第四章 对象的组合

4.1 设计线程安全类

通过封闭技术,可以使得在不对整个程序进行分析的情况下就可以判断一个类是否是线程安全的。

设计过程,包含三个基本要素:

a、找出构成对象状态的所有变量

b、找出约束状态变量的不变性条件

c、建立对象状态的并发访问管理策略

 

分析对象的状态,首先从对象的域开始。如果全是基本数据类型,那么些域构成对象的全部状态。如果是引用其他对象的域,则该对象的状态包含引用对象的域。

 

4.1.1 收集同步需求

4.1.2 依赖状态的操作

4.1.3 状态的所有权:即成员变量所属的类

 

4.2 实例封闭

将数据封装在对象内部(私有成员变量),将数据访问限制在对象的方法上,从而更容易使用锁保证同步

通过封闭机制确保线程安全:

public class PersionSet{

private final Set<Person> mySet = new HashSet<Person>();

public synchronized void addPerson(Person p){

mySet.add(p);

}

 

public synchronized boolean containsPerson(Person p){

return mySet.contains(p);

}

}

 

本实例将HashSet不是线程安全的类,使用内置锁封闭成线程安全的类。但没有考虑Person类的线程安全,如果Person类是可变的,那么在访问从PersonSet中获得Person对象时, 还要额外的同步。

 

4.2.1 java监视器模式,委托机制(使用synchronized内置锁,将不安全的类进行封装,变为线程安全的类,这就是线程安全的委托,将不安全的委托给线程安全的类)

 

4.4 在现有的线程安全类中添加功能

1、不要用子类继承安全类,如果父类的同步策略改变了,则子类线程安全被破坏

2、将安全类做为成员变量,创建一个新的类,在新类中加锁

public class ListHelper<E>{

public List<E> list = Collections.synchronizedList(new ArrayList<E>);

//没有则添加

public synchronized boolean putIfAbsent(E x) {

boolean absent = !list.contains(x);

if(absent) {

list.add(x);

}

 

return ablsent;

}

}

 

不要用上述的方法,上面的表面是使用锁, 但是使用了不同的锁, 所以不能保证同步

 

正确的方式:(在客户端加锁机制)

public class ListHelper<E>{

public List<E> list = Collections.synchronizedList(new ArrayList<E>);

//没有则添加

public void boolean putIfAbsent(E x) {

synchronized(list) {

boolean absent = !list.contains(x);

if(absent) {

list.add(x);

}

 

return ablsent;

}

}

}

 

组合方式:

public class ImprovedList<T> implements List<T>{

private final List<T> list;

public ImprovedList(List<T> list){

this.list = list;

}

//没有则添加

public synchronized boolean putIfAbsent(E x) {

boolean absent = !list.contains(x);

if(absent) {

list.add(x);

}

 

return ablsent;

}

 

public synchronized void clear(){

list.clear();

}

//按照这种方式委托List的其他方法

}

 

第5章 基础构建模块

5.1同步窗口类 Vector HashTable

5.1.1 同步容器类的问题

同步容器类都是线程安全的,但在某些情况下需要额外的客户端加锁来保护复合操作。

如:Vector中定义的两个方法

public static Object getLast(Vector list) {

int lastIndex = list.size()-1;

return list.get(lastIndex);

}

 

public static void deleteLast(Vector list){

int lastIndex = list.size()-1;

list.remove(lastIndex)

}

线程A查找最后一个元素getLast(),线程B删除最后一元素,当A执行完int lastIndex = list.size()-1;时,B将最后一元素删除,这时A将找到最后个元素!

解决方法:在客户端加锁。

public static Object getLast(Vector list) {

synchronized(list){

int lastIndex = list.size()-1;

return list.get(lastIndex);

}

}

 

public static void deleteLast(Vector list){

sychronized(list){

int lastIndex = list.size()-1;

list.remove(lastIndex)

}

}

5.2 并发容器  

并发容器来代替同步容器,可以极大地提高伸缩性并降低风险。

ConcurrentHashMap(用于代替HashTable)、Queue、BlockingQueue

Queue用来临时保存一组待处理的元素,非阻塞的,如果没有元素,则返回空值

它有几种实现:ConcurrentLinkedQueue(这是传统的先进先出队列)

PriorityQueue(非并发的优先队列)

 

BlockingQueue:扩展了Queue,增加了可阻塞的插入和获取等操作。如果队列为空,那么获取元素的操作将一直阻塞,直到队列中出现一个可用的元素。在生产者,消费者这种设计模式中, 阻塞队列是非常有用的。

 

5.2.1 ConcurrentHashMap

ConcurrentHashMap并不是将每个方法都在同一个锁上同步并使得每次只能有一个线程访问容器,而是使用一种粒度更细的加锁机制来实现更大程度的共享,这种机制称为分段锁。ConcurrentHashMap带来的结果是:在并发访问环境下将实现更高的吞吐量,而在单线程环境中只损失非常小的性能。

 

不需要在迭代器上加锁

只有当程序需要加锁Map以进行独占时, 这个时候不能使用ConcurrentHashMap,其他时候都可以使用

 

5.2.3 CopyOnWriteArrayList 替代同步List,  CopyOnWriteArraySet:替代同步Set

 

5.3 阻塞队列和生产者-消费者模式

BlockingQueue有多种实现:LinkedBlockingQueue、ArrayBlockingQueue 都是FIFO队列,二者分别与LinkedList、ArrayList类似

PriorityBlockingQueue是一个按优先级排序的队列,当你希望按照某种顺序而不是FIFO来处理元素时,使用这个队列

SynchronousQueue:实际上不是真正的队列,因为他不会为队列中元素维护存储空间。生产者生产的产品, 可以直接交给消费者来消费,减少时间的延迟。

take、put方法是阻塞的方法

阻塞队列存放任务, 供生产者、消费者使用,同时可以有多个生产者(即多线程)、多个消费者。在执行的过程中, 使用阻塞队列的使用情况,来调整生产者、消费者间的比例

 

在构建高可靠的应用程序时,有界队列是一种强大的资源管理工具:他们抑制并防止产生过多的工作项,使应用程序在负荷过载的情况下变得更加健壮。

 

5.4 阻塞方法与中断方法

 

5.5 同步工具类

5.5.1 闭锁

相当于一扇门,在闭锁到达结束状态前,这扇门一直是关闭的,闭锁可以用来确保某些活动直到其他活动都完成后才继续执行。

CountDownLatch是一种闭锁的实现,可以使一个或多个线程等待一组事件发生。闭锁状态包括一个计数器,该计数器被初始化为一个正数,表示需要等待的事件数量。countDown方法递减计数器,表示有一个事件已经发生了, 而await方法等待计数器达到0,这表示所有需要等待的事件都已经发生。如果计数器非0,那么await会一直阻塞到计数器为0,或者等待中的线程中断,或者等待超时。

 

5.5.2 FutureTask

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值