并发编程实战

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

  • 线程限制:一个线程限制的对象的,通过限制在线程中,而被线程占用,且只能被占有她的线程修改
  • 共享只读:一个共享的只读对象,在没有额外同步的情况下,可以被多个线程并发的访问,但是任何线程都不能修改它,共享只读对象包括可变对象和高效不可变对象。
  • 共享线程安全:一个线程安全的对象在内部进行同步,所以其他线程无须额外的同步,就可以通过公共接口随意访问它。
  • 被守护的:一个被守护的对象只能通过特定的锁来访问,被守护的对象包括那些被线程安全对象封装的对象,和已知被特定的锁来访问,被守护的对象包括那些被线程安全对象封装的对象,和已知被特定的锁保护起来的已发布的对象

设计线程安全类的过程的三个基本要素

  • 确定对象的状态是由那些变量构成的
  • 确定限制状态变量的不变约束
  • 制定一个管理并发访问对象状态的策略

同步策略:定义了对象如何协调对其状态的访问并且不会违反他的不变约束或后验条件。它规定了如何把不变性,线程限制和锁结合起来,从而维护线程的安全性,还指明了哪些锁保护哪些变量。

类的不变约束与方法的后验条件约束了对象合法状态转换

容器类通常表现出一种“所有权分离”的形式:容器拥有容器框架的状态,客户代码拥有存储在容器中对象的状态。例如servlet框架中的ServletContext对象会被多个线程访问,影刺ServletContext必须是线程安全的,所以在调用set/getAttribute时,Servlet不必同步,但是使用存储在ServletContext中的对象时,可能必须要同步,这些对象属于应用程序,servlet容器只是存储他们并代替用程序保管他们,他们应该是线程安全对象、高效不可变对象或者由锁明确保护的对象。

非线程安全的对象在多线程的使用

  1. 线程限制

    • Java监视器模式,遵循java监视器模式的对象封装了所有的可变状态,并由对象自己的内部锁保护。

      1. 委托线程安全

        • 如果勒种还存在复合操作,单独的委托仍然不是线程安全的,这种情况下,类必须提供他自有的锁以保证符合操作是原子的。除非整个复合操作也可以委托给一个状态变量。
        • 如果一个类有多个彼此独立的线程安全的状态变量组成,并且类的操作不包含任何无效状态转换时,可以将线程安全委托给这些状态变量。
        • 当且仅当一个变量没有参与那些涉及其他状态变量的不变约束时,才适合声明为volatile类型。
        • 如果一个状态变量是线程安全的,没有任何不变约束限制它的值,并且没有任何状态转换限制它的操作,那么它可以被安全发布。

      ConcurrentModificationcxeption也可能出现在单线程代码中,当对象不是通过Iterator。remove而是被直接从容器其中删除,就会出现这种情况。

      在迭代期间加锁会有相当长的一段时间把容器锁住,也会破坏程序的可伸缩性,可以复制容器,这样消除了ConcurrentModificationcxeption,复制期间仍然需要加锁。

并发容器

同步容器通过对容器的所有状态进行串行访问,从而实现了他们的线程安全,但是削弱了并发性,当多个线程共同竞争容器级锁时,吞吐量就会降低。

  1. Queue用来临时保存正在等待被进一步处理的一系列元素。
    • ConcurrentLinkedQueue:FIFO队列
    • PriorityQueue:非并发的优先级队列
  2. BlockingQueue扩展了Queue,增加了可阻塞的插入和获取操作,如果队列是空的,一个获取操作会阻塞直到队列中存在可用的元素,如果队列是满的(对于有界队列),插入操作会一直阻塞直到队列中存在可用空间。阻塞队列在生产者-消费者设计中非常有用。
  3. ConcurrentHashMap任意数量的读线程可以并发访问Map,读者和写者也可以并发访问Map,并且有限数量的写线程还可以并发的修改Map。结果是,为并发访问带来更高的吞吐量,同时几乎没有损失单个线程访问的性能。当程序需要在独占访问中加锁时,ConcurrentHashMap无法胜任。
  4. CopyOnWriteArrayList是同步的List一个并发替代品,它提供了更好的并发性,避免在迭代期间对容器加锁和复制。写时复制的容器返回的迭代器不会抛出ConcurrentModificationException,并且返回的元素严格与迭代器创建时一致,不考虑后续的修改。

