java并发编程——概念简介


本文是《java并发编程实战》一书中的第一部分—— 基础知识(前五章)的总结。

一、线程

1.1什么是线程

线程可以理解为进程中独立运行的子任务,也可以理解为轻量级的进程,在大多数的现在操作系统中,都是以线程为基本的调度单位。

1.2线程有哪些优势

线程可以有效地降低程序的开发和维护成本,同时提升复杂应用程序的性能。

  1. 线程能发挥多处理器的能力。如果一个程序中只有一个线程,那么最多同时只能在一个处理器上运行。在多核心的处理器上,单线程的程序只能使用一个核心上的资源,大部分资源无法使用,多线程程序可以同时在多个处理器核心上运行,可以通过提高处理器资源的利用率来提升系统吞吐量。使用多个线程还有助于再单处理器系统上获得更高的吞吐量,例如,当一个线程在等待同步I/O完成操作,另一个线程可以继续运行,使程序能够在I/O阻塞期间继续运行。
  2. 异步事件的简化处理。服务器应用程序在接受来自多个远程客户端的请求时,如果为每个链接的客户端单独分配一个线程来处理请求,就会降低这类程序的开发难度。
  3. 界面的响应更急灵敏。在使用多线程的界面下,在一个线程请求服务端资源并等待返回数据的过程中,另外的线程可以做别的事情,用户不用等待一个线程响应后再触发别的事件,从而使界面有更高的灵敏度。

1.3线程会带来什么危险

  1. 安全性问题。多线程带来的主要问题就是安全性的问题,在没有充足同步的情况下,多个线程的执行顺序是不确定的,这样就是导致和预期的结果不同。
  2. 活跃性问题。当某个操作无法继续执行的时候,就会发生活跃性问题,包括死锁,饥饿,活锁等问题,这些问题往往难以分析,因为他们依赖不同线程的事件发生时序,很难重现。
  3. 性能问题。带来额外的开销,比如,多线程的程序都会面临线程调度的问题,当一个线程临时挂起并转而执行另外一个线程时,会频繁执行切换上下文操作——保存和恢复执行上下文,并且使得CPU将更多的时间话费再线程调度上而不是线程的运行上。当线程共享数据时,必须使用同步机制,而这些机制往往会抑制某些编译器优化,使内存找那个的数据无效,并且会增加共享内存的同步流量,造成性能的额外开销。

二、线程安全性

要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的和可变的状态的访问。“共享”意味着变量可以由多个线程访问,而“可变”意味着变量的值在其生命周期内可以发生变化。

一个对象是否需要是线程安全的,取决于它是否被多个线程访问。要使对象是线程安全的,需要采取同步机制来协同对象可变状态的访问。

如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。有三种方式可以修复这个问题:

  1. 不在线程之间共享该变量
  2. 将变量修改为不可变得变量
  3. 在访问变量时使用同步

对线程安全性的理解

线程安全的定义中,最核心的概念是正确性。正确性的含义是:某个类的行为与其规范完全一致,即该行为的过程和得到的结果和规范所预期的结果是一致的。因此,当多个线程访问某个类时,这个类始终都能表现正确的行为,那么就称这个类时线程安全的。

三、对象共享

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

3.1线程封闭

当某个对象封闭在一个线程中时,对象是线程安全的,即使被封闭的对象本身不是线程安全的。

3.1.1ThreadLocal类

ThreadLocal类是用来维护线程封闭的一种规范方法,这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal为每个使用该变量的线程都存一份独立的副本,调用get方法时总是由当前执行线程再调用set时设置的最新值(后续计划——ThreadLocal源码学习)。

3.1.2栈封闭

栈封闭是线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。同步变量能使对象更易于封闭在线程中。它们位于执行线程的栈中,其他线程无法访问这个栈。

3.1.3Ad-hoc线程封闭

是指维护线程封闭性的职责完全由程序实现来承担。

