JAVA并发编程笔记(一)

一:线程安全

线程安全的代码,其实就是管理对状态的访问,通常是共享的、可变的状态。当多个线程同时访问一个状态变量时,这时才会产生线程不安全。

1:什么是线程安全性

      无状态对象永远是线程安全的。

2:原子性

      有操作A、B,从操作A的角度来说,其他线程操作B时,B要么执行完成,要么一点都未执行。

3:锁

      java的内部锁,synchronized 作用在代码块,方法上,锁在方法上锁对象是类本身,(静态的synchronized 方法从Class对象上获取锁)。同线程可以重进入锁,JVM会进行计数,进入+1,退出-1,为零释放锁。

4:活跃度与性能

程序加锁会影响性能,线程安全和性能不能同时获取,程序的简单性和性能也是相互牵制的,实现一个同步策略时,不要为了性能而牺牲简单性。对于耗时的计算和操作(网络或控制台的I/O),执行这些操作时不要占用锁。

二:共享对象

1:可见性

在没有同步的情况下,编译器、处理器,运行时安排操作的执行顺序可能完全出乎意料。在没有进行恰当同步的多线程程序中,尝试推断那些“必然”发生在内存中的动作时,往往会判断错误。

1.1:过期数据

对一个变量同时进行get和set操作时,会出现过期数据的现象。

1.2:非原子的64位操作

对于非volatile的long和double变量,JVM允许将64位的读或写划分为两个32位的操作,如果读和写发生在不同的线程,这种情况下可能会产生一个值高32位另一个低32位。在多线程中使用共享的、可变的long、double变量也是不安全的。

1.3:锁和可见性

锁不仅仅是关于同步和互斥,也是关于内存可见的,为了保证所有线程都能够看到共享的,可变变量的最新值,读取和写入线程必须使用公共的锁进行同步。

1.4:volatile变量

java提供的volatile变量是一种同步的弱形式,它确保一个变量的更新以可以预见的方式告诉其他线程,意思是被它修饰的变量对其他线程是透明的。使用volatile需要满足以下要求:(1):写入变量时并不依赖变量的当前值,或者保证只有单一线程修改变量的值。(2):变量不需要与其他状态变量共同参与不变约束。(3):访问变量时,没有其他原因需要加锁。

备注:加锁可以保证可见性和原子性,volatile变量只能保证可见性。

2:发布和逸出

发布一个对象的意思是使它能够被当前范围之外的代码所使用。逸出:一个对象尚未准备好时将它发布。

3:线程封闭

3.1:Ad-hoc(非正式的)线程限制

指维护线程限制性的任务全部落在实现实现上的情况。

3.2:栈限制

栈限制是线程限制的一种特例,在栈限制中,只有通过本地变量才能触及对象。要保证对象不要逸出,增加了程序的维护成本。

3.3:ThreadLocal

ThreadLocal将每个线程与持有数值的对象关联在一起,提供了set和get访问器,为每个使用它的线程提供一份单独的拷贝,get总是返回当前执行线程通过set设置的最新值。

4:不可变性

使用final修饰

5:安全发布

5.1:不正确发布,当好对象变坏时

没有正确发布的对象会导致两种错误。首先,发布线程意外的任何线程都能看到Holder的过期值,因而看到的是一个null引用或者旧值,即便此时Holder已经被赋予新值了。更坏的情况是,线程看到的Holder引用是最新的,然而Holder状态却是过期的,这使得程序执行变的更加不可预测:线程首次读取某个域可能看到的是过期值,再次读取该域会得到一个更新值。

5.2:不可变对象和初始化安全性

不可变对象可以在没有额外同步的情况下,安全地用于任意线程,甚至发布它们也不需要同步。

5.3:安全发布的模式

为了安全的发布对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确创建的对象可以通过下列条件安全发布:

(1):通过静态初始化器初始化对象的引用。

(2):将它的引用储存到volatile域或AtomicReference。

(3):将它的引用存储到正确创建的对象的final域中。

(4):或者将它的引用储存到由锁正确保护的域中。

线程安全库中的容器提供了如下线程安全保证:

(1):置入Hashtable,SynchronizedMap,ConcurrentMap中的主键以及键值,会安全的发布到可以从Map获得他们的任意线程中,直接获得和迭代器获得都行。

(2):置入Vector,CopyOnWriteArrayList,CopyOnWriteArraySet,SynchronizedList或者SynchronizedSet中的元素,或安全的发布到可以从容器中获得它的任意线程。

(3):置入BlockingQueue或ConcurrentLinkedQueue的元素,会安全地发布到可以从队列中获得它的线程中。

5.4:高效不可变对象

本身可变的对象,放入到同步容器中,此时状态被固定,这样来达到不可变的目的。比如:将Date的日期值放入到concrrentMap中。

5.5:可变对象

