java多线程

来源:互联网 发布:程序员的本质pdf网盘 编辑:程序博客网 时间:2024/06/05 10:25

明天去面试华为了,个人真的灰常喜欢华为这个公司啊,所以全力以赴!期待还有紧张,好好准备,加油加油!总结了一波java多线程。希望对大家有帮助。

Java多线程

线程和进程对比:

进程(包括:程序段、相关数据段、进程控制块)是一次程序的运行过程,是系统进行资源分配和调度的一个独立单位

 

线程是进程运行和执行的最小调度单位

一个进程可以包含多个线程

创建、撤销、切换开销大,资源需要重新分配和回收

线程之间可以资源共享,线程之间只有独立的栈、本地方法栈以及PC寄存器,所以切换开销小

进程之间相对独立,彼此不会相互影响

线程共享进程的资源可以相互通信和相互影响

守护线程:可以简单理解为后台运行的线程,进程结束,守护线程自然而然地就会结束不需要手动地去关心和通知其状态。JVM的垃圾回收器就是一个守护线程

线程组:ThreadGroup,可以统一设定线程组的一些属性,比如setDeamon(设置守护线程)。

当前线程副本:ThreadLocal,为每个使用该变量的线程提供独立的变量副本,每个线程都可以独立修改自己的副本而不会影响其他线程所对应的副本。

Java内存管理模型(JMM):JMM规定了jvm有主内存和工作内存,主内存其实就是我们常说的java堆内存,存放程序中所有的类实例,静态数据等变量,是多个线程共享的。工作内存存放的是该线程从主内存中拷贝过来的变量以及访问方法所取得的局部变量,是每个线程私有的其他线程不能访问,多个线程之间不能直接互相传递数据通信,只能通过共享变量来进行。

当线程操作某个对象时,执行顺序如下:

从主存复制变量到当前工作内存

执行代码,改变共享变量值

用工作内存数据刷新主存相关内容

线程安全、线程不安全:当多个线程同时操作一个数据结构的时候产生了相互修改和串行的情况,没有保证数据的一致性,我们通常称这种设计的代码为“线程不安全的”

实现线程安全的方式:

synchronized关键字

把代码块声明为synchronized,有两个重要后果,通常是指该代码具有原子性(atomicity)和可见性(visibility)。原子性意味着一个线程一次只能执行由一个指定监控对象(lock)保护的代码,从而防止多个线程在更新共享状态时相互冲突。可见性则更为微妙;它要对付内存缓存和编译器优化的各种反常行为。一般来说,线程以某种不必让其他线程立即可以看到的方式(不管这些线程在寄存器中、在处理器特定的缓存中,还是通过指令重排或者其他编译器优化),不受缓存变量值的约束,但是如果开发人员使用了同步,如下面的代码所示,那么运行库将确保某一线程对变量所做的更新先于对现有synchronized块所进行的更新,当进入由同一监控器(lock)保护的另一个synchronized块时,将立刻可以看到这些对变量所做的更新。类似的规则也存在于volatile变量上。

使用synchronized修饰方法或者代码块,Java中的每个对象都有一个锁,当一个线程访问某个对象的synchronized方法时,将该对象上锁,其他任何线程都无法再去访问该对象的synchronized方法了。直到之前的那个线程执行方法完毕后(或者是抛出了异常),才将该对象的锁释放掉,其他线程才有可能再去访问该对象的synchronized方法。

synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
1.修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2.修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3.修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4.修饰一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

显示锁Lock和ReentrantLock

参考链接:https://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html

Lock所有加锁和解锁的方式都是显示的。ReentrantLock是Lock的实现类,是一个互斥的同步器,它具有扩展的能力。ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,但是添加了类似轮询锁、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM可以花更少的时候来调度线程,把更多时间用在执行线程上,大大提高系统的吞吐量)ReentrantLock有一个主要的缺点就是它可能忘记锁定,在工作中对方法加锁使用频率最高。synchronized仍然有一些优势。比如,在使用synchronized的时候,不可能忘记释放锁;在退出synchronized块时,JVM会为您做这件事。您很容易忘记用finally块释放锁,这对程序非常有害。您的程序能够通过测试,但会在实际工作中出现死锁,那时会很难指出原因(这也是为什么根本不让初级开发人员使用Lock的一个好理由。)

