Java并发编程实践

上传人:xian****hua 文档编号:133838937 上传时间:2022-08-11 格式:DOCX 页数:40 大小:612.15KB
收藏 版权申诉 举报 下载
Java并发编程实践_第1页
第1页 / 共40页
Java并发编程实践_第2页
第2页 / 共40页
Java并发编程实践_第3页
第3页 / 共40页
资源描述:

《Java并发编程实践》由会员分享,可在线阅读,更多相关《Java并发编程实践(40页珍藏版)》请在装配图网上搜索。

1、1 对象的共享关键字synchronized不仅能实现原子性还能确保当一个线程修改了对象状态后,另一个线程就可以看到对象状态的变化(内存可见性)1.1 可见性重排序:在缺乏足够同步的多线程程序中,代码的执行顺序不会按照程序员写好的顺序进行。这是因为Java内存模型允许编译器、CPU对操作的执行顺序进行调整。1.1.1 失效数据在多线程程序中,get方法、set方法都需要进行同步。这是因为get方法在获取变量时可能会获得一个失效的值,这个失效的值就是之前某个线程设置的。虽然已经失效但这个值曾经是正确的。1.1.2 非原子的64位操作Java内存模型要求,非volatile类型的64位数值变量(d

2、ouble和long),JVM允许将64位的读操作和写操作分解为两个32位的操作。那么在多线程的环境中,如果要读取非volatile类型的double、隆就有可能会读取到某个值的高32位和另一个值的低32位组成的一个数值。但目前各种平台的商用虚拟机几乎都把64位数据的读写操作作为原子操作来对待。1.1.3 加锁与可见性加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。同步代码块的锁就是方法调用所在的对象,静态synchronized方法以Class对象作为锁。synchronized可以用于实例变量

3、、对象引用、static方法、类名称字面常量。1. 在某个对象实例内,synchronized aMethod() 可以防止多个线程同时访问这个对象其他的synchronized方法(这个对象还有其他的synchronized方法,如果其中一个线程访问了其中一个synchronized方法,那么其他线程将不能访问此对象另外的synchronized方法)。但不同的对象实例间的synchronized方法是相互独立的,也就是说其他线程照样可以访问相同类的另一个对象实例的synchronized方法。2. synchronized static aMethod()对这个类所有的静态synchron

4、ized方法都会起作用,但不会对非静态的synchronized起作用。这是因为static方法属于类方法,他属于这个Class(注意:这里的Class不是指Class的某个具体对象),那么static方法所获取到的锁就是调用这个方法的对象所属的类,而非static方法获取到的锁就是当前调用这个方法的对象了。3. 除了在方法上用synchronized关键字外,也可以在方法内部的某个区块中用synchronized表示只对这个区块中的资源进行同步访问,例如synchronized(this)/*区块*/的作用域就是当前对象。1.1.3.1 非static方法运行结果是:chunk对象与chun

5、k1对象锁互不干扰。chunk19chunk1.1.3.2 对象引用结果与上面一样,也是chunk与chunk1互不干扰chunk19chunk1.1.3.3 static方法注意这段代码中的chunk与chunk1是互不干扰的,因为他们一个是static一个是非static方法。执行结果是:chunk19chunk这段代码中的chunk与chunk1就可以进行同步操作了。因为他们的锁是同一个锁,执行结果是:9chunkchunk11.1.3.4 类名称字面常量让所有这个类下面的对象都同步的时候,也就是让所有这个类下面的对象共用同一把锁的时候,我们可以在区块中锁定类常量的方式进行。例如:通过锁

6、定lock对象,我们可以实现对这个类所有对象的同步。切记synchronized锁定的是对象,对象不同就不会同步。例如:两个方法就不会同步一个锁定的是lock一个锁定的是类对象。1.1.4 volatile变量volatile是比synchronized更轻量级的同步机制。volatile变量有两种特性,一种是保证此变量对所有线程可见。但volatile变量对所有线程不是立即可见的,每次使用volatile变量时都会将变量值从主内存中取到线程内存里,所以volatile变量在各个线程中是一致的。但volatile变量的运算不能保证在并发的环境下仍然是安全的,这是因为Java里的运算不是原子操作

7、。volatile的使用情况如下:l 运算结果不依赖于变量的当前值,或者能够确保只有单一线程修改变量的值l 变量不需要与其他的状态变量共同参与不变形约束例如:当需要检查某个状态标记以判断是否退出循环的时候可以考虑采用volatile,加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。volatile的第二种特性是禁止指令重排序。Java内存模型对volatile变量的特殊规则如下:l 在线程工作内存中,每次使用volatile变量,都必须先从主内存刷新最新的值,用于保证能及时看见其他线程对volatile变量所做的修改l 每次修改volatile变量后都应立即同步

