并发简介

简介

线程是轻量级的进程。多数操作系统都是以线程作为基本的调度单位,而不是进程。如果没有明确的协同机制,线程彼此间是独立的。由于同一个进程内的所有线程都将彼此共享进程的内存地址空间,因此这些线程都能访问相同的变量并在同一个堆上分配对象,这就需要实现一种比在进程间共享数据粒度更细的数据共享机制。

我们为什么要实现多个程序同时运行?

  1. 资源利用
  2. 公平性
  3. 便利性

线程的优势

线程可以有效的降低程序的开发和维护成本,同时提升复杂应用程序的性能。也可以降低代码的复杂度,使代码更容易编写、阅读和维护。

在GUI中,线程可以提高用户界面的响应灵敏度;在服务器程序中,可以提高资源的利用率和系统的吞吐率。

  1. 发挥多处理器的强大能力
  2. 建模的简单性

对于软件来说,如果程序中只包含一种类型的任务,那么比包含多种不同类型任务的程序要更容易编写。通过线程技术,我们可以把不同的任务分配到不同的线程中,这样我们在编写时就不需要考虑不同任务的协同,只要安心的把一个任务做好即可。

  1. 异步事件的简化处理
  2. 响应更灵敏的用户界面

线程带来的风险

  1. 安全性问题
  2. 活跃性问题

存在某种情况导致某种应该发生的事情没有发生。活跃性一般包括,死锁、活锁、饥饿等。活跃性问题一般很难重现,依赖于不同的线程发生的时序。

  1. 性能问题

“安全性”指永远不会发生糟糕的事情,“活跃性”指某件正确的事情最终会发生。

基础知识

安全性

要编写线程安全的代码,核心在于对状态访问操作进行管理,特别是对共享的(可以由多个线程同时访问)和可变的(变量的值在生命周期中可以发生变化)状态的访问。

JAVA中通过同步技术来保证线程的安全,主要有synchronized(内置锁)、volatile、显式锁以及原子变量。

我们可以通过一下步骤来避免多线程访问一个可变状态所造成的同步问题:

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

什么是线程安全性

线程安全性指的是,当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么这个类就是线程安全的。

线程的正确性指类的行为与其规范完全一致。

原子性

原子指的是一个完整的整体。这里表示,所有的操作应该是作为一个整体完成的。比如:

	int count = 0;
	for(int i=0;int < 10;i++)
		++count;

其中我们知道‘++count’操作就不是一个整体,它包含了3个独立的操作,读取count值,将值加1,然后将计算结果写入count。这是一个“读取-修改-写入”操作序列,每一个状态都会依赖于前一个状态。这样:

线程1:读取-修改-写入

线程2:-------读取-修改-写入

就会造成线程2的值覆盖了线程1的结果,这种由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,叫做竞态条件

竞态条件

一种常见的竞态条件是“先检查后执行”。首先观察到某个条件为真,然后根据这个观察结果采用相应的行为。但是在观察结束到采取行为的中间,状态可能已经发生了变化。

如,在单例模式中,采用延时初始化的方式,如果两个线程都检测到了状态为空,那么显然就会违反单例的特性了。

所以竞态条件,就是一种基于一种可能失效的观察结果来做出判断或是执行某个计算。

原子性就是要把含有竞态条件的操作,整合为一个整体,从而避免了由于竞态条件而导致的线程问题。

加锁机制

JAVA中提供了一些原子变量(util.concurrent.atomic包),可以保证数值和引用的原子性操作。但是并不是使用了原子变量就可以保证操作的原子性,如地图坐标有横坐标和纵坐标组成,每次更新时需要横纵坐标同时保证原子性。这里就需要用到了同步,确保横纵坐标有任何一个修改时,另一个都不可以被操作。

JAVA提供一个内置的锁机制来保证原子性:synchronized。其中锁就是方法调用的对象。静态的 synchronized方法以Class对象作为锁。

内置锁具有,重入特性。意味着如果某个线程试图获取一个已经由它自己持有的锁,那么这个请求一定会成功的。“重入”意味着获取锁的操作的粒度是“线程”,而不是“调用”。

重入的实现机制,每个锁都会含有一个获取计数器和一个所有者线程。当计数器为0时,表示这个锁没有被任何线程持有。

重入机制保证了一个对象的多个同步方法可以被同一个线程访问,而不是死锁。

锁机制可以保证由它保护的代码路径可以串行形式来访问。

用锁来保护状态

对于可能被多个线程同时访问的可变状态,在访问它的时候就需要一个锁,在这种情况下,我们说这个状态是由这个锁保护的。