另一个原因是因为,当JVM用synchronized管理锁定请求和释放时,JVM在生成线程转储时能够包括锁定信息。这些对调试非常有价值,因为它们能标识死锁或者其他异常行为的来源。Lock类只是普通的类,JVM不知道具体哪个线程拥有Lock对象。而且,几乎每个开发人员都熟悉synchronized,它可以在JVM的所有版本中工作。在JDK5.0成为标准(从现在开始可能需要两年)之前,使用Lock类将意味着要利用的特性不是每个JVM都有的,而且不是每个开发人员都熟悉的。

Lock与Synchronized的比较:

Lock使用起来比较灵活,但是必须有释放锁的动作配合

Lock必须手动释放和开启锁,而Synchronized不需要手动释放和开启锁

Lock只适用与代码块锁,而synchronized对象之间是互斥关系

 

volatile关键字

参考链接:http://blog.csdn.net/victor_cindy1/article/details/44310195

原子性:即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

有序性:即程序执行的顺序按照代码的先后顺序执行。

下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

 

1、volatile关键字为域变量的访问提供了一种免锁机制
2、使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,
3、因此每次使用该域要重新计算,而不是使用寄存器中的值
4、volatile不会提供任何原子操作,它也不能用来修饰final类型的变量

volatile关键字的两层语义

保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

禁止进行指令重排序。

前面讲述了源于volatile关键字的一些使用,下面我们来探讨一下volatile到底如何保证可见性和禁止指令重排序的。
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

 

显示锁ReadWriteLock和ReentrantReadWriteLock

ReadWriteLock是一个接口,提供了ReadLock和WriteLock两种锁的操作机制,也就是一个资源能够内多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程。一般使用的场合是一个资源被大量读取操作,只有少量的写操作。包括的路径是java.util.concurrent.locks.ReadWriteLock.不是Lockd的子类,借助Lock来实现读写两个锁并存、互斥的操作。ReentrantReadWriteLock是ReadWriteLock里面唯一的实现类,主要应用场景是多个线程从某个数据结构中读取数据很少有线程修改其中的数据。ReentrantReadWriteLock可以实现更加复杂的锁机制,并发性也高一些。

总结读写锁的特点:

1、         读-读不互斥

2、         读-写互斥

3、         写-写互斥

显示锁StampedLock

首先介绍几个概念:

悲观锁:

假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作

读取悲观锁:

在读取之前一定要判断一下,数据有没有正在被更改

乐观锁:

假定不会发生并发冲突,只在提交操作时检擦是否违反数据完整性

读取乐观锁:

在读取之前就不需要判断数据的一致性,只管读自己的就可以了

StampedLock也是利用Lock机制,再加上stamp作为锁的标志状态,实现锁与锁之间的悲观和乐观。

StampedLock一个基于容量并且带有三种模式的锁,用于控制读取/写入访问。StampedLock的状态由版本和模式组成。锁获取操作返回一个用于展示和访问锁状态的邮编(stamp)变量:这些方法的"try"版本通过返回0代表获取锁失败。锁释放以及其他相关方法需要使用邮编(stamps)变量作为参数,如果他们和当前锁状态不符则失败,这三种模式为:

 

写入:方法writeLock可能为了获取独占访问而阻塞当前线程,返回一个邮编变量,能够在unlockWrite方法中使用从而释放锁。限时和立即版本的tryWriteLock也提供了支持。当锁被写模式所占有,没有读或者乐观的读操作能够成功。

读取:方法readLock可能为了获取非独占访问而阻塞当前线程,返回一个邮编变量,能够在unlockRead方法中用于释放锁。限时和立即版本的tryReadLock也提供了支持。

乐观读取:是方法tryOptimisticRead返回一个非0邮编变量,仅在当前锁没有以写入模式被持有。方法validate返回true如果在获得邮编变量(stamp)之后没有被写模式持有。这种模式可以被看做一种弱版本的读锁,可以被一个写入者在任何时间打断。乐观读取模式仅用于短时间读取操作时经常能够降低竞争和提高吞吐量。当然,它的使用在本质上是脆弱的。乐观读取的区域应该只包括字段,并且在validation之后用局部变量持有它们从而在后续使用。乐观模式下读取的字段值很可能是非常不一致的,所以它应该只用于那些你熟悉如何展示数据,从而你可以不断检查一致性和调用方法validate。比如,当我们第一次读取对象或者数组引用,然后检查它的字段、元素以及方法时。

局部变量

如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,

副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。

ThreadLocal类的常用方法

ThreadLocal():创建一个线程本地变量

get():返回此线程局部变量的当前线程副本中的值

initialValue():返回此线程局部变量的当前线程的"初始值"

