Java并发编程学习全解.docx
《Java并发编程学习全解.docx》由会员分享,可在线阅读,更多相关《Java并发编程学习全解.docx(15页珍藏版)》请在冰豆网上搜索。
![Java并发编程学习全解.docx](https://file1.bdocx.com/fileroot1/2023-6/4/7e4489d9-d36d-42fa-aa93-f8347e9f7ba9/7e4489d9-d36d-42fa-aa93-f8347e9f7ba91.gif)
Java并发编程学习全解
Java并发编程学习
一对象的共享
线程之间对象的共享不仅仅需要有原子性和临界区,还有一个重要方面:
内存可见性
1可见性
读操作的线程并非可以一直获取到写线程写入的最新值,例如:
代码示例
privatestaticbooleanready;
privatestaticintnumber;
privatestaticclassReaderThreadextendsThread{
publicvoidrun(){
while(!
ready)
Thread.yield();
System.out.println(number);
}
}
publicstaticvoidmain(String[]args){
newReaderThread().start();
number=42;
ready=true;
}
以上实例代码可能存在如下问题:
一直循环下去,因为读线程可能看不到写线程写入了ready。
在读线程中输出0,因为主线程可能对number和ready的赋值顺序进行了改变。
这是由于线程之间没有正确使用同步使得数据在多个线程中共享出现错误导致的。
常见的问题包括:
读取到的数据已经失效
没有对64位数据同步读写
失效的数据
以上述例子来说,当读线程读取ready变量的时候,很可能该变量已经失效。
失效的数据通常包括两类数据:
值数据
引用数据
值数据的失效例如一个计数器应用,如果不对count变量进行同步处理的话可能会导致计数不准的问题。
如果对象的引用失效可能会导致一些莫名其妙的问题,比如意料之外的异常,被破坏的数据结果,不精确的计算及无限循环等。
非原子的64位操作
根据Java内存模型的要求,变量的读写操作必须是原子操作。
对于非volatile类型的long和double变量,JVM允许将64位读写操作分解为两个32为的操作,这样在多线程环境中共享可变的long和double变量也是不安全的。
一般的解决方式可以通过加锁或者声明变量为volatile变量。
内置锁可以用于确保某个线程以一种可预测的方式看到另一个线程执行的结果。
加锁的含义不仅仅局限于互斥行为,还包括了内存可见性。
Java同时提供了一种较弱的同步机制,即volatile变量。
volatile变量不同于加锁机制,加锁机制可以保证原子性和可见性,但是volatile只能保证可见性。
volatile变量会确保将变量的更新结果通知到其他线程,被声明volatile变量会有两个效果:
不会对该变量重排序;
不会缓存在寄存器或者其他处理器不可见的地方。
使用volatile变量可以不执行加锁操作,也不会阻塞线程,开销要比同步要低。
但是只能确保可见性,不能确保原子性,更不能保证在volatile变量递增操作的原子性,在使用的时候要注意。
当满足以下所有条件时才应该使用volatile变量:
对变量写入不依赖当前值,或者确保只有单个线程更新变量的值;
该变量不被要求与其他变量同步变化;
在访问该变量时不需要加锁,因为加上锁就没必要使用volatile了。
2发布与逸出
发布一个对象是指使一个对象能在当前作用域之外的代码中使用。
一般是将成员变量或静态变量公开。
对于一个类的外部方法是不完全由该类规定的方法,包括该类以外定义的方法及由该类定义但可以被改写的方法。
当把一个对象传递给一个外部方法,就相当于发布了这个对象。
逸出一个对象是指某个不应该发布的对象被发布
安全构造对象
错误的构造对象方式
publicThisEscape(EventSourcesource){
source.registerListener(newEventListener(){
publicvoidonEvent(Evente){
doSomething(e);
}
});
}
在EventListener中保留了this的引用,因此会将this引用逸出。
一定不要在构造过程中使this引用逸出。
在构造对象的过程中常见的问题:
构造函数中将自己的引用传递给一个线程并将该线程启动;
构造函数中调用一个可被改写的方法
如果构造函数中需要注册事件监听器或者启动线程,一个办法是将构造函数私有化,用工厂模式初始化。
例如:
privatefinalEventListenerlistener;
privateSafeListener(){
listener=newEventListener(){
publicvoidonEvent(Evente){
doSomething(e);
}
};
}
publicstaticSafeListenernewInstance(EventSourcesource){
SafeListenersafe=newSafeListener();
source.registerListener(safe.listener);
returnsafe;
}
3线程间的变量
对于多线程相关的变量,使用的时候有两种策略,一种不希望被共享,使用线程封闭技术将该变量的使用范围限制在一个线程中,如果需要在多个线程之间进行共享,跟据该变量是否可变,需要有不同的共享策略。
3.1线程封闭
线程封闭是指仅在单线程内访问数据,将访问的数据封闭在一个线程中。
常见场景:
JDBC的Connection对象
Command模式
Java中提供一些机制可用来实现线程封闭,例如局部变量和ThreadLocal。
3.1.1Ad-hoc线程封闭
Ad-hoc线程封闭是指维护线程封闭性的职责完全由程序实现来承担。
非常脆弱,一般不建议使用,建议使用其他诸如栈封闭和ThreadLocal。
3.1.2栈封闭
在栈封闭中,只有通过局部变量才能访问对象。
但程序员需要确保该局部引用不会逸出。
3.1.3ThreadLocal类
一种更规范的保证线程封闭的方法就是使用ThreadLocal,set入ThreadLocal的变量每个线程都只有一份独立的副本。
当某个线程第一次调用Thread.get的时候,就会通过initialValue获取初始值:
privateThreadLocalconnectionHolder
=newThreadLocal(){
publicConnectioninitialValue(){
try{
returnDriverManager.getConnection(DB_URL);
}catch(SQLExceptione){
thrownewRuntimeException("UnabletoacquireConnection,e");
}
};
};
publicConnectiongetConnection(){
returnconnectionHolder.get();
}
以上代码可以为每个线程维持一个数据库连接。
3.2线程共享的对象
3.2.1不可变对象
线程安全是不可变对象的固有属性之一
Java中,不可变不代表对象的所有域都声明为final类型,当满足以下条件的时候,对象才是不可变的:
对象创建以后其状态不能修改
对象的所有域都是final类型
对象正确被创建,没有在构造期间this引用逸出
不可变对象的内部仍可以使用可变对象来管理他们的状态,例如:
@Immutable
publicfinalclassThreeStooges{
privatefinalSetstooges=newHashSet();
publicThreeStooges(){
stooges.add("Moe");
stooges.add("Larry");
stooges.add("Curly");
}
publicbooleanisStooge(Stringname){
returnstooges.contains(name);
}
publicStringgetStoogeNames(){
Liststooges=newVector();
stooges.add("Moe");
stooges.add("Larry");
stooges.add("Curly");
returnstooges.toString();
}
}
虽然用于管理ThreeStooges对象的Set对象时可变的,但是ThreeStooges对象是不可变的。
不可变对象可以提供弱原子性。
当需要同时更新多个域时,可以将多个域放入一个不可变对象中保持原子性。
例如:
publicclassOneValueCache{
privatefinalBigIntegerlastNumber;
privatefinalBigInteger[]lastFactors;
publicOneValueCache(BigIntegeri,
BigInteger[]factors){
lastNumber=i;
lastFactors=Arrays.copyOf(factors,factors.length);
}
publicBigInteger[]getFactors(BigIntegeri){
if(lastNumber==null||!
lastNumber.equals(i))
returnnull;
else
returnArrays.copyOf(lastFactors,lastFactors.length);
}
}
每次更新Cache都需要更新lastNumber和lastFactors,更新这两个域需要以原子方式执行某个操作。
调用的时候:
privatevolatileOneValueCachecache=newOneValueCache(null,null);
publicvoidservice(ServletRequestreq,ServletResponseresp){
BigIntegeri=extractFmRequest(req);
BigInteger[]factors=cache.getFactors(i);
if(factors==null){
factors=factor(i);
cache=newOneValueCache(i,factors);
}
encodeIntoResponse(resp,factors);
}
voidencodeIntoResponse(ServletResponseresp,BigInteger[]factors){
}
BigIntegerextractFromRequest(ServletRequestreq){
returnnewBigInteger("7");
}
BigInteger[]factor(BigIntegeri){
//Doesn'treallyfactor
returnnewBigInteger[]{i};
}
将cache设置为volatile使的更新了该域后其他线程会立即看到新缓存的数据。
3.2.2事实不可变对象
事实不可变对象是指那些从技术上看是可变的,但是其状态在发布之后不会在改变的对象。
在没有额外同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。
3.2.3可变对象
对于可变对象,不仅发布对象的时候需要使用同步,而且每次访问对象同样需要使用同步确保后续操作修改的可见性。
3.3安全发布
三类对象
不可变对象:
可以通过任意机制发布;
事实可变对象:
必须通过安全方式发布;
可变对象:
可变对象必须通过安全方式发布,而且必须是线程安全或者进行锁保护。
并发共享对象时的策略
线程封闭:
栈封闭或者ThreadLocal。
只读共享:
共享的对象包括不可变对象和事实不可变对象。
线程安全共享:
线程安全对象在其内部实现同步。
保护对象:
锁保护
二对象的组合
主要介绍一些组合模式,使得一个类更容易成为线程安全的类,在维护这些类时不会无意中破坏类的安全性保证。
1.设计线程安全的类
设计线程安全类的过程中,需要包含以下三个基本要素:
找出构成对象状态的所有变量;
找出约束状态变量的不变性条件;
建立对象状态的并发访问管理策略。
对象的状态
如果所有的域都是基本类型,则这些域构成对象的全部状态;
如果包含其他对象,该对象的状态将包括被引用对象的域。
同步策略规定了如何将不变性条件、线程封闭和加锁机制结合起来以维护线程的安全性,并且规定了哪些变量由哪些锁来保护。
1.1收集同步需求
**尽量多的使用final域。
**final类型的域使用的越多,状态空间就越小,越能简化对象可能状态的分析过程。
在单个或多个域上,某一个操作如果存在的无效的状态转换,需要对该操作进行同步。
无效的状态转换包括不满足:
不变性条件,例如:
int变量超出最大最小值。
后验条件,例如:
counter当前值是17,那么下一个操作结束一定是18.
如果某个操作存在无效的状态转换,那么该操作必须是原子的,即需要同步。
1.2分析依赖状态的操作
某些方法包含一些先验条件才能执行,例如:
不能够从空队列中删除一个值。
单线程程序中如果遇到无法满足先验条件的情况可以直接返回失败,但是并发程序中先验条件可能因为其他线程的执行而变成真,因此要一直等待先验条件为真再执行。
1.3分析状态的所有权
所有权在Java中只是一个设计中的要素,在语言层面没有明显的变现。
所有权意味着控制权,如果发布了某个可变对象的引用,则意味着共享控制权。
在定义哪些变量构成对象的状态时,只考虑对象拥有的数据。
2.同步的实现
同步的实现手段主要有三个:
*对象封闭
*委托线程安全性给底层状态变量
*现有类的封装
2.1对象封闭
当一个对象封闭在另一个对象中时,由于被访问的对象所有代码路径都是已知的,因此更易于对代码进行分析。
被封闭的作用域可以是:
一个实例中:
作为一个私有成员
某个作用域中:
作为局部变量
线程里:
将对象从一个方法传递到另一个方法
通过封闭机制确保线程安全
@GuardedBy("this")privatefinalSetmySet=newHashSet();
publicsynchronizedvoidaddPerson(Personp){
mySet.add(p);
}
publicsynchronizedbooleancontainsPerson(Personp){
returnmySet.contains(p);
}
需要注意的是,在这个实现中,需要Person也是一个线程安全的类,否则一样会线程不安全。
也需要注意,不应该让一个本该封闭的对象逸出作用域。
Java中很多容器比如ArrayList和HashMap不是线程安全的类,但类库通过Collections.synchronizedList及类似方法产生一个容器的“装饰器”,使其访问变成了线程安全的。
2.1.1Java监视器模式
遵循Java监视器模式的对象会把对象的所有可变状态都封装起来,并由自己的内置锁保护。
使用监视器模式
publicfinalclassCounter{
@GuardedBy("this")privatelongvalue=0;
publicsynchronizedlonggetValue(){
returnvalue;
}
publicsynchronizedlongincrement(){
if(value==Long.MAX_VALUE)
thrownewIllegalStateException("counteroverflow");
return++value;
}
}
Counter中封装了一个变量value,对该变量的所有访问都需要通过Counter的方法执行,这些方法都是同步的。
Java监视器仅仅是一种编码约定。
私有私有锁保护状态
publicclassPrivateLock{
privatefinalObjectmyLock=newObject();
@GuardedBy("myLock")Widgetwidget;
voidsomeMethod(){
synchronized(myLock){
//Accessormodifythestateofwidget
}
}
}
实现过程中,使用私有锁而不是对象的内置锁可以避免让客户代码错误的得到锁。
2.2线程安全性委托
如果一个类是由多个独立且线程安全的状态变量组成,所有操作中都不会破坏已有的限制条件,那么可以将线程安全性委托给底层的状态变量去做。
如果某个类含有复合操作,即多个状态变量之间相互影响,仅仅委托给底层的状态变量去做就不足以保证线程安全性,需要在该类中加锁以保证线程安全性。
发布底层的状态变量如果一个状态变量是线程安全的,并且没有任何不变性条件约束他的值,也不存在任何不允许的状态转换,那么就可以安全的发布这个变量。
例如Counter,value需要是正值(约束条件)且必须递增(如果发布客户可以减少,存在不允许的状态转换),因此不能安全的发布Counter中的value状态。
publicclassVisualComponent{
privatefinalListkeyListeners
=newCopyOnWriteArrayList();
privatefinalListmouseListeners
=newCopyOnWriteArrayList();
publicvoidaddKeyListener(KeyListenerlistener){
keyListeners.add(listener);
}
publicvoidaddMouseListener(MouseListenerlistener){
mouseListeners.add(listener);
}
publicvoidremoveKeyListener(KeyListenerlistener){
keyListeners.remove(listener);
}
publicvoidremoveMouseListener(MouseListenerlistener){
mouseListeners.remove(listener);
}
}
将VisualComponent里面的keyListeners和mouseListeners发不出去是安全的。
因为没有在其监听器列表的合法状态上施加任何约束,这些域发布不会破坏线程安全。
2.3给现有的类中增加功能
给现有的类增加新的功能并保持线程安全性有两种方式。
客户端加锁
将加锁代码分布在多个类中,破坏同步策略的封装性。
组合
以给一个list增加putIfAbsent功能为例:
客户端加锁
classGoodListHelper{
publicListlist=Collections.synchronizedList(newArrayList());
publicbooleanputIfAbsent(Ex){
synchronized(list){
booleanabsent=!
list.contains(x);
if(absent)
list.add(x);
returnabsent;
}
}
}
组合方式
publicclassImprovedListimplementsList{
privatefinalListlist;
/**
*PRE:
listargumentisthread-safe.
*/
publicImprovedList(Listlist){this.list=list;}
publicsynchronizedbooleanputIfAbsent(Tx){
booleancontains=list.contains(x);
if(contains)
list.add(x);
return!
contains;
}
//PlainvanilladelegationforListmethods.
//MutativemethodsmustbesynchronizedtoensureatomicityofputIfAbsent.
...
}