一种常见的加锁约定是,将所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步。

对象的共享

synchronized除了有实现原子性或是确定“临界区”的功能外,还有一个重要的功能,内存可见性。

我们不仅希望控制线程不要使用其他线程正在修改的状态,同时线程修改状态后其他线程可以看到这个线程修改后的状态。就比如,竞态条件下,一个线程修改了一个状态可能会被另一个线程所覆盖,导致别的线程看不到之前线程的修改状态。同步保证了线程状态修改后可以被其他线程观察到。

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

失效数据

我们读取的数据,可能在我们读取后发生变化,那么我们读取的数据就是一个失效数据。

非原子的64位操作

当线程在没有同步的情况下读取一个变量时,可能会得到一个失效值,但至少这个值是之前某个线程设置的值,而不是一个随机值。这种安全性保证叫做最低安全性。

绝大多数变量都适用于最低安全性。但是存在一个例外,即非volatile的64位变量(double和long)。这是因为,JAVA内存模型要求所有变量的读取和写入操作都必须是原子操作,但是对于非volatile类型的double和long变量,JVM允许将64位的读写操作分解为2个32位操作。

所以对于多数情况下,double或是long变量是线程不安全的,需要用锁保护起来或是用volatile关键字修饰。

volatile

volatile是一种比synchronized轻量级的关键字。

volatile保证它修饰的变量在多个线程间的可见性,始终可以读取到最新值,确保变量的更新操作可以通知到其他变量。volatile变量不会被缓存在寄存器或是其他处理器不可见的地方。

一个变量设置为volatile后,编译器在运行时会注意到这个变量是共享的,因此不会将该变量上的操作于其他内存操作一起重新排序。

但是volatile关键字不保证原子性,它只能保证可见性,这点是不同于锁机制的。

发布与逸出

发布一个对象的意思指使对象能够在当前作用域之外的代码中使用。当一个不该发布的对象被发布时,就被称为逸出。

当发布一个对象时,其内部的非私有域中的引用的所有对象同样会发布。

不恰当的发布,可能会在我们不知道的情况下,对象被一个未知的线程操作。好的封装可以避免这种情况的发生。

需要注意的是,放置在创建的时候由this导致的隐性逸出。

线程封闭

最简单的避免同步问题的方法就是不同步,^_^。我们如果可以确认一个数据只会在单线程内访问,那么就不要共享,这就叫做线程封闭。数据在线程内,不与其他线程关联,也就不存在同步问题了。比如,Swing中使用了大量的线程封闭技术。

线程封闭不是java语言强制的。我们需要在程序设计中考虑如何实现,并在代码中完成实现。

  1. Ad-hoc线程封闭

维护线程封闭性的职责完全由程序来实现。但是由于JAVA语言特性,它是很脆弱的。

  1. 栈封闭

在栈封闭中只能通过局部变量才能访问对象

  1. ThreadLocal类

通过ThreadLocal的get与set方法,可以为每个使用该变量的线程都保存一份独立的副本。可以简单的把ThreadLocal理解为线程内的一个全局的Map。但是它会降低代码的重用性,并在类之间引入隐含的耦合性,所以我们在使用时要多注意。

不变性

满足同步的另一种方法就是使用不可变对象。一个不可变的对象,对于各个线程来说必定是一样的。所以不可变对象一定是线程安全的。

一个在创建后其状态不能被修改,那么这个对象就是不可变对象。不可变对象的状态,只能由构造函数来控制。不可变对象的要求:

  1. 对象创建后状态就不能修改
  2. 对象的所有域都是final型
  3. 对象是正确创建(在创建期间,this没有逸出)

当然我们不一定非要使用不可变对象,我们也可以在普通对象中使用不可变的域。就像是“除非需要更高的可见性,否则应将所有的域都声明为私有域”所说的,我们也可以说,“除非需要某个域是可变的,否则应将所有的域都声明为final的”

安全发布

对于可变对象的安全发布,需要保证对的引用以及对象的状态必须同时对其他线程可见。可以通过下述方式安全的发布对象:

  • 在静态初始化函数中初始化一个对象的引用

public static Holder holder = new Holder(23);

静态初始化器由JVM在类的初始化阶段执行。由于在JVM内部存在有同步机制,所以可以保证通过这种方式发布的对象是安全的。

  • 将对象的引用保存到colatile域或是AtomicReferance对象中
  • 将对象的引用保存到某个正确构造对象的final域中
  • 将对象的引用保存到一个由锁保护的域中。

事实不可变对象,尽管从技术上来讲是可变的,但其状态在实际使用中不会发生变化。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值