set(Tvalue):将此线程局部变量的当前线程副本中的值设置为value

用原子变量实现线程同步

需要使用线程同步的根本原因在于对普通变量的操作不是原子的。


那么什么是原子操作呢?
原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作
即-这几种行为要么同时完成,要么都不完成。

在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,
使用该类可以简化线程同步。

其中AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),
但不能用于替换Integer;可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。

AtomicInteger类常用方法:
AtomicInteger(int initialValue) : 创建具有给定初始值的新的AtomicInteger
addAddGet(int dalta) : 以原子方式将给定值与当前值相加
get() : 获取当前值

 

死锁

参考链接:http://blog.csdn.net/abigale1011/article/details/6450845/

所谓死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

四个必要条件

1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。

2)不可剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

3)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。

4)循环等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

死锁的预防

前面介绍了死锁发生时的四个必要条件,只要破坏这四个必要条件中的任意一个条件,死锁就不会发生。这就为我们解决死锁问题提供了可能。一般地,解决死锁的方法分为死锁的预防,避免,检测与恢复三种(注意:死锁的检测与恢复是一个方法)。我们将在下面分别加以介绍。

死锁的预防是保证系统不进入死锁状态的一种策略。它的基本思想是要求进程申请资源时遵循某种协议,从而打破产生死锁的四个必要条件中的一个或几个,保证系统不会进入死锁状态。

〈1〉打破互斥条件。即允许进程同时访问某些资源。但是,有的资源是不允许被同时访问的,像打印机等等,这是由资源本身的属性所决定的。所以,这种办法并无实用价值。

〈2〉打破不可剥夺条件。就是说,当一个进程已占有了某些资源,它又申请新的资源,但不能立即被满足时,它必须释放所占有的全部资源,以后再重新申请。这种预防死锁的方法实现起来困难,会降低系统性能。

〈3〉打破请求和保持条件。可以实行资源预先分配策略。即进程在运行前一次性地向系统申请它所需要的全部资源。如果某个进程所需的全部资源得不到满足,则不分配任何资源,此进程暂不运行。只有当系统能够满足当前进程的全部资源需求时,才一次性地将所申请的资源全部分配给该进程。但是,这种策略也有如下缺点:(1)在许多情况下,一个进程在执行之前不可能知道它所需要的全部资源。这是由于进程在执行时是动态的,不可预测的;(2)资源利用率低。无论所分资源何时用到,一个进程只有在占有所需的全部资源后才能执行。即使有些资源最后才被该进程用到一次,但该进程在生存期间却一直占有它们,造成长期占着不用的状况。这显然是一种极大的资源浪费;(3)降低了进程的并发性。因为资源有限,又加上存在浪费,能分配到所需全部资源的进程个数就必然少了。

<4>打破循环等待条件,实行资源有序分配策略。采用这种策略,即把资源事先分类编号,按号分配,使进程在申请,占用资源时不会形成环路。所有进程对资源的请求必须严格按资源序号递增的顺序提出。进程占用了小号资源,才能申请大号资源,就不会产生环路,从而预防了死锁。这种策略与前面的策略相比,资源的利用率和系统吞吐量都有很大提高,但是也存在以下缺点:(1)限制了进程对资源的请求,同时给系统中所有资源合理编号也是件困难事,并增加了系统开销;

(2)为了遵循按编号申请的次序,暂不使用的资源也需要提前申请,从而增加了进程对资源的占用时间。

死锁的避免

上面我们讲到的死锁预防是排除死锁的静态策略,它使产生死锁的四个必要条件不能同时具备,从而对进程申请资源的活动加以限制,以保证死锁不会发生。下面我们介绍排除死锁的动态策略--死锁的避免,它不限制进程有关申请资源的命令,而是对进程所发出的每一个申请资源命令加以动态地检查,并根据检查结果决定是否进行资源分配。就是说,在资源分配过程中若预测有发生死锁的可能性,则加以避免。这种方法的关键是确定资源分配的安全性。

1.安全序列

我们首先引入安全序列的定义:所谓系统是安全的,是指系统中的所有进程能够按照某一种次序分配资源,并且依次地运行完毕,这种进程序列{P1,P2,...,Pn}就是安全序列。如果存在这样一个安全序列,则系统是安全的;如果系统不存在这样一个安全序列,则系统是不安全的。

安全序列{P1,P2,...,Pn}是这样组成的:若对于每一个进程Pi,它需要的附加资源可以被系统中当前可用资源加上所有进程Pj当前占有资源之和所满足,则{P1,P2,...,Pn}为一个安全序列,这时系统处于安全状态,不会进入死锁状态。 