8、到主内存中,用于保证其他线程可以看到对volatile变量所做的修改l volatile修饰的变量需保证代码的执行顺序与程序的顺序相同1.1.5 synchronized与volatilel volatile的字面意思就是“易变的”,其意思就是告诉Java的内存模型存储在寄存器中存放的当前变量是不确定的,需要从主存中读取,修改完成后也要立即将变量写入到主存中去。而synchronized则是锁定的当前变量,只有当前线程才可以访问该变量,其他线程将被阻塞。l volatile只能由于变量级别,而synchronized却可以用于变量和方法级别l volatile仅能保证变量的修改可见性不能保证变

9、量的修改原子性,而synchronized既能保证变量的修改可见性也能保证变量的修改原子性l volatile不会造成线程堵塞,而synchronized可能会造成线程阻塞l volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化1.1.6 synchronized与Lock在代码层面上来说,synchronized类似面向对象,它可以修饰变量、方法、类、对象等。而Lock却像是面向过程,在需要的时候获取锁,结束的时候释放锁(一般是在finally中释放)。从性能上来说,在低并发的情况下synchronized要比Lock性能好,在高并发的情况下,Lo

10、ck要比synchronized的性能好很多。ReentrantLock拥有synchronized相同的并发性和语义,此外还多了锁投票、定时锁等待和中断锁等待。例如线程A和线程B都要获得对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定,如果使用synchronized,如果A不释放,B将一直等待下去无法中断。如果使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时候以后中断等待,而去干别的事情。synchronized是在JVM层面实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM将自动释放锁。Lock则不行,

11、因为Lock通过代码实现它必须在finally中释放锁。synchronized在生成线程转储时生成文件对开发人员找出死锁原因很有帮助,而Lock对于查找死锁原因将很困难,这个问题在Java6中已经得到解决,它提供了一个管理和调试的接口,锁可以通过这个接口进行注册,并通过其他管理和调试接口,从线程转储中得到ReentrantLock的加锁信息。Synchronized获取锁和释放锁这样的行为是成对出现的,而ReentrantLock却不是这样的,举例来说在一个链表结构中,可以将ReentrantLock加到链表结构的每个节点上来减少锁的粒度,从而允许不同的线程操作链表的不同部分。1.2 发布与

