多线程基础知识

2017-09-07  本文已影响0人  汪梓文

基础知识

同步关键
1、对共享状态的管理,保证对共享状态操作的原子性,避免静态条件。同步机制(****synchronized****)

同步机制关键字:synchronized,保证了同步代码块中的代码执行顺序。
友情提示:在一开始就设计一个线程安全的类,比以后再将类修改为线程安全的类更容易。
编写并发程序的编程方法先使代码正确运行、在提高代码的速度,提高速度这一步最好是当性能测试结果和应用需求显示必须要提高性能,以及测量结果表明这个优化在实际环境中确实能带来性能提升时,才进行优化

本文中所有的提到的“状态”一词指的是存储数据的意思。

要想编写稳定可靠的并发程序,关键在于正确的使用线程和锁,当然这些终归只是一种实现机制。编写线程安全的核心代码在于对状态(数据)访问的管理,特别是对共享的(****shared****)可变的(****mutable****)状态(数据)的访问

共享和可变是多线程状态管理中重要的两个部分。其中共享指的是状态被多个线程使用,可变指的是变量的值在其生命周期内发生变化

一个对象是否需要线程安全,取决于这个对象是否被多线程访问。(迄今为止,写的所有代码出了dao对数据库的读取是多线程的外,业务层面几乎都是单线程访问对象。要改变这一状态需要深入理解线程的使用场景。不包括框架自己的实现)其中是否被多线程访问指的是程序访问对象的方式不是对象要实现功能的方式

多个线程访问状态变量时,如果对状态需要执行写入操作,就必须为这个共享状态采取同步机制来协助这些对象对变量的访问

多个线程同时访问一个状态变量时,如果这个状态变量没有使用合适的同步机制。书中提供了三种修复方式:
不在线程之间共享该状态变量。
将状态变量修改为不可变得变量,防止多线程对状态变量执行写入操作,而导致数据读取异常。
在访问该状态变量时使用同步机制。

再往下看时,我们需要问自己一个问题怎么区分线程安全的类和非线程安全的类“安全”的含义是什么???????


线程安全最核心的概念是“正确性”,如何理解正确性呢???正确性的定义又是什么???

在通读书中整段话后。我觉得要定义正确性就要先知道设计这个类的初衷是什么,需要这个类完成什么事,并且输出的结果符合设计这个类时的规范,调用者不需要额外的同步、协同操作

书中对正确性的定义****:当多个线程访问这个类时,不管运行时环境采用何种调用方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类为线程安全的。

无状态对象一定是线程安全的,在书中对无状态对象的界定是:线程执行过程中临时状态仅存储在线程栈的局部变量中,并且只能由当前线程访问。多个线程访问同一个对象,线程之间不能有共享状态,防止影响对方的计算结果

多线程中对共享状态执行写入操作时,必须保证操作的原子性。对多线程中原子性的理解http://www.parallellabs.com/2010/04/15/atomic-operation-in-multithreaded-application/

竟态条件是什么?书中定义的是当多个线程访问同一个共享状态时,由于执行时序的问题导致返回的结果不正确。称之为“竟态条件”

本书中“先检查在执行”一节的例子中,初始化数据时多线程情况下,如果不保证数据的原子性,返回的结果就违反了数据完整性约束,多个线程得到的结果和这个函数规定的结果不同。我们将“先检查后执行”、“读取-修改-写入”等操作统一称之为复合操作(包含了一组必须以原子方式执行的操作以确保线程安全。)

想要避免竟态条件的问题,就要在共享的变量中加入某种同步机制来确保其他线程只能在修改操作之前或者之后读取或写入状态。

应尽可能的使用现有的线程安全对象来管理状态,毕竟别人专门写的多线程更加稳定可靠,也更容易维护和验证线程安全性。

不变性(immutable)和只读(read only)的区别,immutable指的是一个状态一生只能被赋予一次值,不会再改变。只读是指一个值只能被读取,但是值不能被直接改变,年龄就是系统按照每年来递增,不是主动修改的值。