阻塞队列和生产者消费者

  1. BlockingQueue提供了可阻塞的put take如果队列满,那么put一直会阻塞到有空间可用,如果队列为空,take方法也会被阻塞,队列长度可以有限,也可以无限,无限的队列永远不会充满,所以他的put永远不会阻塞。
  2. 有界队列是强大的资源管理工具,用来建立可靠的应用程序,他们遏制那些可以生产过多工作量,具有威胁的活动,从而让你的程序在面对超负荷工作时更加健壮。
  3. LinkedBlockingQueue和ArrayBlockingQueue是FIFO队列,PriorityBlockingQueue是按优先级顺序排列的队列(比较元素的自然序-实现了Comparable或者使用Comparator进行排序。SynchronousQueue不是一个真正的队列 ,他不会为队列元素维护任何存储空间。但是维护一个排队的线程清单。除非另一个线程已经准备好参与移交工作,否则put和take会一直阻塞,只有在消费者充足的时候比较合适。

当一个方法能够抛出InterruptEcxeption的时候,说明该方法是一个可阻塞方法,如果他被中断,可以提前结束阻塞状态。

中断是一种协作机制,一个线程不能迫使其他线程停止正在做的事情,或者去做其他事情;当线程A中断B时,A仅仅是要求B达成某个方便停止的关键点时,停止正在做的事情-如果这样做是正确的。

在代码中调用了一个会抛出InterruptedException 方法,该方法就成为一个阻塞方法,为响应中断做好准备。

  • 传递InterruptedEcxeption给你的调用者
  • 恢复中断,有时候不能抛出InterruptedException,比如代码是Runnable的一部分时,在这种情况下必须捕获异常,并且在当前线程中通过调用Interrupt从中断中恢复过来

Synchronizer

​ 他是一个对象,根据自身的状态调节线程的控制流。阻塞夫列可以扮演一个Synchronizer的角色,包括信号量(semaphore )关卡(barrier)闭锁(latch)

  • 闭锁
    • 延迟线程的进度到达终止(terminal)状态。直到闭锁到达终点状态之前,没有线程能够通过,在终点状态到来的时候,允许所有线程都通过。一旦到达终点状态,他就不能再改变状态了,所以他会永远保持敞开的状态。闭锁可以用来确保特定活动直到其他的活动完成才发生。

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

    • FutureTask也可作为闭锁,他的视线描述了一个抽象的可携带结果的计算,FutureTassk的计算是通过Callable实现的,他等价于一个携带结果的Runnable,有三个状态:等待 执行 完成。完成包括所有计算以任意的方式结束,包括正常结束,取消和异常。一旦进入完成状态,他会永远停止在这个状态上。

    • Future.get的行为依赖于任务的状态,如果已完成,get可以立即得到返回的结果,否则会被阻塞直到任务转入完成状态,然后会返回结果或者抛出异常。

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

    • Semaphore管理一个有效的许可集;许可的初始量通过构造函数传递给Semaphore。活动能够获得许可,并在使用之后释放许可。如果已经没有可用的许可了,那么acquire会被阻塞,直到有可用的为止(或者直到被中断或操作超时)。release方法向信号量返回一个许可,计算信号量的一种退化形式是元信号,一个初始值为1的信号量,可用来实现互斥锁(mutex)他有不可重入锁的语义。

  • 关卡
    • 闭锁是一次性使用的对象,一旦进入最终状态,就不能被重置了。
    • 关卡和闭锁都能够阻塞一组线程,直到某些事件发生,但是关卡是所有线程必须同时到达关卡点,才能继续处理。闭锁是等待事件
    • CyclicBarrier 允许给定数量的成员多次集中在一个关卡点,这在并行迭代算法中非常有用,把一个问题拆成若干相互独立的子问题。当线程到达关卡点时,调用await阻塞,直到所有线程都到达关卡点。关卡会被突破,所有线程就释放,关卡会重置以备下一次使用。如果对await调用超时或者阻塞中的线程被中断,关卡就被认为是失败的,所有对await未完成的调用都用过BrokenBarrierException终止,成功通过关卡,await会为每个线程返回一个唯一的索引号,用它来选举产生一个领导,在下一次迭代中承担一些特殊工作。

Excutor框架

  1. newFixedThreadPool 创建一个定长的线程池,每当提交一个任务就创建一个线程,直到达到池的最大长度,线程池就会保持长度不再变化
  2. newCachedThreadPool 创建一个可缓存的线程池,如果当前线程池的长度超过了处理的需要,他可以灵活的回收空闲的线程,当需求增加时,可以添加新的线程,并且不会对池的长度做任何限制。
  3. newSingleThreadPool 创建一个单线程化的executor,只创建唯一的工作线程来执行任务,如果这个线程异常结束,会有另一个来取代他,executor会保证任务按照任务队列所规定的顺序执行
  4. newScheduledThreadPool 创建一个定长的线程池,而且支持定时的以及周期性任务,类似于Timer

Timer 支持绝对时间,由此任务对系统时钟的改变是敏感的,ScheduledThreadPoolExecutor只支持相对时间

取消和关闭

可取消的:当外部代码能够在活动自然完成之前,把它更改为完成状态,那么这个活动被称为可取消的。

  1. 用户请求的取消:用户点击程序界面上的“cancel”按钮。或者通过管理接口请求取消,比如JMX。

  2. 限时活动:一个应用程序需要在有限的时间内搜索问题空间,并在规定的时间内选者最好的解决方案,比如计时器超时,正在搜索的任务会被取消。

  3. 应用程序事件:一个应用程序对问题空间进行分解搜索,使不同的任务搜索问题空间中不同的区域。当其中一个任务找到了解决方案时,所有的其他任然正在搜索的任务都将被取消。

  4. 错误:当一个网络爬虫任务发生错误时,所有的搜索任务都会被取消,此时可以记录他们的当前状态,以便稍后重新启动。

  5. 关闭:一个程序或服务关闭时,必须对正在处理和等待处理的任务执行某种操作,在平缓的关闭过程中,当前的执行任务将继续执行直到完成,立即关闭过程中,当前的任务可能取消。

性能与可伸缩性

  1. JVM实现阻塞行为时可以采用(等待时间短)自旋等待或者(等待时间长)操作系统挂起被阻塞的线程。

  2. 可伸缩性的主要威胁就是独占方式的资源锁,影响锁上的发生竞争的可能性:锁的请求频率、持有锁的时间。

  3. 降低锁的竞争程度的方式:

    • 减少锁的持有时间:缩小锁的范围、减小锁的粒度(锁分解、锁分段)
    • 降低锁的请求频率:锁分段,避免热点域
    • 使用带有协调机制的独占锁,这些机制允许更高的并发性:使用并发容器、读-写锁、不可变对象一级原子变量。
  4. ConcurrentHashmap为每段维护一个计数器,计算size时候对每个分段中的元素数量相加。

  5. CPU没有得到充分利用的原因:

    • 负载不充足
    • I/O密集
    • 外部限制
    • 锁竞争

并发程序的测试

  1. 安全性测试(不发生任何错误的行为):
  2. 活跃性测试(某个良好的行为终究会发生):

显示锁

  1. Lock 无条件的,可轮询的,定时的以及可中断的锁获取操作,加锁,解锁方法都是显示的。
  2. ReentrantLock实现了Lock,并且提供了与synchronized相同的互斥性与可见性。在获取/释放ReentrantLock时,有与进入或退出同步代码块相同的内存语义,还有可重入的加锁语义。
  3. 内置锁无法中断一个正在等待获取锁的线程,或者无法在请求获取一个锁时无限地等待下去,必须在获取该锁的代码块释放无法实现非阻塞的加锁规则。死锁发生只能重启程序,防止死锁就是在构造程序时避免出现不一致的锁顺序,可定时可轮询的锁
  4. ReadWriteLock:允许一个写操作。多个读操作。
    • 重入性:ReentrantReadWritelock:写锁只有唯一的所有者,并且只能由获得锁的线程来释放,读锁类似于一个Semaphore而不是锁,只是维护读线程的数量,不考虑他们的标识。

自定义的同步工具

  1. 通知:等一个条件变成true时候 发出通知

    • notify 在JVM拥有的众多线程中选择一个唤醒 可能导致丢失信号
      • 所有线程的类型都相同,只有一个条件谓词与条件队列相关,并且每个线程从wait返回后将执行相同的操作。
      • 单进单出,在条件变量上的每次通知,最多只能唤醒一个线程来执行。
    • notifyAll 唤醒所有线程

JVM在版本升级过程中,并发性能主要提升都来自于对非阻塞算法的使用,非阻塞算法通过底层的并发源于(比较并交换)来维持线程的安全性。

Java内存模型(JMM)

  1. Happens-Before规则:
    • 程序顺序规则
    • 监视器锁规则
    • volatile变量规则
    • 线程启动规则
    • 县城结束规则
    • 中断规则
    • 终结器规则
    • 传递性
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值