不可变对象可以通过任意机制发布,高效不可变对象必须安全发布,可变对象必须要安全发布,同时必须要线程安全或者被锁保护。

5.6:安全地共享对象

在并发程序中,使用和共享对象的一些有效的策略:

(1):线程限制:一个线程限制的对象,通过限制在线程中,而被线程独占,且只能被占有它的线程修改。

(2):共享只读:一个共享的只读对象,在没有额外同步的情况下,可以被多个线程并发地访问,但是任何线程都不能修改他,共享只读对象包括可变对象可高效不可变对象。

(3):共享线程安全:一个线程安全的对象在内部进行同步,所以其他线程无须额外同步,就可以通过公共接口所以地访问它。

(4):被守护的:一个被守护的对象只能通过特定的锁来访问,被守护的对象包括哪些被线程安全对象封装的对象,和已知被特定的锁保护起来的已发布对象。

三:组合对象

3.1:设计线程安全的类

设计线程安全类的过程包含3个要素:(1):确定对象状态是由哪些变量构成;(2):确定限制状态变量的不变约束;(3):制定一个管理并发访问对象状态的策略。3个要点:(1):收集同步需求。(2):状态依赖的操作。(3):状态所有权。

3.2:实例限制

即使一个类不是线程安全的,可以通过限制使它成为线程安全的,比如ArrayList和HashMap这样的基本容器类是非线程安全的,但是类库提供了包装器方法(Collections.synchronizedList及其同族的方法 - - Decorator模式)。

3.2.1:Java监视器模式

java中Vector和Hashtable这样的核心类都使用了java监视器模式。下图是使用私有锁保护状态:

3.3:委托线程安全

在实践中,委托是创建线程安全类最有效的策略之一,只需用已有的线程安全类来管理所有状态即可,

3.4:向已有的线程安全类添加功能

Java中提供的有线程安全的类,例如current包下的类。我们可以在这些类的基础上添加功能,来实现我们的线程安全。

3.4.1:客户端加锁

实现过程一定要注意操作的原子性。

3.4.2:组合

3.5:同步策略的文档化

这是一个良好的习惯,可以极大地方便以后代码的维护,比如那些变量声明为volatile类型,那些变量被锁保护,那些操作必须是原子的等等。

4:构建块

4.1:同步容器

同步容器(例如早期JDK的Vector和Hashtable,以及Collections.synchronizedXxx工厂方法创建的)都是线程安全的,但是进行复合操作的时候就不敢保证它的线程安全性了,可以通过客户端加锁或其他方式解决问题。

此外,还会有迭代器和ConcurrentModificationException,在迭代中出现ConcurrentModificationException有两种情况:一种是迭代过程是多线程访问,二种是单线程迭代过程中没有使用Iterator.remove删除对象,使用其他方法。在迭代过程中可以通过加锁的方式解决这种问题,但是对性能的消耗要认真考虑,这种情况下要综合考虑了。

4.2:并发容器

同步容器通过对容器的所有状态进行串行访问,从而实现了它们的线程安全,这样的代价是削弱了并发性,多个线程共同竞争容器级锁时,吞吐量就会降低。这就引出了并发容器,ConcurrentHashMap、CopyWriteArrayList、ConcurrentMap、Queue用来临时保存正在等待被进一步处理的一系列元素(操作不会阻塞,如果队列为空,直接返回空值。)、BlockingQueue(扩展了Queue,增加了可阻塞的插入和获取操作,如果队列是空的,获取操作会一直等待有可用元素出现,如果队列是满的,一直阻塞到有可用空间。在生产-消费模式非常有用)、ConcurrentLinkedQueue传统的FIFO队列、PriorityQueue(非并发)具有优先级顺序的队列、ConcurrentSkipListMap(替代同步SortedMap)和ConcurrentSkipList(替代同步SortedSet)。

4.2.1:ConcurrentHashMap

ConcurrentHashMap使用一个更加细化的锁机制--分离锁,这个锁机制允许更深层次的共享访问,任意数量的读线程可以并发访问Map,读者和写者也可以访问Map,并且有限数量的写线程还可以并发修改Map。结果是,为并发访问带来更高的吞吐量,同时单线程访问的性能几乎没有损失。

ConcurrentHashMap提供了不会抛出ConcurrentModificationException的迭代器,因此不需要在容器迭代中加锁,ConcurrentHashMap返回的迭代器具有弱一致性,而非及时失败的,弱一致性的迭代器可以容许并发修改,当迭代器被创建时,它会遍历已有的元素,并且可以感应到迭代器被创建后,对容器的修改。

注意:ConcurrentHashMap不能在独占访问中被加锁,我们不能使用客户端加锁来创建新的原子操作。

4.2.2:CopyWriteArrayList

