Java并发编程实践.docx
- 文档编号:9850334
- 上传时间:2023-02-07
- 格式:DOCX
- 页数:42
- 大小:567.12KB
Java并发编程实践.docx
《Java并发编程实践.docx》由会员分享,可在线阅读,更多相关《Java并发编程实践.docx(42页珍藏版)》请在冰豆网上搜索。
Java并发编程实践
1对象的共享
关键字synchronized不仅能实现原子性还能确保当一个线程修改了对象状态后,另一个线程就可以看到对象状态的变化(内存可见性)
1.1可见性
重排序:
在缺乏足够同步的多线程程序中,代码的执行顺序不会按照程序员写好的顺序进行。
这是因为Java内存模型允许编译器、CPU对操作的执行顺序进行调整。
1.1.1失效数据
在多线程程序中,get方法、set方法都需要进行同步。
这是因为get方法在获取变量时可能会获得一个失效的值,这个失效的值就是之前某个线程设置的。
虽然已经失效但这个值曾经是正确的。
1.1.2非原子的64位操作
Java内存模型要求,非volatile类型的64位数值变量(double和long),JVM允许将64位的读操作和写操作分解为两个32位的操作。
那么在多线程的环境中,如果要读取非volatile类型的double、隆就有可能会读取到某个值的高32位和另一个值的低32位组成的一个数值。
但目前各种平台的商用虚拟机几乎都把64位数据的读写操作作为原子操作来对待。
1.1.3加锁与可见性
加锁的含义不仅仅局限于互斥行为,还包括内存可见性。
为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。
同步代码块的锁就是方法调用所在的对象,静态synchronized方法以Class对象作为锁。
synchronized可以用于实例变量、对象引用、static方法、类名称字面常量。
1.在某个对象实例内,synchronizedaMethod(){}可以防止多个线程同时访问这个对象其他的synchronized方法(这个对象还有其他的synchronized方法,如果其中一个线程访问了其中一个synchronized方法,那么其他线程将不能访问此对象另外的synchronized方法)。
但不同的对象实例间的synchronized方法是相互独立的,也就是说其他线程照样可以访问相同类的另一个对象实例的synchronized方法。
2.synchronizedstaticaMethod(){}对这个类所有的静态synchronized方法都会起作用,但不会对非静态的synchronized起作用。
这是因为static方法属于类方法,他属于这个Class(注意:
这里的Class不是指Class的某个具体对象),那么static方法所获取到的锁就是调用这个方法的对象所属的类,而非static方法获取到的锁就是当前调用这个方法的对象了。
3.除了在方法上用synchronized关键字外,也可以在方法内部的某个区块中用synchronized表示只对这个区块中的资源进行同步访问,例如synchronized(this){/**区块**/}的作用域就是当前对象。
1.1.3.1非static方法
运行结果是:
chunk对象与chunk1对象锁互不干扰。
chunk1
9chunk
1.1.3.2对象引用
结果与上面一样,也是chunk与chunk1互不干扰
chunk1
9chunk
1.1.3.3static方法
注意这段代码中的chunk与chunk1是互不干扰的,因为他们一个是static一个是非static方法。
执行结果是:
chunk1
9chunk
这段代码中的chunk与chunk1就可以进行同步操作了。
因为他们的锁是同一个锁,执行结果是:
9chunk
chunk1
1.1.3.4类名称字面常量
让所有这个类下面的对象都同步的时候,也就是让所有这个类下面的对象共用同一把锁的时候,我们可以在区块中锁定类常量的方式进行。
例如:
通过锁定lock对象,我们可以实现对这个类所有对象的同步。
切记synchronized锁定的是对象,对象不同就不会同步。
例如:
两个方法就不会同步一个锁定的是lock一个锁定的是类对象。
1.1.4volatile变量
volatile是比synchronized更轻量级的同步机制。
volatile变量有两种特性,一种是保证此变量对所有线程可见。
但volatile变量对所有线程不是立即可见的,每次使用volatile变量时都会将变量值从主内存中取到线程内存里,所以volatile变量在各个线程中是一致的。
但volatile变量的运算不能保证在并发的环境下仍然是安全的,这是因为Java里的运算不是原子操作。
volatile的使用情况如下:
●运算结果不依赖于变量的当前值,或者能够确保只有单一线程修改变量的值
●变量不需要与其他的状态变量共同参与不变形约束
例如:
当需要检查某个状态标记以判断是否退出循环的时候可以考虑采用volatile,加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。
volatile的第二种特性是禁止指令重排序。
Java内存模型对volatile变量的特殊规则如下:
●在线程工作内存中,每次使用volatile变量,都必须先从主内存刷新最新的值,用于保证能及时看见其他线程对volatile变量所做的修改
●每次修改volatile变量后都应立即同步到主内存中,用于保证其他线程可以看到对volatile变量所做的修改
●volatile修饰的变量需保证代码的执行顺序与程序的顺序相同
1.1.5synchronized与volatile
●volatile的字面意思就是“易变的”,其意思就是告诉Java的内存模型存储在寄存器中存放的当前变量是不确定的,需要从主存中读取,修改完成后也要立即将变量写入到主存中去。
而synchronized则是锁定的当前变量,只有当前线程才可以访问该变量,其他线程将被阻塞。
●volatile只能由于变量级别,而synchronized却可以用于变量和方法级别
●volatile仅能保证变量的修改可见性不能保证变量的修改原子性,而synchronized既能保证变量的修改可见性也能保证变量的修改原子性
●volatile不会造成线程堵塞,而synchronized可能会造成线程阻塞
●volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化
1.1.6synchronized与Lock
在代码层面上来说,synchronized类似面向对象,它可以修饰变量、方法、类、对象等。
而Lock却像是面向过程,在需要的时候获取锁,结束的时候释放锁(一般是在finally中释放)。
从性能上来说,在低并发的情况下synchronized要比Lock性能好,在高并发的情况下,Lock要比synchronized的性能好很多。
ReentrantLock拥有synchronized相同的并发性和语义,此外还多了锁投票、定时锁等待和中断锁等待。
例如线程A和线程B都要获得对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定,如果使用synchronized,如果A不释放,B将一直等待下去无法中断。
如果使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时候以后中断等待,而去干别的事情。
synchronized是在JVM层面实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM将自动释放锁。
Lock则不行,因为Lock通过代码实现它必须在finally中释放锁。
synchronized在生成线程转储时生成文件对开发人员找出死锁原因很有帮助,而Lock对于查找死锁原因将很困难,这个问题在Java6中已经得到解决,它提供了一个管理和调试的接口,锁可以通过这个接口进行注册,并通过其他管理和调试接口,从线程转储中得到ReentrantLock的加锁信息。
Synchronized获取锁和释放锁这样的行为是成对出现的,而ReentrantLock却不是这样的,举例来说在一个链表结构中,可以将ReentrantLock加到链表结构的每个节点上来减少锁的粒度,从而允许不同的线程操作链表的不同部分。
1.2发布与逸出
发布对象:
指的是使它能够被当前范围之外的代码所使用,例如将一个指向该对象的引用保存到其他代码可以访问的地方,或者在某一个非私有的方法中返回该引用,或者将引用传递到其他类的方法中。
当发布某个对象时,可能会间接地发布其他对象,例如当发布HashMap对象时,HashMap对象中保存的元素同样也会被发布。
一般说来,如果一个已经发布的对象能够通过非私有的变量引用和方法调用到达其他的对象,那么这些对象也都会被发布。
引用逸出:
对象逸出指的是一个对象尚未准备好时将它发布。
在构造器中启动线程,会使尚未构造完成的对象发布出去,造成逸出。
原则:
为防止逸出,对象必须要被完全构造完后,才可以被发布(最好的解决办法是采用同步)
this逸出是指构造函数返回之前其他线程就持有该对象的引用,this逸出经常发生在构造函数中启动线程和注册监听器。
如
publicclassThisEscape{
publicThisEscape(){
newThread(newEscapeRunnable()).start();
}
privateclassEscapeRunnableimplementsRunnable{
publicvoidrun(){
//通过ThisEscape.this就可以引用外围类对象,但是此时外围类对象还没有构造完成,即发生了外围类的this引用的逸出。
所以在构造函数中创建Thread对象后不要启动Thread。
可以提供一个start或者init方法负责启动线程。
}
}
}
用工厂方法可以防止this引用逸出
1.3线程封闭
线程封闭:
只在单线程内访问数据,这种技术称为线程封闭
当对象被封闭在一个线程中,自动成为线程安全的,即使被封闭的对象本身不是线程安全的。
如JDBC连接池,Connection对象并不是线程安全的,但是大多数请求(Servlet请求或者EJB调用)都是由单个线程采用同步的方式来处理,因封闭而安全。
1.3.1Ad-hoc线程封闭(弱限制)
维护线程封闭性的职责完全由程序来实现,这就是Ad-hoc线程封闭。
如单线程+volatile=避免竞争+可见性=线程安全
这是一种完全靠程序员实现的线程封闭,是最糟糕的一种封闭,因此可以忽略它。
1.3.2栈封闭(强限制)
只能通过局部变量才能访问对象,局部变量只存储在执行线程的栈中,其他线程无法访问这个栈,这样在线程内部使用非线程安全的对象仍然能保证线程安全。
例如:
如果在线程内部发布了局部变量(一个collection对象)的引用或者将该collection对象中的数据发布,那么封闭性将会被破坏,导致collection对象的逸出
1.3.3ThreadLocal类(强限制)
ThreadLocal对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享。
当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免在每次执行时都重新分配该临时对象。
就可以使用ThreadLocal。
但是除非这个操作的执行频率非常高,或者分配操作的开销非常高,否则ThreadLocal不能带来性能的提升。
在Java5.0中,Integer.toString()由之前的ThreadLocal对象保存12字节大小的缓冲区变成了每次调用时分配一个新的缓冲区,对于像临时缓冲区这样简单的对象,除非频繁操作否则ThreadLocal没有性能优势。
概念上,可以讲ThreadLocal
每个线程都保持对其线程局部变量副本的隐式引用,只要线程是活动的并且ThreadLocal实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。
前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
1.4不变性
当满足以下条件时,对象才是不可变的:
●对象创建后其状态就不能再改变
●对象的所有域都是final类型
●对象是正确创建的(在对象的创建期间,this引用没有逸出)
特别说明:
不可变对象的域并未全部声明为final类型。
如String会惰性地(lazily)的计算哈希值:
当第一次调用hashcode()时,String计算哈希值,并将它缓存在一个非final域中。
这个域有一个非默认的值,在每次计算中得得到相同的结果。
1.4.1Final域
final类型的域是不能修改的,但如果final域引用的对象是可变的,那么这么被引用的对象是可以修改的,不变的是final域的引用。
1.4.1.1弱不变模式
一个类的实例的状态是不可变化的,但是这个类的实例引用的实例可能具有变化的状态,这就是弱不变模式
一个类满足以下条件就是弱不变模式:
●对象没有任何方法会修改对象的状态,当对象的构造函数对对象的状态初始化之后,对象的状态便不再改变。
●所有的属性都应当是私有的,以防客户端对象直接修改任何的内部状态。
●这个对象所引用的对象如果是可变对象的话,必须设法限制外界对这个对象的访问,以防止对这些对象的修改。
如果可能应该尽量在不变对象的构造器内利用clone或者copy方法将可变对象复制给不可变对象的属性。
1.4.1.2强不变模式
一个类的实例的状态不会改变,同时它的子类的实例也具有不可变化的状态。
这样的类符合强不变模式。
要实现强不变模式,一个类必须首先满足弱不变模式所要求的所有条件,并且还要满足下面条件之一:
●所考虑的类所有的方法都应当是final,这样这个类的子类不能够置换掉此类的方法。
●这个类本身就是final的,那么这个类就不可能会有子类,从而也就不可能有被子类修改的问题
1.4.2示例:
使用Volatile类型来发布不可变对象
如果访问和更新多个相关变量出现竟态条件,那么可以通过将这些变量全部保存在一个不可变对象总来消除,然后使用volatile类型来确保不可变对象的可见性,这样可以在不显示使用锁的情况下保证线程安全性。
1.5安全发布
1.5.1不正确的发布:
正确的对象被破坏
未被正确发布的对象存在一个问题就是除了发布对象的线程外,其他线程可能看到的是一个无效值。
1.5.2不可变对象与初始化安全性
如果final类型的域所指向的是可变对象,那么在访问这些域所指向的对象的状态时仍然需要同步
1.5.3安全发布的常用模式
要安全的发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。
一个正确构造的对象可以通过以下方式来安全的发布:
●在静态初始化函数中或者静态初始化器中初始化一个对象引用,例如:
publicstaticHolderholder=newHolder(42);
●将对象的引用保存到volatile类型的域或者AtomicReferance对象中。
●将对象的引用保存到某个正确构造对象的final类型域中
●将对象的引用保存到一个由锁保护的域中,例如将对象放置到Vectory、Hashtable,synchronizedMap,ConcurrentMap等。
1.5.4事实不可变对象
是指一个对象在技术上是可变的,但其状态在发布后不会被修改。
这种对象称为“事实不可变对象”,事实不可变对象不需要满足不可变对象的条件,但其需要被安全的发布,不可变对象不需要被安全发布。
例如将事实不可变对象Date,存储到synchronizedMap中可以确保Date对象被安全的发布
1.5.5可变对象
对象的发布需求取决于它的可见性:
●不可变对象可以通过任意机制来发布
●事实不可变对象必须通过安全方式来发布
●可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来
1.5.6安全的共享对象
在并发程序总使用和共享对象时,有以下策略:
●线程封闭:
线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。
●只读共享:
在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它,共享的只读对象包括不可变对象和事实不可变对象
●线程安全共享:
线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步
●保护对象:
被保护的对象只能通过持有特定的锁来访问。
保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。
2对象的组合
2.1设计线程安全的类
封装能保护线程的安全性
在设计线程安全类的过程中,需要包含以下三个基本要素
●找出构成对象状态的所有变量
●找出约束状态变量的不变性条件
●建立对象状态的并发访问管理策略
2.1.1收集同步需求
要确保类的线程安全性,就需要确保它的不变性条件不会在并发访问的情况下被破坏。
如果在类中包含同时约束多个状态变量的不变性条件,那么在访问这些相关变量时要持有保护这些变量的锁
不可变约束:
判断某个状态是合法的还是非法的(不变式表达了对状态的约束,这些状态是应该符合这个约束的值的组合。
不变式可以代表某种业务规则。
)
后验条件:
会指出某种状态的转换是非法的(针对方法,规定了在调用方法之后必须为真的条件)
2.1.2依赖状态的操作
先验条件:
针对方法,规定了在调用方法之前必须为真的条件
要想实现某个等待先验条件为真时才执行的操作,一种更简单的方法是通过现有库中的类(例如阻塞队列或者信号量)
某些对象有基于先验条件,若一个操作基于状态的先验条件,则它被称为是状态依赖的类
2.1.3状态的所有权
如果以某个对象为根节点构造一张对象图,那么该对象的状态将是对象图中所有对象包含的域的一个子集。
2.2实例封闭
将数据封装在对象内部,把对数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。
Java集合类中有一组工厂方法,可以通过装饰器模式将线程不安全的集合变为线程安全的集合,例如ArrayList和HashMap是线程不安全的,但Collections.synchronizedList及类似方法可以将线程不安全的集合变为线程安全的集合。
2.2.1Java监视器模式
Java监视器模式:
符合监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护
使用私有锁对象而不是对象的内置锁,可以使客户代码无法得到锁,这样就可以将锁封装起来。
publicclassPrivateLock{
privatefinalObjectmyLock=newObject();
voidsomeMethod(){
synchronized(myLock){
//doSomething
}
}
}
2.2.2示例:
车辆追踪
一个符合监视器模式的对象可以利用synchronized方法将对线程不安全的对象的操作封装在自己内部,这样就变成线程安全的了。
2.3线程安全性的委托
一般情况下,线程安全的组件组合起来也是线程安全的。
可以将线程安全性委托给多个状态变量,只要这些变量是彼此独立的。
如果状态变量间有额外的限制,那么就需要保证组合操作时原子性的。
如果一个状态变量是安全的,且没有任何不变约束限制他,那么可以被安全的发布。
2.4在现有的线程安全类中添加功能
可以扩展现有线程安全类,并添加新操作
2.4.1客户端加锁
客户端加锁是指,对于使用某个对象X的客户端代码,使用X本身用于保护其状态的锁来保护客户代码
有时没法扩展,如synchronizedList方法后的集合,这样的情况下要用Helper类,但这里就需要客户端代码中获取线程安全类的锁来加锁
publicclassListHeper
publicList
…
publicBooleanputIfAbsend(Ex){
synchronized(list){
…
}
}
}
2.4.2组合
可以通过组合现有的类(装饰器模式)来组合成一个线程安全的类
publicclassImprovedList
privatefinalList
publicImprovedList(List
publicsynchronizedBooleanputIfAbsend(Tx){
……
}
}
3基础构建模块
3.1同步容器类
JDK中同步容器类实现线程安全的方式是:
将他们的状态封装起来,并对每个公有方法都进行同步,使得每次只有一个线程能访问容器的状态
3.1.1同步容器类的问题
虽然容器是线程安全的,但对于复合操作(迭代、条件运算、跳转[根据指定顺序找到当前元素的下一个元素]),就需要进行客户端加锁。
否则在多线程执行时会发生异常。
publicstaticObjectgetLast(Vectorlist){
synchronized(list){
intlastIndex=list.size()–1;
returnlist.get(lastIndex);
}
}
3.1.2迭代器与ConcurrentModificationException
容器在迭代时被修改,就会抛出ConcurrentModificationException,要想避免抛出ConcurrentModificationException,一种方法是在迭代期间对容器加锁,另一种方法是“克隆”容器,在副本上进行迭代。
3.1.3隐藏迭代器
容器的toString、hashCode、equals、containsAll、removeAll等方法都会间接的导致迭代操作。
封装对象的状态有助于维持不变形条件,那么封装对象的同步机制同样有助于确保实施同步策略。
3.2并发容器
JDK目前新增了ConcurrentMap(对复合操作支持)、Queue(保存一组待处理的数据)、BlockingQueue(扩展自Queue)
容器
对应的同步容器
HashMap
ConcurrentHashMap
List
CopyOnWriteArrayList
TreeMap
ConcurrentSkipListMap
TreeSet
ConcurrentSkipListSet
Queue
ConcurrentLinkedQueue、PriorityQueue(非并发)
3.2.1ConcurrentHashMap
ConcurrentHashMap并不是将每个方法都在同一个锁上同步并使得每次只能有一个线程访问容器,而是使用分段锁机制,在这种机制中,任意数量的读取线程可以并发的访问Map,执行读取操作的线程和执行写入操作的线程可以并发地访问Map,并且一定数量的写入线程可以并发的修改Map。
ConcurrentHashMap提供的迭代器不会抛出ConcurrentModificationExceptio
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Java 并发 编程 实践
![提示](https://static.bdocx.com/images/bang_tan.gif)