虽然存在安全序列时一定不会有死锁发生,但是系统进入不安全状态(四个死锁的必要条件同时发生)也未必会产生死锁。当然,产生死锁后,系统一定处于不安全状态。

2.银行家算法

这是一个著名的避免死锁的算法,是由Dijstra首先提出来并加以解决的。

[背景知识]

一个银行家如何将一定数目的资金安全地借给若干个客户,使这些客户既能借到钱完成要干的事,同时银行家又能收回全部资金而不至于破产,这就是银行家问题。这个问题同操作系统中资源分配问题十分相似:银行家就像一个操作系统,客户就像运行的进程,银行家的资金就是系统的资源。

死锁的检测与恢复

1.死锁检测与恢复是指系统设有专门的机构,当死锁发生时,该机构能够检测到死锁发生的位置和原因,并能通过外力破坏死锁发生的必要条件,从而使得并发进程从死锁状态中恢复出来。

2.死锁的恢复

一旦在死锁检测时发现了死锁,就要消除死锁,使系统从死锁状态中恢复过来。

(1)最简单,最常用的方法就是进行系统的重新启动,不过这种方法代价很大,它意味着在这之前所有的进程已经完成的计算工作都将付之东流,包括参与死锁的那些进程,以及未参与死锁的进程。

(2)撤消进程,剥夺资源。终止参与死锁的进程,收回它们占有的资源,从而解除死锁。这时又分两种情况:一次性撤消参与死锁的全部进程,剥夺全部资源;或者逐步撤消参与死锁的进程,逐步收回死锁进程占有的资源。一般来说,选择逐步撤消的进程时要按照一定的原则进行,目的是撤消那些代价最小的进程,比如按进程的优先级确定进程的代价;考虑进程运行时的代价和与此进程相关的外部作业的代价等因素。

此外,还有进程回退策略,即让参与死锁的进程回退到没有发生死锁前某一点处,并由此点处继续执行,以求再次执行时不再发生死锁。虽然这是个较理想的办法,但是操作起来系统开销极大,要有堆栈这样的机构记录进程的每一步变化,以便今后的回退,有时这是无法做到的。

线程安全的集合类

1、HashTable

函数都是同步的,线程同步Synchronized

2、ConcurrentHashHshMap

线程安全,效率高于HashTable,使用ReentrantLock实现线程安全

3、CopyOnWriteArrayList:

List接口的实现类使用ReentrantLock实现线程安全

4、CopyOnWriteArraySet:

在CopyOnWriteArrayList基础上使用了java的装饰模式、

CopyOnWriteCopyOnWrite机制介绍:

CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

在使用CopyOnWriteArrayList之前,我们先阅读其源码了解下它是如何实现的。以下代码是向ArrayList里添加元素,可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。读的时候不需要加锁,如果读的时候有多个线程正在向ArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的ArrayList。

CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。当用户搜索时,会检查当前关键字在不在黑名单当中,如果在,则提示不能搜索。

CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。

 

内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的YongGC和FullGC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的FullGC,应用响应时间也随之变长。

针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。

数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

5、Vector

Vector是矢量队列,它继承了AbstractList,实现了List、RandomAccess,Cloneable,Java.io.Serializable接口。

ector继承了AbstractList,实现了List,它是一个队列,因此实现了相应的添加、删除、修改、遍历等功能。

Vector实现了RandomAccess接口,因此可以随机访问。

Vector实现了Cloneable,重载了clone()方法,因此可以进行克隆。

Vector实现了Serializable接口,因此可以进行序列化。

Vector的操作是线程安全的。利用Synchronized关键字实现同步,保证线程安全

6、StringBufferStringBuilder

String和StringBuffer、StringBuilder相比,String是不可变的,String的每次修改操作都是在内存中重新new一个对象出来,而StringBuffer、StringBuilder则不用,并且提供了一定的缓存功能,默认16个字节数组的大小,超过默认的数组长度时,则扩容为原来字节数组的长度*2+2。
StringBuffer和StringBuilder相比,StringBuffer是synchronized的,是线程安全的,而StringBuilder是非线程安全的,单线程情况下性能更好一点;使用StringBuffer和StringBuilder时,可以适当考虑下初始化大小,较少扩容的次数,提高代码的高效性。
String+过程:
java中String的+运算符编译后其实是转换成了这样的代码:


Stringb=newStringBuilder().append("a").append(bb).toString();

