摘要
对于多核服务器,合理的并发能够显著的提升数据的处理效率,并发可以将程序断开为多个片段,在单独的处理器上运行每个片段,即在这些处理器之间分配多个任务;对于单核服务器来讲,如果有阻塞线程,那么可以使用并发来执行其他的程序,否则开多线程是没有意义的,只会徒增线程切换的消耗;我们平时用到的tomcat、nginx等服务都是多线程的,对于每一个请求,都是一个新的线程;
参考连接:
java并发编程与高并发解决方案
深入理解Java中的锁
Java线程池实现原理详解
Java高并发之BlockingQueue
基本的线程机制
我们可以将程序划分多个分离的、独立运行的任务,通过使用多线程机制,这些任务都将由执行线程来驱动。一个线程就是在进程中的单一的顺序控制流,因此,单个进程可以拥有多个并发执行的任务,但是程序使每个任务都觉得好像有其自己的CPU一样,但其底层机制是切分CPU时间。在使用线程时,CPU将会轮流给每个任务分配其占用时间,每个任务都觉得自己一直在占用CPU,但事实上CPU时间是划分为片段分配给了所有任务。线程的一个好处就是我门可以从这个层次抽身出来,不用知道程序运行在一个CPU还是多个。如果程序运行的太慢,为机器增添一个CPU就能很容易的加快程序的运行速度,多任务和多线程往往是使用多处理器系统的最合理方式。
Java内存模型
同步规则
1、如果要把一个变量从主内存中复制到工作内存,就需要按顺序的执行read和load操作,如果把变量从工作内存中同步回主内存,就需要按顺序的执行store和write操作。但java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行
2、不允许read和load、store和write操作之一单独出现
3、不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中
4、不允许一个线程无原因的(没发生过任何assign操作)把数据从工作内存同步回主内存中
5、一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作
6、一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现
7、如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前需要重新执行load或assign操作初始化变量的值
8、如果一个变量实现没有被lock操作锁定,怎不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量
9、对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)
同步操作
lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态
unlock(解锁):作用于主内存变脸个,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量
store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以遍随后的write的操作
write(写入):作用于主内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中
线程的有序性
线程遵循happens-before原则:
1 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
注:在单线程中,看起来是这样的,虚拟机可能会对代码进行指令重排序,虽然重排序了,但是运行结果在单线程中和指令书写顺序是一致的,事实上,这条规则是用来保证程序单在单线程中执行结果的正确性,无法保证程序在多线程中的正确性
2 锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作
3 volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
4 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
前四条规则比较重要
5 线程启动规则:Thread对象的start()方法先行发生于次线程的每一个动作
6 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码监测到中断事件的发生
7 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
8 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
Thread类
方法:
String getName() 返回该线程的名称。
void setName(String name) 改变线程名称,使之与参数 name 相同。
int getPriority() 返回线程的优先级。
void setPriority(int newPriority) 更改线程的优先级。
boolean isDaemon() 测试该线程是否为守护线程。
常用方法:
1、sleep()
线程睡眠,立即释放CPU,但并不释放锁,静态方法,作用于当前线程;
2、interrupt()
中断线程
3、yield()与join()
理论上,yield意味着放手,放弃,投降。一个调用yield()方法的线程告诉虚拟机它乐意让其他线程占用自己的位置。这表明该线程没有在做一些紧急的事情。注意,这仅是一个暗示,并不能保证不会产生任何影响。
线程实例的方法join()方法可以使得一个线程在另一个线程结束后再执行。如果join()方法在一个线程实例上调用,当前运行着的线程将阻塞直到这个线程实例完成了执行。
代码示例:
public void test() throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread1-1");
Thread.yield();
System.out.println("thread1-2");
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread2");
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread3");
}
});
t1.start();
t1.join();
t2.start();
t3.start();
System.out.println("111111111111");
}
4、wait()与notify()
wait使得当前线程立刻停止运行,处于等待状态(WAIT),并将当前线程置入锁对象的等待队列中,直到被通知(notify)或被中断为止。
使用条件:wait方法只能在同步方法或同步代码块中使用,而且必须是内建锁。wait方法调用后立刻释放对象锁
notify唤醒处于等待状态的线程
使用条件:notify()也必须在同步方法或同步代码块中调用,用来唤醒等待该对象的其他线程。如果有多个线程在等待,随机挑选一个线程唤醒(唤醒哪个线程由JDK版本决定)。notify方法调用后,当前线程不会立刻释放对象锁,要等到当前线程执行完毕后再释放锁,notifyAll()唤醒所有被阻塞的线程。
代码示例:
public class WaitTest implements Runnable{
private boolean flag;
private Object object;
public WaitTest(boolean flag, Object object){
this.flag = flag;
this.object = object;
}
private void waitTest(){
synchronized (object){
System.out.println(Thread.currentThread().getName() + " wait begin...");
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " wait end...");
}
}
private void notifyTest(){
synchronized (object){
System.out.println(Thread.currentThread().getName() + " notify begin...");
object.notifyAll();
System.out.println(Thread.currentThread().getName() + " notify end...");
}
}
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
WaitTest test = new WaitTest(false, object);
Thread t1 = new Thread(test, "thread1");
for(int i = 0; i < 5; i++){
WaitTest waitTest = new WaitTest(true, object);
Thread t2 = new Thread(waitTest, "thread2" + i);
t2.start();
}
Thread.sleep(2000);
t1.start();
}
@Override
public void run() {
if(flag){
waitTest();
}else{
notifyTest();
}
}
}
5、run()与start()
run是实现Runnable接口的方法,直接diaoyongrun方法并不会实现多线程,只是执行了run方法里的代码,start()方法是开始线程。
定义线程
方式一:继承Thread类,重写run()方法
代码:
public class ThreadTest extends Thread {
@Override
public void run() {
System.out.println("thread->" + Thread.currentThread().getName());
}
public static void main(String[] args){
ThreadTest test = new ThreadTest();
test.start();
}
}
或直接快速实现:
public static void main(String[] args){
new Thread(new Runnable() {
@Override
public void run() {
System.out.print(Thread.currentThread().getName());
}
}).start();
}
方式二:实现Runable接口,实现run()方法
代码:
public class RunnableTest implements Runnable {
@Override
public void run() {
System.out.println("thread->" + Thread.currentThread().getName());
}
public static void main(String[] args){
RunnableTest test = new RunnableTest();
new Thread(test).start();
}
}
方式三:实现Callable接口,重写call()方法,和FutureTask一起使用,call()方法可以有返回值,使用FutureTask的get()方法获取;
代码:
public class CallableTest implements Callable {
private final String RESULT = "SUCCESS";
@Override
public String call() {
System.out.println("thread->" + Thread.currentThread().getName());
return RESULT;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
CallableTest test = new CallableTest();
FutureTask<String> future = new FutureTask<String>(test);
new Thread(future).start();
System.out.println("result:" + future.get());
}
}
锁
常见的锁有synchronized、volatile、偏向锁、轻量级锁、重量级锁
自旋锁
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
但是线程自旋是需要消耗cup的,说白了就是让cup在做无用功,线程不能一直占用cup自旋做无用功,所以需要设定一个自旋等待的最大时间。如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
在JDK 1.6中引入了自适应的自旋锁。 自适应意味着自旋的时间不再固定了, 而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。 如果在同一个锁对象上, 自旋等待刚刚成功获得过锁, 并且持有锁的线程正在运行中, 那么虚拟机就会认为这次自旋也很有可能再次成功, 进而它将允许自旋等待持续相对更长的时间, 比如100个循环。 另外, 如果对于某个锁, 自旋很少成功获得过, 那在以后要获取这个锁时将可能省略掉自旋过程, 以避免浪费处理器资源。
偏向锁
偏向锁(顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁)
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
获取偏向锁:
1、访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。
2、如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。
3、如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。
4、如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
5、执行同步代码。
以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
偏向锁的撤销:
当有另外的线程视图锁定某个已经被偏向过得对象,jvm就需要撤销偏向锁。线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
synchronized
synchronized是并发编程中最基本的同步工具,属于重量级锁,也是Java内置同步机制,提供了互斥性和可见性的语义,可以用过使用它来保证并发的安全。
三种用法:
1、对象锁
当使用synchronized修饰类普通方法时,那么当前加锁的级别就是实例对象,当多个线程并发访问该对象的同步方法、同步代码块时,会进行同步。
2、类锁
当使用synchronized修饰类静态方法时,那么当前加锁的级别就是类,当多个线程并发访问该类(所有实例对象)的同步方法以及同步代码块时,会进行同步。
3、同步代码块
当使用synchronized修饰代码块时,那么当前加锁的级别就是synchronized(X)中配置的x对象实例,当多个线程并发访问该对象的同步方法、同步代码块以及当前的代码块时,会进行同步。
使用同步代码块时要注意的是不要使用String类型对象,因为String常量池的存在,所以很容易导致出问题。
实现原理:
synchronized与其他锁不同,它是内置在JVM中的,从JVM规范中看,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的。monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。
方法级的同步是隐式的, 即无须通过字节码指令来控制, 它实现在方法调用和返回操作之中。 虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。 当方法调用时, 调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置, 如果设置了, 执行线程就要求先成功持有管程, 然后才能执行方法, 最后当方法完成(无论是正常完成还是非正常完成) 时释放管程。 在方法执行期间, 执行线程持有了管程, 其他任何线程都无法再获取到同一个管程。 如果一个同步方法执行期间抛出了异常, 并且在方法内部无法处理此异常, 那么这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。
volatile
volatile是一种轻量级锁,被他修饰的共享变量具有可见性。每个线程在工作中都有自己的工作内存,线程修改的共享变量是从主存拷贝的副本,当一个共享变量被volatile修饰时,他会保证修改的值会被立即更新到主存,当其他线程需要读取时,要从内存中读取新值。
实现原理:
被volatile修饰的共享变量在进行写操作的时候,将当前处理器缓存行的数据写回到系统内存,这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
使用场景:
1.访问变量不需要加锁(加锁的话使用volatile就没必要了)
2、对变量的写操作不依赖于当前值(因为他不能保证原子性)
3.该变量没有包含在具有其他变量的不变式中。
Atomic包
AtomicXxxx类中方法incrementAndGet(),incrementAndGet方法中调用unsafe.getAndAddInt(),getAndAddInt方法中主题是do-while语句,while语句中调用compareAndSwapInt(var1, var2, var5, var5 + var4)
compareAndSwapInt方法就是CAS核心:
在死循环内,不断尝试修改目标值,直到修改成功,如果竞争不激烈,修改成功率很高,否则失败概率很高,性能会受到影响
jdk8中新增LongAdder,它和AtomicLong比较
优点:性能好,在处理高并发情况下统计优先使用LongAdder
AtomicReference、AtomicReferenceFieldUpdater原子性更新字段(字段要求volatile修饰,并且是非static)
AtomicStampReference:CAS的ABA问题
ABA问题:变量已经被修改了,但是最终的值和原来的一样,那么如何区分是否被修改过呢,用版本号解决
AtomicBoolean可以让某些代码只执行一次
代码:
AtomicInteger atomicInteger = new AtomicInteger(0);
@Test
public void atomicIntTest() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(5);
for(int i = 0; i < 5; i++){
new Thread(new Runnable() {
@Override
public void run() {
for(int j = 0; j < 10; j++){
System.out.println(atomicInteger.addAndGet(1));
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
latch.countDown();
}
}).start();
}
latch.await();
}
CountDownLatch
CountDownLatch是一个同步辅助类,应用场景:并行运算,所有线程执行完毕才可执行。
代码示例:
public static void main(String[] args) throws Exception {
int n = Integer.MAX_VALUE;
CountDownLatch latch = new CountDownLatch(5);
for(int i = 0; i < 5; i++){
new Thread(new Runnable() {
@Override
public void run() {
for(int j = 0; j < 10; j++){
addNum();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
latch.countDown();
}
}).start();
}
latch.await();
System.out.print(num);
}
Semaphore
Semaphore可以很容易控制某个资源可悲同时访问的线程个数,和CountDownLatch使用有些类似,提供acquire和release两个方法,acquire是获取一个许可,如果没有就等待,release是在操作完成后释放许可出来。Semaphore维护了当前访问的线程的个数,提供同步机制来控制同时访问的个数,Semaphore可以实现有限大小的链表,重入锁(如ReentrantLock)也可以实现这个功能,但是实现上比较复杂。
Semaphore使用场景:适用于仅能提供有限资源,如数据库连接数。
代码示例:
@Test
public void test() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(5);
Semaphore semaphore = new Semaphore(3);
for(int i = 0; i < 5; i++){
new Thread(new Runnable() {
@Override
public void run() {
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int j = 0; j < 10; j++){
System.out.println(num ++);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
semaphore.release();
latch.countDown();
}
}).start();
}
latch.await();
System.out.println(num);
}
CyclicBarrier
与CountDownLatch相似,都是通过计数器实现,当某个线程调用await方法,该线程就进入等待状态,且计数器进行加1操作,当计数器的值达到设置的初始值,进入await等待的线程会被唤醒,继续执行他们后续的操作。由于CyclicBarrier在释放等待线程后可以重用,所以又称循环屏障。使用场景和CountDownLatch相似,可用于并发运算。
CyclicBarrier和CountDownLatch区别:
1、 CountDownLatch计数器只能使用一次,CyclicBarrier的计数器可以使用reset方法重置循环使用
2、 CountDownLatch主要是视线1个或n个线程需要等待其他线程完成某项操作才能继续往下执行,CyclicBarrier主要是实现多个线程之间相互等待知道所有线程都满足了条件之后才能继续执行后续的操作,CyclicBarrier能处理更复杂的场景
代码示例:
CyclicBarrier barrier = new CyclicBarrier(5);
@Test
public void test(){
for(int i = 0; i < 10; i++){
final int num = i;
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("线程" + num + "即将执行");
barrier.await();
System.out.println("线程" + num + "即将完毕");
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
ReentrantLock
reentrantLock(可重入锁)和synchronized区别
1 可重入性:同一线程可以重入获得相同的锁,计数器加1,释放锁计数器减1
synchronized也是可重入锁
2 锁的实现:synchronized依赖jvm实现(操作系统级别的实现),reentrantLock是jdk实现的(用户自己编程实现)
3 性能区别:synchronized在优化前性能比reentrantLock差,优化后性能有了恨到提升,相同条件下优先使用synchronized
4 功能区别:1)便利性方面,synchronized使用简单,reentrantLock需要手工加锁和释放锁2)锁的细粒度和灵活度方面,reentrantLock优于synchronized
5 reentrantlock独有的功能:1)可指定是公平锁还是非公平锁,synchronized只能是非公平锁 2)提供了一个Condition类,可以分组唤醒需要唤醒的线程 3)能够提供中断等待锁的线程机制,lock.lockInterruptibly()
代码示例:
int n = 0;
private Lock lock = new ReentrantLock();
//Semaphore semaphore = new Semaphore(3);
public static void main(String[] args) throws InterruptedException {
LockTest test = new LockTest();
CountDownLatch latch = new CountDownLatch(5);
for(int i = 0; i < 5; i++){
new Thread(new Runnable() {
@Override
public void run() {
for(int j = 0; j < 10; j++){
try {
test.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
latch.countDown();
}
}).start();
}
latch.await();
System.out.println(test.n);
}
private void add() throws InterruptedException {
lock.lock();
//semaphore.acquire();
n++;
Thread.sleep(10);
//semaphore.release();
lock.unlock();
}
ReentrantReadWriteLock
悲观写锁,即当所有读锁释放之后,才能加写锁,对于读多写少的程序,会引起堵塞或者死锁
代码示例:
@Test
public void test() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(20);
for(int i = 0; i < 10; i++){
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Cache.getValueByKey("name"));
latch.countDown();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
Cache.put("name", Thread.currentThread().getName() + "szz");
System.out.println(Thread.currentThread().getName() + "data has been write");
latch.countDown();
}
}).start();
}
latch.await();
}
static class Cache{
static Map<String, Object> data = new HashMap<>();
static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
static Lock readLock = lock.readLock();
static Lock writeLock = lock.writeLock();
public static Object getValueByKey(String key){
readLock.lock();
try {
return data.get(key);
} finally {
readLock.unlock();
}
}
public static void put(String key, Object value){
writeLock.lock();
try {
data.put(key, value);
} catch (Exception e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
}
Condition
多线程建协调通信的工具类
condition对象是依赖于lock对象的,意思就是说condition对象需要通过lock对象进行创建出来(调用Lock对象的newCondition()方法)。consition的使用方式非常的简单。但是需要注意在调用方法前获取锁。
代码示例:
public class ConditionTest {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
CountDownLatch latch = new CountDownLatch(2);
@Test
public void test() throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
conditionWait();
latch.countDown();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
conditionSignal();
latch.countDown();
}
}).start();
latch.await();
}
private void conditionWait(){
lock.lock();
try {
System.out.println("获取锁并等待信号");
condition.await();
System.out.println("获取信号");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
private void conditionSignal(){
lock.lock();
try {
System.out.println("获取锁");
Thread.sleep(2000);
System.out.println("发送信号");
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
FutureTask与Future
Feature接口,可以得到任务的返回值
FeatureTask父类是RunnableFeature,RunnableFeature继承了Runnable和Feature两个接口
代码示例:
private ExecutorService executor = new ThreadPoolExecutor(3, 5, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5));
@Test
public void test() throws ExecutionException, InterruptedException {
Future<String> future = executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " is running");
return "success";
}
});
System.out.println(future.get());
}
@Test
public void taskTest() throws ExecutionException, InterruptedException {
FutureTask<String> task = new FutureTask(new Callable() {
@Override
public String call() throws Exception {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " is running");
return "success";
}
});
executor.submit(task);
System.out.println(task.get());
executor.shutdown();
}
Fork/Join
将大人物切分成多个小任务并行执行,最后将结果汇总,思想和mapreduce类似。采用工作窃取算法,充分利用线程并行计算
代码示例:
public class ForkJoinTest {
private static class RecursiveTaskTest extends RecursiveTask<Integer> {
private final int start;
private final int end;
public RecursiveTaskTest(int start, int end){
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
if(end - start <= 3){
return IntStream.rangeClosed(start, end).sum();
}else{
int mid = (start + end) / 2;
RecursiveTaskTest one = new RecursiveTaskTest(start, mid);
RecursiveTaskTest two = new RecursiveTaskTest(mid + 1, end);
one.fork();
two.fork();
return one.join() + two.join();
}
}
}
@Test
public void test() throws ExecutionException, InterruptedException {
ForkJoinPool pool = new ForkJoinPool();
ForkJoinTask<Integer> task = pool.submit(new RecursiveTaskTest(0, 100));
System.out.println(task.get());
}
private static AtomicInteger sum = new AtomicInteger(0);
private static class RecursiveActionTest extends RecursiveAction {
private int start;
private int end;
public RecursiveActionTest(int start, int end){
this.start = start;
this.end = end;
}
@Override
protected void compute() {
if(end - start <= 3){
sum.addAndGet(IntStream.rangeClosed(start, end).sum());
}else{
int mid = (start + end) / 2;
RecursiveActionTest one = new RecursiveActionTest(start, mid);
RecursiveActionTest two = new RecursiveActionTest(mid + 1, end);
one.fork();
two.fork();
}
}
}
@Test
public void actionTest() throws InterruptedException {
final ForkJoinPool pool = new ForkJoinPool();
pool.submit(new RecursiveActionTest(0, 100));
pool.awaitTermination(100, TimeUnit.MILLISECONDS);
System.out.println(sum);
}
}
线程池
优势:
1、降低系统资源消耗,重用已存在的线程,降低线程创建和销毁造成的消耗;
2、提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
3、方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的,需要保持当前执行线程的现场,并恢复要执行线程的现场);
4、提供更强大的功能,延时定时线程池。
主要参数:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)
1、corePoolSize(线程池基本大小):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,(除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。)
2、maximumPoolSize(线程池最大大小):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
3、keepAliveTime(线程存活保持时间)当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
4、workQueue(任务队列):用于传输和保存等待执行任务的阻塞队列。
5、threadFactory(线程工厂):用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。
Executors工具类创建线程池
newFixedThreadPool:使用的构造方式为new ThreadPoolExecutor(var0, var0, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()),设置了corePoolSize=maxPoolSize,keepAliveTime=0(此时该参数没作用),无界队列,任务可以无限放入,当请求过多时(任务处理速度跟不上任务提交速度造成请求堆积)可能导致占用过多内存或直接导致OOM异常
newSingleThreadExector:使用的构造方式为new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), var0),基本同newFixedThreadPool,但是将线程数设置为了1,单线程,弊端和newFixedThreadPool一致
newCachedThreadPool:使用的构造方式为new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue()),corePoolSize=0,maxPoolSize为很大的数,同步移交队列,也就是说不维护常驻线程(核心线程),每次来请求直接创建新线程来处理任务,也不使用队列缓冲,会自动回收多余线程,由于将maxPoolSize设置成Integer.MAX_VALUE,当请求很多时就可能创建过多的线程,导致资源耗尽OOM
newScheduledThreadPool:使用的构造方式为new ThreadPoolExecutor(var1, 2147483647, 0L, TimeUnit.NANOSECONDS, new ScheduledThreadPoolExecutor.DelayedWorkQueue()),支持定时周期性执行,注意一下使用的是延迟队列,弊端同newCachedThreadPool一致。
FixedThreadPool和SigleThreadExecutor中之所以用LinkedBlockingQueue无界队列,是因为设置了corePoolSize=maxPoolSize,线程数无法动态扩展,于是就设置了无界阻塞队列来应对不可知的任务量;而CachedThreadPool则使用的是SynchronousQueue同步移交队列,为什么使用这个队列呢?因为CachedThreadPool设置了corePoolSize=0,maxPoolSize=Integer.MAX_VALUE,来一个任务就创建一个线程来执行任务,用不到队列来存储任务;SchduledThreadPool用的是延迟队列DelayedWorkQueue。在实际项目开发中也是推荐使用手动创建线程池的方式,而不用默认方式,关于这点在《阿里巴巴开发规范》中是这样描述的:
beforeExecute与afterExecute
留给开发人员去重写方法体实现自己的业务逻辑,非常适合做钩子函数,在任务run方法的前后增加业务逻辑,比如添加日志、统计等。这个和我们springmvc中拦截器的preHandle和afterCompletion方法很类似,都是对方法进行环绕,类似于spring的AOP
代码示例:
public class CustomExecutor extends ThreadPoolExecutor {
public CustomExecutor(int i, int i1, long l, TimeUnit timeUnit, BlockingQueue<Runnable> blockingQueue) {
super(i, i1, l, timeUnit, blockingQueue);
}
@Override
protected void beforeExecute(Thread thread, Runnable runnable) {
super.beforeExecute(thread, runnable);
System.out.println(thread.getName() + "is running");
}
@Override
protected void afterExecute(Runnable runnable, Throwable throwable) {
super.afterExecute(runnable, throwable);
System.out.println("thread is finished");
}
}
自定义线程池
1、使用原生构造方法。
2、 使用guava包中的ThreadFactoryBuilder工厂类来构造线程池,可以指定线程组名称。
代码示例:
private ExecutorService executor = new CustomExecutor(3, 6, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5));
private ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("thread-pool-%d").build();
private ExecutorService executorService = new ThreadPoolExecutor(3, 6, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5),
threadFactory, new ThreadPoolExecutor.AbortPolicy());
@Test
public void test(){
for(int i = 0; i < 11; i++){
executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName());
return null;
}
});
}
executor.shutdown();
}
handler拒绝策略
AbortPolicy:中断抛出异常
DiscardPolicy:默默丢弃任务,不进行任何通知
DiscardOldestPolicy:丢弃掉在队列中存在时间最久的任务
CallerRunsPolicy:让提交任务的线程去执行任务(对比前三种比较友好一丢丢)
关闭线程池
shutdownNow():立即关闭线程池(暴力),正在执行中的及队列中的任务会被中断,同时该方法会返回被中断的队列中的任务列表
shutdown():平滑关闭线程池,正在执行中的及队列中的任务能执行完成,后续进来的任务会被执行拒绝策略
isTerminated():当正在执行的任务及对列中的任务全部都执行(清空)完就会返回true
workQueue队列
SynchronousQueue(同步移交队列):队列不作为任务的缓冲方式,可以简单理解为队列长度为零
LinkedBlockingQueue(无界队列):队列长度不受限制,当请求越来越多时(任务处理速度跟不上任务处理速度造成请求堆积)可能导致内存占用过多或OOM
ArrayBlockintQueue(有界队列):队列长度受限,当队列满了就需要创建多余的线程来执行任务
数组阻塞队列 ArrayBlockingQueue
一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。队列的头部 是在队列中存在时间最长的元素。队列的尾部 是在队列中存在时间最短的元素。新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素。
这是一个典型的“有界缓存区”,固定大小的数组在其中保持生产者插入的元素和使用者提取的元素。一旦创建了这样的缓存区,就不能再增加其容量。试图向已满队列中放入元素会导致操作受阻塞;试图从空队列中提取元素将导致类似阻塞。
此类支持对等待的生产者线程和使用者线程进行排序的可选公平策略。默认情况下,不保证是这种排序。然而,通过将公平性 (fairness) 设置为 true 而构造的队列允许按照 FIFO 顺序访问线程。公平性通常会降低吞吐量,但也减少了可变性和避免了“不平衡性”。
延迟队列DelayQueue
Delayed 元素的一个无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部 是延迟期满后保存时间最长的 Delayed 元素。如果延迟都还没有期满,则队列没有头部,并且 poll 将返回 null。当一个元素的 getDelay(TimeUnit.NANOSECONDS) 方法返回一个小于等于 0 的值时,将发生到期。即使无法使用 take 或 poll 移除未到期的元素,也不会将这些元素作为正常元素对待。例如,size 方法同时返回到期和未到期元素的计数。此队列不允许使用 null 元素。
链阻塞队列 LinkedBlockingQueue
LinkedBlockingQueue 内部以一个链式结构(链接节点)对其元素进行存储。如果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。
LinkedBlockingQueue 内部以 FIFO(先进先出)的顺序对元素进行存储。队列中的头元素在所有元素之中是放入时间最久的那个,而尾元素则是最短的那个。
具有优先级的阻塞队列 PriorityBlockingQueue
PriorityBlockingQueue 类实现了 BlockingQueue 接口。
一个无界阻塞队列,它使用与类 PriorityQueue 相同的顺序规则,并且提供了阻塞获取操作。虽然此队列逻辑上是无界的,但是资源被耗尽时试图执行 add 操作也将失败(导致OutOfMemoryError)。此类不允许使用 null 元素。依赖自然顺序的优先级队列也不允许插入不可比较的对象(这样做会导致抛出 ClassCastException)。
此类及其迭代器可以实现 Collection 和 Iterator 接口的所有可选 方法。iterator() 方法中提供的迭代器并不 保证以特定的顺序遍历 PriorityBlockingQueue 的元素。如果需要有序地进行遍历,则应考虑使用 Arrays.sort(pq.toArray())。此外,可以使用方法 drainTo 按优先级顺序移除 全部或部分元素,并将它们放在另一个 collection 中。
在此类上进行的操作不保证具有同等优先级的元素的顺序。如果需要实施某一排序,那么可以定义自定义类或者比较器,比较器可使用修改键断开主优先级值之间的联系。例如,以下是应用先进先出 (first-in-first-out) 规则断开可比较元素之间联系的一个类。要使用该类,则需要插入一个新的 FIFOEntry(anEntry) 来替换普通的条目对象。
同步队列 SynchronousQueue
SynchronousQueue 类实现了 BlockingQueue 接口。
SynchronousQueue 是一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素。
据此,把这个类称作一个队列显然是夸大其词了。它更多像是一个汇合点
阻塞双端队列 BlockingDeque
java.util.concurrent 包里的 BlockingDeque 接口表示一个线程安放入和提取实例的双端队列。
BlockingDeque 类是一个双端队列,在不能够插入元素时,它将阻塞住试图插入元素的线程;在不能够抽取元素时,它将阻塞住试图抽取的线程。
deque(双端队列) 是 “Double Ended Queue” 的缩写。因此,双端队列是一个你可以从任意一端插入或者抽取元素的队列。
链阻塞双端队列 LinkedBlockingDeque
一个基于已链接节点的、任选范围的阻塞双端队列。
可选的容量范围构造方法参数是一种防止过度膨胀的方式。如果未指定容量,那么容量将等于 Integer.MAX_VALUE。只要插入元素不会使双端队列超出容量,每次插入后都将动态地创建链接节点。
大多数操作都以固定时间运行(不计阻塞消耗的时间)。异常包括 remove、removeFirstOccurrence、removeLastOccurrence、contains、iterator.remove() 以及批量操作,它们均以线性时间运行。