一定要注意,就算一个类中一个状态是线程安全的,对这个状态的操作可以当作原子性操作。但是如果一个类多个状态,就算每个状态都是线程安全的,也并不能能保证在多线程情况下对多个状态操作的原子性,导致破换了数据的完整性。

同步代码块包括两个部分:一个做为锁的对象引用、一个做为由这个锁保护的代码块


死锁是什么:当线程 1 获得锁 a 等待获得锁 b ,同时线程 2 获得锁 b 等待获得锁 a 时,就会出现死锁的情况,解决死锁的方式 ****http://wiki.jikexueyuan.com/project/java-concurrent/deadlock-prevention.html


嵌套管程死锁:当线程 1 持有锁 a,同时等待从线程 2 发来信号,但线程 2 需要锁 a 来发送信号给线程 1 。此时就导致了嵌套管程死锁的发生 ****http://wiki.jikexueyuan.com/project/java-concurrent/nested-monitor-lockout.html


死锁和嵌套管程的区别:
死锁:死锁是双方都在等待对方释放锁。
嵌套管程:当线程 1 持有锁 a,同时等待从线程 2 发来信号,但线程 2 需要锁 a 来发送信号给线程 1 。由于需要锁 a 才能发送信号给线程 1 ,但锁 a 又一直被等待信号的线程占用导致嵌套管程死锁。


单线程重入锁的概念:当一个程序在运行的时候,执行线程可以再次进入并执行当前程序,仍然符合设计预期的结果。与多线程的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。
可重入函数应当满足以下条件:
1、不能含有静态(全局)非常量数据
2、不能返回静态非常量数据的地址
3、只能处理由调用者提供的数据。
4、不依赖但实例模式资源的锁
5、调用(call)函数也不必然需是可重入的


重入死锁的概念:重入死锁在单线程重入的基础上的扩展。当一个线程重新获取锁,读写锁或其他不可重入的同步器时,就可能发生重入锁死。可重入是线程可以重新获得它已持有的锁。java的sycharonized块是可重入的。
如何避免重入死锁:
1、编写代码时避免再次获取已经持有的锁。
2、改写成可重入锁

在考虑加锁时,因尽量避免将执行时间过程的代码包含在锁中。

第三章:对象的共享

可见性
加锁的含义不止是为了互斥行为,还包括内存可见性,为了确保所有线程看到共享变量最新值,所有read and write 操组都必须在同一个锁上同步