volatile变量上存在一种特殊的线程封闭。只要能确保只有一个线程对共享的volatile变量执行写操作,那么就可以安全地在这些共享的volatile变量上执行“读取-修改-写入”的操作。这种情况下,相当于修改操作封闭在单个线程中以防止竞态条件的发生,并且volatile变量的可见性还保证了其他线程能看到最新的值。

3.2可见性

可见性是一种复杂的属性,因为可见性的错误总是会违背我们的直觉。当读、写操作在不同的线程中执行的时候,我们无法确保执行读操作的线程能适时地看到其他线程写入的值。要解决这个问题,必须使用同步机制。

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

3.2.1失效数据

当多个线程对一个变量执行读写操作时,一个线程可能获得变量的最新值,另一个变量可能活得失效值。失效值可能导致一些严重的安全问题或者活跃性问题。

3.2.2加锁与可见性

内置锁可以用于确保线程以一种可预测的方式来查看另一个线程的执行结果。

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

3.2.3volatile变量

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

虽然volatile变量很方便,但是也存在一些局限性。比如:volatile的语气不足以确保递增操作(count++)的原子性。

加所机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。

当且仅当满足一下所有条件时,才应该使用voaltile变量:

  1. 对变量的写操作不依赖变量的当前值,或者能确保只有单个线程更新变量的值;
  2. 该变量不会与其他状态变量译器纳入不可变性条件中;
  3. 在访问变量时不需要加锁。

四、对象的组合

4.1设计线程安全的类

在设计线程安全类的过程中,需要包含一下三个基本要素:

  1. 找出构成对象状态的所有变量
  2. 找出约束状态变量的不变性条件
  3. 建立对象状态的并发访问管理策略

4.2实例封闭

如果某对象不是线程安全的,那么可以通过多种技术使其在多线程程序中安全的使用。可以确保该对象只能单线程访问,或者通过一个锁来保护对该对象的所有访问。将数据封装在对象内部,可以将数据的访问限制在对象的访问上,从而更容易确保线程在访问数据时总能持有正确的锁。

4.3线程安全性的委托

如果一个类时由多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效状态转换,那么可以将线程安全性委托给底层的状态变量。

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

java类库中包含有很多的“基础模块”类。通常,我们应该优先选择重用这些现有的类而不是创建新的类,重用能降低开发工作量、开发风险以及维护成本。有时候,某个现有的线程安全类能支持我们需要的所有操作,但更多时候,现有的类只能支持大部分的操作,此时就需要在不破坏线程安全性的情况下加一个新的操作。

4.5将同步策略文档化

在文档中说明客户代码需要了解的线程安全性保证,以及代码维护人员需要了解的同步策略。

五、基础构建模块

5.1同步容器类

同步容器类包括VectorHashtable等,这些类实现线程安全的方式是:将他们的状态封装起来,b
并对每个公共方法进行同步,使得每次只有一个线程能访问容器的状态。

由于同步容器类要遵守同步策略,即支持客户端加锁,因此可能会创建一些新的操作,只要我们知道应该使用哪一个锁,那么这些新操作就与同期的其他操作一样都是原子操作。同步容器类通过自身的锁来保护它的每个方法。

5.2并发容器

同步器将所有对容器状态的访问都串行化,以实现他们的线程安全问题。这种方法的代价是严重降低并发性,当多个线程竞争容器锁时,吞吐量将严重降低。通过并发容器代替同步容器,可极大地提高伸缩性并降低风险。

java1.5中增加了ConcurrentHashMap,用来代替同步且基于散列的Map,以及CopyOnWriteArrayList,用于在遍历操作为主要操作的情况下同步的List。另外还增加了两种新的容器类型:Queue和BlockingQueue。Queue用来临时保存一组待处理的元素。它提供了几种实现,包括:ConcurrentLinkedQueue——一个传统的先进先出队列,以及PriorityQueue——一个(非并发)优先队列。BlockingQueue扩展了Queue,增加了可阻塞的插入和获取等操作。如果队列为空,那么获取元素操作将一直阻塞,直到队列中出现了一个可用元素。如果队列已满,那么插入元素的操作将一直阻塞,直到队列中出现可用的空间。

ConcurrentHashMap

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值