String每做一次+操作,都new一个新的对象。

阻塞队列

BlockingQueue即阻塞队列,从阻塞这个词可以看出,在某些情况下对阻塞队列的访问可能会造成阻塞。被阻塞的情况主要有如下两种:

1.当队列满了的时候进行入队列操作

2.当队列空了的时候进行出队列操作

因此,当一个线程试图对一个已经满了的队列进行入队列操作时,它将会被阻塞,除非有另一个线程做了出队列操作;同样,当一个线程试图对一个空队列进行出队列操作时,它将会被阻塞,除非有另一个线程进行了入队列操作。

在Java中,BlockingQueue的接口位于java.util.concurrent包中(在Java5版本开始提供),由上面介绍的阻塞队列的特性可知,阻塞队列是线程安全的。

阻塞队列主要用在生产者/消费者的场景,下面这幅图展示了一个线程生产、一个线程消费的场景:负责生产的线程不断的制造新对象并插入到阻塞队列中,直到达到这个队列的上限值。队列达到上限值之后生产线程将会被阻塞,直到消费的线程对这个队列进行消费。同理,负责消费的线程不断的从队列中消费对象,直到这个队列为空,当队列为空时,消费线程将会被阻塞,除非队列中有新的对象被插入。

BlockingQueue只是java.util.concurrent包中的一个接口,而在具体使用时,我们用到的是它的实现类,当然这些实现类也位于java.util.concurrent包中。在Java6中,BlockingQueue的实现类主要有以下几种:

1.ArrayBlockingQueue(ReentrantLock锁机制)

2.DelayQueue(ReentrantLock锁机制)

3.LinkedBlockingQueue(ReentrantLock锁机制)

4.PriorityBlockingQueue(ReentrantLock锁机制)

5.SynchronousQueue(Synchronized)

 

ArrayBlockingQueue

ArrayBlockingQueue是一个有边界的阻塞队列,它的内部实现是一个数组。有边界的意思是它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。

ArrayBlockingQueue是以先进先出的方式存储数据,最新插入的对象是尾部,最新移出的对象是头部。下面是一个初始化和使用ArrayBlockingQueue的例子:

BlockingQueuequeue=newArrayBlockingQueue(1024);

queue.put("1");

Objectobject=queue.take();

DelayQueue

DelayQueue阻塞的是其内部元素,DelayQueue中的元素必须实现java.util.concurrent.Delayed接口,这个接口的定义非常简单:

publicinterfaceDelayedextendsComparable<Delayed>{

longgetDelay(TimeUnitunit);

}

getDelay()方法的返回值就是队列元素被释放前的保持时间,如果返回0或者一个负值,就意味着该元素已经到期需要被释放,此时DelayedQueue会通过其take()方法释放此对象。

从上面Delayed接口定义可以看到,它还继承了Comparable接口,这是因为DelayedQueue中的元素需要进行排序,一般情况,我们都是按元素过期时间的优先级进行排序。

其实DelayQueue应用场景很多,比如定时关闭连接、缓存对象,超时处理等各种场景,下面我们就拿学生考试为例让大家更深入的理解DelayQueue的使用。

LinkedBlockingQueue

LinkedBlockingQueue阻塞队列大小的配置是可选的,如果我们初始化时指定一个大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE的容量。它的内部实现是一个链表。

和ArrayBlockingQueue一样,LinkedBlockingQueue也是以先进先出的方式存储数据,最新插入的对象是尾部,最新移出的对象是头部。下面是一个初始化和使LinkedBlockingQueue的例子:

BlockingQueue<String>unbounded=newLinkedBlockingQueue<String>();

BlockingQueue<String>bounded=newLinkedBlockingQueue<String>(1024);

bounded.put("Value");

Stringvalue=bounded.take();

PriorityBlockingQueue

PriorityBlockingQueue是一个没有边界的队列,它的排序规则和java.util.PriorityQueue一样。需要注意,PriorityBlockingQueue中允许插入null对象。

所有插入PriorityBlockingQueue的对象必须实现java.lang.Comparable接口,队列优先级的排序规则就是按照我们对这个接口的实现来定义的。

另外,我们可以从PriorityBlockingQueue获得一个迭代器Iterator,但这个迭代器并不保证按照优先级顺序进行迭代。

SynchronousQueue