非原子的64位操组(https://www.zhihu.com/question/38816432
long and double 在64位操作系统中会将一个read and write操作分为两个32位read and write操作。因此java虚拟机估计程序员在long and double上实现volatile操作。


Volatile 变量
被标记为volatile的java变量”被存储在主内存中”。更准确的意思指,每个变量的读取都将从当前电脑主内存中读取,并不是从这个cpu缓存中读取。每个变量的写入操作都将写入到主内存中,并不只是这个cpu中。


实际上,jdk5 以后保证 volatile 变量不只是从主内存中读取和写入.


在多线程中非volatile的变量操作,每个变量都将从主内存中拷贝到 cpu 缓存中,如果计算机有一个以上的cpu,那么每个线程可以运行在不同的cpu中,意味着每个线程变量可以拷贝到不同的cpu缓存中,
在jvm中,使用非volatile变量时无法保证何时 Reads 数据从主内存到cpu缓存中或何时写入数据从cpu缓存到主内存中。这会导致一些问题的发生。下面举例:


有一个类,包含一个非volatile变量。想象一下,如果有一个线程修改了这个变量,但有两个线程读取这个变量的时候。因为这个变量没被申明为volatile,所以无法保证这个变量什么时候从cpu缓存写入到主内存。意味着这个变量的值可以和主内存的不一样。这将导致其他线程无法看到最新值,因为还没有写入到主内存中。这是一个“visibility(能见度)”的问题。


将变量申明为volatile后,所有的写入操作将立即写入到主内存中。并且所有的读取操作都将从主内存中读取。
volatile 关键字保证不只是读取和写入直接访问主内存,还保证了:
1、如果线程A写入到volatile变量,随后线程b读取相同的变量。那么在写入volatile变量之前线程a可见的非volatile变量,也将在对线程b可见。开发人员可以利用这个延展性,不必为每一个非volatile变量声明volatile,只需要为一个或几个变量申明volatile。
public class Exchanger {

private Object   object       = null;
private volatile hasNewObject = false;

public void put(Object newObject) {
    while(hasNewObject) {
        //wait - do not overwrite existing new object
    }
    object = newObject;
    hasNewObject = true; //volatile write
}

public Object take(){
    while(!hasNewObject){ //volatile read
        //wait - don't take old object (or null)
    }
    Object obj = object;
    hasNewObject = false; //volatile write
    return obj;
}

}


2、volatile****变量的读取和写入不会被****jvm****重排序(jvm会重新排序指令保证性能,只要jvm检测到排序后没有改变行为).指令前和后可以重排序,但是不能混淆volatile的读取和写入指令。无论任何指令读取或写入都保证发生在volatile的读取或写入指令之后(Whatever instructions follow a read or write of a volatile variable are guaranteed to happen after the read or write)

while(hasNewObject) {
//wait - do not overwrite existing new object
}
hasNewObject = true; //volatile write
object = newObject;

由于Jvm可以对指令重排序优化性能,如果上述代码被jvm重排序,会影响object变量的visibility(能见度)。首先,线程B可能在看到hasNewObject设置为true之前,线程A实际上已经为object变量设置了一个新的值,现在甚至无法保证为object设置新值后何时写回到主内存中。

为了防止发生上述情况,volatile****附带“发生前保证”的特性在****volatile****指令读取或写入执行前保证指令不会发生改变。volatile 变量执行写入and读取前后的命令可以重排序,但是volatile指令不能重排序。

volatile****不适用的场景,当一个volatile被多个线程执行读取和写入操作,volatile无法保证原子性,有可能在同一时刻有多个线程同时读取volatie variable最新的值,在同时递增1 在写入主内存中,都是直接写入到主内存中。这种情况导致了竞争条件的发生,这种情况下应该采用synchronized块来确保读取和写入的原子操作。

final和重排序:
在构造函数对一个final域的写入和对构造对象赋值操作的指令顺序不会被jvm虚拟机改变。
禁止把final域的写入操作重排序到构造函数外面,保证了final在对象引用为任意线程可见之前,完成对final域的初始化,普通域不保证。
初次读取包含final域的对象与随后读取的对象中的final域的指令顺序不会改变。

发布:当一个对象能够在当前作用域之外的代码中使用,就称之为发布。
逃逸:当一个本不该被发布的私有对象被其他对象调用,就称之为逃逸。例如在对象构造完成之前就引用了这个构造对象,就会破坏线程安全性。
当在构造函数没有返回前,就显式或者隐式的把构造对象发布出去,会造成this引用逸出。****不要再构造过程中使用this引用逸出

什么是线程封闭:当访问共享的可变的数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术被称为线程封闭(Thread Confinement)

Ad-hoc线程封闭:ad-hoc线程封闭是指,维护线程封闭性的职责完全由程序来承担。并且ad-hoc线程非常脆弱,没有一种语言特性的支持将对象封装到目标线程中。个人理解就是,所有的状态仅在当前线程使用,多个线程之间的数据不共享,所以难以维护。

栈封闭:栈封闭是一种线程封闭的特例。在线程封闭中,只有通过局部变量才能访问对象。局部变量固有的属性之一就是封闭在执行线程中。其他线程无法访问这个栈。栈封闭也被称为线程内部使用或线程局部使用。比ad-hoc封闭更容易维护,也更健壮。

ThreadLocal:这个类能使线程中的某个值与保存值的对象关联起来。TheadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程存有一份独立的副本,因此get总是返回由当前线程在调用set时设置的最新值。ThreadLocal通常用于防止对可变的单实例变量或全局变量进行共享例如:EJB、J2EE容器需要的将一个事物容器与某个执行中的线程关联起来。通过将事务上下文保存在静态的ThreadLocal对象中,可以很容易的实现这个功能:当框架代码需要判断当前运行的是哪个事物时,只需要从这个ThreadLocal对象中读取事物上下文。缺点:使用这个机制的代码与框架耦合在一起。threadlocal变量类似于全局变量,会降低代码的可重用性,并在类与类之间引入隐含的耦合性,因此使用时要格外小心。


不变性:满足同步需求的另一个方法是不可变对象。如果某个对象在被创建后其状态就不能被修改,那么这个对象就被称为不可变的对象。只要状态不改变,那么这些不变形条件就能得以满足。所以不可变对象一定是线程安全的。在程序设计中一个最困难的地方就是判断复杂对象的可能状态。不可变对象就不需要判断恶意代码或者有问题的代码破坏,因此可以安全的共享和发布这些对象,而无需创建保护性副本。最后一个要求就是“正确的构建对象”,要保证在构造过程中防止this逸出

Final域:final域类型的域是不能修改的,但如果final域引用的对象是可变的,那么这个被引用的对象是可以修改的。final域确保了初始化过程的安全性,从而可以不受限制的访问不可变对象,并在共享这些对象时无需同步。对象中的可变状态越少越好、越简单。除非需要更高的可见性,否则应将所有的域都声明为私有域。除非需要某个域可变,否则应将其声明为final域。都是良好的编程习惯。


Final例子:在某些情况下不可变对象能提供一种弱形式的原子性。****Final keywork ****是并发库中非常重要但很容易忽略的工具,需要时,final可以确保在构建一个对象时,另一个线程不能访问未构造完成的对象状态。这是因为当做对象属性时,final****有一下重要的特性:在构造函数退出时,****final****字段的值是保证对于其他线程可见的。


安全发布的方式:
1、在静态构造函数中初始化一个对象引用。静态初始化器由于jvm的类在初始化阶段执行。由于在jvm内部存在着同步机制,因此通过这种方式初始化人和对象都可以安全发布。
2/、将对象引用保存到volatile类型的域或者atomicreferance对象中,来确保对象的可见性,和写入操作的原子性
3、将对象的引用保存到某个的正确构造对象的final类型域中
4、将对象的引用保存到一个由锁保护的域中。


对象安全发布需求取决于它的可变性:
1、不可变对象可以通过任意机制来发布。
2、事实不可变对象必须通过安全方式发布。
3、可变对象必须通过安全方式发布,并且必须是线程安全的或者由某个锁保护起来


“事实不可变对象”是什么,在对象发布后不会再被修改,那么就足以确保在没有额外同步的情况下访问这些对象都是安全的。


可变对象必须保证安全发布,并且必须是线程安全的或者由某个锁保护起来。


当发布一个对象是,必须明确的说明对象的访问方式:
1、线程封闭:线程封闭的对象只能由一个线程拥有,对象被封闭在线程中,并且只能由这个线程修改。

2、只读共享:在没有额外的同步情况下,共享的只读对象可以由多个线程并发访问,但人和线程都不能修改。共享的只读对象包括不可变对象和事实不可变对象,不包含可变对象。

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

4、保护对象:被保护的对象只能通过持有特定的锁来访问。保护对象包括封封装在其他线程安全对象的对象(类似封装在atomicreferance对象中的对象),以及已发布的并且由某个特定锁保护的对象。

第四章:对象的组合

如何设计一个线程安全的类:
1、找出构成对象的所有变量

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

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

同步策略(synchroization policy)定义了如何在不违背对象不变条件或后验条件的情况下对其状态的访问操作进行协同。同步策略还规定了如何将不可变性、线程封闭与加锁机制等结合以维护线程的安全性,并且还规定了哪儿些变量由哪儿些锁来保护。要确保开发人员可以对类进行分析和维护,就必须将同步策略写为正式文档。

状态推断中,状态空间表示状态的取值范围,状态空间越小,越容易判断线程的状态。final越多越能简化对象可能状态的分析过程。

上一篇下一篇

猜你喜欢

热点阅读