12、逸出发布对象:指的是使它能够被当前范围之外的代码所使用,例如将一个指向该对象的引用保存到其他代码可以访问的地方,或者在某一个非私有的方法中返回该引用,或者将引用传递到其他类的方法中。当发布某个对象时,可能会间接地发布其他对象,例如当发布HashMap对象时,HashMap对象中保存的元素同样也会被发布。一般说来,如果一个已经发布的对象能够通过非私有的变量引用和方法调用到达其他的对象,那么这些对象也都会被发布。引用逸出:对象逸出指的是一个对象尚未准备好时将它发布。在构造器中启动线程,会使尚未构造完成的对象发布出去,造成逸出。原则:为防止逸出,对象必须要被完全构造完后,才可以被发布(最好的解决办法

13、是采用同步)this逸出是指构造函数返回之前其他线程就持有该对象的引用,this逸出经常发生在构造函数中启动线程和注册监听器。如public class ThisEscapepublic ThisEscape()new Thread(new EscapeRunnable().start();private class EscapeRunnable implements Runnablepublic void run()/通过ThisEscape.this就可以引用外围类对象,但是此时外围类对象还没有构造完成,即发生了外围类的this引用的逸出。所以在构造函数中创建Thread对象后不要启动Th

14、read。可以提供一个start或者init方法负责启动线程。用工厂方法可以防止this引用逸出1.3 线程封闭线程封闭:只在单线程内访问数据,这种技术称为线程封闭当对象被封闭在一个线程中,自动成为线程安全的,即使被封闭的对象本身不是线程安全的。如JDBC连接池,Connection对象并不是线程安全的,但是大多数请求(Servlet请求或者EJB调用)都是由单个线程采用同步的方式来处理,因封闭而安全。1.3.1 Ad-hoc线程封闭(弱限制)维护线程封闭性的职责完全由程序来实现,这就是Ad-hoc线程封闭。如单线程+volatile=避免竞争+可见性=线程安全这是一种完全靠程序员实现的线程封

15、闭,是最糟糕的一种封闭,因此可以忽略它。1.3.2 栈封闭(强限制)只能通过局部变量才能访问对象,局部变量只存储在执行线程的栈中,其他线程无法访问这个栈,这样在线程内部使用非线程安全的对象仍然能保证线程安全。例如:如果在线程内部发布了局部变量(一个collection对象)的引用或者将该collection对象中的数据发布,那么封闭性将会被破坏,导致collection对象的逸出1.3.3 ThreadLocal类(强限制)ThreadLocal对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享。当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免在每

16、次执行时都重新分配该临时对象。就可以使用ThreadLocal。但是除非这个操作的执行频率非常高,或者分配操作的开销非常高,否则ThreadLocal不能带来性能的提升。在Java5.0中,Integer.toString()由之前的ThreadLocal对象保存12字节大小的缓冲区变成了每次调用时分配一个新的缓冲区,对于像临时缓冲区这样简单的对象,除非频繁操作否则ThreadLocal没有性能优势。概念上,可以讲ThreadLocal看成是map(Thread,T)对象,其中保存了特定于该线程的值,但事实并非如此,特定于线程的值保存在Thread对象中。每个线程都保持对其线程局部变量副本的隐

17、式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。1.4 不变性当满足以下条件时,对象才是不可变的:l 对象创建后其状态就不能再改变l 对象的所有域都是final类型l 对象是正确创建的(在对象的创建期间,this引用没有逸出)特别说明:不可变对象的域并未

18、全部声明为final类型。如String会惰性地(lazily)的计算哈希值:当第一次调用hashcode()时,String计算哈希值,并将它缓存在一个非final域中。 这个域有一个非默认的值,在每次计算中得得到相同的结果。1.4.1 Final域final类型的域是不能修改的,但如果final域引用的对象是可变的,那么这么被引用的对象是可以修改的,不变的是final域的引用。1.4.1.1 弱不变模式一个类的实例的状态是不可变化的,但是这个类的实例引用的实例可能具有变化的状态,这就是弱不变模式一个类满足以下条件就是弱不变模式:l 对象没有任何方法会修改对象的状态,当对象的构造函数对对象的

19、状态初始化之后,对象的状态便不再改变。 l 所有的属性都应当是私有的,以防客户端对象直接修改任何的内部状态。 l 这个对象所引用的对象如果是可变对象的话,必须设法限制外界对这个对象的访问,以防止对这些对象的修改。如果可能应该尽量在不变对象的构造器内利用clone或者copy方法将可变对象复制给不可变对象的属性。1.4.1.2 强不变模式一个类的实例的状态不会改变,同时它的子类的实例也具有不可变化的状态。这样的类符合强不变模式。要实现强不变模式,一个类必须首先满足弱不变模式所要求的所有条件,并且还要满足下面条件之一: l 所考虑的类所有的方法都应当是final,这样这个类的子类不能够置换掉此类的

20、方法。 l 这个类本身就是final的,那么这个类就不可能会有子类,从而也就不可能有被子类修改的问题1.4.2 示例:使用Volatile类型来发布不可变对象如果访问和更新多个相关变量出现竟态条件,那么可以通过将这些变量全部保存在一个不可变对象总来消除,然后使用volatile类型来确保不可变对象的可见性,这样可以在不显示使用锁的情况下保证线程安全性。1.5 安全发布1.5.1 不正确的发布:正确的对象被破坏未被正确发布的对象存在一个问题就是除了发布对象的线程外,其他线程可能看到的是一个无效值。1.5.2 不可变对象与初始化安全性如果final类型的域所指向的是可变对象,那么在访问这些域所指向

21、的对象的状态时仍然需要同步1.5.3 安全发布的常用模式要安全的发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全的发布:l 在静态初始化函数中或者静态初始化器中初始化一个对象引用,例如:public static Holder holder = new Holder(42);l 将对象的引用保存到volatile类型的域或者AtomicReferance对象中。l 将对象的引用保存到某个正确构造对象的final类型域中l 将对象的引用保存到一个由锁保护的域中,例如将对象放置到Vectory、Hashtable,synchronizedMap

22、,ConcurrentMap等。1.5.4 事实不可变对象是指一个对象在技术上是可变的,但其状态在发布后不会被修改。这种对象称为“事实不可变对象”,事实不可变对象不需要满足不可变对象的条件,但其需要被安全的发布,不可变对象不需要被安全发布。例如将事实不可变对象Date,存储到synchronizedMap中可以确保Date对象被安全的发布1.5.5 可变对象对象的发布需求取决于它的可见性:l 不可变对象可以通过任意机制来发布l 事实不可变对象必须通过安全方式来发布l 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来1.5.6 安全的共享对象在并发程序总使用和共享对象时,

23、有以下策略:l 线程封闭:线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。l 只读共享:在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它,共享的只读对象包括不可变对象和事实不可变对象l 线程安全共享:线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步l 保护对象:被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。2 对象的组合2.1 设计线程安全的类封装能保护线程的安全性在设计线程安全类的过程中,需要包含以下

24、三个基本要素l 找出构成对象状态的所有变量l 找出约束状态变量的不变性条件l 建立对象状态的并发访问管理策略2.1.1 收集同步需求要确保类的线程安全性,就需要确保它的不变性条件不会在并发访问的情况下被破坏。如果在类中包含同时约束多个状态变量的不变性条件,那么在访问这些相关变量时要持有保护这些变量的锁不可变约束:判断某个状态是合法的还是非法的(不变式表达了对状态的约束,这些状态是应该符合这个约束的值的组合。不变式可以代表某种业务规则。)后验条件:会指出某种状态的转换是非法的(针对方法,规定了在调用方法之后必须为真的条件)2.1.2 依赖状态的操作先验条件:针对方法,规定了在调用方法之前必须为真

25、的条件要想实现某个等待先验条件为真时才执行的操作,一种更简单的方法是通过现有库中的类(例如阻塞队列或者信号量)某些对象有基于先验条件,若一个操作基于状态的先验条件,则它被称为是状态依赖的类2.1.3 状态的所有权如果以某个对象为根节点构造一张对象图,那么该对象的状态将是对象图中所有对象包含的域的一个子集。2.2 实例封闭将数据封装在对象内部,把对数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。Java集合类中有一组工厂方法,可以通过装饰器模式将线程不安全的集合变为线程安全的集合,例如ArrayList和HashMap是线程不安全的,但Collections.syn

26、chronizedList及类似方法可以将线程不安全的集合变为线程安全的集合。2.2.1 Java监视器模式Java监视器模式:符合监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护使用私有锁对象而不是对象的内置锁,可以使客户代码无法得到锁,这样就可以将锁封装起来。public class PrivateLockprivate final Object myLock = new Object();void someMethod()synchronized(myLock)/doSomething2.2.2 示例:车辆追踪一个符合监视器模式的对象可以利用synchroni

27、zed方法将对线程不安全的对象的操作封装在自己内部,这样就变成线程安全的了。2.3 线程安全性的委托一般情况下,线程安全的组件组合起来也是线程安全的。可以将线程安全性委托给多个状态变量,只要这些变量是彼此独立的。如果状态变量间有额外的限制,那么就需要保证组合操作时原子性的。如果一个状态变量是安全的,且没有任何不变约束限制他,那么可以被安全的发布。2.4 在现有的线程安全类中添加功能可以扩展现有线程安全类,并添加新操作2.4.1 客户端加锁客户端加锁是指,对于使用某个对象X的客户端代码,使用X本身用于保护其状态的锁来保护客户代码有时没法扩展,如synchronizedList方法后的集合,这样的

28、情况下要用Helper类,但这里就需要客户端代码中获取线程安全类的锁来加锁public class ListHeperpublic List list = Collections.synchronizedList(new ArrayList();public Boolean putIfAbsend(E x)synchronized(list)2.4.2 组合可以通过组合现有的类(装饰器模式)来组合成一个线程安全的类public class ImprovedList implements Listprivate final List list;public ImprovedList(List l

29、ist) this.list = list;public synchronized Boolean putIfAbsend(T x)3 基础构建模块3.1 同步容器类JDK中同步容器类实现线程安全的方式是:将他们的状态封装起来,并对每个公有方法都进行同步,使得每次只有一个线程能访问容器的状态3.1.1 同步容器类的问题虽然容器是线程安全的,但对于复合操作(迭代、条件运算、跳转根据指定顺序找到当前元素的下一个元素),就需要进行客户端加锁。否则在多线程执行时会发生异常。public static Object getLast(Vector list)synchronized(list)int la

30、stIndex = list.size() 1;return list.get(lastIndex);3.1.2 迭代器与ConcurrentModificationException容器在迭代时被修改,就会抛出ConcurrentModificationException,要想避免抛出ConcurrentModificationException,一种方法是在迭代期间对容器加锁,另一种方法是“克隆”容器,在副本上进行迭代。3.1.3 隐藏迭代器容器的toString、hashCode、equals、containsAll、removeAll等方法都会间接的导致迭代操作。封装对象的状态有助于维

31、持不变形条件,那么封装对象的同步机制同样有助于确保实施同步策略。3.2 并发容器JDK目前新增了ConcurrentMap(对复合操作支持)、Queue(保存一组待处理的数据)、BlockingQueue(扩展自Queue)容器对应的同步容器HashMapConcurrentHashMapListCopyOnWriteArrayListTreeMapConcurrentSkipListMapTreeSetConcurrentSkipListSetQueueConcurrentLinkedQueue、PriorityQueue(非并发)3.2.1 ConcurrentHashMapConcurr

32、entHashMap并不是将每个方法都在同一个锁上同步并使得每次只能有一个线程访问容器,而是使用分段锁机制,在这种机制中,任意数量的读取线程可以并发的访问Map,执行读取操作的线程和执行写入操作的线程可以并发地访问Map,并且一定数量的写入线程可以并发的修改Map。ConcurrentHashMap提供的迭代器不会抛出ConcurrentModificationException,因此不必在迭代过程中加锁。他的迭代器具有弱一致性,可以容忍并发的修改。当创建迭代器时会遍历已有的元素,并可以感应到迭代器的修改。注意:l 对整个map的操作,如size和isEmpty方法被弱化,其值被弱化,可能得到

33、的值已经过期或者不准确。l 此Map没有实现对Map加锁以提供独占访问3.2.2 额外的原子Map操作当有“若没有则添加”,“若相等则移除”这样的复合操作时就应该考虑使用ConcurrentMap了3.2.3 CopyOnWriteArrayList只要一个事实不可变的对象被正确的发布,那么访问他就不需要额外的同步,在每次修改时,都会创建一个新的容器副本,他提供了更好的并发性,并避免了迭代期对容器加锁和复制返回的迭代器不会抛出ConcurrentModificationException,并且返回的元素与迭代器创建时的元素完全一致,而不必考虑之后修改操作带来的影响。3.3 阻塞队列和生产者-消

34、费者模式阻塞队列提供了put和take方法,分别在队列满和空的时候阻塞。阻塞队列的实现:l 先进先出:LinkedBlockingQueue、ArrayBlockingQueuel 优先级排序队列:PriorityBlockingQueuel 同步队列:SynchronousQueue,没有存储能力,此队列除非消费者充足,已经为下个操作做好准备,不然put和take方法会一直阻塞(仅当有足够多的消费者,并且总是有一个消费者准备好获取交付的工作时,才适合使用同步队列)生产者和消费者最好并行度相同,否则会将二者的并行度降为二者中最小的并行度。在多线程的环境中,通过队列可以很容易的实现数据共享,比如

35、经典的“生产者”和“消费者”模型中,通过队列可以很便利的实现两者之间的数据共享。假如我们有若干生产者线程,又有若干消费者线程。如果生产者线程需要把准备好的数据共享给消费者线程,利用队列的方式来传递线程,就可以很方便的解决他们之间数据共享的问题。但如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况,该怎么办呢?理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么生产者必须暂停等待一下(阻塞生产者进程),以便等待消费者把累积的数据处理完毕,反之亦然。在concurrent包发布之前,在多线程环境下,我们程序员都必须自己去控制这些细

36、节,尤其是还要兼顾效率和线程安全,这会给程序带来很大的复杂性。有了BlockingQueue后我们再也不用考虑这些细节了,我们不用考虑什么时候阻塞线程,什么时候唤醒线程了。3.3.1 BlockingQueue对象3.3.1.1 ArrayBlockingQueue基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。ArrayBlockingQueue在生产者放入数据和消费者消费数据时,

37、都是共用同一个锁对象,这也意味着两者无法并行运行,这点是与LinkedBlockingQueue不同的;按照实现原理分析来看,ArrayBlockingQueue完全可以采用分离锁,从而实现生产者和消费者操作的并行运行,但JDK并没有这样做,其原因是ArrayBlockingQueue的数据写入和获取操作已经很轻巧了,以至于引入分离锁除了给代码带来额外的复杂性外,在性能上完全不能占到任何便宜。ArrayBlockingQueue和LinkedBlockingQueue还有一个明显的区别是,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者会产生一个额外的Node对象,这在长时间内需

38、要高效并发处理大数据量的系统中,对于GC的影响还是很大的。而在创建ArrayBlockingQueue时,我们还可以控制对象的内部锁是否采用公平锁,默认采用非公平锁。公平锁就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取出执行。非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。3.3.1.2 LinkedBlockingQueue基于链表的阻塞队列,同ArrayBlockingQueue类似,其内部也维持着一个数据缓冲队列(

39、该队列由一个链表组成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大缓存数量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端也是同样的道理。而LinkedBlockingQueue之所以能够高效的并发处理数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行的操作队列中的数据,这样就能提高整个队列的并发性能。作为开发者,我们需要注意的是,如果构造

40、一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。FixedThreadPool内部使用的就是LinkedBlockingQueue队列。3.3.1.3 SynchronousQueue一种无缓冲的等待队列,类似于无中介的直接交易,有点像原始社会的生产者和消费者,生产者拿着物品直接去市场销售给最终得消费者,而消费者也得直接在市场上寻找所需商品的直接生产者。如果任

41、何一方没有找到合适的目标,那么只能等待。相对于有缓冲的BlockingQueue来说,少了一个中间经销商环节(缓冲区),如果有经销商,生产者将产品直接给经销商,而无需在意经销商会将这些产品卖给哪些消费者。由于经销商可以库存一部分商品,因此相对于直接交易模式,总体来说采用中间经销商的模式会吞吐量高一些(可以批量买卖);但另一方面,又因为经销商的引入,使得产品从生产者到消费者中间增加了额外的交易环节,单个产品的及时响应性能可能会降低。声明一个SynchronousQueue有两种不同的方式,它们之间有着不太一样的行为。公平模式和非公平模式的区别:如果采用公平模式:SynchronousQueue会

42、采用公平锁,并配合一个FIFO队列来阻塞多余的生产者和消费者,从而体系整体的公平策略;但如果是非公平模式(SynchronousQueue默认):SynchronousQueue采用非公平锁,同时配合一个LIFO队列来管理多余的生产者和消费者,而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。CacheThreadPool中的队列使用的是SynchronousQueue队列。3.3.2 串行线程封闭在线程间移交可变对象时,通过连续的线程限制可以保证其他线程只有唯一一个可以接收到对象。3.3.3 双端队列与工作密取Ja

43、va6新增了两种容器类型,Deque和BlockingDeque,实现了在队列头和队列尾部进行购高效的插入和删除操作双端队列适用于工作密取工作模式(与生产者-消费者模式不同)。每个消费者有自己独立的双端队列,当一个消费者的双端队列为空时,它会在另一个线程的队列队尾查找新的任务,从而确保每个线程都保持忙碌状态。3.4 阻塞方法和中断方法线程被阻塞的原因有多种:等待IO操作结束,等待获得一个锁,等待从Thread.sleep方法中醒来,或是等待另一个线程的计算结果。阻塞线程必须等待某个不受他控制的事件发生后才能继续执行,例如等待IO操作结束,等待某个锁变成可用。如果一个方法可以抛出Interrup

44、tedException,那么证明此方法是可以阻塞的,也同时标明如果阻塞中遭遇中断事件,那么阻塞可以提前结束。当在代码中调用了一个阻塞方法后,咱们自己的方法也就变成了阻塞方法。当线程A中断B时,是要求B在达到某个方便的时间点时,停止真正做的事件,前提是B愿意停下来处理InterruptedException异常:l 传递异常:直接抛出或者先捕获在抛出l 恢复中断:捕获并调用interruptl 特殊情况:对Thread进行扩展3.5 同步工具类同步工具类都包含一些特定的结构化属性:他们封装了一些状态,这些状态将决定执行同步工具类的线程是继续执行还是等待,此外还提供了一些方法对状态进行操作,以及

45、另一些方法用于高效的等待同步工具类进入到预期状态。3.5.1 闭锁闭锁的工作就像一道门,在门关闭的时候没有线程可以通过,当闭锁到达结束状态后,门就会打开并永远保持打开状态,此时线程才可以通过。CountDownLatch是一个计数器的闭锁实现,只不过这个计数器的操作是原子的,同时只能有一个线程去操作此计数器,通过countDown方法对计数器减小,而await方法则一直阻塞直到计数器变为0或者线程中断或超时3.5.2 FutureTask同样是闭锁,描述了一个抽象意义的可携带结果的计算,计算是通过Callable实现的,相当于一个可生成结果的Runnable,并且可以处于以下三种状态:等待运行

46、、正在运行、运行完成(正常完成、因取消而结束、因异常而结束)。一旦进入完成状态将会永远停留在这个状态上。Future有一个get方法,如果任务完成则返回结果,否则将会阻塞直到任务完成或者抛出异常。Callable表示的任务可以抛出受检查的或未受检查的异常,并且也可以抛出Error,这些异常都会被封装到ExecutionException中。这就使得调用get后的异常处理代码比较复杂。注意:如果在主线程需要执行比较耗时的操作时,但又不想阻塞主线程时,可以将这些操作交给Future在后台完成,在主线程将来需要时,就可以通过Future对象获得后台执行的结果和状态。3.5.3 信号量计数信号量用来控

47、制同事访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。Semaphore管理一组虚拟的许可,许可的数量通过构造器来指定。通过acquire获得许可,如有没有许可,其将一直阻塞到有许可位置,release方法释放许可。Semaphore可以用于实现资源池及阻塞容器3.5.4 栅栏栅栏类似于闭锁,栅栏与闭锁的关键区别在于,所有线程必须同时到达栅栏位置,才能继续执行,闭锁用于等待事件,栅栏用于等待其他线程。CyclicBarrier可以使一定数量的参与方反复地在栅栏位置汇集,当线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都到达栅栏位置。如果对await方法调用超时,

48、或者await阻塞的线程被中断,那么栅栏就被认为是打破了,所有阻塞的await调用都将终止并抛出BrokenBarrierException。成功的通过栅栏await方法会为每个线程返回一个唯一的到达索引号,可以方便下次特殊处理。也可以在构造函数的Runnable参数传递一个在所有线程到达通过栅栏的执行方法。Exchanger也是栅栏,是一种两方栅栏,在栅栏点会交换数据,当两个线程通过Exchanger交换对象时,这种交换就把这两个对象安全地发布给另一方4 任务执行4.1 在线程中执行任务任务:任务通常是一些抽象且离散的工作单元4.1.1 串行地执行任务当任务数量少且执行时间很长可以使用单线程

49、串行处理机制4.1.2 显式地为任务创建线程在正常负载情况下,为每个任务分配一个线程可以提升串行的执行性能。4.1.3 无限制创建线程的不足线程生命周期的开销非常高:创建线程需要资源资源消耗:活跃的线程会消耗系统资源,尤其是内存;大量线程在竞争CPU资源时还将产生其他的性能开销,建议可运行的线程的数量不应多于CPU数量稳定性:JVM的启动参数、Thread构造函数中请求栈的大小,以及底层操作系统对栈地址空间的限制都会对线程的稳定性造成影响。破坏这些限制后有可能造成OutOfMemoryError。4.2 Executor框架4.2.1 Executor简介Executor将任务的提交与执行解耦

50、,用Runnable来表示任务。Executor基于生产者-消费者模式,提交任务的操作相当于生产者,执行任务的操作相当于消费者Executor将为我们管理Thread对象,如果再程序中看到new Thread(runnable).start()。那么就可以考虑用Executor来代替会更加灵活1. Executor定义了最基本的void execute(Runnable command)方法,用于接收用户提交的任务2. ExecutorService定义了线程池终止和创建及提交FutureTask任务支持的方法。3. AbstractExecutorService 是抽象类,主要实现了Exec

51、utorService和FatureTask的相关的任务创建和提交的方法。4. ThreadPoolExecutor是最核心的一个类,是线程池的内部实现,线程池的功能基本上都在这个类中有实现。5. SheduledThreadPoolExecutor在ThreadPoolExecutor的基础上增加了定时调度的功能,任务可以在一定延时时间后被执行。4.2.2 线程池线程池,是指管理一组同构工作线程的资源池,线程池与工作队列绑定,在工作队列中保存了所有等待执行的任务,工作线程就会从工作队列中获取执行任务,执行完毕后返回线程池等待执行下一个任务。线程池好处:减少创建线程的开销,从而提高了响应性。只

52、配置线程池可能仍然无法避免内存耗尽,仍然需要对工作队列的长度进行限制。ExecutorService继承了Executor,并提供了生命周期的支持(运行、关闭、终止),Executors创建的线程池都实现了ExecutorService。4.2.2.1 ThreadPoolExecutor原理4.2.2.1.1 线程池本身的状态4.2.2.1.2 等待任务队列和工作集4.2.2.1.3 线程池的状态锁线程池内部的状态变化,如改变线程池大小,都需要该锁4.2.2.1.4 线程的存活时间和大小4.2.2.1.5 拒绝策略和线程工厂4.2.2.1.6 线程池完成任务数4.2.2.2 ThreadPo

53、olExecutor内部工作原理ThreadPoolExecutor的内部工作原理总结起来是5句话:1. 如果当前池大小poolSize小于corePoolSize,则创建新线程执行任务2. 如果当前池大小poolSize大于corePoolSize,且等待队列未满,则进入等待队列3. 如果当前池大小poolSize大于corePoolSize且小于maximumPoolSize,且等待队列已满,则创建信线程执行任务4. 如果当前池大小poolSize大于corePoolSize且大于maximumPoolSize,且等待队列已满,则调用拒绝策略来处理该任务5. 线程池里的每个线程执行完任务后

54、不会立即退出,而是会去检查等待队列里是否还有线程任务需要执行,如果再keepAliveTime时间内等不到新的任务了,那么线程就会退出。线程池中最重要的方法是由Executor接口定义的execute方法,是任务提交的入口。上面代码比较复杂,我们改写一下,再解释运行原理:execute()方法中,调用了三个私有方法l addIfUnderCorePoolSize():在线程池大小小于核心线程池大小的情况下,扩展线程池l addIfUnderMaximumPoolSize():在线程池大小小于线程池大小上限的情况下,扩展线程池l ensureQueuedTaskHandled():保证在线程池关

55、闭的情况下,新加入队列的线程也能正确处理注意:CacheThreadPool和FixedThreadPool的逻辑实现都是在ThreadPoolExecutor中实现的,他们的区别就是属性corePoolSize以及workQueue的不同。command线程运行的整个逻辑在addIfUnderCorePoolSize方法中实现,请看此方法:ensureQueuedTaskHandled()方法的作用是,当线程池处于非运行状态(非RUNNING状态),刚加入缓存队列的任务,能被正常移除或调度。4.2.2.2.1 线程复用线程池里的每个线程在执行完任务后不会立即退出,而是取检查下等待队列里是否还

56、有线程任务需要执行,如果再keepAliveTime里等不到新的任务,那么线程才会退出,这个功能实现的关键在于Worker,线程池在执行Runnable任务的时候,并不单纯的把Runnable任务创建成一个Thread,而是会把Runnable封装成Worker任务。Worker里面包装了firstTask属性,在构造worker的时候传进来的那个Runnable任务就是firstTask。同时也实现了Runnable接口,所以是个代理模式,看下面方法Worker的run方法是一个循环,第一次循环运行的必然是firstTask,在运行完firstTask之后,并不会立刻结束,而是会调用getT

57、ask获取新的任务(getTask从等待队列里获取新的任务),如果再keepAliveTime时间内得到新的任务则继续执行,如果不能得到新的任务则选择退出。这样就保证了多个任务可以复用一个线程,而不是每次都创建信的线程。4.2.2.2.2 拒绝策略RejectedExecutionHandler是拒绝的策略。常见有以下几种:l AbortPolicy:不执行,会抛出RejectedExecutionException异常。l CallerRunsPolicy:由调用者(调用线程池的主线程)执行。l DiscardOldestPolicy:抛弃等待队列中最老的。l DiscardPolicy:不

58、做任何处理,即抛弃当前任务。4.2.2.2.3 创建线程池Executors类,提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。1. public static ExecutorService newFixedThreadPool(int nThreads)创建固定数目线程的线程池。以共享的无界队列方式来运行这些线程。在任意点,大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任

59、务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。2. public static ExecutorService newCachedThreadPool()创建一个可缓存可变长的线程池(根据系统需要创建线程),调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。3. public static ExecutorService newSingleThreadExecutor()创建一个单线程化的Executor。4. public static ScheduledEx

60、ecutorService newScheduledThreadPool(int corePoolSize)创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。4.2.2.2.4 shutdown和shutdownNowshutdown()新的任务不会再被提交到线程池,但之前的都会依旧执行,通过中断方式停止空闲的(根据没有获取锁来确定)线程。shutdownNow()则向所有正在执行的线程发出中断信号以尝试终止线程,并将工作队列中的任务以列表方式的结果返回。两者区别:l 是一个要将线程池推到SHUTDOWN状态,一个将推到STOP状态l 并且对运行的线程处理方式不同,

61、shutdown()只中断空闲线程,而shutdownNow()会尝试中断所有活动线程l 还有就是对队列中的任务处理,shutdown()队列中已有任务会继续执行,而shutdownNow()会直接取出不被执行相同的是都在最后尝试将线程池推到TERMINATED状态4.2.2.2.5 submit方法和execute方法1. 两者执行任务最后都会通过Executor的execute方法来执行2. 二者接收的参数不一样3. submit有返回值,返回结果包含在Future中,而execute方法没有返回值4. 如果在线程中会抛出异常,而我们又希望外面的调用者感受到这些异常,那么就得使用submi

62、t方法,通过Future.get方法获取异常。submit会将runnable包装成FutureTask,其run方法会捕捉被包装的Runnable Object的run方法抛出的异常,待submit方法所返回的的Future Object调用get方法时,将执行任务时捕获的异常包装成来抛出。5. 而对于execute方法,则会直接抛出异常,该异常不能被捕获,想要在出现异常时做些处理,可以实现Thread.UncaughtExceptionHandler接口4.3 找出可利用的并行性简单来说Executor是Runnable和Callable的调度容器,Future包含具体任务的异步执行结果。

63、Future可以查看任务是否完成,也可以阻塞在get方法上一直等待结果返回。Runnable与Callable的区别就是Runnable的run方法不能有返回值,就算是通过Future也看不到返回结果。Executor的执行任务有4个生命周期:创建、提交、开始和完成。对于已经提交未开始的任务可以取消,对于开始的任务只能中断。Executor接口执行已提交的任务的对象。此接口提供一种将任务提交与每个任务将如何运行的机制(包括线程使用的细节、调度等)分离开来的方法。通常使用Executor 而不是显式地创建线程。Executor在执行时使用内部的线程池完成操作。CompletionService结合了Executor和BlockingQueue,可以利用Callable提交任务用take或者poll获取结果,好像打包的Future。CompletionService。与ExecutorService最主要的区别在于提交的任务不一定是按照加入时的顺序完成的。CompletionService对Executo

展开阅读全文
温馨提示:
1: 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
2: 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
3.本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
5. 装配图网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
关于我们 - 网站声明 - 网站地图 - 资源地图 - 友情链接 - 网站客服 - 联系我们

copyright@ 2023-2025  zhuangpeitu.com 装配图网版权所有   联系电话:18123376007

备案号:ICP2024067431-1 川公网安备51140202000466号


本站为文档C2C交易模式,即用户上传的文档直接被用户下载,本站只是中间服务平台,本站所有文档下载所得的收益归上传人(含作者)所有。装配图网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。若文档所含内容侵犯了您的版权或隐私,请立即通知装配图网,我们立即给予删除!