SynchronousQueue队列内部仅允许容纳一个元素。当一个线程插入一个元素后会被阻塞,除非这个元素被另一个线程消费。可以看做一个传球手,负责把生产者线程处理的数据直接传递给消费者线程,本身不存储任何元素,其吞吐量高于LinkedBlockinQueue和ArrayBlockingQueue.有两种实现方式,一种是配合使用FIFO队列(公平)另一种是配合使用LIFO队列(不公平)

CAS是单词compare and set的缩写,意思是指在set之前先比较该值有没有变化,只有在没变的情况下才对其赋值。

 

ConcurrentLinkedQueue与LinkedBlockingQueue的对比

由于2采用读写锁的形式对读写进行控制,可能会在锁的获取与释放上损失一定的性能。所以当有多个消费者时多用1。

而对于2,我们在其源码中可以看到,获取队首元素有take与poll方法,这两者的最本质区别在于,当队列为空时take线程会被阻塞,调用wait()方法释放其所占有的资源。当有新元素入队时会被notify,但是对于poll,若队列为空,会直接返回null,所以在多线程中,如果消费者速度大于生产者速度,会导致队列经常为空,这时如果不在poll的返回值为空时进行必要的处理,会导致线程空转,最坏情况下会导致cpu使用率飙升。我们可以采用锁的形式在线程内部主动wait(),而在入队时notify或者notifyall来参照take方法,防止大量的线程空转。

 

对于1,由于其内部没有类似于2的take方法,除了poll与peek之外,没有提供别的获取元素的方法,这两者的区别在于是否会弹出队首元素。但是这时如果消费速度大于生产速度,同样会产生上面的问题,这时我们就必须采用显式的方法加锁调用wait()方法,防止cpu等资源的浪费。提高并发性能。

性能区别:

1、对于LinkedBlockingQueue,take方法虽然在内部实现了加锁wait(),但是由于其他的开销,导致性能相比于poll+wait()有所下降。但是如果采用poll方法,那么由于大量的线程存在空转的情况,导致争用处理机,导致性能急剧下降。

2、对于ConcurrentLinkedQueue,由于内部采用CAS保证并发安全,在采用poll+wait()时相比前者有所提升,但是不是很明显,但是对于poll方式,由于去除了锁的开销,同时虽然相比于其自身的poll+wait()方式性能下降不少,但是相对于LinkedBlockingQueue,性能提升相当明显。

同步计数器Semaphore

一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。拿到信号量的线程可以进入代码,否则就等待。通过acquire()和release()获取和释放访问许可。

线程的单例模式

参考链接:http://blog.csdn.net/jason0539/article/details/23297037

单例模式是为确保一个类只有一个实例,并为整个系统提供一个全局访问点的一种模式方法。

从概念中体现出了单例的一些特点:

(1)、在任何情况下,单例类永远只有一个实例存在

(2)、单例需要有能力为整个系统提供这一唯一实例  

 

单例对象通常作为程序中的存放配置信息的载体,因为它能保证其他对象读到一致的信息

比如说打印机:只能有一个对象,避免两个打印作业同时输出到打印机中。

 

单例实现:

饿汉式单例

饿汉式单例是指在方法调用前,实例就已经创建好了。下面是实现代码:

package org.mlinge.s01;

 

public class MySingleton {

     

      private static MySingleton instance = newMySingleton();

     

      private MySingleton(){}

     

      public static MySingleton getInstance() {

             return instance;

      }

     

}

懒汉式单例:

懒汉式单例是指在方法调用获取实例时才创建实例,因为相对饿汉式显得“不急迫”,所以被叫做“懒汉模式”。下面是实现代码:

//单例模式-懒汉式单例

public classLazySingleton {

     //私有静态对象,加载时候不做初始化

     private static LazySingletonm_intance=null;

     // 私有构造方法,避免外部创建实例

     private LazySingleton(){}

 

     /**

      * 静态工厂方法,返回此类的唯一实例.

      * 当发现实例没有初始化的时候,才初始化.

      * @return LazySingleton

      */

     synchronized public static LazySingletongetInstance(){

         if(m_intance==null){

             m_intance=new LazySingleton();

         }

         return m_intance;

     }

}

登记式单例    

这个单例实际上维护的是一组单例类的实例,将这些实例存放在一个Map(登记薄)中,对于已经登记过的实例,则从工厂直接返回,对于没有登记的,则先登记,而后返回。

 

 

/**

* 单例模式- 登记式单例

*/