CopyWriteArrayList是"写入时复制copy-on-write",只要有效的不可变对象被正确发布,那么访问它将不再需要更多的同步,在每次需要修改时,它们会重新创建和发布一个新的容器拷贝,以此来实现可变性。"写入时复制copy-on-write"的迭代器保留一个底层基础数组的引用,这个数组作为迭代器的起点,永远不会被改变,对它的同步是为了确保数组内容的可见性,因此多个线程进行迭代时,不会形成干扰。显然,对这种容器进行频繁修改时会消耗很大的性能,但是对其的访问远远大于对其的修改时,使用它是非常有效率的。

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

阻塞队列提供了可阻塞的put和take方法,它们与可定时的offer和poll是等价的,如果Queue满了,put方法会一直等待到有空间可以用。如果Queue空了,take方法会一直阻塞到有元素可用。Queue的长度可以无限也可以有限,无限的put没有阻塞。这种模式支持生产者-消费者模式

4.3.1:双端队列和窃取工作

Java6增加的Deque和BlockingDeque扩展了Queue和BlockingQueue,它是一个双端队列,允许高效的在头和尾分别进行插入和移除,两个实现类--ArrayDeque和LinkedBlockingDeque。

4.4:Synchronizer

Synchronizer是一个对象,它根据本身的状态调节线程的控制流,Synchronizer包括阻塞队列、闭锁(latch)、信号量(semaphore)、关卡(barrier)。这些都有一个类似的特征,他们封装状态,这些状态决定着线程执行到某一点是通过还是被迫等待。

4.4.1:闭锁(latch)

闭锁(latch):他可以延迟线程的进度直到线程到达终点状态,一个闭锁就像一个门,在到达终点状态之前门一直闭着,不让线程通过,达到终点状态时,门打开,允许所有线程通过。一旦闭锁到达终点状态,它就不能再改变状态了,会一直保持敞开状态。

CountDownLatch是一个灵活的闭锁实现,允许一个或多个线程等待一个事件集的发生,闭锁状态包括一个计数器,初始化为一个正数,用来表示等待的事件数,countDown方法对计数器做减操作,表示一个事件已经发生,await方法等待计数器达到零,此时所有需要等待的事件都已发生。

4.4.2:FutureTask

FutureTask可以作为闭锁,FutureTask的计算是通过Callable,它等价于一个可携带结果的Runnable,并且有3个状态:等待、运行和完成。完成的意思是以任意的方式结束(正常结束、取消和异常),一旦FutureTask进入完成状态,将永远停在这个状态。Future.get的状态依赖于任务的状态,任务完成立即返回结果,否则会被阻塞知道任务转入完成状态,返回结果或抛出异常。FutureTask把计算结果从运行计算的线程传送到需要这个结果的线程,这个过程是线程安全的。

4.4.3:信号量

计数信号量(Couning Semaphore)用来控制能同时访问某特定资源的活动的数量或同时执行某一给定操作的数量。计数信号量可以用来实现资源池或者给一个容器限定边界。

Semaphore管理一个有效的许可集(permit),许可的初始量通过构造函数传递给Semaphore,活动能够获得许可(只要还有剩余许可),并在使用之后释放许可。假如没有可用的许可,acquire会一直阻塞到有可用的为止(或者被中断或者操作超时),realease方法向信号量返回一个许可。一个计算量为1的Semaphore,二元信号量可用作互斥锁,拥有这个唯一的许可,就拥有了这个互斥锁。信号量可以用来实现资源池,比如数据库连接池,一个定长的池,为空时阻塞,不为空解除阻塞,以一个池的大小初始化Semaphore,获取资源之前先调用acquire获取一个许可,调用realease把许可放回资源池。

4.4.4:关卡

关卡类似闭锁,能阻塞一组线程,不同点在于所有线程必须同时到达关卡点,才能继续处理,闭锁等待的是事件,关卡等待是其他线程。

CyclicBarrier允许一个给定数量的成员多次集中在一个关卡点,(并行迭代算法中非常有用)调用await阻塞了,所有线程到达关卡,关卡突破,所有线程释放,关卡重置以备下次使用。如果对await的调用超时,或阻塞中的线程被中断,那么关卡被认为是失败的,所有对await未完成的调用都通过BrokenBarrierException终止。成功通过关卡,await为每个线程返回一个唯一的到达索引号,可以用它来"选取"产生一个领导,在下一次迭代中承担一些特殊工作。CyclicBarrier也允许你向构造函数传递一个关卡行为(Barrier Action),这是一个Runnable,当成功通过关卡的时候,会(在一个子任务线程中)执行,但是在阻塞线程被释放之前是不能执行的。

Exchanger是关卡的另一种形式,它是一个两步关卡,在关卡点会交换数据,当两方进行的活动不对称时,Exchanger是非常有用的,例如:当一个线程向缓冲写入一个数据,只是另一个线程充当消费者使用这个数据;这些线程可以使用Exchanger进行会面,并用完整的缓冲与空缓冲进行交换,交换时是线程安全的。

 

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值