转自:http://blog.csdn.net/johnstrive/article/details/50667557
说在前面
本文绝大部分参考《JAVA高并发程序设计》,类似读书笔记和扩展。
走入并行世界
概念
同步(synchronous)与异步(asynchronous)
同步和异步通常来形容一次方法调用。同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续执行任务。
异步方法更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的工作。异步方法通常会在另外的线程中“真实”的执行。整个过程不会阻碍调用者的工作。
并发(concurrency)和并行(parallelism)
链接:并发Concurrent与并行Parallel的区别
临界区
临界区表示一种公共资源或者说是共享资源,可以被多个线程使用。但是每一次只能有一个线程使用它,一旦临界区资源被占用,其他线程要想得到这个资源就必须等待。
在并行程序中。临界区资源是保护对象。就比如大家公用的一台打印机,必然是一个人打完另一个人的才能打印,否则就会出乱子。
阻塞(blocking)与非阻塞(non-blocking)
阻塞和非阻塞通常来形容多线程间的相互影响。比如一个线程占用了临界区资源,那么其他所有需要这个资源的线程都需要在临界区中等待。等待会导致线程挂起,这种情况就是阻塞。此时如果占用这个资源的线程一直不愿释放资源,那么其他所有阻塞在这个临界区上的线程都不能工作。
反之就是非阻塞,它强调没有一个线程可以妨碍其他线程执行。所有线程都会尝试不断前向执行。
死锁(deadlock)、饥饿(starvation)和活锁(livelock)
这三种情况都属于线程活跃性问题。如果发现上述情况,那么相关线程可能就不再活跃,也就是说它可能很难再继续执行任务了。
1 死锁
应该是最糟糕的情况之一。它们彼此相互都占用着其他线程的资源,都不愿释放,那么这种状态将永远维持下去。
死锁是一个很严重的问题,应该避免和小心。就如4辆小汽车,互相都占用对方的车道,无法正常行驶。
2 饥饿
是指一个或多个线程因为种种原因一直无法得到所需要的资源,导致一直无法执行,比如它的线程优先级太低,高优先级的线程一直抢占它所需要的资源。另一种可能是某一个线程一直占用着关键资源不放,导致其他需要这个资源的线程一直无法得到这个资源,无法正常执行。与死锁相比,饥饿还是可能在一段时间内解决的,比如高优先级的线程执行完任务后,不在抢占资源,资源得到释放。
3 活锁
是非常有趣的情况,也是最难解决的情况。这就比如,大家在一个两人宽的桥上走路,双方都很有礼貌。都在第一时间礼让对方,一个往左一个往右,导致两人都无法正常通行。放到线程中,就体现为,两个线程都拿到资源后都主动释放给他人使用,那么就会出现资源不断的在两个线程中跳动,而没有一个线程可以拿到资源后正常执行,这个就是活锁。
并发级别
由于临界区的存在,多线程之间的并发必须受到控制。根据控制并发的策略,我们可以把并发的级别进行分类,大致上可以分为阻塞、无饥饿、无障碍,无锁和无等待几种。
阻塞(blocking)
一个线程是阻塞的,那么在其他线程释放资源之前,当前线程无法继续执行。当我们使用synchronized关键字,或者重入锁时,我们得到的就是阻塞的线程。
无论是synchronized还是重入锁,都会在视图执行后续代码前得到临界区的锁,如果得不到,线程就会被挂起等待,直到占有了所需要的资源为止。
无饥饿
如果线程间是有优先级的,那么线程调用总是会倾向于满足高优先级的线程。也就是说对同一个资源的分配是不公平的。对于非公平的锁来说,系统允许高优先级的线程插队,这样有可能导致低优先级的线程产生饥饿。但如果锁是公平的,满足先来后到,那么饥饿就不会产生,不管新来的线程优先级多高,要想获得资源就必须排队。那么所有的线程都有机会执行。
无障碍(obstruction-Free)
无障碍是一种最弱的非阻塞调度。两个线程如果是无障碍的执行,那么他们不会因为临界区的问题导致一方被挂起。大家都可以大摇大摆进入临界区工作。那么如果大家都修改了共享数据怎么办呢?对于无障碍的线程来说,一旦出现这种情况,当前线程就会立即对修改的数据进行回滚,确保数据安全。但如果没有数据竞争发生,那么线程就可以顺利完成自己的工作,走出临界区。
如果阻塞控制的方式比喻成悲观策略。也就是说系统认为两个线程之间很有可能发生不幸的冲突,因此,保护共享数据为第一优先级。相对来说,非阻塞的调度就是一种乐观策略,他认为多线程之间很有可能不会发生冲突,或者说这种概率不大,但是一旦检测到冲突,就应该回滚。
从这个策略来看,无障碍的多线程程序不一定能顺利执行。因为当临界区的字眼存在严重的冲突时,所有线程可能都进行回滚操作,导致没有一个线程可以走出临界区。所以我们希望在这一堆线程中,至少可以有一个线程可以在有限时间内完成自己的操作,至少这可以保证系统不会再临界区进行无线等待。
一种可行的无障碍实现可以依赖一个“一致性标记”来实现。线程在操作之前,先读取并保持这个标记,在操作完后,再次读取,检查这个标记是否被修改过,如果前后一致,则说明资源访问没有冲突。如果不一致,则说明资源可能在操作过程中与其他写线程冲突,需要重试操作。任何对保护资源修改之前,都必须更新这个一致性标记,表示数据不安全。
无锁(lock-free)
无锁的并行都是无障碍的。在无锁的情况下,所有的线程都能尝试对临界区的资源进行访问,但不同的是,无锁的并发保证必然有一个线程能够在有限步内完成操作离开临界区。
在无锁的调度中,一个典型的特点是可能会包含一个无穷循环。在这个循环中线性不断尝试修改共享数据。如果没有冲突,修改成功,那么线程退出,否则尝试重新修改。但无论如何,无锁的并行总能保证有一个线程可以胜出,不至于全军覆没。至于临界区中竞争失败的线程,则不断重试。如果运气不好,总是不成功,则会出现类似饥饿的现象,线程会停止不前。
无等待(wait-free)
无锁是要求至少有一个线程在有限步内完成操作,而无等待则是在无锁的基础之上进一步扩展。他要求所有线程都必须在有限步内完成操作。这样就不会引起饥饿问题。如果限制这个步骤上限,还可以分为有界无等待和线程无关的无等待几种,它们之间的区别只是对循环次数的限制不同。
一种典型的无等待结构是RCU(read-copy-update)。它的基本思想是,对数据的读可以不加控制,因此所有读线程都是无等待的,它们既不会被锁定等待也不会引起任何冲突。但在写数据时,先取得原始数据的副本,接着只修改副本数据,修改完后,在合适的时机回写数据。
有关并行的两个重要定律
Amdahl定律
加速比定义:加速比= 优化前系统耗时/优化后系统耗时
根据Amdahl定律,使用多核CPU对系统进行优化,优化的效果取决于CPU的数量以及系统中串行程序的比重。CPU数量越多,串行化比重越低,则优化效果越好。仅提高CPU核数不降低系统串行程序比重,也无法提高系统性能。
Gustafson定律
根据Gustafson定律,我们更容易发现,如果串行化比例很小,并行化比例很大,那么加速比就是处理器的个数。只要不断增加CPU核数,就可以提高系统性能。
JAVA内存模型(JMM)
由于并发程序要比串行程序复杂的多,其中一个重要的原因是并发程序下数据访问的一致性和安全性将受到严重的挑战。因此我们需要在深入了解并行机制之前,再定义一种规则,保证多线程程序可以有效的,正确的协同工作。而JMM也就为此而生。JMM的关键技术点都是围绕多线程的原子性、可见性和有序性来建立的。
原子性(atomicity)
指一个操作是不可中断的。即使多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
比如对一个静态变量int i赋值,A线程赋值1,B线程赋值-1,那么这个变量i的结果可能是1或者-1,没有其他情况。这就是原子性。
但如果是给一个long型赋值的话就没那么幸运了。在32位系统下,long型数据的读写不是原子性的(因为long有64位)。
在32位的java虚拟机上运行如下例子,就会出现非原子性的问题了。
public class E1 {
public static long t=0;
public static class ChangT implements Runnable{
private long to;
public ChangT(long to) {
this.to = to;
}
@Override
public void run() {
while (true){
E1.t = to;
Thread.yield();
}
}
}
public static class ReadT implements Runnable{
@Override
public void run() {
while (true){
long tmp = E1.t;
if (tmp != 111L && tmp != -999L && tmp != 333L && tmp != -444L)
System.out.println(tmp);
Thread.yield();
}
}
}
public static void main(String[] a){
new Thread(new ChangT(111L)).start();
new Thread(new ChangT(-999L)).start();
new Thread(new ChangT(333L)).start();
new Thread(new ChangT(-444L)).start();
new Thread(new ReadT()).start();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
理想的结果可能是什么都不输出,但是,一旦运行,就会有大量的输出一下信息
...
-4294966963
4294966852
-4294966963
...
我们可以看到读取线程居然读取到不可能存在的数据。因为32为系统中的long型数据的读和写不是原子性的,多线程之间互相干扰了。
如果我们给出结果中几个数值的2进制,大家就会更清晰的认识了。
-999 = 1111111111111111111111111111111111111111111111111111110000011001
-444 = 1111111111111111111111111111111111111111111111111111111001000100
111 = 0000000000000000000000000000000000000000000000000000000001101111
333 = 0000000000000000000000000000000000000000000000000000000101001101
4294966852 = 0000000000000000000000000000000011111111111111111111111001000100
-4294967185 = 1111111111111111111111111111111100000000000000000000000001101111
上面这几个数值的补码形式,也是在计算机内真实存储的内容。不难发现4294966852其实是111或333的前32为夹杂着-444的后32位的数据。而-4294967185其实是-999或-444夹杂111后32位的数据。换句话说,由于并行的关系数字被写乱了。或者读的时候读串位了。
通过这个例子,大家应该对原子性应该有基本的认识。
可见性(visibility)
可见性是指当一个线程修改了一个共享变量。其他线程是否可以立即知道这个修改。对于串行程序来说这个问题是不存在的。但这个问题在并行程序中就很有可能出现。如果一个线程修改了某一个全局变量。其他线程未必可以马上知道这个修改。如果CPU1和CPU2上各运行了一个线程,它们共享变量t。由于编译器优化或者硬件优化缘故。在CPU1上的线程将变量t进行了优化,将其缓存在cache中或者寄存器里。这种情况下如果CPU2上的某个线程修改了t的实际值,那么CPU1上的线程可能就无法意识到这个改动,依旧会读取cache或者寄存器中的旧值。因此就产生了可见性的问题。可见性问题在并行程序中也是需要重点关注的问题之一。
可见性问题是一个综合性问题,处理上述提到的缓存优化和硬件优化会导致可见性问题外,指令重排以及编译器的优化,都有可能导致这个问题。
附两个例子便于理解可见性问题。
有序性(ordering)
有序性是三个问题中最难理解的,对于一个线程的执行代码而言,我们总是习惯性的认为代码的执行是从先往后的,依次执行的。这么理解也不能完全说是错误的。在一个线程的情况下确实是从先往后。但是在并发时,程序的执行就可能出现乱序,写在前面的代码可能会后执行。
有序性的问题的原因是因为程序在执行的时候,可能发生指令重排,重排后的指令和原指令的顺序未必一致。
指令重排有一个基本的前提是,保证串行语义的一致性。指令重排不会使串行的语义逻辑发生问题。因此在串行代码中不必担心这个问题。而在多线程间就无法保证了。
so,问题来了。为什么会指令重排呢?
这完全是基于性能考虑。
我们知道一条指令的执行是可以分很多步骤的。简单的说可以分如下几步:
- 取指 IF
- 译码和取寄存器操作数 ID
- 执行或者有效地址计算 EX
- 存储器访问 MEM
- 回写 WB
我们的汇编指令也不是一步就执行完了。在CPU的实际工作中,还是要分几步去执行的。当然,每个步骤涉及的硬件也可能不同。比如,取指会用到PC寄存器和存储器,译码会用到指令寄存器组,执行会使用ALU(算术逻辑单元(arithmetic and logic unit) 是能实现多组算术运算和逻辑运算的组合逻辑电路,简称ALU。主要功能是二进制算数运算),写回时需要寄存器组。
由于一个步骤可能使用不同的硬件完成,因此,就发明了流水线技术来执行指令。
- 指令1 IF ID EX MEM WB
- 指令2 IF ID EX MEM WB
可以看到,到两条指令执行时,第一条指令其实还未执行完,这样的好处是,假设每一步需要1毫秒,那么第2条指令需要等待5毫秒才能执行。而通过流水线技术,指令2就只需等待1毫秒。这样有了流水线就可以让CPU高效的执行。但是,流水线总是害怕被中断。流水线满载的时候性能确实相当不错,但是一旦中断,所有硬件设备都会进入停顿期,再次满载又需要几个周期,因此性能损失会比较大,所以我们就需要想办法来不让流水线中断。
之所以需要指令重排就是避免流水线中断,或尽量少的中断流水线。当然指令重排只是减少中断的一种技术,实际上CPU设计中,我们还有更多的软硬件技术来防止中断。具体大家就自己探究吧。
通过例子我们加深下理解。
示例 1 :
A = B + C
执行过程。
左边是汇编指令,LW表示load,其中LW R1,B表示把B的值加载到R1寄存器中。ADD就是加法,把R1,R2的值想加放到R3中。SW表示store,就是将R3寄存器的值保存到变量A中。
//A = B + C 执行过程
LW R1,B IF ID EX MEM WB
LW R2,C IF ID EX MEM WB
ADD R3,R1,R2 IF ID X EX MEM WB
SW A,R3 IF X ID EX MEM WB
左边是指令由上到下执行,右边是流水线情况。在ADD上的大叉表示一个中断。因为R2中的数据还没准备好,所以ADD操作必须进行一次等待。由于ADD的延迟,后面的指令都要慢一拍。
示例 2 :
a = b + c ;
d = e - f ;
执行过程如下
其实就是将中断的时间去做别的事情,如load数据。这样时间就可以规划衔接好。有点儿像项目管理中优化关键路径。由此可见,指令重排对于提高CPU处理性能是十分必要的,虽然确实带来了乱序的问题,但这点儿牺牲完全值得的。
JMM 参考资料
深入理解JVM—JVM内存模型
Java内存模型
深入理解Java内存模型之系列篇
程晓明-深入理解Java内存模型
哪些指令不能重排:
虽然java虚拟机和执行系统会对指令进行一定的重排,但是指令重排是有原则的。
- 原则基本包括以下:
1 程序顺序原则:一个线程内保证语义的串行性
Eg:
a=1;
b=a+1;
第二条语句依赖于第一条执行结果。所以不允许指令重排。
2 volatile规则:volatile变量的写,先发生与读,这保证了volatile变量的可见性。
3 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
Eg:
锁规则强调,unlock操作必然发生在后续的对同一个锁的lock之前,也就是说,
如果对一个锁解锁后,在加锁,那么加锁的动作绝对不能重排到解锁动作之前。
很显然,如果这么做,加锁行为是无法获得这把锁的。
4 传递性:A先于B,B先于C,那么A必然先于C
5 线程的start()方法先于它的每一个动作
6 线程的所有操作先于线程的终结(Thread.join())
7 线程的中断(interrupt())先于被中断线程的代码
8 对象的构造函数执行、结束先于finalize()方法
基础
线程生命周期
线程所有的状态都在Thread.State枚举类中定义
public enum State {
/**
* 表示刚刚创建的线程,这种线程还没开始执行。
**/
NEW,
/**
* 调用start()方法后,线程开始执行,处于RUNNABLE状态,
* 表示线程所需要的一切资源以及准备好。
**/
RUNNABLE,
/**
* 当线程遇到synchronized同步块,就进入了BLOCKED阻塞状态。
* 这时线程会暂停执行,直到获得请求的锁。
**/
BLOCKED,
/**
* WAITING和TIMED_WAITING都表示等待状态,他们是区别是WAITING表示进入一个无时间限制的等待
* TIMED_WAITING会进入一个有时间限制的等待。
* WAITING的状态正是在等待特殊的事件,如notify()方法。而通过join()方法等待的线程,则是等待目标线程的终止。
* 一旦等到期望的时间,线程就会继续执行,进入RUNNABLE状态。
* 当线程执行完后进入TERMINATED状态,表示线程执行结束。
**/
WAITING,
TIMED_WAITING,
TERMINATED;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
线程的基本操作
启动初始化及基本方法
参考 多线程基础
终止线程
一个线程执行完后会结束,无须手动关闭,但是如一些系统性服务的线程基本都是一个大的循环,一般情况不会终止。
如何才能正常关闭线程呢?JDK提供了一个Thread.stop方法就可以立即关闭一个线程。但是这个方法太暴力,基本不会使用。并且stop()方法也是标记要废弃的方法。stop()强行的将执行中的线程关闭,可能会造成数据不一致问题。
看图说话:
举个栗子:
public class ThreadStopExample {
public static User u = new User();
public static void main(String[] a){
/**
* 开启读取线程
*/
new Thread(new readObj(),"读--线程").start();
while (true){
Thread t = new Thread(new changeObj(),"写--线程");
t.start();
try {
/**
* 主线程sleep 150毫秒,处理业务
*/
Thread.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* 将写线程停止
*/
t.stop();
}
/**
* 执行结果:
* 观察这些值,name属性永远比id小,是因为它永远是上一次的值,就是因为stop(),无法完整的完成id和name赋值.
*
* 为什么会不一致呢?
* 因为 User 通过 changeObj()方法不断改变,当changeObj方法设置id后,需要处理其他花费100毫秒的业务.完成后设置name的值.
* 在这100毫秒中,调用changeObj()的主线程恰好执行了stop()方法,
* 虽然已经设置了User的id属性值,但User的name属性依然是上次循环的值.没来得及赋值就stop()了.
* 所以这就是为什么stop()会产生不一致问题.
*
* User{id=1455613327, name='1455613326'}
* User{id=1455613329, name='1455613328'}
* User{id=1455613331, name='1455613330'}
* User{id=1455613331, name='1455613330'}
* User{id=1455613331, name='1455613330'}
* .......
*/
}
/**
* 修改操作
*/
public static class changeObj implements Runnable{
@Override
public void run() {
while (true){
synchronized(u){
int v = (int) (System.currentTimeMillis()/1000);
u.setId(v);
try {
/**
* sleep 100毫秒,处理业务
*/
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
u.setName(String.valueOf(v));
}
Thread.yield();
}
}
}
/**
* 读取操作
*/
public static class readObj implements Runnable{
@Override
public void run() {
while (true) {
synchronized (u) {
/**
* 当ID 不等于 name时,打印.
*
*/
if (u.getId() != Integer.parseInt(u.getName())){
System.out.println(u);
}
}
Thread.yield();
}
}
}
public static class User{
private int id ;
private String name ;
public User() {
this.id = 0;
this.name = "0";
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
如何正确的stop,如何不写坏对象,请看修改后的代码如下,我们采用自己的方式去达到线程stop,当然还有其他更好的方案。
public static class changeObj implements Runnable{
volatile static boolean stopMe = false;
@Override
public void run() {
while (true){
if (stopMe){
System.out.println("exit by stopMe...");
break;
}
synchronized(u){
...
}
...
}
}
}
public static void main(String[] a){
while (true){
...
changeObj.stopMe = true;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
线程中断
线程中断是重要的线程协作机制,中断就是让线程停止执行,但这个停止执行非stop()的暴力方式。JDK提供了更安全的支持,就是线程中断。
线程中断并不会使线程立即停止,而是给线程发送一个通知,告诉目标线程有人希望你退出。至于目标线程接到通知后什么时候停止,完全由目标线程自行决定。这点很重要,如果线程接到通知后立即退出,我们就又会遇到类似stop()方法的老问题。
与线程有关的三个方法,
1、中断线程
public void Thread.interrupt()
说明:Thread.interrupt() 是一个实例方法,他通知目标线程中断,也就是设置中断标志位。中断标志位表示当前线程已经被中断了。
2、判断是否被中断
public boolean Thread.isInterrupted()
说明:Thread.isInterrupted() 也是实例方法,他判断当前线程是否被中断(通过检查中断标志位)
3、判断是否被中断,并清除当前中断状态
public static boolean Thread.interrupted()
说明:Thread.interrupted() 是静态方法,判断当前线程的中断状态,但同时会清除当前线程的中断标志位状态。
实例1
看起来和stopMe的手法一样,但是中断功能更为强劲,比如遇到sleep()或wait()这样的操作时,就只能用中断标识了。
public class InterruptExample {
public static void main(String [] a){
Thread t1 = new Thread("线程小哥 - 1 "){
@Override
public void run() {
while (true){
/**
* 必须得判断是否接受到中断通知,如果不写退出方法,也无法将当前线程退出.
*/
if (Thread.currentThread().isInterrupted()){
System.out.println(Thread.currentThread().getName() + " Interrupted ... ");
break;
}
Thread.yield();
}
}
};
t1.start();
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* 给目标线程发送中断通知
* 目标线程中必须有处理中断通知的代码
* 否则,就算发送了通知,目标线程也无法停止.
*/
t1.interrupt();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
实例2
public class InterruptExample {
public static void main(String [] a){
Thread t1 = new Thread("线程小哥 - 1 "){
@Override
public void run() {
while (true){
/**
* 必须得判断是否接受到中断通知,如果不写退出方法,也无法将当前线程退出.
*/
if (Thread.currentThread().isInterrupted()){
System.out.println(Thread.currentThread().getName() + " Interrupted ... ");
break;
}
try {
/**
* 处理业务逻辑花费10秒.
* 而在这时,主线程发送了中断通知,当线程在sleep的时候如果收到中断
* 则会抛出InterruptedException,如果在异常中不处理,则线程不会中断.
*
*/
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println("我错了....");
/**
* 在sleep过程中,收到中断通知,抛出异常.可以直接退出线程.
* 但如果还需要处理其他业务,则需要重新中断自己.设置中断标记位.
* 这样在下次循环的时候 线程发现中断通知,才能正确的退出.
*/
Thread.currentThread().interrupt();
}
Thread.yield();
}
}
};
t1.start();
try {
/**
* 处理业务500毫秒
* 然后发送中断通知,此时t1线程还在sleep中.
*/
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* 给目标线程发送中断通知
* 目标线程中必须有处理中断通知的代码
* 否则,就算发送了通知,目标线程也无法停止.
*/
t1.interrupt();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
等待(wait)和通知(notify)
为了支持多线程之间的协作,JDK提供了两个非常重要的等待方法wait()和nofity()方法。这两个方法并不是Thread类中的,而是Object类,这意味着任何对象都可以调用这两个方法。
比如线程A调用了obj.wait()方法,那么线程A就会停止执行而转为等待状态,进入obj对象的等待队列。这个等待队列可能有多个线程,因为系统运行多个线程同时等待同一个对象。其他线程调用obj.notify()方法时,它就会从等待队列中随机选择一个线程并将其唤醒。注意着个选择是不公平的,是随机的。
obj.wait()方法并不是可以随便调用。他必须包含在对应的synchronized语句中。无论是wait还是notify都必须首先获得目标对象的一个监视器。而正确执行wait方法后,会释放这个监视器,这样其他等待obj上的线程才能获得这个监视器,不至于全部无法执行。
在调用obj.notify()前,同样也必须获得obj的监视器,索性wait方法已经释放了监视器。唤醒某个线程后(假设唤醒了A),A线程要做的第一件事并不是执行后续的代码,而是要尝试重新获得obj监视器。而这个监视器也正是A执行wait方法前所只有的那个obj监视器。如果暂时无法获得。A还必须要等待这个监视器。当A获得监视器后,才能真正意义上的继续执行。
注意:wait方法和sleep方法都可以让线程等待若干时间,处理wait方法可以唤醒之外,另外一个主要区别是wait方法会释放目标对象的锁,而sleep方法不会释放。
例子:
public class WaitNotifyExample {
public static void main (String [] a){
Thread a1 = new A();
Thread b1 = new B();
a1.start();
b1.start();
/**
* 执行结果:
* A start ...
* A wait for obj ...
* B start ... notify one Thread...
* B end
* 这里间隔2秒
* A end
* */
}
final static Object obj = new Object();
public static class A extends Thread{
@Override
public void run() {
synchronized (obj){
System.out.println("A start ... ");
try {
System.out.println("A wait for obj ... ");
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("A end");
}
}
}
public static class B extends Thread{
@Override
public void run() {
synchronized (obj){
System.out.println("B start ... notify one Thread...");
obj.notify();
System.out.println("B end");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
挂起(suspend)和继续执行(resume)线程
这两个方法虽然已经不推荐使用了。但是这里再提一下,不推荐使用suspend挂起线程是因为suspend挂起线程后不释放锁资源,导致其他线程想要访问这个锁资源时都会被等待。无法正常运行。而suspend挂起的线程居然还是RUNNABLE状态,这也严重影响了我们队系统当前状态的判断。
示例
public class SuspendExample {
public static Object u = new Object();
static ChangeObj c1 = new ChangeObj("T1");
static ChangeObj c2 = new ChangeObj("T2");
public static class ChangeObj extends Thread{
public ChangeObj(String name) {
super(name);
}
@Override
public void run() {
synchronized (u) {
System.out.println("Thread in : " + getName());
Thread.currentThread().suspend();
System.out.println("resume by : " + getName());
}
}
}
public static void main(String[] a) throws InterruptedException {
c1.start();
/**
* 主线程工作100毫秒,(非常关键)
* 这里的意思是
* 第一:为了演示,保证c1能抢占到资源,让主线程sleep后再启动c2
* 第二:保证c1能在执行resume的时候执行完成.这样才能保证c1本身可以有效释放资源.
* 假设c1中执行业务耗时500毫秒后 才执行suspend.(将注释[1]放开).而主线程仅仅sleep100毫秒后执行了c1.resume().
* 这样就导致c1无法释放锁,结果打印的是
* Thread in : T1
* sleep 500ms : T1
* 无法再继续走下去.
*/
Thread.sleep(100);
c2.start();
c1.resume();
/**
* 解决c2挂起无法继续的方法:
* 1 将主线程sleep1000毫秒,保证c1在1000毫秒内执行完成,
* 但是这不是最好的方法,因为c1有可能在1000毫秒内执行不完
* Thread.sleep(1000);
* 2 将c2.resume() 放到c1.join后面.
*/
c2.resume();
c1.join();
System.out.println(Thread.currentThread().getName() + " 结束工作...after c1");
c2.join();
System.out.println(Thread.currentThread().getName() + " 结束工作...after c2");
}
/**
* 错误的 结果是:
* Thread in : T1
* resume by : T1
* Thread in : T2
* main 结束工作...after c1
* 并且程序一直挂起,无法结束.
* 打印线程信息可以发现
* "T2@431" prio=5 tid=0xd nid=NA runnable
* java.lang.Thread.State: RUNNABLE
* at java.lang.Thread.suspend0(Thread.java:-1)
* at java.lang.Thread.suspend(Thread.java:1029)
* at com.iboray.javacore.Thread.T2.SuspendExample$ChangeObj.run(SuspendExample.java:31)
* - locked <0x1b3> (a java.lang.Object)
*
* "main@1" prio=5 tid=0x1 nid=NA waiting
* java.lang.Thread.State: WAITING
* at java.lang.Object.wait(Object.java:-1)
* at java.lang.Thread.join(Thread.java:1245)
* at java.lang.Thread.join(Thread.java:1319)
* at com.iboray.javacore.Thread.T2.SuspendExample.main(SuspendExample.java:75)
*
* 正确的 结果是:
* Thread in : T1
* resume by : T1
* Thread in : T2
* main 结束工作...after c1
* resume by : T2
* main 结束工作...after c2
*/
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
示例2
通过wait和notify方式实现suspend和resume效果。这种方式类似于我们自己实现stop那样
public class Suspend1Example {
public static Object u = new Object();
public static void main(String[] a) throws InterruptedException {
ChangeObj c = new ChangeObj();
ReadObj r = new ReadObj();
c.start();
r.start();
Thread.sleep(1000);
c.suspendMe();
System.out.println(" suspend ChangeObj 3s... ");
Thread.sleep(3000);
c.resumeMe();
/**
* 执行结果
* 刚开始ChangeObj与ReadObj交叉执行
in ChangeObj...
in ChangeObj...
in ReadObj...
in ChangeObj...
in ReadObj...
suspend ChangeObj 3s... 主线程执行c.suspendMe()
in ChangeObj... ...
in ChangeObj...suspend ChangeObj进入WAIT状态
in ReadObj... ReadObj独自执行
in ReadObj... ...
in ReadObj... ...
in ReadObj... ...
in ReadObj... ...
in ReadObj... ...
in ChangeObj...resume ChangeObj进入RUNNABLE状态
in ChangeObj... 随后ChangeObj与ReadObj又开始交叉执行
in ChangeObj...
in ReadObj...
in ChangeObj...
in ReadObj...
*/
}
public static class ChangeObj extends Thread{
volatile boolean suspend = false;
public void suspendMe(){
this.suspend = true;
}
public void resumeMe(){
this.suspend = false;
synchronized (this){
this.notify();
}
}
@Override
public void run() {
while (true){
synchronized (this){
if (suspend){
try {
System.out.println(" in ChangeObj...suspend ");
this.wait();
System.out.println(" in ChangeObj...resume ");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
synchronized (u){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(" in ChangeObj... ");
}
Thread.yield();
}
}
}
public static class ReadObj extends Thread{
@Override
public void run() {
while (true){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (u){
System.out.println(" in ReadObj... ");
}
Thread.yield();
}
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
等待线程结束(join)和谦让(yield)
一个线程的输入可能依赖于另一或者多个线程的输出,此时,这个线程就需要等待依赖线程执行完毕,才能继续执行,JDK提供了join操作来实现这个功能。方法签名:
public final void join() throws InterruptedException
//包含最大等待时机,如果超过给定时间目标线程还未执行完成,当前线程会跳出阻塞 继续执行
public final synchronized void join(long mills) throws InterruptedException
join的本质是让调用线程wait()在当前线程对象实例上。当执行完成后,被等待的线程会在退出前调用notifyAll()通知所有的等待线程继续执行。因此,需要注意,不要在应用程序中,在Thread上使用类似wait()或者notify()等方法,因为这很有可能影响系统API的工作,或者被系统API锁影响
yield是一个静态方法,一旦执行,它会使当前线程让出CPU,然后继续加入争抢CPU的线程中。
volatile与JMM
当我们使用volatile来修饰变量,就等于告诉虚拟机这个变量极有可能被某些程序或者线程修改。为了确保这个变量修改后,应用程序范围内的所有线程都能够看到。虚拟机就必须采用一些特殊的手段保证这个变量的可见性。这样就可以解决之前咱们在32位虚拟机上用多线程修改long 的值了。
volatile并不代表锁,他无法保证一些符合操作的原子性。他只能保证一个线程修改了数据后,其他线程能够看到这个改动,但当多个线程同时修改某一个数据时,却依然会产生冲突。他只能保证单个变量的完整性和可见性。保证原子性还的靠类似synchronized方式去解决。
线程组
如果线程数量很多,而且功能分配比较明确,就可以将相同的线程放置在一个线程组里面。
public class ThreadGroupName implements Runnable{
public static void main(String[] a){
ThreadGroup threadGroupName = new ThreadGroup("printGroup");
Thread t1 = new Thread(threadGroupName,new ThreadGroupName(),"T1");
Thread t2 = new Thread(threadGroupName,new ThreadGroupName(),"T2");
t1.start();
t2.start();
System.out.println("printGroup线程组 活动线程数 : " + threadGroupName.activeCount());
threadGroupName.list();
/**
* printGroup线程组 活动线程数 : 2
* This is printGroup : T1
* java.lang.ThreadGroup[name=printGroup,maxpri=10]
* Thread[T1,5,printGroup]
* Thread[T2,5,printGroup]
* */
}
@Override
public void run() {
String groupName =
Thread.currentThread().getThreadGroup().getName() + " : "
+ Thread.currentThread().getName();
while (true){
System.out.println(" This is " + groupName);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread.yield();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
守护线程(daemon)
守护线程是一个特殊的线程,他在后台完成系统性的服务,比如垃圾回收等等。用户线程可以理解为系统工作线程,他们会完成业务操作。当用户线程全部结束后,系统就无事可做了。守护线程守护的对象也不存在了。因此当一个程序中就只有守护线程时,java虚拟机就会自然退出。
public class DaemonExample extends Thread{
@Override
public void run() {
while (true){
System.out.println("I am a Daemon Thread .. ");
try {
Thread.sleep(500);
System.out.println("I am a Daemon Thread ..after sleep ");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] a) throws InterruptedException {
Thread t = new DaemonExample();
t.setDaemon(true);
t.start();
Thread.sleep(2000);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
线程优先级
优先级高的线程在竞争资源的时候回更加有优势,更可能抢占到资源,当然这只是一个概率问题,高优先级的线程可能也会抢占失败。他和线程优先级调度以及底层操作系统有密切关系。在各平台上表现不一。
线程安全与synchronized
非线程安全写入例子
就算是volatile修饰的变量,也无法保证正确写入,要从根本上解决这个问题,我们就必须保证多个线程之间是完全同步的。也就是Thread1在写入时,Thread2既不能读也不能写。这时我们就得通过synchronized关键字来解决了。它的工作是对同步代码加锁,使得每一次,只能有一个线程进入同步块。从而保证线程间的安全性,说白了就是让并行程序串行执行。
synchronized用法
1、指定加锁对象:对给定对象加锁,进入同步代码块要获得给定对象的锁。
public class SynchronizedExample implements Runnable{
static final Object o = new Object();
static int a = 0;
@Override
public void run() {
for (int i = 0;i < 10000 ; i ++){
synchronized (o){
a++;
}
}
}
public static void main(String[] ac ) throws InterruptedException {
Thread t1 = new Thread(new SynchronizedExample());
Thread t2 = new Thread(new SynchronizedExample());
Thread t3 = new Thread(new SynchronizedExample());
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println("add result : " + a);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
2、直接作用于实例方法:相当于对当前实例加锁,进入同步代码块要获得当前实例的锁。
public class SynchronizedExample implements Runnable{
static int a = 0;
@Override
public void run() {
for (int i = 0;i < 10000 ; i ++){
synchronized (this){
a++;
}
}
}
public static void main(String[] ac ) throws InterruptedException {
SynchronizedExample aca = new SynchronizedExample();
Thread t1 = new Thread(aca);
Thread t2 = new Thread(aca);
Thread t3 = new Thread(aca);
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println("add result : " + a);
}
}
/**
* 结果是 :
* add result : 30000
*/
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
3 、 直接作用于静态方法:相当于对当前类加锁,进入同步块要获得当前类的锁。
说明: 这个锁的影响范围更广,只要是调用这个类的方法,都必须拿到这个类的锁。
3.1 、 加到静态方法上
public class SynchronizedExample implements Runnable{
static int a = 0;
@Override
public void run() {
for (int i = 0;i < 10000 ; i ++){
add();
}
}
public static synchronized void add(){
a++;
}
public static void main(String[] ac ) throws InterruptedException {
Thread t1 = new Thread(new SynchronizedExample());
Thread t2 = new Thread(new SynchronizedExample());
Thread t3 = new Thread(new SynchronizedExample());
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println("add result : " + a);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
3.2 、 加到类上
public class SynchronizedExample implements Runnable{
static int a = 0;
@Override
public void run() {
for (int i = 0;i < 10000 ; i ++){
synchronized (SynchronizedExample.class){
a++;
}
}
}
public static void main(String[] ac ) throws InterruptedException {
....
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
实例1
public class SynchronizedExample implements Runnable{
static SynchronizedExample obj = new SynchronizedExample();
static int a = 0;
@Override
public void run() {
for (int i = 0;i < 10000 ; i ++){
add();
}
}
public synchronized void add(){
a ++ ;
}
public static void main(String[] ac ) throws InterruptedException {
/**
* 注意Thread的创建方式,这里使用Runnable创建两个线程,并且这两个线程
* 都指向同一个Runnable接口实例(obj对象),这样才能保证两个线程在工作时
* 能够关注到同一个对象的锁上去.从而保证线程安全
*
* 而以下同步方法是错误的,因为创建的线程不是关注在同一个对象锁上.解决方法看示例2
* Thread t1 = new Thread(new SynchronizedExample());
* Thread t1 = new Thread(new SynchronizedExample());
*/
Thread t1 = new Thread(obj);
Thread t2 = new Thread(obj);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("add result : " + a);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
示例2
public class SynchronizedExample implements Runnable{
static SynchronizedExample obj = new SynchronizedExample();
static int a = 0;
@Override
public void run() {
for (int i = 0;i < 10000 ; i ++){
add();
}
}
public static synchronized void add(){
a ++ ;
}
public static void main(String[] ac ) throws InterruptedException {
/**
* 将synchronized作用到静态方法,即使两个线程指向不同的Runnable对象,
* 但由于方法块需要请求的是当前类的锁,而非当前实例,因此线程间还是可以同步的.
*/
Thread t1 = new Thread(new SynchronizedExample());
Thread t2 = new Thread(new SynchronizedExample());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("add result : " + a);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
除了线程同步,确保线程安全外,synchronized还可以保证线程见的可见性和有序性。
并发下的ArrayList
直接看示例
public class ArrayListExample implements Runnable{
static ArrayList arrayList = new ArrayList<>();
@Override
public void run() {
for (int i=0;i<100000;i++){
arrayList.add(i);
}
}
public static void main(String[] a) throws InterruptedException {
Thread t1 = new Thread(new ArrayListExample());
Thread t2 = new Thread(new ArrayListExample());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("ArrayList size : " + arrayList.size());
/**
* 执行结果:
*
* 结果一:
* ArrayList size : 100004
* Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: 49
* at java.util.ArrayList.add(ArrayList.java:459)
* at com.iboray.javacore.Thread.T2.ArrayListExample.run(ArrayListExample.java:15)
* at java.lang.Thread.run(Thread.java:745)
* 这是因为ArrayList在扩容过程中,内部一致性被破坏,但由于没有锁的保护,
* 另外一个线程访问到了不一致的内部状态,导致越界错误.
* 结果二:
* ArrayList size : 199368
* ArrayList大小小于200000,这是因为两个线程同时对ArrayList中的同一个位置进行赋值导致的.
* 而Vector却不会有这个问题
*/
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
错误的加锁
将锁加在int类型上。
public void run() {
for (int i = 0;i < 10000 ; i ++){
synchronized (i){
a ++ ;
}
}
}*/
似乎加锁的逻辑没问题,但是Integer在java中属于不变对象,也就是对象一旦创建就不可修改了。和String一样。所以这里的i每次都是一个新的integer对象,锁都加到了不同的对象上。
for循环的i每次实际上是使用了Integer.valueOf()方法创建一个新的integer对象,并将它赋值为变量i。也就是i = Integer.valueOf(i.intValue() + 1);
JDK并发包
同步控制
重入锁ReentrantLock
重入锁可以完全替代synchronized关键字,并且性能也好于synchronized。但从JDK6.0开始,synchronized的性能有所提升,两者在性能上差不多。
需要注意的是使用重入锁时,我们必须指定何时上锁,何时释放。正因为这也,重入锁对逻辑控制的灵活性要远远好于synchronized。在退出临界区时,务必释放锁。否则 后果你懂得。
之所以叫重入锁,是因为一个线程可以两次获得同一把锁,在释放的时候也必须释放相同次数的锁。
示例
public class ReentrantLockExample implements Runnable{
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0;
@Override
public void run() {
for (int j=0;j<10000;j++){
lock.lock();
lock.lock();
lock.lock();
try {
i ++;
} finally {
lock.unlock();
lock.unlock();
lock.unlock();
}
}
}
public static void main(String[] a) throws InterruptedException {
ReentrantLockExample re = new ReentrantLockExample();
Thread t1 = new Thread(re);
Thread t2 = new Thread(re);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.print(i);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
中断响应ReentrantLock.lockInterruptibly()
对synchronized来说,如果一个线程在等待锁,那么结果只有两种,要么它得到了这把锁,要么它保持等待。而重入锁则提供了另外一种可能,那就是线程可以在等待的过程中中断,我们可以根据需要取消对锁的请求。也就是说,如果一个线程正在等待锁,那么它依然可以收到一个通知,被告知无须等待,可以停止了。
lockInterruptibly()方法是一个可以对中断进行响应的锁申请动作,即在等待锁的过程中,中断响应。
示例
public class ReentrantLockExample1 implements Runnable{
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lock;
/**
* 控制加锁顺序,制造死锁
* @param lock
*/
public ReentrantLockExample1(int lock) {
this.lock = lock;
}
@Override
public void run() {
try {
/**
* 1号线程,先占用 1号锁,再申请 2号锁
* 2号线程,先占用 2号锁,再申请 1号锁
* 这样就很容易造成两个线程相互等待.
*/
if (lock == 1){
//加入优先响应中断的锁
lock1.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + " 进入...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* 这时候,1号线程 想要持有 2号锁 ,但是2号线程已经先占用了2号锁,所以1 号线程等待.
* 2号线程也一样,占用着2号锁 不释放,还想申请1号锁,而1号锁 被1号线程占用且不释放.
*/
lock2.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + " 完成...");
}else {
lock2.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + " 进入...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock1.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + " 完成...");
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 被中断,报异常...");
e.printStackTrace();
} finally {
if (lock1.isHeldByCurrentThread()) {
System.out.println(Thread.currentThread().getName() + " 释放...");
lock1.unlock();
}
if (lock2.isHeldByCurrentThread()) {
System.out.println(Thread.currentThread().getName() + " 释放...");
lock2.unlock();
}
System.out.println(Thread.currentThread().getName() + " 线程退出...");
}
}
public static void main(String[] a) throws InterruptedException {
ReentrantLockExample1 re1 = new ReentrantLockExample1(1);
ReentrantLockExample1 re2 = new ReentrantLockExample1(2);
Thread t1 = new Thread(re1," 1 号线程 ");
Thread t2 = new Thread(re2," 2 号线程 ");
t1.start();
t2.start();
//主线程sleep 2秒,让两个线程相互竞争资源.造成死锁
Thread.sleep(2000);
//中断2号线程
t2.interrupt();
/* 执行结果:
1 号线程 进入...
2 号线程 进入...
2 号线程 被中断,报异常... // 执行 t2.interrupt();
java.lang.InterruptedException
2 号线程 释放...
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
2 号线程 线程退出...
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
1 号线程 完成... // 只有1号线程能执行完成
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
1 号线程 释放...
at com.iboray.javacore.Thread.T3.ReentrantLockExample1.run(ReentrantLockExample1.java:55)
1 号线程 释放...
at java.lang.Thread.run(Thread.java:745)
1 号线程 线程退出...
*/
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
锁申请等待限时ReentrantLock.tryLock
除了等待外部通之外,避免死锁还有另外一种方法,就是限时等待,给定一个等待时间让线程自动放弃。
tryLock(时长,计时单位)
,若超过设定时长还没得到锁就返回false,若成功获得锁就返回true。
tryLock()
,若没有参数,当前线程会尝试获得锁,如果申请锁成功,则返回true,否则立即返回false。这种模式不会引起线程等待,因此不会产生死锁。
示例
public class ReentrantLockExample2 implements Runnable{
public static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 申请资源...");
try {
//申请3秒,如果获取不到,返回false,退出.
if (lock.tryLock(3, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + " 获得资源,开始执行...");
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + " 执行完成...");
}else {
System.out.println(Thread.currentThread().getName() + " 申请不到资源,先走了...");
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 中断...");
e.printStackTrace();
}finally {
if (lock.isHeldByCurrentThread()) {
System.out.println(Thread.currentThread().getName() + " 释放锁...");
lock.unlock();
}
}
}
public static void main(String[] a) throws InterruptedException {
ReentrantLockExample2 re = new ReentrantLockExample2();
Thread t1 = new Thread(re," 1 号线程 ");
Thread t2 = new Thread(re," 2 号线程 ");
t1.start();
t2.start();
/*
执行结果:
1 号线程 申请资源...
2 号线程 申请资源...
1 号线程 获得资源,开始执行...
2 号线程 申请不到资源,先走了... //等待了3秒后,依然申请不到锁,就返回false
1 号线程 执行完成...
1 号线程 释放锁...
*/
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
公平锁ReentrantLock(true)
公平锁会按照实际的先后顺序,保证先到先得,它不会产生饥饿,只要排队,最终都可以等到资源。在创建重入锁时,通过有参构造函数,传入boolean类型的参数,true表示是公平锁。实现公平所必然要维护一个有序队列,所以公平锁的实现成本高,性能相对也非常低,默认情况下,锁是非公平的。
示例
public class ReentrantLockExample3 implements Runnable{
public static ReentrantLock lock = new ReentrantLock(true);
static int i = 0;
@Override
public void run() {
for (int j = 0;j<5;j++){
lock.lock();
try {
i++;
System.out.println(Thread.currentThread().getName() + " 获得锁 " + i);
} finally {
lock.unlock();
}
}
}
public static void main(String[] a) throws InterruptedException {
ReentrantLockExample3 re = new ReentrantLockExample3();
Thread t1 = new Thread(re," 1 号线程 ");
Thread t2 = new Thread(re," 2 号线程 ");
Thread t3 = new Thread(re," 3 号线程 ");
Thread t4 = new Thread(re," 4 号线程 ");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
ReentrantLock的以上几个重要的方法
lock() 获取锁,如果锁被占用,则等待
lockInterruptibly() 获取锁,但优先响应中断
tryLock() 尝试获取锁,如果成功返回true,否则返回false。该方法不等待,立即返回。
tryLock(long time,TimeUnit unit) 在给定时间内获取锁。
unlock() 释放锁。
就重入锁实现来看,他主要集中在java 层面。在重入锁实现中,主要包含三个要素:
1 原子状态。原子状态使用CAS操作来存储当前锁的状态,判断锁是否已经被别的线程持有。
2 等待队列。所有没有请求成功的线程都进入等待队列进行等待。当有线程释放锁后,系统就从当前等待队列中唤醒一个线程继续工作。
3 阻塞原语park()和unpack(),用来挂起和恢复线程。没有得到锁的线程将会被挂起。
Condition条件
Condition是与重入锁ReentrantLock相关联的,通过Lock接口的Condition newCondition()方法可以生产一个和当前重入锁绑定的Condition实例,利用Condition对象,我们就可以让线程在合适的时间等待,或者在特定的时刻得到通知继续执行。
Condition接口提供了如下基本方法
await() 会使当前线程等待,同时释放当前锁,当其他线程中使用singal()或者singalAll()方法时,线程会重新获得锁并继续执行。或者当线程被中断时,也能跳出等待。这和wait()类似。
awaitUninterruptibly()和await()方法类似,但它不会再等待过程中响应中断。
singal() 用于唤醒一个等待队列中的线程。singalAll()是唤醒所有等待线程。
示例
public class ConditionExample implements Runnable{
public static ReentrantLock rel = new ReentrantLock();
public static Condition condition = rel.newCondition();
@Override
public void run() {
try {
rel.lock();
System.out.println(Thread.currentThread().getName() + " 获取到锁...");
condition.await();
System.out.println(Thread.currentThread().getName() + " 执行完成");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
rel.unlock();
System.out.println(Thread.currentThread().getName() + " 释放锁");
}
}
public static void main(String[] a) throws InterruptedException {
ConditionExample re = new ConditionExample();
Thread t1 = new Thread(re,"1 号线程 ");
t1.start();
Thread.sleep(2000);
rel.lock();
condition.signal();
rel.unlock();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
信号量 Semaphore
Semaphore可以指定多个线程同时访问某一个资源,在构造Semaphore对象时,必须指定信号量的准入数,即同时能申请多少个许可,当每个线程每次只能申请一个许可时,就相当于有多少线程可以同时访问某个资源。
示例
public class SemaphoreExample implements Runnable {
public static final Semaphore s = new Semaphore(5);
@Override
public void run() {
try {
if (s.tryAcquire(1500, TimeUnit.SECONDS)) {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " 完成了任务..");
s.release();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] a) throws InterruptedException {
ExecutorService exec = Executors.newFixedThreadPool(20);
final SemaphoreExample re = new SemaphoreExample();
for (int i=0;i<20;i++){
exec.submit(re);
}
exec.shutdown();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
ReadWriteLock 读写锁
读写分离锁可以有效的减少所竞争,以提升系统性能。但需要注意是的线程间 读读、读写、写写中后两者依然需要互斥。
系统中,读的次数远远大于写的操作,读写锁就可以发挥最大的功效
示例
public class ReadWriteLockExample {
private static Lock lock = new ReentrantLock();
private static ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock();
private static Lock readLock = rwlock.readLock();
private static Lock writeLock = rwlock.writeLock();
private int value;
public Object HandleRead(Lock lock) throws InterruptedException {
try {
lock.lock();
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " Read...");
return value;
} finally {
lock.unlock();
}
}
public void HandleWrite(Lock lock,int index) throws InterruptedException {
try {
lock.lock();
Thread.sleep(1000);
value = index;
System.out.println(Thread.currentThread().getName() + " Write...");
}finally {
lock.unlock();
}
}
public static void main(String[] a ) throws InterruptedException {
final ReadWriteLockExample rwle = new ReadWriteLockExample();
Runnable readR = new Runnable() {
@Override
public void run() {
try {
rwle.HandleRead(readLock);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Runnable writeR = new Runnable() {
@Override
public void run() {
try {
rwle.HandleWrite(writeLock,new Random().nextInt());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for (int i=0;i<18;i++){
Thread s = new Thread(readR);
s.start();
}
for (int i=18;i<20;i++){
Thread s = new Thread(writeR);
s.start();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
倒计时 CountDownLatch
CountDownLatch主要用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
示例
public class CountDownLatchExample implements Runnable{
static final CountDownLatch cdl = new CountDownLatch(10);
static final CountDownLatchExample cdle = new CountDownLatchExample();
@Override
public void run() {
try {
Thread.sleep(new Random().nextInt(10) * 1000);
System.out.println(Thread.currentThread().getName() + " 部件检查完毕...");
cdl.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] a) throws InterruptedException {
ExecutorService exec = Executors.newFixedThreadPool(10);
for (int i=0;i<10;i++){
exec.submit(cdle);
}
cdl.await();
System.out.println(Thread.currentThread().getName() + " 所有检查完成,上跑道起飞...");
exec.shutdown();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
循环栅栏 CyclicBarrier
CyclicBarrier是另一种多线程并发控制工具,和CountDownLatch类似,但是它可以在计数器完成一次计数后,执行某个动作。
示例
public class CyclicBarrierExample {
public static class Soldier implements Runnable{
private String name;
private CyclicBarrier cyclicBarrier;
public Soldier(String name, CyclicBarrier cyclicBarrier) {
this.name = name;
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
try {
System.out.println(name + " 来报道..");
cyclicBarrier.await();
doWork();
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
void doWork(){
try {
Thread.sleep(new Random().nextInt(10) * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + " 任务已完成..");
}
}
public static class doOrder implements Runnable{
boolean flag;
int n;
public doOrder(boolean flag, int n) {
this.flag = flag;
this.n = n;
}
@Override
public void run() {
if (flag){
System.out.println("司令 : 士兵 " + n +"个 任务完成");
}else {
System.out.println("司令 : 士兵 " + n +"个 集合完毕");
flag = true;
}
}
}
public static void main(String[] a){
final int n = 10;
boolean flag = false;
Thread[] allSoldier = new Thread[n];
CyclicBarrier c = new CyclicBarrier(n, new doOrder(flag,n));
for (int i=0;i
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
线程阻塞 LockSupport
LockSupport可以在线程任意位置让线程阻塞,它弥补了由resume()在前发生导致线程无法正常执行的问题。和wait()相比,它不需要获得锁也不会抛出InterruptedException异常。
LockSupport的静态方法park()可以阻塞当前线程。还有parkNanos()、parkUntil()等方法,它们实现了一个限时的等待。
unpack()继续执行。即使unapt()操作发生在park()之前它也可以使下一次的park()操作立即返回。
示例
public class LockSupportExample {
public static final Object o = new Object();
public static class ChangeObj extends Thread{
public ChangeObj(String name) {
super(name);
}
@Override
public void run() {
synchronized (o){
System.out.println(Thread.currentThread().getName() + " coming....");
LockSupport.park();
}
}
}
public static void main(String[] a) throws InterruptedException {
Thread t1 = new ChangeObj(" T1 ");
Thread t2 = new ChangeObj(" T2 ");
t1.start();
Thread.sleep(1000);
/**
* t2开始后直接就停止,都没执行到挂起方法.随后t1.resume()方法可以正确执行,
* 但t2都没有挂起却执行了resume.导致t2永远是挂起.
* 还有一个麻烦的问题是t2的线程信息中显示居然还是RUNNABLE状态.后患无穷.
* 所以,我们采用LockSupport来替代suspend和resume
*/
t2.start();
LockSupport.unpark(t1);
LockSupport.unpark(t2);
t1.join();
t2.join();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
线程复用:线程池
多线程的软件设计方法确实可以最大限度地发挥现代处理器多核处理的计算能力,提高系统的吞吐量和性能,但是,若不加控制和管理的随意使用线程,对系统的性能反而会产生不利的影响。
大量的线程会抢占宝贵的内存资源,如果处理不当可能会导致Out ofMemory异常。大量的线程回收也会给GC带来很大的压力。
在实际生产环境中,线程的数量必须得到控制,盲目的大量创建线程对系统性能是有伤害的。
为了避免频繁的创建和销毁线程,可以让创建的线程进行复用。当完成工作时,并不着急关闭,而是将这个线程退回到线程池,方便其他人使用。
JDK对线程池的支持
ThreadPoolExecutor表示一个线程池。Executors类则扮演线程池工厂角色,通过Executors可以取得一个具有特定功能的线程池。从UML图中亦可知,ThreadPoolExecutor实现了Executor接口,因此通过这个接口,任何Runnable对象都可以被ThreadPoolExecutor线程池调度。
Executor框架提供了各种类型的线程池,主要有以下工厂方法。
public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newCachedThreadPool()
public static ScheduledExecutorService newSingleThreadScheduledExecutor()
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
以上方法返回了具有不同工作特性的线程池,具体说明如下:
1. newFixedThreadPool,返回一个固定数量的线程池。当一个新任务提交时,如果有空闲线程,则执行。否则新任务暂存在一个任务队列中,待有空闲时,便处理在任务队列中的任务。
2. newSingleThreadExecutor,返回一个线程的线程池。当多余一个新任务提交时,会暂存在一个任务队列中,待有空闲时,按先入先出的顺序处理在任务队列中的任务。
3. newCachedThreadPool,返回一个可根据实际情况调整线程数量的线程池,线程数量不确定,若有空闲,则会有限复用线程。否则创建新线程处理任务。所有线程在当前任务执行完后,将返回线程池待复用。
4. newSingleThreadScheduledExecutor,返回一个ScheduledExecutorService对象,线程池大小为1。ScheduledExecutorService在Executor接口之上扩展了在给定时间执行某任务的功能。如果在某个固定的延时之后执行,或周期性执行某个任务。可以用这个工厂。
newScheduledThreadPool,返回一个ScheduledExecutorService对象,但该线程可以指定线程数量。
固定线程池
示例:
public class ExecutorExample {
public static class MyTask implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getId() + " to do sth...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] a ){
MyTask myTask = new MyTask();
ExecutorService es = Executors.newFixedThreadPool(3);
for (int i =0;i<12;i++){
es.submit(myTask);
}
es.shutdown();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
计划任务
newScheduledThreadPool返回一个ScheduledExecutorService对象,可以根据实际对线程进行调度。
public ScheduledFuture> schedule(Runnable command,long delay, TimeUnit unit);
public ScheduledFuture> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
public ScheduledFuture> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
ScheduledExecutorService不会立即安排执行任务,他类似Linux中的crontab工具。如果任务遇到异常,则后续的所有子任务都会停止执行。因此,必须保证异常被及时处理,为周期性任务的稳定调度提供条件。
示例
public class ScheduledExecutorExample {
public static void main(String[] a){
ScheduledExecutorService ses = Executors.newScheduledThreadPool(10);
ses.scheduleAtFixedRate(new Runnable(){
@Override
public void run() {
long s = System.currentTimeMillis();
try {
System.out.println(Thread.currentThread().getId() + " start doSth...");
/**
* 在这种情况下,输出结果1
* 意思就是,执行时间大于调度时间后,线程会在上一个任务结束后,立即被调用。
*/
Thread.sleep(new Random().nextInt(10) * 1000);
long e = System.currentTimeMillis();
System.out.println(Thread.currentThread().getId() + " finish..." +( (e -s)/1000) +"s");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},1,3, TimeUnit.SECONDS);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
核心线程池的内部实现
对于核心的几个线程池,无论是newFixedThreadPool()、newSingleThreadExecutor()还是newCacheThreadPool方法,虽然看起来创建的线程具有完全不同的功能特点,但其内部均使用了ThreadPoolExecutor实现。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
由以上线程池的实现可以看到,它们都只是ThreadPoolExecutor类的封装。我们看下ThreadPoolExecutor最重要的构造函数:
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
workQueue
只提交但未执行的任务队列,它是一个BlockingQueue接口的对象,仅用于存放Runnable对象,根据队列功能分类,在ThreadPoolExecutor的构造函数中可使用以下几种BlockingQueue。
1. 直接提交的队列:该功能由synchronousQueue对象提供,synchronousQueue对象是一个特殊的BlockingQueue。synchronousQueue没有容量,每一个插入操作都要等待一个响应的删除操作,反之每一个删除操作都要等待对应的插入操作。如果使用synchronousQueue,提交的任务不会被真实的保存,而总是将新任务提交给线程执行,如果没有空闲线程,则尝试创建线程,如果线程数量已经达到了最大值,则执行拒绝策略,因此,使用synchronousQueue队列,通常要设置很大的maximumPoolSize值,否则很容易执行拒绝策略。
2. 有界的任务队列:有界任务队列可以使用ArrayBlockingQueue实现。ArrayBlockingQueue构造函数必须带有一个容量参数,表示队列的最大容量。public ArrayBlockingQueue(int capacity)
。当使用有界任务队列时,若有新任务需要执行时,如果线程池的实际线程数量小于corePoolSize,则会优先创建线程。若大于corePoolSize,则会将新任务加入等待队列。若等待队列已满,无法加入,则在总线程数不大于maximumPoolSize的前提下,创建新的线程执行任务。若大于maximumPoolSize,则执行拒绝策略。可见有界队列仅当在任务队列装满后,才可能将线程数量提升到corePoolSize以上,换言之,除非系统非常繁忙,否则确保核心线程数维持在corePoolSize。
3. 无界的任务队列:无界队列可以通过LinkedBlockingQueue类实现。与有界队列相比,除非系统资源耗尽,无界队列的任务队列不存在任务入队失败的情况。若有新任务需要执行时,如果线程池的实际线程数量小于corePoolSize,则会优先创建线程执行。但当系统的线程数量达到corePoolSize后就不再创建了,这里和有界任务队列是有明显区别的。若后续还有新任务加入,而又没有空闲线程资源,则任务直接进入队列等待。若任务创建和处理的速度差异很大,无界队列会保持快速增长,知道耗尽系统内存。
4. 优先任务队列:带有优先级别的队列,它通过PriorityBlokingQueue实现,可以控制任务执行的优先顺序。它是一个特殊的无界队列。无论是ArrayBlockingQueue还是LinkedBlockingQueue实现的队列,都是按照先进先出的算法处理任务,而PriorityBlokingQueue根据任务自身优先级顺序先后执行,在确保系统性能同时,也能很好的质量保证(总是确保高优先级的任务优先执行)。
回顾
ThreadPoolExecutor的任务调度逻辑
newFixedThreadPool()方法的实现,它返回一个corePoolSize和maximumPoolSize一样的,并使用了LinkedBlockingQueue任务队列(无界队列)的线程池。当任务提交非常频繁时,该队列可能迅速膨胀,从而系统资源耗尽。
newSingleThreadExecutor()返回单线程线程池,是newFixedThreadPool()方法的退化,只是简单的将线程池数量设置为1.
newCachedThreadPool()方法返回corePoolSize为0而maximumPoolSize无穷大的线程池,这意味着没有任务的时候线程池内没有现场,而当任务提交时,该线程池使用空闲线程执行任务,若无空闲则将任务加入SynchronousQueue队列,而SynchronousQueue队列是直接提交队列,它总是破事线程池增加新的线程来执行任务。当任务执行完后由于corePoolSize为0,因此空闲线程在指定时间内(60s)被回收。对于newCachedThreadPool(),如果有大量任务提交,而任务又不那么快执行时,那么系统变回开启等量的线程处理,这样做法可能会很快耗尽系统的资源,因为它会增加无穷大数量的线程。
使用自定义线程池时,要根据具体应用的情况,选择合适的并发队列作为任务的缓冲。当线程资源紧张时,不同的并发队列对系统行为和性能的影响均不同。
ThreadPoolExecutor核心调度代码
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
/**
* workerCountOf(c)获取当前线程池线程总数
* 当前线程数 小于 corePoolSize核心线程数时,会将任务通过addWorker(command, true)方法直接调度执行。
* 否则进入下个if,将任务加入等待队列
**/
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
/**
* workQueue.offer(command) 将任务加入等待队列。
* 如果加入失败(比如有界队列达到上限或者使用了synchronousQueue)则会执行else。
*
**/
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
/**
* addWorker(command, false)直接交给线程池,
* 如果当前线程已达到maximumPoolSize,则提交失败执行reject()拒绝策略。
**/
else if (!addWorker(command, false))
reject(command);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
拒绝策略
线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。
JDK内置的拒绝策略如下:
1. AbortPolicy : 直接抛出异常,阻止系统正常运行。
2. CallerRunsPolicy : 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
3. DiscardOldestPolicy : 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
4. DiscardPolicy : 该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。
以上内置拒绝策略均实现了RejectedExecutionHandler接口,若以上策略仍无法满足实际需要,完全可以自己扩展RejectedExecutionHandler接口。RejectedExecutionHandler的定义如下。
public interface RejectedExecutionHandler {
/**
* @param r 请求执行的任务
* @param executor 当前线程池
*/
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
实例1:
/**
* 这个案例中,MyTask处理任务需要100毫秒,必然会在执行机构任务后,执行拒绝策略,导致大量的任务直接丢弃.
* 在实际应用中,我们可以将更详细的信息记录在日志中,来分析系统的负载和任务丢失的情况.
*
*
*/
public class RejectedPolicyExample {
public static class MyTask implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getId() + " coming...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] a) throws InterruptedException {
ExecutorService es = new ThreadPoolExecutor(
5,
5,
0L,
TimeUnit.SECONDS, new ArrayBlockingQueue(10)
/**
* 注释块 2
* 自定义拒绝策略.
* 我们不抛出异常,因为万一在任务提交端没有进行异常处理
* 则有可能使得整个系统崩溃,这不是我们希望遇到的.这比只内置的discardPolicy高级一点点
* 查看 结果2
*/
/*,new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println(r.toString() + "discard...");
}
}*/);
MyTask myTask = new MyTask();
for (int i =0;i<100;i++){
/**
* 注释块 3
* 如果没有拒绝策略,则很有可能抛出异常,在这里进行捕获,保证系统一直执行完成.
* 查看 结果3
*/
// try {
es.submit(myTask);
// } catch (Exception e) {
// System.out.println(" i am discard...");
// }
Thread.sleep(10);
}
es.shutdown();
}
/*
* 结果1:
*
* 注释块2 和注释块3 都注释.
*
* 运行一段时间后抛出异常,
* 继续执行等待队列中保存的任务,
* 执行完后无法关闭线程池,主线程一直保持.
*
* 11 coming...
12 coming...
13 coming...
14 coming...
Exception in thread "main" java.util.concurrent.RejectedExecutionException:
Task java.util.concurrent.FutureTask@63961c42 rejected from
java.util.concurrent.ThreadPoolExecutor@65b54208
[Running, pool size = 5, active threads = 5, queued tasks = 10, completed tasks = 10]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution
(ThreadPoolExecutor.java:2047)
.......
报错后,执行10个任务,线程都处于WAITING状态,无法继续执行.
10 coming...
11 coming...
12 coming...
13 coming...
14 coming...
10 coming...
11 coming...
12 coming...
13 coming...
14 coming...
最后打印出线程信息如下:
........
"pool-1-thread-5"
java.lang.Thread.State: WAITING (parking)
...
"pool-1-thread-4"
java.lang.Thread.State: WAITING (parking)
...
"pool-1-thread-3"
java.lang.Thread.State: WAITING (parking)
...
"pool-1-thread-2"
java.lang.Thread.State: WAITING (parking)
...
"pool-1-thread-1"
java.lang.Thread.State: WAITING (parking)
...
* */
/*
* 结果2:
* 直到运行完成,并且线程池关闭.
*
* 0 coming...
11 coming...
12 coming...
13 coming...
14 coming...
java.util.concurrent.FutureTask@77459877discard....
....
java.util.concurrent.FutureTask@33c7353adiscard...
10 coming...
11 coming...
...
* */
/*
* 结果3:
* 直到运行完成,并且线程池关闭.
* 执行一段时间后,可以看到线程是5个5个执行的.
* 因为设置了核心线程数和最大线程数都是是5个.
*
* 14 coming...
10 coming...
11 coming...
12 coming...
13 coming...
14 coming...
i am discard...
i am discard...
....
10 coming...
11 coming...
....
12 coming...
* */
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
自定义线程创建ThreadFactory
自定义线程池
线程池的作用就是为了线程复用,也就是避免线程频繁的创建
但是,最开始的线程从何而来,就是ThreadFactory.
ThreadFactory是一个接口,它有一个方法是创建线程
Thread newThread(Runnable r);
自定义线程可以跟踪线程何时创建,自定义线程名称/组/优先级信息.
甚至可以设置为守护线程.总之自定义线程池可以让我们更加自由的设置线程池中的所有线程状态.
实例1
public class ThreadFactoryExample {
public static class MyTask implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " coming...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] a ) throws InterruptedException {
MyTask myTask = new MyTask();
ExecutorService es = new ThreadPoolExecutor(
5
, 5
, 0L
, TimeUnit.SECONDS
, new ArrayBlockingQueue(10)
, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("T " + t.getId() + "_" +System.currentTimeMillis());
t.setDaemon(true);
System.out.println("Create a Thread Name is : "+t.getName());
return t;
}
});
for (int i=0;i<10;i++){
es.submit(myTask);
}
Thread.sleep(2000);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
扩展线程池
我们想监控每个人物的执行开始时间 结束时间等细节,我们可以通过扩展ThreadPoolExecutor扩展线程池.他提供了beforExecute(),afterExecute(),和terminated()三个接口对线程池进行控制.
实例1
public class ThreadPoolExecutorExample {
public static class MyTask implements Runnable{
private String name;
public MyTask(String name) {
this.name = name;
}
@Override
public void run() {
try {
System.out.println(name + " do sth..");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] a) throws InterruptedException {
ExecutorService es = new ThreadPoolExecutor(
5
,5
,0L
, TimeUnit.SECONDS
,new LinkedBlockingDeque()) {
/**
* 创建ThreadPoolExecutor的匿名内部类的子类
* @param t the thread that will run task 将要运行任务的线程
* @param r the task that will be executed 将要执行的任务
*/
@Override
protected void beforeExecute(Thread t, Runnable r) {
System.out.println("start execute .." + ((MyTask)r).name);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
System.out.println("after execute .." + ((MyTask)r).name);
}
@Override
protected void terminated() {
System.out.println("exit execute ..");
}
};
for (int i=0;i<10 ;i++){
MyTask myTask = new MyTask("T_" + i);
es.execute(myTask);
Thread.sleep(100);
}
/**
* 他不会暴力的关闭,而会等待所有线程执行完后关闭线程
* 可以简单的理解为shutdown只是发送一个关闭信号,
* 但在shutdown之后,线程就不能再接受其他任务了.
*/
es.shutdown();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
优化线程池线程数量
线程池的大小对系统性能有一定的影响,过大或过小的线程数量都无法发挥最优的系统性能,因此要避免极大和极小两种情况。
在《java Concurrency in Practice》中给出了一个估算线程池大小的经验公式:
Ncpu = CPU数量
Ucpu = 目标CPU的使用率(0 ≤ Ucpu ≤ 1 )
W/C = 等待时间与计算时间的比率
最优的池大小等于
Nthreads = Ncpu * Ucpu * (1+W/C)
在java中可以通过Runtime.getRuntime().availableProcessors()
取得可用CPU数量。
JDK并发容器
JDK除了提供主语同步控制,线程池等基本工具外,为了提高大家的效率,还未大家准备了一批好用的容器类
包括链表,队列,HashMap等.它们都是线程安全的.
ConcurrentHashMap : 一个高效的线程安全的HashMap
CopyOnWriteArrayList : 在读多写少的场景中,性能非常好,远远高于vector.
ConcurrentLinkedQueue : 高效并发队列,使用链表实现,可以看成线程安全的LinkedList.
BlockingQueue : 一个接口,JDK内部通过链表,数组等方式实现了这个接口,表示阻塞队列,非常适合用作数据共享通道.
ConcurrentSkipListMap : 跳表的实现,这是一个Map,使用跳表数据结构进行快速查找.
另外Collections工具类可以帮助我们将任意集合包装成线程安全的集合.
线程安全的HashMap
大家都制定HashMap在多线程环境中是线程不安全的,会产生相互引用的错误.
通过Collections.synchronizedMap(new HashMap<>());来包装一个线程安全的HashMap.
它使用委托,将自己所有的Map相关的功能交给HashMap实现,而自己负责包装线程安全.
它其实是通过指定对象mutex实现对这个m的互斥操作.
private static class SynchronizedMap<K,V> implements Map<K,V>, Serializable {
private final Map m;
final Object mutex;
......
public V get(Object key) {
synchronized (mutex) {return m.get(key);}
}
public V put(K key, V value) {
synchronized (mutex) {return m.put(key, value);}
}
......
}
虽然可以实现线程安全,但无论写入还是读取都需要获取mutex这把锁,所以性能不是太好,
我们倾向于使用ConcurrentHashMap来实现高并发下的安全的HashMap.
高效读写队列 ConcurrentLinkedQueue
队列Queue是常用的数据结构之一,JDK提供了一个ConcurrentLinkedQueue类用来实现高并发的队列
高效读取 CopyOnWriteArrayList
很多场景中都是读远远高于写操作,那么每次对读取进行加锁其实是一种资源浪费.根据读写锁的思想,读锁和读锁之间不冲突.
但是读操作会受到写操作的阻碍,在写操作发生时,读就必须等待,否则可能读到不一致的数据.同理读操作正在进行的时候,
程序也不能进行写入.
JDK提供了CopyOnWriteArrayList类,读取是完全不加锁的,并且写入也不会阻塞读取操作,这样一来性能大大提升了.
其实,就是在写操作时,进行一次自我复制,当List需要修改时,并不修改原有内容(这对于保证当前读线程的数据一致性非常重要)
而对原内容进行一次复制,将修改内容写入副本.写完后,再将修改完的副本替换原来的操作.这就保证了写操作不会影响读了.
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
final transient ReentrantLock lock = new ReentrantLock();
private transient volatile Object[] array;
final void setArray(Object[] a) {
array = a;
}
final Object[] getArray() {
return array;
}
private E get(Object[] a, int index) {
return (E) a[index];
}
public E get(int index) {
return get(getArray(), index);
}
.......
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
数据共享通道 BlockingQueue
解决多线程数据共享问题,可以使用 BlockingQueue 接口来实现.
public interface BlockingQueue extends Queue
具体实现类如下:
1. ArrayBlockingQueue
2. DelayedWorkQueue
3. DelayQueue
4. LinkedBlockingQueue
5. SynchronousQueue
6. BlockingDeque
7. PriorityBlockingQueue
ArrayBlockingQueue 基于数组实现,更适合做有界队列,因为可容纳的最大元素需要在创建时指定.毕竟数组动态扩展不太方便.
LinkedBlockingQueue 基于链表实现,适合做无界队列,或者边界值非常大的队列,它不会因为初始容量大,而一口气吃掉内存.
BlockingQueue之所以适合作为数据共享通道,关键还在blocking上.blocking阻塞的意思,
当服务线程(指不断获取队列中消息进行处理的线程)处理完成队列中的消息后,它如何知道吓一跳消息何时到来.
一种简单的办法是不断间隔循环和监控这个队列,但会造成不必要的资源浪费.循环周期也难以确定.而blockingQueue会
让服务线程在队列为空的时候等待,当有新消息进入队列后自动将线程唤醒.
ArrayBlockingQueue 内部元素都放置在一个对象数组中.final Object[] items;
向队列中压入元素可以使用offer()方法和put()方法,对于offer(),如果队列已经满了,它会立即返回false.这不是我们需要的.
put()方法是将元素压入队列末尾.但如果队列满了,它会一直等待,知道队列中有空闲的位置.
从队列中弹出元素可以使用pull()和take()方法,它们都是从头部获取一个元素,不同的是如果队列为空pull()直接返回null,
而take()方法会等待,知道队列内有可用元素.
因此put()方法和take()方法提现了blocking的关键.
为了做好等待和通知两件事在ArrayBlockingQueue内部定义了一些字段,当执行take()操作时,如果队列为空,则让当前线程
等待在notEmpty上,新元素入队时,则进行一次notEmpty通知.
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;
.....
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();
}
private E dequeue() {
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal();
return x;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
随机数据结构 跳表 SkipList
跳表是一种用来快速查找的数据结构,有点类似平衡树,对于平衡树的插入和删除往往可能导致平衡树进行一次全局的调整.
而对跳表的插入和删除只需要对局部进行操作即可.这样带来的好处是,在高并发情况下,如果是平衡树,那么会需要一个全局锁
来保证线程安全.而跳表只需要部分锁即可.,所以在并发数据结构中,JDK使用跳表来实现一个Map
跳表的另一个特点是随机算法,跳表本身维护了多个链表,并且链表是分层的.没上面一层链表都是下面一层的子集,一个元素插入到哪些层完全是随机的.
跳表内所有链表的元素都是排序的.查找时可以从顶链表开始,一旦发现被查找的元素大于当前链表中的值,就会转入下一层链表继续查找.也就是说搜索是跳跃式的.
跳表是一种空间换时间的算法.
使用跳表实现Map和哈希算法实现Map的另一个不同是,哈希不会保存元素的顺序,而跳表内的所有元素都是排序的.因此对跳表遍历时会得到一个有序的结果.
跳表的内部有几个关键的数据结构组成.首先是Node,一个Node表示一个节点,
每个Node还会指向下一个Node,因此还有next元素.
对Node的所有操作,使用CAS方法.
static final class Node<K,V> {
final K key;
volatile Object value;
volatile Node next;
.....
boolean casValue(Object cmp, Object val) {
return UNSAFE.compareAndSwapObject(this, valueOffset, cmp, val);
}
boolean casNext(Node cmp, Node val) {
return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
}
另一个重要的是Index元素,顾名思义是索引的意思,内部还包装了Node,同时增加了向下down和向右right的引用.
整个跳表是根据Index进行全网的组织.
static class Index<K,V> {
final Node node;
final Index down;
volatile Index right;
.....
}
此外对于每一层的表头,还需要记录当前处于那一层,为此还需要一个HeadIndex的数据结构,表示链表的头部第一个Index.
static final class HeadIndex<K,V> extends Index<K,V> {
final int level;
HeadIndex(Node node, Index down, Index right, int level) {
super(node, down, right);
this.level = level;
}
}
对于跳表的操作就是组织好这些Index之间的连接关系.
锁的优化及注意事项
锁是最常用的同步方法之一,在高并发环境下,激烈的锁竞争会导致性能下降,
对多线程来说,系统除了处理功能需求之外,还需额外的维护多线程环境的特有信息,如线程本身的元数据,线程的调度,上下文切换等.
合理的并发,才能将多核CPU的性能发挥到极致.
提高”锁”性能的几点建议
减少锁持有的时间
在锁的竞争中,单个线程对锁的持有时间与系统性能有直接的关系.应该尽可能的减少锁的占有时间,以减少线程之间互斥的可能.
减少锁的持有时间有助于降低锁冲突的可能性,进而提高系统的并发能力.
public synchronized void method()
//优化方法之一是,只在必要时进行同步,这样就能明显的减少线程持有锁的时间,提高系统吞吐量.
public void method()
otherMethod();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
减小锁粒度
减小锁的粒度也是一种削弱多线程锁竞争的有效手段.这种技术典型的应用场景就是ConcurrentHashMap类的实现.
对于HashMap来说,最重要的两个方法是put()和get().concurrentHashMap内部进一步分了若干个小的HashMap,称之为(SEGMENT).
默认情况下,一个ConcurrentHashMap进一步细分为16个段.如果增加表项,并不是将整个HashMap加锁,而是首先根据hashcode得到该
表项应该被放在哪个段中,然后对该段加锁,完成put()操作.只要被加入的数据不存放在同一个表项,则多个线程的put()操作可以做到真正的并行.
由于默认16个段所以ConcurrentHashMap最多可以同时接受16个线程同时插入.
所谓减少锁粒度,就是指缩小锁定对象范围,从而减少锁冲突的可能性,进而提高系统的并发能力.
读写分离锁替换独占锁
使用读写锁ReadWriteLock可以提高系统性能.如果说减少锁粒度是通过分割数据结构实现的,那么读写锁则是对系统功能点的分割.
在读多写少的场合使用读写锁可以有效替身系统的并发能力.
锁分离.
将读写锁思想进一步延伸就是锁分离.读写锁依据读写操作功能上的不同,进行了有效的锁分离.
依据应用程序的功能特点,使用类似的分离思想,也可以对独占锁进行分离.一个典型的案例就是LinkedBlockingQueue的实现.
take()和put()方法虽然都对队列进行了修改操作,但由于是链表,因此,两个操作分别作用于队列的前端和末尾,理论上两者并不冲突.
使用独占锁,则要求在进行take和put操作时获取当前队列的独占锁,那么take和put酒不可能真正的并发,他们会彼此等待对方释放锁.
在JDK的实现中,取而代之的是两把不同的锁,分离了take和put操作.削弱了竞争的可能性.实现类取数据和写数据的分离,实现了真正意义上成为并发操作.
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
private final ReentrantLock takeLock = new ReentrantLock();
private final Condition notEmpty = takeLock.newCondition();
private final ReentrantLock putLock = new ReentrantLock();
private final Condition notFull = putLock.newCondition();
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
Node node = new Node(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
锁粗化
如果对一个锁不停地进行请求,同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能优化.
虚拟机需要一连串对同一把锁不断进行请求和释放操作时,便会把所有的锁操作整合成对锁的一次请求,从而减少对锁的请求同步次数,这就是锁的粗化.
public void demoMethod(){
synchronized(lock){
}
.....
synchronized(lock){
}
}
整合如下:
public void demoMethod(){
synchronized(lock){
.....
}
}
性能优化就是根据运行时的真实情况对各个资源点进行权衡折中的过程.锁粗化的思想和减少锁持有时间是相反的,但在不同场合,他们的效果并不相同.所以大家要根据实际情况进行权衡.
Java虚拟机对锁优化所做的努力
锁偏向
偏向锁是一种针对加锁操作的优化手段,他的核心思想是:如果一个线程获得了锁,那么锁就进行偏向模式.当这个线程再次请求锁时,无需再做任何同步操作.这样就节省了大量操作锁的动作,从而提高程序性能.
因此,对于几乎没有锁竞争的场合,偏向锁有比较好的优化效果.因为极有可能连续多次是同一个线程请求相同的锁.而对于锁竞争激烈的程序,其效果不佳.
使用Java虚拟机参数:-XX:+UseBiasedLocking 可以开启偏向锁.
轻量级锁
如果偏向锁失败,虚拟机并不会立即挂起线程.它还会使用一种称为轻量级的锁的优化手段.轻量级锁只是简单的将对象头部作为指针,指向持有锁的线程堆栈内部,来判断一个线程是否持有对象锁.
如果线程获得轻量锁成功,则可以顺利进入临界区.如果失败,则表示其他线程争抢到了锁,那么当前线程的锁请求就会膨胀为重量级锁.
自旋锁
锁膨胀后,虚拟机为了避免线程真实的在操作系统层面挂起,虚拟机还做了最后的努力就是自旋锁.如果一个线程暂时无法获得索,有可能在几个CPU时钟周期后就可以得到锁,
那么简单粗暴的挂起线程可能是得不偿失的操作.虚拟机会假设在很短时间内线程是可以获得锁的,所以会让线程自己空循环(自旋),如果尝试若干次后,可以得到锁,那么久可以顺利进入临界区,
如果还得不到,才会真实地讲线程在操作系统层面挂起.
锁消除
锁消除是一种更彻底的锁优化,Java虚拟机在JIT编译时,通过对运用上下文的扫描,取出不可能存在的共享资源竞争锁,节省毫无意义的资源开销.
锁消除设计的一项关键技术是逃逸分析,就是观察某个变量是否会跳出某个作用域,(比如对Vector的一些操作).
public String[] createStrings(){
Vector v = new Vector<>();
for (int i=0;i<10;i++)
v.add(Integer.toString(i));
return v.toArray(new String[]{});
}
逃逸分析必须在-server模式下进行,可以使用-XX:+DoEscapeAnalysis 参数打开逃逸分析
使用-XX:+EliminateLocks 参数可以打开锁消除.
未完待续2016年2月26日