public classRegSingleton {

     /**

      * 登记薄,用来存放所有登记的实例

      */

     private static Map m_registry = newHashMap();

     //在类加载的时候添加一个实例到登记薄

     static {

         RegSingleton x = new RegSingleton();

         m_registry.put(x.getClass().getName(),x);

     }

 

     /**

      * 受保护的默认构造方法

      */

     protected RegSingleton() {}

 

     /**

      * 静态工厂方法,返回指定登记对象的唯一实例;

      * 对于已登记的直接取出返回,对于还未登记的,先登记,然后取出返回

      * @param name

      * @return RegSingleton

      */

     public static RegSingletongetInstance(String name) {

         if (name == null) {

             name = "RegSingleton";

         }

         if (m_registry.get(name) == null) {

             try {

                 m_registry.put(name,(RegSingleton) Class.forName(name).newInstance());

             } catch (InstantiationException e){

                 e.printStackTrace();

             } catch (IllegalAccessException e){

                 e.printStackTrace();

             } catch (ClassNotFoundException e){

                 e.printStackTrace();

             }

         }

         return m_registry.get(name);

     }

 

     /**

      * 一个示意性的商业方法

      * @return String

      */

     public String about() {

         return "Hello,I amRegSingleton!";

     }

}

 

线程池:

参考链接:http://www.cnblogs.com/exe19/p/5359885.html

                     http://cuisuqiang.iteye.com/blog/2019372

 

Executor是一个顶层接口,在它里面只声明了一个方法execute(Runnable),返回值为void,参数为Runnable类型,从字面意思可以理解,就是用来执行传进去的任务的;

然后ExecutorService接口继承了Executor接口,并声明了一些方法:submit、invokeAll、invokeAny以及shutDown等;

抽象类AbstractExecutorService实现了ExecutorService接口,基本实现了ExecutorService中声明的所有方法;

然后ThreadPoolExecutor继承了类AbstractExecutorService。

在ThreadPoolExecutor类中有几个非常重要的方法:

execute()

submit()

shutdown()

shutdownNow()

 

execute()方法实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。

 

submit()方法是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果(Future相关内容将在下一篇讲述)。

 

shutdown()和shutdownNow()是用来关闭线程池的。

从上面的代码可以得知,ThreadPoolExecutor继承了AbstractExecutorService类,并提供了四个构造器,事实上,通过观察每个构造器的源码具体实现,发现前面三个构造器都是调用的第四个构造器进行的初始化工作。

ThreadPoolExecutor

在ThreadPoolExecutor类中提供了四个构造方法:

public classThreadPoolExecutor extends AbstractExecutorService {

    .....

    public ThreadPoolExecutor(intcorePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,

            BlockingQueue<Runnable>workQueue);

 

    public ThreadPoolExecutor(intcorePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,

            BlockingQueue<Runnable>workQueue,ThreadFactory threadFactory);

 

    public ThreadPoolExecutor(intcorePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,

            BlockingQueue<Runnable>workQueue,RejectedExecutionHandler handler);

 

    public ThreadPoolExecutor(intcorePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,

        BlockingQueue<Runnable>workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);

    ...

}

 

 下面解释下一下构造器中各个参数的含义:

 

corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;

maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;

keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;

unit:参数keepAliveTime的时间单位

Executor是线程的工厂类,方便快速地创建很多线程池,提供了一些静态工厂,生成一些常用的线程池。常用的方法有以下几种:

newCachedThreadPool()

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程

public staticExecutorService newCachedThreadPool()创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。注意,可以使用 ThreadPoolExecutor 构造方法创建具有类似属性但细节不同(例如超时参数)的线程池。

返回一个根据ThreadPoolExecutor创建一个线程为0,来一个线程就在线程池里面创建一个的SynchronizedQueue

ExecutorServicecachedThreadPool = Executors.newCachedThreadPool();

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

final int index = i;

try {

Thread.sleep(index *1000);

} catch(InterruptedException e) {

e.printStackTrace();

}

 

cachedThreadPool.execute(newRunnable() {

@Override

public void run() {

System.out.println(index);

}

});

}

newFixedThreadPool(intnThreads)

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待

public static ExecutorService newFixedThreadPool(int nThreads)创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。Executors.newFixedThreadPool(3)的实现中通过ThreadPoolExecutor创建一个指定大小的LinledBlockingQueue的线程池。

ExecutorServicefixedThreadPool = Executors.newFixedThreadPool(3);

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

final int index = i;

fixedThreadPool.execute(newRunnable() {

 

@Override

public void run() {

try {

System.out.println(index);

Thread.sleep(2000);

} catch(InterruptedException e) {

// TODOAuto-generated catch block

e.printStackTrace();

}

}

});

}

newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。Executors.newSingleThreadExecutor()的实现方法中是返回一个根据ThreadPoolExecutor创建一个LinkedBlockingQueue的一个大小的线程,采用默认的异常策略。

ExecutorServicesingleThreadExecutor = Executors.newSingleThreadExecutor();

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

final int index = i;

singleThreadExecutor.execute(newRunnable() {

 

@Override

public void run() {

try {

System.out.println(index);

Thread.sleep(2000);

} catch(InterruptedException e) {

// TODOAuto-generated catch block

e.printStackTrace();

}

}

});

}

newScheduledThreadPool

创建一个定长线程池,支持定时及周期性任务执行

ScheduledExecutorServicescheduledThreadPool = Executors.newScheduledThreadPool(5);

scheduledThreadPool.schedule(newRunnable() {

 

@Override

public void run() {

System.out.println("delay3 seconds");

}

}, 3,TimeUnit.SECONDS);

线程池的好处

降低资源消耗

提高响应速度

提高线程的可管理性

防止服务器过载,形成内存溢出,或者CPU耗尽

线程池的应用范围:

1、         需要大量的线程来完成任务,且完成任务的时间比较短

2、         对性能要求苛刻的应用,比如要求服务器迅速响应客户请求

3、         接收突发性的大量请求

Fork/Join框架

http://www.infoq.com/cn/articles/fork-join-introduction

Fork/Join框架是Java7提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。

我们再通过Fork和Join这两个单词来理解下Fork/Join框架,Fork就是把一个大任务切分为若干子任务并行的执行,Join就是合并这些子任务的执行结果,最后得到这个大任务的结果。比如计算1+2+。。+10000,可以分割成10个子任务,每个子任务分别对1000个数进行求和,最终汇总这10个子任务的结果。

工作窃取算法

 

工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。

那么为什么需要使用工作窃取算法呢?假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。

 

工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。

Fork/Join框架的介绍

 

我们已经很清楚Fork/Join框架的需求了,那么我们可以思考一下,如果让我们来设计一个Fork/Join框架,该如何设计?这个思考有助于你理解Fork/Join框架的设计。

 

第一步分割任务。首先我们需要有一个fork类来把大任务分割成子任务,有可能子任务还是很大,所以还需要不停的分割,直到分割出的子任务足够小。

 

第二步执行任务并合并结果。分割的子任务分别放在双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都统一放在一个队列里,启动一个线程从队列里拿数据,然后合并这些数据。

 

Fork/Join使用两个类来完成以上两件事情:

 

ForkJoinTask:我们要使用ForkJoin框架,必须首先创建一个ForkJoin任务。它提供在任务中执行fork()和join()操作的机制,通常情况下我们不需要直接继承ForkJoinTask类,而只需要继承它的子类,Fork/Join框架提供了以下两个子类:

RecursiveAction:用于没有返回结果的任务。

RecursiveTask:用于有返回结果的任务。

ForkJoinPool:ForkJoinTask需要通过ForkJoinPool来执行,任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。

Fork/Join框架的异常处理

 

ForkJoinTask在执行的时候可能会抛出异常,但是我们没办法在主线程里直接捕获异常,所以ForkJoinTask提供了isCompletedAbnormally()方法来检查任务是否已经抛出异常或已经被取消了,并且可以通过ForkJoinTask的getException方法获取异常。

 

使用Fork/Join框架

让我们通过一个简单的需求来使用下Fork/Join框架,需求是:计算1+2+3+4的结果。

使用Fork/Join框架首先要考虑到的是如何分割任务,如果我们希望每个子任务最多执行两个数的相加,那么我们设置分割的阈值是2,由于是4个数字相加,所以Fork/Join框架会把这个任务fork成两个子任务,子任务一负责计算1+2,子任务二负责计算3+4,然后再join两个子任务的结果。

因为是有结果的任务,所以必须继承RecursiveTask,实现代码如下:

通过这个例子让我们再来进一步了解ForkJoinTask,ForkJoinTask与一般的任务的主要区别在于它需要实现compute方法,在这个方法里,首先需要判断任务是否足够小,如果足够小就直接执行任务。如果不足够小,就必须分割成两个子任务,每个子任务在调用fork方法时,又会进入compute方法,看看当前子任务是否需要继续分割成孙任务,如果不需要继续分割,则执行当前子任务并返回结果。使用join方法会等待子任务执行完并得到其结果。

 


0 0
原创粉丝点击