目录
3.4 高效读写的队列:深度剖析ConcurrentLinkedQueue类
3.5 高效读取:不变模式下的CopyOnWriteArrayList类
4.7 CopyOnWriteArrayList类与ConcurrentLinkedQueue类
4.5 带有时间戳的对象引用:AtomicStampedReference
4.7 让普通变量也享受原子操作:AtomicIntegerFieldUpdater
4.9 让线程之间互相帮助:细看SynchronousQueue的实现
4.2 使用Disruptor框架实现生产者-消费者模式的案例
一 走入并行世界
1 并行概念
1.1.1 同步和异步
同步放大调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。异步方法调用更像是一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。而异步方法通常会在另外一个线程中真是的执行。
1.1.2 并发与并行
并发和并行都可以表示为两个或多个任务一起执行,但是偏重点有所不同。并发偏重于多个任务交替执行,而多个任务间还有可能是串行的。并行才是真正意义的同时执行。
1.1.3 临界区
临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。在并行程序中,临界区资源是保护的对象。
1.1.4 阻塞和非阻塞
阻塞和非阻塞通常用来形容多线程间的相互影响。比如一个线程占用了临界区资源,那么其他所有需要这个资源的线程在这个临界区中必须等待。等待会导致线程挂起,这种情况就是阻塞,此外,如果占用资源的线程一直不愿意释放资源,那么其他所有阻塞在这个临界区的线程都不能工作。
非阻塞的意思与之相反,它强调没有一个线程可以妨碍其它线程执行。所有的线程都会尝试不断前向执行。
1.1.5 死锁、饥饿和活锁
死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
死锁发生的条件
互斥条件:线程对资源的访问是排他性的,如果一个线程对占用了某资源,那么其他线程必须处于等待状态,直到资源被释放。
请求和保持条件:线程T1至少已经保持了一个资源R1占用,但又提出对另一个资源R2请求,而此时,资源R2被其他线程T2占用,于是该线程T1也必须等待,但又对自己保持的资源R1不释放。
不剥夺条件:线程已获得的资源,在未使用完之前,不能被其他线程剥夺,只能在使用完以后由自己释放。
环路等待条件:在死锁发生时,必然存在一个“进程-资源环形链”,即:{p0,p1,p2,…pn},进程p0(或线程)等待p1占用的资源,p1等待p2占用的资源,pn等待p0占用的资源。(最直观的理解是,p0等待p1占用的资源,而p1而在等待p0占用的资源,于是两个进程就相互等待)
活锁:是指线程1可以使用资源,但它很礼貌,让其他线程先使用资源,线程2也可以使用资源,但它很绅士,也让其他线程先使用资源。这样你让我,我让你,最后两个线程都无法使用资源。
饥饿:是指如果线程T1占用了资源R,线程T2又请求封锁R,于是T2等待。T3也请求资源R,当T1释放了R上的封锁后,系统首先批准了T3的请求,T2仍然等待。然后T4又请求封锁R,当T3释放了R上的封锁之后,系统又批准了T4的请求…,T2可能永远等待。
1.2 并发级别
根据控制并发的策略,我们可以把并发的级别进行分类,大致上可以分为阻塞、无障碍、无锁、无等待几种。
1.3 回到java:JMM
1.3.1 原子性
原子性是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
1.3.2 可见性
可见性是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。
1.3.3 有序性
1.4 哪些指令不能重排:Happen-Before规则![](https://img-blog.csdnimg.cn/20190902211413795.png)
二 JAVA并行程序基础
1 .1并发概念
1.1.1 线程与进程
进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统的基础。
线程是轻量级的进程,程序执行的最小单位。使用多线程而不是用多进程去进行并发程序的设计,是因为线程间的切换和调度的成本远远小于进程。
1.2 线程的基本操作
1.2.1 新建线程
public class Thread01 {
public static void main(String[] args) {
Thread t1 = new Thread();
t1.start();
}
}
线程执行start()方法,start()方法会新建一个线程去调用run()方法。直接使用run()方法,它会当做普通方法使用。
上述中线程什么都没有做,如果想让线程做些什么,就必须重载run()方法。
//线程实现方法1 继承Thread
public static void main(String[] args) {
Thread t1 = new Thread(){
@Override
public void run() {
System.out.println("hello world!");
}
};
t1.start();
}
//实现线程方法 2 实现Runable接口
public class MyRunable implements Runnable {
@Override
public void run() {
System.out.println("hello world 1!");
}
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunable());
t1.start();
}
}
1.2.2 终止线程
stop()方法可以实现线程的终止,但是它可能会带来数据不一致问题。
//例如下面代码中会造成数据不一致性问题
public class StopThreadUnsafe {
public static User u = new User();
public static class User{
private int id;
private String name;
public User(){
id = 0;
name = "0";
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
public static class ChangeObjectThread extends Thread{
@Override
public void run() {
while (true){
synchronized (u){
int v = (int)(System.currentTimeMillis()/1000);
u.setId(v);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
u.setName(String.valueOf(v));
}
Thread.yield();
}
}
}
public static class ReadObjectThread extends Thread{
@Override
public void run() {
while (true) {
synchronized (u) {
if (u.getId() != Integer.parseInt(u.getName())) {
System.out.println(u.toString());
}
Thread.yield();
}
}
}
}
public static void main(String[] args) {
new ReadObjectThread().start();
while (true){
Thread t = new ChangeObjectThread();
t.start();
try {
Thread.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.stop();
}
}
}
如何避免上述数据不一致问题?或者是如何终止线程?
可以选择增加标记变量stopme。例如下面代码:
public class StopThreadUnsafe {
public static User u = new User();
public static class User{
private int id;
private String name;
public User(){
id = 0;
name = "0";
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
public static class ChangeObjectThread extends Thread{
volatile boolean stopme = false;
public void stopMe(){
stopme = true;
}
@Override
public void run() {
while (true){
if(stopme){
System.out.println("exit by stop me");
break;
}
synchronized (u){
int v = (int)(System.currentTimeMillis()/1000);
u.setId(v);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
u.setName(String.valueOf(v));
}
Thread.yield();
}
}
}
public static class ReadObjectThread extends Thread{
@Override
public void run() {
while (true) {
synchronized (u) {
if (u.getId() != Integer.parseInt(u.getName())) {
System.out.println(u.toString());
}
Thread.yield();
}
}
}
}
public static void main(String[] args) {
new ReadObjectThread().start();
while (true){
Thread t = new ChangeObjectThread();
t.start();
try {
Thread.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();
}
((ChangeObjectThread) t).stopMe();
}
}
}
1.2.3 线程中断
中断线程,将会设置该线程的中断状态位,即设置为true,中断的结果线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序本身。线程会不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为true)。它并不像stop方法那样会中断一个正在运行的线程。
与线程中断有关的方法:
public static void main(String[] args) {
Thread t1 = new Thread(){
@Override
public void run() {
while (true){
if(Thread.currentThread().isInterrupted()){
System.out.println("线程被中断");
break;
}
Thread.yield();
}
}
};
t1.start();
t1.interrupt();
}
public static void main(String[] args) {
Thread t1 = new Thread(){
@Override
public void run() {
while (true){
if(Thread.currentThread().isInterrupted()){
System.out.println("线程被中断");
break;
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println("Interruted When Sleep");
Thread.currentThread().interrupt();
}
Thread.yield();
}
}
};
t1.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.interrupt();
}
上述代码中run方法中sleep()方法被下一个sleep()方法中断抛出异常。
1.2.4 等待和通知
1.2.5 挂起和继续执行线程
这是两个被标注为废弃方法,线程挂起(suspend)和继续执行(resume)是一对相反的操作,被挂起的线程必须要等到resume()操作后才能继续指定。但是不推荐使用suspend()挂起线程,因为它导致线程暂停的同时不会释放任何锁,直到对应线程进行了resume()操作后被挂起的线程才能继续。但是resume()如果在suspend()方法前执行,就会导致线程永远挂起。如下图可见:
1.2.6 等待线程结束(join)和谦让(yield)
1.3 volatile和Java内存模型
volatile变量对保证操作的原子性是有非常大的帮助的,但是volatile并不能替代锁,它无法保证一些复合操作的原子性。volatile变量能够在多线程环境下,当某个线程对它进行修改时,能保证其它线程的可见性。
1.4 线程组
public class ThreadGroupName implements Runnable{
public static void main(String[] args) {
ThreadGroup tg = new ThreadGroup("PrintGroup");
Thread t1 = new Thread(tg,new ThreadGroupName(),"T1");
Thread t2 = new Thread(tg,new ThreadGroupName(),"T2");
t1.start();
t2.start();
System.out.println(tg.activeCount());
tg.list();
}
@Override
public void run() {
String groupAndName = Thread.currentThread().getThreadGroup().getName()+"-"+Thread.currentThread().getName();
while (true){
System.out.println("I am" + groupAndName);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
1.5 驻守后台:守护线程(Daemon)
public class DaemonT {
public static class DaemonM extends Thread{
@Override
public void run() {
while (true){
System.out.println("I am Alive");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
Thread t = new DaemonM();
t.setDaemon(true);
t.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
开启守护线程必须在start()方法前,否则会抛出异常java.lang.IllegalThreadStateException,线程成为用户线程。
1.6 线程优先级
java中的线程可以有自己的优先级,优先级高的线程在竞争资源时会更有优势,更可能抢占资源。
public class PriorityDemo {
public static class HightPriority extends Thread{
static int count = 0;
public void run(){
while (true){
synchronized (PriorityDemo.class){
count++;
if(count>10000000){
System.out.println("HightPriority is complete");
break;
}
}
}
}
}
public static class LowPriority extends Thread{
static int count = 0;
public void run(){
while (true){
synchronized (PriorityDemo.class){
count++;
if(count>10000000){
System.out.println("LowPriority is complete");
break;
}
}
}
}
}
public static void main(String[] args) {
Thread high = new HightPriority();
LowPriority low = new LowPriority();
high.setPriority(Thread.MAX_PRIORITY);
low.setPriority(Thread.MIN_PRIORITY);
low.start();
high.start();
}
}
1.7 线程安全的概念和关键字synchronized
并发程序开发的一大关注重点就是线程安全,程序并行化是为了获得更高的执行效率,但是高效率不能以牺牲正确性为代价。volatile虽然能确保一个线程修改了数据后,其他线程能够看到这个改动,但是当两个线程同时修改某一个数据时,依然会产生冲突。例如下例:
public class AccountingVol implements Runnable{
static AccountingVol instance = new AccountingVol();
static volatile int i = 0;
public static void increase(){
i++;
}
@Override
public void run() {
for(int j = 0; j<10000000; j++){
increase();
}
}
public static void main(String[] args) {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(i);
}
}
因为两个线程执行时出现线程冲突,导致两个线程i++操作最终i的值只增加了1。
要解决这个问题,就必须对i操作进行同步。
//对instance加锁
@Override
public void run() {
synchronized (instance){
for(int j = 0; j<10000000; j++){
increase();
}
}
}
关键字synchronized的作用是实现线程间的同步,它的工作是对同步的代码加锁,使得每一次,只能有一个线程进入同步块,从而保证线程间的安全性。
关键字synchronized可以有多种用法,这里做一个简单的整理。
- 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁;
- 直接作用域实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例(同一个实例)的锁;
- 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。
实现上述i++线程安全的方法2
public class AccountVol2 implements Runnable {
static AccountVol2 instance = new AccountVol2();
static volatile int i = 0;
public synchronized void increase(){
i++;
}
@Override
public void run() {
for (int j = 0; j < 10000000; j++){
increase();
}
}
public static void main(String[] args) {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(i);
}
}
实现上述i++线程安全的方法3
public class AccountingVol3 implements Runnable {
static int i = 0;
public static synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j = 0; j<10000000;j++){
increase();
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new AccountingVol3());
Thread t2 = new Thread(new AccountingVol3());
t1.start();t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(i);
}
}
1.8 并发下的ArrayList
ArrayList是一个线程不安全的容器。如果在多线程中使用ArrayList,可能会导致程序出错。
public class ArrayListMultiThread {
static ArrayList<Integer> a1 = new ArrayList<>(10);
public static class AddThread implements Runnable{
@Override
public void run() {
for(int i = 0;i<1000000;i++){
a1.add(i);
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new AddThread());
Thread t2 = new Thread(new AddThread());
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(a1.size());
}
}
1.9 并发下的HashMap
HashMap不是线程安全的。
public class HashMapMultiThread {
static Map<String,String> map = new HashMap<String,String>();
public static class AddThread implements Runnable{
int start = 0;
public AddThread(int start){
this.start = start;
}
@Override
public void run() {
for(int i = start; i<100000;i+=2){
map.put(Integer.toString(i),Integer.toBinaryString(i));
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new HashMapMultiThread.AddThread(0));
Thread t2 = new Thread(new HashMapMultiThread.AddThread(1));
t1.start();t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(map.size());
}
}
多线程环境下,HashMap的内部结构会遭到破坏,结构形成环,这样对其遍历时便形成了死循环。解决方案最简单就是使用ConcurrentHashMap。
三 JDK并发包
1 多线程的团队协作:同步控制
1.1 关键字synchronized的功能扩展:重入锁
重入锁在jdk1.6以前可以完全替代关键字synchronized,它的性能远优于synchronized,但从jdk1.6开始,对synchronized进行了大量的优化,使得两者的性能差距不大。
重入锁使用java.util.concurrent.locks.ReentrantLock类实现。示例1如下:
public class ReenLockDemo implements Runnable{
public static ReentrantLock lock = new ReentrantLock();
static int i = 0;
@Override
public void run() {
for(int j = 0; j<10000000; j++){
lock.lock();
try {
i++;
}finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
ReenLockDemo h = new ReenLockDemo();
Thread t1 = new Thread(h);
Thread t2 = new Thread(h);
t1.start();t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(i);
}
}
ReentrantLock锁需要手动指定何时加锁,何时释放锁。重入锁对逻辑控制的灵活性要远远优于synchronized。但是在退出临界区资源时,必须释放锁,否则其他线程就无法访问临界区资源。
为什么称为“重入锁”?
因为这种锁可以反复进入(但反复进入限于一个线程),一个线程连续两次获得同一把锁是允许的。如果不允许这样操作,当同一个线程在第2次获得锁时,将会和自己产生死锁,程序就会卡死在第2次申请锁的过程中。但是同一个线程多次获得锁,就必须释放响应次数的锁,若超过次数释放锁,便会抛出java.lang.IllegalMonitorStateException异常,反之,释放次数少了,就还会持有这个线程的锁,别的线程无法获得这个锁。
重入锁的高级功能:
1 中断响应
对于synchronized来说,如果一个线程在等待锁,那么结果只有两种情况,要么获得锁继续执行,要么保持等待。而重入锁,提供另外一种可能,就是线程可以被中断,就是在等待锁的过程中,程序可以根据需要取消对锁的请求。
2 锁申请等待限时
除了等待外部通知之外,要避免死锁还有另外一种方法,那就是限时等待,如果给定一个等待时间,让线程自动放弃,那对系统来说是有意义的。可以使用tryLock()方法
3 公平锁
当使用synchronized关键字进行锁控制,那么产生的锁就是非公平的,当多个线程同时申请一个锁时,系统会在这个锁的等待队列中随机挑选一个,因此不能保证其公平性。
公平锁会按照时间的先后顺序,保证先到者先得,后到者后得。它的特点就是不会产生饥饿现象。但是开启使用公平锁,实现公平锁就要求系统维护一个有序队列,因此公平锁的实现成本比较高,性能低下。
public class FairReenLock implements Runnable {
public static ReentrantLock fairLock = new ReentrantLock(true);
@Override
public void run() {
while (true){
try {
fairLock.lock();
System.out.println(Thread.currentThread().getName()+"获得锁");
} finally {
fairLock.unlock();
}
}
}
public static void main(String[] args) {
FairReenLock r1 = new FairReenLock();
Thread t1 = new Thread(r1,"Thread-t1");
Thread t2 = new Thread(r1,"Thread-t2");
t1.start();t2.start();
}
}
就重入锁得实现来看,它主要集中在java层面。在重入锁的实现中,主要包含三个要素:
- 原子状态。原子状态使用CAS操作才存储当前锁的状态,判断锁是否已经被别的线程持有了;
- 等待队列。所有没有请求到锁的线程,会进入等待队列进行等待。待有线程释放锁后,系统就能从等待队列中唤醒一个线程,继续工作;
- 阻塞原语park()和unpark(),用来挂起和恢复线程。没有得到锁的线程将会被挂起。
1.2 重入锁的好搭档:Condition
Condition的作用和wait()和notify()方法是大致相同的。但是wait()和notify()方法是与synchronized关键字合作使用的,而Condition是与重入锁相关联的。通过lock接口的Condition newCondition()方法可以生成一个与当前重入锁绑定的Condition实例。利用Condition对象,我们就可以让线程在合适的时间等待,或者在某一个特定的时刻得到通知,继续执行。
Condition的使用:
public class ReentrantLockCondition implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
public static Condition condition = lock.newCondition();
@Override
public void run() {
try {
lock.lock();
condition.await();
System.out.println("Thread is going on!");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockCondition rl = new ReentrantLockCondition();
Thread t1 = new Thread(rl);
t1.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
condition.signal();
lock.unlock();
}
}
1.3 允许多个线程同时访问:信号量(Semaphore)
信号量为多线程协作提供了更为强大的控制方法。从广义上来说,信号量是对锁的扩展。无论是内部锁synchronized还是重入锁ReentrantLock,一次都只允许一个线程访问一个资源,而信号量却可以指定多个线程,同时访问某一个资源。
信号量的构造函数:
public class SemapDemo implements Runnable {
final Semaphore semp = new Semaphore(5);
@Override
public void run() {
try {
semp.acquire();
Thread.sleep(2000);
System.out.println(Thread.currentThread().getId()+":done!");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semp.release();
}
}
public static void main(String[] args) {
ExecutorService exec = Executors.newFixedThreadPool(20);
final SemapDemo demo = new SemapDemo();
for(int i = 0; i < 20; i++){
exec.submit(demo);
}
}
}
上述代码,声明了一个包含5个许可的信号量。意味着同时可以有5个线程进人临界区。
1.4 ReadWriteLock读写锁
读写分离锁可以有效地帮助减少锁竞争,提升系统性能。用锁分离的机制来提升性能非常容易理解,比如线程A1、A2、A3进行写操作,B1、B2、B3进行读操作,如果使用重入锁或者内部锁,从理论上说所有读之间、读与写之间、写与写之间都是串行操作。当B1进行读取时,B2、B3需要等待锁。由于读操作并不对数据的完整性造成破坏,这种等待是不合理的,因此读写锁有了发挥的余地。
public class ReadWriteLockDemo{
private static Lock lock = new ReentrantLock();
private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private static Lock readLock = readWriteLock.readLock();
private static Lock writeLock = readWriteLock.writeLock();
private int value;
public Object handleRead(Lock lock) throws InterruptedException {
try {
lock.lock();
Thread.sleep(1000);
return value;
} finally {
lock.unlock();
}
}
public void handleWrite(Lock lock,int index) throws InterruptedException {
try {
lock.lock();
Thread.sleep(1000);
value = index;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
final ReadWriteLockDemo demo = new ReadWriteLockDemo();
Runnable readRunable = new Runnable() {
@Override
public void run() {
try {
demo.handleRead(readLock);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Runnable writeRunable = new Runnable() {
@Override
public void run() {
try {
demo.handleWrite(writeLock,new Random().nextInt());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for (int i = 0; i<18; i++){
new Thread(readRunable).start();
}
for(int i=18;i<20;i++){
new Thread(writeRunable).start();
}
}
}
1.5 倒计数器:CountDownLatch
CountDownLatch是一个非常实用的多线程控制工具类。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计数结束,再开始执行。
public class CountDownLatchDemo implements Runnable {
static final CountDownLatch end = new CountDownLatch(10);
static final CountDownLatchDemo demo = new CountDownLatchDemo();
@Override
public void run() {
try {
Thread.sleep(new Random().nextInt(10)*1000);
System.out.println("check complete");
end.countDown();//通知CountDownLatch,一个线程完成任务,倒计数器减1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ExecutorService exec = Executors.newFixedThreadPool(10);
for(int i = 0; i<10;i++){
exec.submit(demo);
}
try {
// 等待
end.await();//要求主线程等待所有检查任务全部完成,待10个任务全部完成后,主线程才能继续执行。
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Fire!");
exec.shutdown();
}
}
1.6 循环栅栏:CyclicBarrier
CyclicBarrier是另外一种多线程并发控制工具。和CountDownLatch非常相似,它也可以实现线程间的计数等待,但它的功能比CountDownLatch更加复杂且强大。
public class CyclicBarrierDemo {
public static class Soldier implements Runnable{
private String soldier;
private final CyclicBarrier cyclic;
public Soldier(String soldier, CyclicBarrier cyclic) {
this.soldier = soldier;
this.cyclic = cyclic;
}
@Override
public void run() {
try {
cyclic.await();//await()会触发一次计数,第一次计数
doWork();
cyclic.await();//第二次计数
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
public void doWork(){
try {
Thread.sleep(Math.abs(new Random().nextInt()%10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(soldier + ":任务完成");
}
}
public static class BarrierRun implements Runnable{
boolean flag;
int N;
public BarrierRun(boolean flag, int n) {
this.flag = flag;
N = n;
}
@Override
public void run() {
if(flag){
System.out.println("司令:[士兵" + N + "个,任务完成!]");
}else{
System.out.println("司令:[士兵" + N + "个,集合完毕!]");
flag = true;
}
}
}
public static void main(String[] args) {
final int N = 10;
Thread[] allSoldier = new Thread[N];
boolean flag = false;
CyclicBarrier cyclic = new CyclicBarrier(N,new BarrierRun(false,N));
System.out.println("集合队伍");
for (int i = 0; i < N; i++){
System.out.println("士兵" + i +"报道");
allSoldier[i] = new Thread(new Soldier("士兵"+i,cyclic));
allSoldier[i].start();
}
}
}
1.7 线程阻塞工具类:LockSupport
LockSupport是一个非常方便实用的线程阻塞工具,它可以在线程内任意位置让线程阻塞。与Thread.suspend()方法相比,它弥补了由于resume()方法导致线程无法继续执行的情况。和Object.await()方法相比,它不需要先获得某个对象的锁,也不会抛出InterruptedException异常。
public class LockSupportDemo {
public static Object u = new Object();
static ChangeObjectThread t1 = new ChangeObjectThread("t1");
static ChangeObjectThread t2 = new ChangeObjectThread("t2");
public static class ChangeObjectThread extends Thread{
public ChangeObjectThread(String name){
super.setName(name);
}
@Override
public void run() {
synchronized (u){
System.out.println("in " + getName());
LockSupport.park();
}
}
}
public static void main(String[] args) {
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
LockSupport.unpark(t1);
LockSupport.unpark(t2);
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
1.8 Guava和RateLimiter限流
Guava是Google下的一个核心库,提供了一大批设计精良、使用方便的工具类。
public class RateLimiterDemo {
static RateLimiter limiter = RateLimiter.create(2);
public static class Task implements Runnable{
public void run() {
System.out.println(System.currentTimeMillis());
}
}
public static void main(String[] args) {
for(int i = 0; i < 50; i++){
limiter.acquire();
new Thread(new Task()).start();
}
}
}
2 线程复用:线程池
2.1 什么是线程池?
2.2 JDK对线程池的支持
为了更好地控制多线程,JDK提供了一套Executor框架,帮助开发人员有效地进行线程控制,其本质就是一个线程池。
- newFixedThreadPool()方法:该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行,若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理任务队列中的任务。
- newCachedThreadPool()方法:该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
- newSingleThreadExecutor()方法:该方法返回一个只有一个线程的线程池。若多余一个任务被提交到改线程池,任务会被保存到一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
- newSingleThreadScheduledExecutor()方法:该方法返回一个ScheduledExecutorService对象,线程池大小为1。ScheduledExecutorService接口在ExecutorServcie接口之上扩展了在给定时间执行某任务的功能,如在某个固定的延时之后执行,或者周期性执行某个任务。
- newScheduledThreadPool()方法:该方法也返回一个ScheduledExecutorService对象,但该线程池可以指定线程数量。
1 固定大小的线程池
public class ThreadPoolDemo {
public static class MyTask implements Runnable{
public void run() {
System.out.println(System.currentTimeMillis() + ":Thread ID:" + Thread.currentThread().getId());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
MyTask task = new MyTask();
ExecutorService es = Executors.newFixedThreadPool(5);
for(int i = 0; i < 10; i++){
es.submit(task);
}
}
}
2 计划任务
newScheduledThreadPool()返回的是一个ScheduleExecutorService对象,可以根据时间需要对线程进行调度。它的一些主要方法如下:
ScheduleExecutorService不会立即安排任务,它其实是起到了计划任务的作用,它会在指定的时间,对任务进行调度。
public class ScheduledExecutorServiceDemo {
public static void main(String[] args) {
ScheduledExecutorService ses = Executors.newScheduledThreadPool(10);
ses.scheduleAtFixedRate(new Runnable() {
public void run() {
try {
Thread.sleep(8000);
System.out.println(System.currentTimeMillis()/1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},0,2, TimeUnit.SECONDS);
}
}
2.3 核心线程池的内部实现
以上参数中大部分很简单,只有参数workQueue和handler需要进行详细说明。
2.4 超负载了怎么办:拒绝策略
当任务数量超过系统实际承载能力时就需要用到拒绝策略了。
public class RejectThreadPoolDemo {
public static class MyTask implements Runnable{
public void run() {
System.out.println(System.currentTimeMillis()+ ":Thread ID:" + Thread.currentThread().getId());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
MyTask task = new MyTask();
ExecutorService es = new ThreadPoolExecutor(5, 5, 0L,
TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(10), Executors.defaultThreadFactory(),
new RejectedExecutionHandler() {
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println(r.toString() + "is discard");
}
});
for(int i = 0; i < Integer.MAX_VALUE; i++){
es.submit(task);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2.5 自定义线程创建:ThreadFactory
线程池的主要作用是为了线程复用,也就是避免了线程的频繁创建。线程从何而来?答案是ThreadFactory。
当线程池需要新建线程时,就要调用这个方法。
public class SimpleDefThreadFactory {
public static class MyTask implements Runnable{
public void run() {
System.out.println(System.currentTimeMillis()+ ":Thread ID:" + Thread.currentThread().getId());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
MyTask task = new MyTask();
ExecutorService es = new ThreadPoolExecutor(5, 5, 0L,
TimeUnit.MILLISECONDS, new SynchronousQueue<Runnable>(), new ThreadFactory() {
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
System.out.println("create:" + t);
return t;
}
});
for(int i = 0; i < 5; i++){
es.submit(task);
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2.6 扩展线程池
ThreadPoolExecutor是一个可以扩展的线程池,它提供了beforeExecute()、afterExecute()和terminated()三个接口用来对线程池进行控制。
public class ExtThreadPool {
public static class MyTask implements Runnable{
public String name;
public MyTask(String name){
this.name = name;
}
public void run() {
System.out.println("正在执行:" + ":Thread ID: " + Thread.currentThread().getId() + ", Task Name=" + name);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService es = new ThreadPoolExecutor(5,5,0L,
TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()){
@Override
protected void beforeExecute(Thread t, Runnable r) {
System.out.println("准备执行:" + ((MyTask) r).name);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
System.out.println("执行完成:" + ((MyTask) r).name);
}
@Override
protected void terminated() {
System.out.println("线程池退出");
}
};
for(int i = 0; i<5; i++){
MyTask task = new MyTask("Task-Geym" + i);
es.execute(task);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
es.shutdown();
}
}
2.7 优化线程池线程数量
线程池的大小对系统的性能有一定的影响,过大或者过小的线程数量都无法发挥最优的系统性能,但是线程池大小不用过于精确,只要避免极大和极小两种情况,线程池的大小对系统的性能影响不大。确定线程池大小需要考虑CPU数量、内存大小等因素。
2.8 堆栈去哪里了:在线程池中寻找堆栈
public class DivTask implements Runnable {
int a,b;
public DivTask(int a, int b){
this.a = a;
this.b = b;
}
public void run() {
double re = a / b;
System.out.println(re);
}
public static void main(String[] args) {
ThreadPoolExecutor pools = new ThreadPoolExecutor(0,Integer.MAX_VALUE,0L,
TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
for(int i = 0; i < 5; i++){
pools.submit(new DivTask(100,i));
}
}
}
上述缺失了一个值是由于除以0导致的。使用线程池虽然是好事,但是它很有可能会吃掉程序抛出的异常,导致我们对程序的错误一无所知。
实现堆栈信息打印:
public class TraceThreadPoolExecutor extends ThreadPoolExecutor {
public TraceThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
public void execute(Runnable command) {
super.execute(command);
}
@Override
public Future<?> submit(Runnable task) {
return super.submit(wrap(task,clientTrace(),Thread.currentThread().getName()));
}
private Exception clientTrace(){
return new Exception("Client stack trace");
}
private Runnable wrap(final Runnable task, final Exception clientStack,String clientThreadName){
return new Runnable() {
public void run() {
try {
task.run();
} catch (Exception e) {
e.printStackTrace();
}
}
};
}
public static void main(String[] args) {
ThreadPoolExecutor pools = new TraceThreadPoolExecutor(0, Integer.MAX_VALUE,0L,
TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
for(int i = 0; i<5; i++){
pools.execute(new DivTask(100,i));
}
}
}
2.9 分而治之:Fork/Join框架
分而治之,就是将一个大的任务划分为多个小任务执行,然后将小任务结果汇总。
但是在实际使用中,若无顾忌使用fork()方法将导致系统开启过多线程而影响性能。所以JDK中给出了一个ForkJoinPool线程池,对于fork()方法并不急着开启线程,而是提交给ForkJoinPool线程池进行处理,以节省系统资源。
public class CountTask extends RecursiveTask<Long>{
private static final int THRESHOLD = 10000;
private long start;
private long end;
public CountTask(long start, long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
long sum = 0;
boolean canCompute = (end - start) < THRESHOLD;
if(canCompute){
for(long i = start; i <= end; i++){
sum += i;
}
}else{
long step = (start + end) / 100;
ArrayList<CountTask> subTasks = new ArrayList<>();
long pos = start;
for(int i = 0; i<100; i++){
long lastOne = pos + step;
if(lastOne > end){
lastOne = end;
}
CountTask subTask = new CountTask(pos, lastOne);
pos += step + 1;
subTasks.add(subTask);
subTask.fork();
}
for(CountTask t : subTasks){
sum += t.join();
}
}
return sum;
}
public static void main(String[] args) {
ForkJoinPool forkJoinPool = new ForkJoinPool();
CountTask task = new CountTask(0, 200000L);
ForkJoinTask<Long> result = forkJoinPool.submit(task);
try {
long res = result.get();
System.out.println("sum=" + res);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
2.10 Guava中对线程池的扩展
除JDK内置的线程池以外,Guava对线程池也进行了一定的扩展,主要体现在MoreExecutors工具类中。
1 特殊的DirectExecutor线程池
在MoreExecutors中,提供了一个简单但是非常重要的线程池实现,即DirectExecutor线程池。DirectExecutor线程池不会真的创建或者使用额外线程,它总是将任务在当前线程中直接执行。
为什么需要线程池?这是软件设计的需要。
public class DirectExecutorDemo {
public static void main(String[] args) {
Executor executor = MoreExecutors.directExecutor();
executor.execute(()->System.out.println("I am running in " + Thread.currentThread().getName()));
}
}
可以看出这个Runnable接口在主线程中执行。
2 Daemon线程池
public class DaemonExecutorsDemo {
public static void main(String[] args) {
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(2);
MoreExecutors.getExitingExecutorService(executor);
executor.execute(()->System.out.println("I am running in " + Thread.currentThread().getName()));
}
}
3 对Future模式的扩展
3 JDK的并发容器
3.1 工具类:并发集合简介
JDK提供的这些容器大部分在java.util.concurrent包中。
- ConcurrentHashMap:这是一个高效的并发HashMap。你可以把它;理解为线程安全的HashMap;
- CopyOnWriteArrayList:这是一个List,从名字看就知道它和ArrayList是一族的,在读多写少的场合,这个List的性能非常好,远远优于Vector;
- ConcurrentLinkedQueue:高效的并发队列,使用链表实现。可以看做一个线程安全的LinkedList;
- BlockingQueue:这是一个接口,JDK通过链表、数组等方式实现了这个接口,表示阻塞队列,非常适合作为数据共享的通道;
- ConcurrentSkipListMap:跳表的实现,这是一个Map,使用跳表的数据结构进行快速查找。
除以上并发包中的专有数据结构外,java.util下的Vector是线程安全的(性能和上述工具较差),另外Collections工具类可以帮助我们将任意集合包装成线程安全的集合。
3.2 线程安全的HashMap
实现一个线程安全的HashMap:
一种可行的办法是使用Collections.synchronizedMap()方法包装我们的HashMap,这样产生的HashMap就是线程安全的。示例如下:
public class HashMapThread {
public static Map m = Collections.synchronizedMap(new HashMap<>());
public static class MyTask implements Runnable{
public int start = 0;
public MyTask(int start){
this.start = start;
}
@Override
public void run() {
for(int i = start; i < 100000; i+=2){
// m.put("");
m.put(Integer.toString(i),Integer.toBinaryString(i));
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new HashMapThread.MyTask(0));
Thread t2 = new Thread(new HashMapThread.MyTask(1));
t1.start();t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(m.size());
}
}
Collections.synchronizedMap()方法会生成一个名为SynchronizedMap的Map,它使用委托,将自己所有Map相关的功能传入交给传入的HashMap实现,而自己则负责保证线程安全。
而其他所有相关的Map操作都会使用这个mutex进行同步,从而实现线程安全。但是这个Map虽然可以满足线程安全需求,但是性能不好,因为读取或者写入都要获得mutex的锁,会导致所有对Map的操作进入等待状态,直到mutex锁可用。
另一种更好的解决方案就是使用ConcurrentHashMap。
3.3 有关List的线程安全
ArrayList和Vector都是使用数组作为内部实现,ArrayList不是线程安全的,Vector是线程安全的。LinkedList是基于链表实现List,它也不是线程安全的。可以使用Collections.synchronizedList()实现LinkedList的线程安全。
public static List<String> l = Collections.synchronizedList(new LinkedList<String>());
这样生成的List对象就是线程安全的了。
3.4 高效读写的队列:深度剖析ConcurrentLinkedQueue类
ConcurrentLinkedQueue是使用链表作为数据结构实现的高效并发的队列。它之所以有很好的性能,是因为其内部复杂的实现。
下面介绍它的具体实现:
3.5 高效读取:不变模式下的CopyOnWriteArrayList类
为了将读取的性能发挥到极致,JDK中提供了CopyOnWriteArrayList类。对它来说,读取时完全不用加锁的,并且更好的消息是,写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待,这样读操作的性能就会大幅度提升。
CopyOnWrite就是在写入操作时,进行一次自我复制。换句话说,当这个List需要修改时,我并不修改原有的内容(这对于保证当前在读线程的数据一致性非常重要),而是对原有的数据进行一次复制,将修改的内容写入副本中。写完之后,再用修改完的副本替换原来的数据,这样就可以保证写操作不会影响读了。
读取代码没有任何同步控制和锁操作,理由就是内部数组array不会发生修改,只会被另一个array替换,因此可以保证数据安全。
写入操作使用锁,当然这个锁仅限于控制写-写的情况。其重点在于第7行代码进行了内部元素的完整复制。因此会产生一个新的数组newElements。将新的元素加入newElements,然后在第9行使用新的数组替换老的数组。整个过程不会影响读取,并且修改完后,读取线程可以立即察觉到这个修改(因为array是volatile类型)。
3.6 数据共享通道:BlockingQueue
在多线程开发中,多个线程间的数据共享成为一个问题,比如线程A希望给线程发一条消息,用什么方式告知线程B是比较合理的呢?
而BlockingQueue值之所以适合做数据共享的通道,其关键是在于Blocking上。Blocking是阻塞的意思,当服务线程(服务线程指不断获取队列中的消息,进行处理的线程)处理完成队列中的所有消息后,它如何知道下一条消息何时到来呢?
一种方案是让这个线程按照一定时间间隔不停地循环和监控这个队列,但是方案可行,造成资源浪费。BlockingQueue解决了这个问题,它会让服务线程在队列为空时进行等待,当有新的消息进入队列后,自动将线程唤醒。
3.7 随机数据结构:跳表(SkipList)
在JDK中的并发包中,除常用的哈希表外,还实现了一种有趣的数据结构----跳表。跳表是一种可以用来快速查找的数据结构,有点类似于平衡树,它们都可以对元素进行快速查找。但是一个重要的区别是:对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整,而对跳表的插入和删除只需要对整个数据结构的局部进行操作即可。这样带来的好处是:在高并发的情况下,你会需要一个全局锁来保证整个平衡树的线程安全。而对于跳表,你只需要部分锁即可。这样在高并发环境下,你就可以拥有更好的性能。就查询性能,因为跳表的时间复杂度是O(logn),所以在并发数据结构中,JDK使用跳表来实现Map。
跳表是一种使用空间换时间的算法。
跳表内的所有链表的元素都是排序的。查找时,可以从顶级链表开始找。一旦发现查找的元素大于当前链表中的取值,就会转入下一层链表继续找。
public class ConcurrentSkipListMapDemo {
public static void main(String[] args) {
Map<Integer,Integer> map = new ConcurrentSkipListMap<>();
for(int i = 0; i < 10; i++){
map.put(i,i);
}
for (Map.Entry<Integer,Integer> entry:
map.entrySet()) {
System.out.print(entry.getKey()+ " ");
}
}
}
有上图可知,跳表和HashMap不同,对跳表的遍历输出是有序的。
4 使用JMH进行性能测试
在软件开发中,除要写出正确的代码之外,还需要写出高效的代码。这在并发编程中更加重要,原因主要有两点,首先,一部分并发程序由串行程序改造而来,其目的就是提高系统性能,因此,自然需要有一种对两种算法进行性能比较。其次,由于业务原因引入的多线程有可能因为线程并发控制导致性能损耗,因此要评估损耗的比重是否可以接受。
4.1 什么是JMH?
JMH(Java Microbenchmark Harness)是一个在OpenJDK项目中发布的,专门用于性能测试的框架,其精度可以达到毫秒级。通过JMH可以对多个方法的性能进行定量分析。比如,当要知道执行一个函数需要多少时间,或者当对一个算法有多种不同实现时,需要选择性能最好的那个。
4.2 Hello JMH
引入JMH的Maven依赖:
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.21</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.21</version>
<scope>provided</scope>
</dependency>
示例代码如下:
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class JMHSample {
@Benchmark
public void wellHelloThere(){
}
public static void main(String[] args) {
Options opt = new OptionsBuilder().include(JMHSample.class.getSimpleName()).forks(1).build();
try {
new Runner(opt).run();
} catch (RunnerException e) {
e.printStackTrace();
}
}
}
我的代码是在Idea中运行结果如下:
F:\software\java\jdk\bin\java.exe "-javaagent:F:\software\ideajava\IntelliJ IDEA 2018.3\lib\idea_rt.jar=11816:F:\software\ideajava\IntelliJ IDEA 2018.3\bin" -Dfile.encoding=UTF-8 -classpath F:\software\java\jdk\jre\lib\charsets.jar;F:\software\java\jdk\jre\lib\deploy.jar;F:\software\java\jdk\jre\lib\ext\access-bridge-32.jar;F:\software\java\jdk\jre\lib\ext\cldrdata.jar;F:\software\java\jdk\jre\lib\ext\dnsns.jar;F:\software\java\jdk\jre\lib\ext\jaccess.jar;F:\software\java\jdk\jre\lib\ext\jfxrt.jar;F:\software\java\jdk\jre\lib\ext\localedata.jar;F:\software\java\jdk\jre\lib\ext\nashorn.jar;F:\software\java\jdk\jre\lib\ext\sunec.jar;F:\software\java\jdk\jre\lib\ext\sunjce_provider.jar;F:\software\java\jdk\jre\lib\ext\sunmscapi.jar;F:\software\java\jdk\jre\lib\ext\sunpkcs11.jar;F:\software\java\jdk\jre\lib\ext\zipfs.jar;F:\software\java\jdk\jre\lib\javaws.jar;F:\software\java\jdk\jre\lib\jce.jar;F:\software\java\jdk\jre\lib\jfr.jar;F:\software\java\jdk\jre\lib\jfxswt.jar;F:\software\java\jdk\jre\lib\jsse.jar;F:\software\java\jdk\jre\lib\management-agent.jar;F:\software\java\jdk\jre\lib\plugin.jar;F:\software\java\jdk\jre\lib\resources.jar;F:\software\java\jdk\jre\lib\rt.jar;D:\软件\ThThread\target\classes;C:\Users\24koby\.m2\repository\com\google\guava\guava\18.0\guava-18.0.jar;C:\Users\24koby\.m2\repository\org\openjdk\jmh\jmh-core\1.21\jmh-core-1.21.jar;C:\Users\24koby\.m2\repository\net\sf\jopt-simple\jopt-simple\4.6\jopt-simple-4.6.jar;C:\Users\24koby\.m2\repository\org\apache\commons\commons-math3\3.2\commons-math3-3.2.jar com.hdu.mythread.JMHSample
# JMH version: 1.21
# VM version: JDK 1.8.0_102, Java HotSpot(TM) Client VM, 25.102-b14
# VM invoker: F:\software\java\jdk\jre\bin\java.exe
# VM options: -javaagent:F:\software\ideajava\IntelliJ IDEA 2018.3\lib\idea_rt.jar=11816:F:\software\ideajava\IntelliJ IDEA 2018.3\bin -Dfile.encoding=UTF-8
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.hdu.mythread.JMHSample.wellHelloThere
# Run progress: 0.00% complete, ETA 00:01:40
# Fork: 1 of 1
# Warmup Iteration 1: 0.001 us/op
# Warmup Iteration 2: 0.001 us/op
# Warmup Iteration 3: 0.001 us/op
# Warmup Iteration 4: 0.001 us/op
# Warmup Iteration 5: 0.001 us/op
Iteration 1: 0.001 us/op
Iteration 2: 0.001 us/op
Iteration 3: 0.001 us/op
Iteration 4: 0.001 us/op
Iteration 5: 0.001 us/op
Result "com.hdu.mythread.JMHSample.wellHelloThere":
0.001 ±(99.9%) 0.001 us/op [Average]
(min, avg, max) = (0.001, 0.001, 0.001), stdev = 0.001
CI (99.9%): [0.001, 0.001] (assumes normal distribution)
# Run complete. Total time: 00:01:42
REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.
Benchmark Mode Cnt Score Error Units
JMHSample.wellHelloThere avgt 5 0.001 ± 0.001 us/op
Process finished with exit code 0
4.3 JMH的基本概念和配置
为了能够更好地使用JMH的各项功能,首先需要对JMH的基本概念有所了解。
1 模式(Mode)
Mode表示JMH的测量方式和角度,共有4种:
- Throughput:整体吞吐量,表示1秒内可以执行多少次调用;
- AverageTime:调用的平均时间,指每一次调用所需要的时间;
- SampleTime:随机取样,最后输出取样结果的分布,例如“99%的调用在xxx毫秒以内,99%的调用在xxx毫秒以内”;
- SingleShotTime:以上模式都是默认一次Iteration是1秒,唯有SingleShotTime只运行一次,往往同时把warmup次数设为0,用于测试冷启动时的性能。
2 迭代(Iteration)
迭代是JMH的一次测量单位。在大部分测量模式下,一次迭代表示1秒。在这1秒内会不间断调用被测方法,并采样计算吞吐量,平均时间。
3 预热(Warmup)
由于Java虚拟机的JIT的存在,同一个方法在JIT编译前后的时间将会不同。通常只考虑方法在JIT编译后的性能。
4 状态(State)
通过State可以指定一个对象的作用范围,范围主要有两种。一种为线程范围,也就是一个对象只会被一个线程访问。在多线程池测试时,会为每一个线程生成一个对象。另一种是基准测试范围(Benchmark),即多个线程共享一个实例。
5 配置类(Options/OptionsBuilder)
4.4 理解JMH中的Mode
4.5 理解JMH中的State
4.6 性能思考
性能是一个重要且很复杂的话题,简单来说,性能调优就是要加快系统的执行,因此就是要尽可能使用执行快的组件。对性能的优化和研究需要在各种不同的场景下,对组件进行全方位的性能分析,并结合实际应用情况进行取舍和权衡。
以HashMap和ConcurrentHashMap为例:
4.7 CopyOnWriteArrayList类与ConcurrentLinkedQueue类
CopyOnWriteArrayList类与ConcurrentLinkedQueue类是两个重要的高并发队列。CopyOnWriteArrayList通过写复制来提升并发能力。ConcurrentLinkedQueue通过CAS操作和锁分离来提高系统性能。在实际应用中,如何选择?
当有少许写入,在并发场景下,复制的消耗相对较小,当元素总量不大时,在绝大部分场景中,CopyOnWriteArrayList类要优于ConcurrentLinkedQueue类。
第四章 锁的优化及注意事项
1 有助于提高所性能的几点建议
锁的竞争必然会导致程序的整体性能下降,为了将这种副作用降到最低,这里提出一些关于使用锁的建议。
1.1 减少锁持有时间
对于使用锁进行并发控制的应用程序,在锁竞争过程中,单个县城对锁的持有时间与系统性能有直接关系。如果线程持有锁的时间越长,那么相对地,锁的竞争程度也越激烈。
1.2 减少锁粒度
减少锁粒度也是一种削弱多线程竞争的有效手段,这种技术典型的使用场景就是ConcurrentHashMap类的实现。
对于HashMap来说,最重要的两个方法是get()和put()方法。一种最自然的想法就是,对整个HashMap加锁从而得到一个线程安全的对象,但是这样做,加锁粒度太大。对于ConcurrentHashMap类,它内部进一步细分了若干个小的HashMap,称之为段(Segment)。在默认情况下,一个ConcurrentHashMap类可以细分为16个段。
但是减小锁粒度会带来一个新的问题,即当系统需要取得全局锁时,其消耗的资源会比较多。仍然以ConcurrentHashMap为例,虽然其put()方法很好地分离了锁,但是当试图访问ConcurrentHashMap的全局信息时,就需要同时取得所有段的锁方能顺利实施。比如ConcurrentHashMap类的size()方法,它将返回ConcurrentHashMap类的有效表项的数量,即ConcurrentHashMap类的全部表项之和。要获取这个信息需要取得所有子段的锁,因此其size()方法的部分代码如下:
1.3 用读写分离锁来替换独占锁
1.4 锁分离
读写锁的思想进一步延伸就是锁分离。读写锁根据读写操作功能上的不同,进行了有效的锁分离。依据应用程序的功能特点,使用类似的分离思想,也可以对独占锁进行分离。一个典型的案例就是java.util.concurrent.LinkedBlockingQueue的实现。
在LinkedBlockingQueue的实现中,take()函数和put()函数分别实现了从队列中取得数据和往队列中增加数据的功能。虽然两个函数都对当前队列进行了修改操作,但由于LinkedBlockingQueue是基于链表的,因此两个操作分别作用于队列的前端和尾端,从理论上说两者不冲突。
如果使用独占锁,则要求在两个操作进行时获取当前队列的独占锁,那么take()方法和put()方法就不可能真正的并发,在运行时,它们会彼此等待对方释放锁资源。在这种情况下,所竞争相对比较激烈,从而影响程序在高并发时的性能。
因此在JDK的实现中, 并没有采用这样的方式,取而代之的是用两把不同的锁分离了take()和put()方法的操作。
通过takeLock和putLock两把锁,LinkedBlockingQueue实现了取数据和写数据的分离,是两者在真正意义上成为高并发的操作。
1.5 锁粗化
为此,虚拟机在遇到一连串连续地对同一个锁不断进行请求和释放的操作时,便会把所有的锁操作整合成对锁的一次请求,从而减少对锁的请求同步的次数,这个操作叫做锁的粗化。
2 Java虚拟机对锁优化所做的努力
作为一款共用平台,JDK本身也为并发程序的性能绞尽脑汁。在JDK内部也想进一切办法提供并发时的系统吞吐量。下面提供几种JDK内部锁优化策略。
2.1 偏向锁
2.2 轻量级锁
如果偏向锁失败,那么虚拟机并不会立即挂起线程,它还会使用一种称为轻量级锁的优化手段。轻量级锁的操作很方便,它只是简单地将对象头部作为指针指向持有锁的线程堆栈的内部,来判断一个线程是否持有对象锁。如果线程获得轻量级锁成功,则可以顺利进入临界区。如果轻量级锁加锁失败,则表示其他线程抢先争夺到了锁,那么当前线程的锁请求就会膨胀为重量级锁。
2.3 自旋锁
锁膨胀后,为了避免线程真实地在操作系统层面挂起,虚拟机还会做最后的努力-----自旋锁。当前线程暂时无法获得锁,而且什么时候可以获得锁是一个未知数,也许在几个CPU时钟周期后就可以得到锁。如果这样,简单粗暴地挂起线程可能是一种得不偿失的操作。系统会假设在不久的将来,线程可以得到这把锁。因此,虚拟机会让当前线程做几个空循环(这也是自旋的含义),在经过若干次循环后,如果可以得到锁,那么就顺利进入临界区。如果还不能获得锁,才会真的将线程在操作系统层面挂起。
2.4 锁消除
锁消除是一种更彻底的锁优化。java虚拟机在JIT编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁。通过锁消除,可以节省毫无意义的请求锁时间。
3 ThreadLocal
3.1 ThreadLocal的简单实用
从ThreadLocal的名字可以看到,这是一个线程安全的局部变量。也就是说,只有当前线程可以访问。既然是只有当前线程可以访问的数据,自然是线程安全的。
简单实例:
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadLocalDemo {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static class ParseDate implements Runnable{
int i = 0;
public ParseDate(int i){
this.i = i;
}
@Override
public void run() {
try {
Date t = sdf.parse("2015-03-29 19:29:"+i%60);
System.out.println(i + ":" + t);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(10);
for(int i =0; i< 1000; i++){
es.execute(new ParseDate(i));
}
}
}
因为SimpleDateFormat.parse()方法不是线程安全的,因此在线程池中共享这个对象必然导致错误。
通果对parse()方法加锁,可以解决。
使用ThreadLocal为每一个线程创造一个SimpleDateFormat对象实例。
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadLocalDemo {
private static ThreadLocal<SimpleDateFormat> t = new ThreadLocal<SimpleDateFormat>();
public static class ParseDate implements Runnable{
int i = 0;
public ParseDate(int i){
this.i = i;
}
@Override
public void run() {
try {
if(t.get() == null){
t.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
Date h = t.get().parse("2015-03-29 19:29:" + i%60);
System.out.println(i + ":" + h);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(10);
for(int i =0; i< 1000; i++){
es.execute(new ParseDate(i));
}
}
}
3.2 ThreadLocal的实现原理
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadLocalDemoGC {
static volatile ThreadLocal<SimpleDateFormat> t1 = new ThreadLocal<SimpleDateFormat>(){
@Override
protected void finalize() throws Throwable {
System.out.println(this.toString()+ "is gc");
}
};
static volatile CountDownLatch cd = new CountDownLatch(10000);
public static class ParseDate implements Runnable{
int i = 0;
public ParseDate(int i){
this.i = i;
}
@Override
public void run() {
if(t1.get() == null){
t1.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"){
@Override
protected void finalize() throws Throwable {
System.out.println(this.toString() + "is gc");
}
});
System.out.println(Thread.currentThread().getId() + ":create SimpleDateFormat");
try {
Date t = t1.get().parse("2015-03-29 19:29:" + i % 60);
} catch (ParseException e) {
e.printStackTrace();
}finally {
cd.countDown();
}
}
}
}
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(10);
for(int i = 0; i < 10000; i++){
es.execute(new ParseDate(i));
}
try {
cd.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("mission complete!!");
t1 = null;
System.gc();
System.out.println("first GC complete");
// 在设置ThreadLocal的时候,会清除ThreadLocalMap中的无效对象
t1 = new ThreadLocal<SimpleDateFormat>();
cd = new CountDownLatch(10000);
for(int i = 0; i < 10000; i++){
es.execute(new ParseDate(i));
}
try {
cd.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.gc();
System.out.println("second GC complete");
}
}
3.3 对性能有何帮助
如果共享对象对于竞争的处理容易引起性能损失,我们还是应该考虑使用ThreadLocal为每个线程分配单独的对象。
4 无锁
对于并发控制来说,锁是一种悲观的策略。它总是假设每一次的临界区操作会产生冲突,因此,必须对每次操作都小心翼翼。如果有多个线程同时需要访问临界区资源,则宁可牺牲性能让线程进行等待,所以说锁会阻塞线程执行。而无锁是一种乐观的策略,它会假设对资源的访问是没有冲突的。既然没有冲突,自然不需要等待,所以所有的线程都可以在不停顿的状态下持续执行。那遇到冲突怎么办?无锁的策略使用一种比较变换(CAS)的技术来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突为止。
4.1 与众不同的并发策略:比较变换
4.2 无锁的线程安全整数:AtomicInteger
为了让Java程序员能够受益于CAS等CPU指令,JDK并发包中有一个atomic包,里面实现了一些直接使用CAS操作的线程安全的类型。
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerDemo {
static AtomicInteger i = new AtomicInteger();
public static class AddThread implements Runnable{
@Override
public void run() {
for(int k = 0; k < 10000; k++){
i.incrementAndGet();
}
}
}
public static void main(String[] args) {
Thread[] ts = new Thread[10];
for(int k = 0; k < 10; k++){
ts[k] = new Thread(new AddThread());
}
for(int k = 0; k<10;k++){
ts[k].start();
}
for(int k = 0; k<10;k++){
try {
ts[k].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(i);
}
}
4.3 Java中的指针:Unsafe类
4.4 无锁的引用对象:AtomicReference
4.5 带有时间戳的对象引用:AtomicStampedReference
示例代码:
import java.util.concurrent.atomic.AtomicStampedReference;
public class AtomicStampedReferenceDemo {
static AtomicStampedReference<Integer> money = new AtomicStampedReference<>(19,0);
public static void main(String[] args) {
for(int i = 0; i < 3; i++){
final int timestamp = money.getStamp();
new Thread(){
@Override
public void run() {
while (true){
while (true){
Integer m = money.getReference();
if(m < 20){
if(money.compareAndSet(m,m+20,timestamp,timestamp+1)){
System.out.println("余额小于20元,充值成功!余额:" + money.getReference() + "元");
break;
}
}else{
System.out.println("余额大于20元 不用充值");
break;
}
}
}
}
}.start();
}
new Thread(){
@Override
public void run() {
for(int i = 0; i<100; i++){
while (true){
int timestamp = money.getStamp();
Integer m = money.getReference();
if(m>10){
System.out.println("余额大于10元");
if(money.compareAndSet(m,m-10,timestamp,timestamp+1)){
System.out.println("成功消费10元 当前余额为:" + money.getReference());
break;
}
}else {
System.out.println("没有足够的余额!");
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
可以看到账户只被增加了一次。
4.6 数组也能无锁:AtomicIntegerArray
除了提供基本数据类型外,JDK还为我们提供了数组等复合结构,当前可用的原子数组有:AtomicIntegerArray,AtomicLongArray,ArrayReferenceArray,分别表示整型数组,long型数组和引用数组。
示例代码:
import java.util.concurrent.atomic.AtomicIntegerArray;
public class AtomicIntegerArrayDemo {
static AtomicIntegerArray arr = new AtomicIntegerArray(10);
public static class AddThread implements Runnable{
@Override
public void run() {
for(int k = 0; k < 10000; k++){
arr.getAndIncrement(k % arr.length());
}
}
}
public static void main(String[] args) {
Thread[] ts = new Thread[10];
for(int k = 0; k<10; k++){
ts[k] = new Thread(new AddThread());
}
for(int k = 0; k<10; k++){
ts[k].start();
}
for(int k = 0; k<10; k++){
try {
ts[k].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(arr);
}
}
4.7 让普通变量也享受原子操作:AtomicIntegerFieldUpdater
有时候,由于初期考虑不周,或者后期的需求变化,一些普通变量可能也会有线程安全的需求。如果改动不大,则可以简单地修改程序中每一个使用或者读取这个变量的地方。则显然,这样并不符合软件设计中的一条重要原则----开闭原则。也就是系统对功能的增加应该是开放的,而对修改应该是相对保守的。而且如果系统里使用到这个变量的 地方特别多,一个一个修改也是一件令人厌烦的事情(很多情况下可能是只读的,并无线程安全的强烈要求,完全可以保持原样)。
示例代码:
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
public class AtomicIntegerFieldUpdaterDemo {
public static class Candidate{
int id;
volatile int score;
}
public final static AtomicIntegerFieldUpdater<Candidate> scoreUpdater =
AtomicIntegerFieldUpdater.newUpdater(Candidate.class,"score");
// 检查Updater是否正确工作
public static AtomicInteger allScore = new AtomicInteger(0);
public static void main(String[] args) {
final Candidate stu = new Candidate();
Thread[] t = new Thread[1000];
for(int i = 0; i<1000;i++){
t[i] = new Thread(){
@Override
public void run() {
if(Math.random()>0.4){
scoreUpdater.incrementAndGet(stu);
allScore.incrementAndGet();
}
}
};
}
for(int i = 0; i<1000;i++){
t[i].start();
}
for(int i = 0; i<1000; i++){
try {
t[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("score=" + stu.score);
System.out.println("allScore="+allScore);
}
}
4.8 挑战无锁算法:无锁的Vector算法
4.9 让线程之间互相帮助:细看SynchronousQueue的实现
5 有关死锁
第五章 并行模式与算法
1 探讨单例模式
单例模式是设计模式中使用最为普遍的模式之一。它是一种对象创建模式,用于产生一个对象的具体实例,它可以确保系统中一个类只产生一个实例。在Java中,这样的行为能带来两大好处:
- 对于频繁使用的对象,可以省略new操作花费的时间,这对于那些重量级对而言,是非常可观的一笔系统开销;
- 由于new操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻GC压力,缩短GC停顿时间。
public class Singleton {
private Singleton(){
System.out.println("Singleton is create");
}
private static Singleton instance = new Singleton();
private static Singleton getInstance(){
return instance;
}
}
public class SingleTon {
private static int status = 0;
private SingleTon(){}
private static SingleTon instance = new SingleTon();
public static SingleTon getInstance(){
return instance;
}
}
上面这个代码在引用status实例时会导致类将进行初始化并创建instance实例(任何对SingleTon类实例或方法的调用都会导致类初始化,instance就会被创建)。但是代码中并没有需求创建单例,但是new SingleTon()也会被调用。
改进的SingleTon类:使用懒加载,它只会在instance第一次使用时创建对象。代码如下:
public class LazySingleTon {
private static LazySingleTon instance = null;
private LazySingleTon(){}
public static synchronized LazySingleTon getInstance(){
if(instance == null){
return new LazySingleTon();
}
return instance;
}
}
单例模式实现方法3:
public class StaticSingleTon {
private StaticSingleTon(){
System.out.println("SingleTon 实例创建");
}
private static class SingleHolder{
private static StaticSingleTon instance = new StaticSingleTon();
}
public static StaticSingleTon getInstance(){
return SingleHolder.instance;
}
}
2 不变模式
在并行软件开发过程中给,同步操作似乎是必不可少的。当多线程对同一个对象进行读写操作时,为了保证对象数据的一致性和正确性,有必要对对象进行同步。而同步操作对系统性能有相当的损耗。为了能尽可能地去除掉这些同步操作,提高并行程序性能,可以使用一种不可改变的对象,依靠对象的不变性,可以确保其在没有同步操作的多线程环境下依然始终保持内部状态的一致性和正确性。这就是不变模式。
不变模式天生就是多线程友好的,他的核心思想是,一个对象一旦被创建,则它的内部状态永远不会发生改变。所以,没有一个线程可以修改其内部状态和数据,同时其内部状态也绝不会自行发生改变。基于这些特性,对不变对象的多线程操作不需要进行同步控制。
同时,不变模式和只读属性是由区别的,不变模式是比只读属性具有更强的一致性和不变性。对只读属性的对象而言,对象本身不能被其他线程修改,但是兑现的自身状态却可能自行修改。
3 生产者-消费者模式
生产者-消费者实例:
Producer
import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class Producer implements Runnable{
private volatile boolean isRunning = true;
private BlockingQueue<PCData> queue;
private static AtomicInteger count = new AtomicInteger();
private static final int SLEEPTIME = 1000;
public Producer(BlockingQueue<PCData> queue){
this.queue = queue;
}
public void run() {
PCData data = null;
Random r = new Random();
System.out.println("start producer id = " + Thread.currentThread().getId());
while (isRunning){
try {
Thread.sleep(r.nextInt(SLEEPTIME));
} catch (InterruptedException e) {
e.printStackTrace();
}
data = new PCData(count.incrementAndGet());
System.out.println(data + "is put into queue");
try {
if(!queue.offer(data,2, TimeUnit.SECONDS)){
System.out.println("failed to put data:" + data);
}
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
}
}
public void stop(){
isRunning = false;
}
}
Consumer:
import java.text.MessageFormat;
import java.util.Random;
import java.util.concurrent.BlockingQueue;
public class Consumer implements Runnable{
private BlockingQueue<PCData> queue;
private static final int SLEEPTIME = 1000;
public Consumer(BlockingQueue<PCData> queue){
this.queue = queue;
}
public void run() {
System.out.println("start Consumer id=" + Thread.currentThread().getId());
Random r = new Random();
while (true){
try {
PCData data = queue.take();
if(null != data){
int re = data.getIntData() * data.getIntData();
System.out.println(MessageFormat.format("{0}*{1}={2}",data.getIntData(),data.getIntData(),re));
Thread.sleep(r.nextInt(SLEEPTIME));
}
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
}
}
}
PCData:
public final class PCData {
private final int intData;
public PCData(int data){
this.intData = data;
}
public PCData(String data){
this.intData = Integer.valueOf(data);
}
@Override
public String toString() {
return "PCData{" +
"intData=" + intData +
'}';
}
public int getIntData() {
return intData;
}
}
Main:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
public class Main {
public static void main(String[] args) {
BlockingQueue<PCData> queue = new LinkedBlockingQueue<PCData>();
Producer producer1 = new Producer(queue);
Producer producer2 = new Producer(queue);
Producer producer3 = new Producer(queue);
Consumer consumer1 = new Consumer(queue);
Consumer consumer2 = new Consumer(queue);
Consumer consumer3 = new Consumer(queue);
ExecutorService service = Executors.newCachedThreadPool();
service.execute(producer1);
service.execute(producer2);
service.execute(producer3);
service.execute(consumer1);
service.execute(consumer2);
service.execute(consumer3);
try {
Thread.sleep(10*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
producer1.stop();
producer2.stop();
producer3.stop();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
service.shutdown();
}
}
4 高性能的生产者-消费者模式:无锁的实现
4.1 无锁的缓存框架:Disruptor
这种固定大小的环形队列的另外一个好处就是可以做到完全的内存复用。在系统的运行过程中,不会有新的空间需要分配或者老的空间需要回收,大大减少系统分配空间及回收空间的额外开销。
4.2 使用Disruptor框架实现生产者-消费者模式的案例
Disruptor的maven依赖:
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.2.1</version>
</dependency>
这里生产者不断产生整数,,消费者读取生产者的数据,并计算其平方。
PCData:数据对象
public class PCData {
private long value;
public long getValue() {
return value;
}
public void setValue(long value) {
this.value = value;
}
}
消费者:实现WorkHandler接口,它来自Disruptor框架。
public class Consumer implements WorkHandler<PCData> {
@Override
public void onEvent(PCData pcData) throws Exception {
System.out.println(Thread.currentThread().getId() + ":Event: -- " +pcData.getValue() * pcData.getValue() + "--");
}
}
小给这的作用是读取数据进行处理,这里数据的读取已经由Disruptor框架进行封装了,onEvent()方法为框架的回调方法,这里只需要简单地进行数据处理即可。
PCDataFactory工厂类:会在Disruptor框架系统初始化时,构造所有的缓冲区中的对象实例(Disruptor框架会预先分配空间)。
import com.lmax.disruptor.EventFactory;
public class PCDataFactory implements EventFactory<PCData> {
@Override
public PCData newInstance() {
return new PCData();
}
}
生产者Producer:
import com.lmax.disruptor.RingBuffer;
import java.nio.ByteBuffer;
public class Producer {
private final RingBuffer<PCData> ringBuffer;
public Producer(RingBuffer<PCData> ringBuffer){
this.ringBuffer = ringBuffer;
}
public void pushData(ByteBuffer bb){
long sequence = ringBuffer.next();
try {
PCData event = ringBuffer.get(sequence);
event.setValue(bb.getLong(0));
} finally {
ringBuffer.publish(sequence);
}
}
}
生产者需要一个RingBuffer的引用,也就是环形缓冲区。它有一个重要的方法pushData()将产生的数据推入缓冲区。方法pushData()接受一个ByteBuffer对象。在ByteBuffer对象中,可以用来包装任何数据类型。这里用来存储long整数,pushData()方法的功能就是将传入的ByteBuffer对象中的数据提取出来,并装载到环形缓冲区。
next()方法得到下一个可用的序列号。通过序列号取得下一个空闲可用的PCData对象,并且将PCData对象的数据设为期望值,这个期望值最终会传递给消费者,最后进行数据发布,只有发布后的数据才会真正被消费者看见。
Main:
import com.lmax.disruptor.BlockingWaitStrategy;
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.dsl.Disruptor;
import com.lmax.disruptor.dsl.ProducerType;
import java.nio.ByteBuffer;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
PCDataFactory factory = new PCDataFactory();
int bufferSize = 1024;
Disruptor<PCData> disruptor = new Disruptor<>(factory,bufferSize,executor,
ProducerType.MULTI,new BlockingWaitStrategy());
disruptor.handleEventsWithWorkerPool(new Consumer(),new Consumer(),new Consumer(),new Consumer());
disruptor.start();
RingBuffer<PCData> ringBuffer = disruptor.getRingBuffer();
Producer producer = new Producer(ringBuffer);
ByteBuffer bb = ByteBuffer.allocate(8);
for(long i = 0; true; i++){
bb.putLong(0,i);
producer.pushData(bb);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("add data " + i);
}
}
}
4.3 提高消费者的响应时间:选择合适的策略
当有新数据在Disruptor框架的环形缓冲区中产生时,消费者如何知道这些新产生的数据呢?或者说,消费者如何监控缓冲区中的信息呢?为此,Disruptor框架提供了集中策略,这些策略由WaitStrategy接口进行封装,主要有以下几种实现:
- BlockingWaitStrategy:这是默认的策略。使用BlockingWaitStrategy和使用BlockingQueue是非常类似的,他们都是使用锁和条件(Condition)进行数据的监控和线程的唤醒。因为涉及线程的切换,BlockingWaitStrategy策略最节省CPU,但是在高并发下它是性能表现最糟糕的一种等待策略。
4.4 CPU Cache的优化:解决伪共享问题
5 Future模式
5.1 Future模式的主要角色
5.2 Future模式的简单实现
5.3 JDK中的Future模式
Future模式很常用,因此JDK内部已经为我们准备了一套完整的实现。
5.4 Guava对Future模式的支持
示例代码:
import java.util.concurrent.Callable;
public class RealData implements Callable<String> {
private String para;
public RealData(String para){
this.para = para;
}
@Override
public String call() throws Exception {
StringBuffer sb = new StringBuffer();
for(int i = 0; i < 10; i++){
sb.append(para);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return sb.toString();
}
}
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
public class FutureGuavaDemo {
public static void main(String[] args) {
ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
ListenableFuture<String> task = service.submit(new RealData("x"));
task.addListener(()->{
System.out.println("异步处理成功");
try {
System.out.println(task.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
},MoreExecutors.directExecutor());
System.out.println("main task done.....");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
可以看到Future的执行费并没有阻塞主线程,主线程很快正常结束,当Future执行完成后自动回调addListener()方法中的代码。
使用Futures工具类后的代码:
import com.google.common.util.concurrent.*;
import java.util.concurrent.Executors;
public class GuavaFutureDemo {
public static void main(String[] args) {
ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
ListenableFuture<String> task = service.submit(new RealData("x"));
Futures.addCallback(task, new FutureCallback<String>() {
@Override
public void onSuccess(String s) {
System.out.println("异步处理成功,result= " + s);
}
@Override
public void onFailure(Throwable throwable) {
System.out.println("异步处理失败,e=" + throwable);
}
},MoreExecutors.newDirectExecutorService());
System.out.println("main task done....");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
5.6 并行流水线
有数据相关性的计算无法并行化。
在玩偶的生产中,头、身体、四肢和服装四个步骤是无法并行化,因为他们有依赖性。但是在制作多个玩偶时,每个步骤就可以并行化了。
Msg类:
public class Msg {
public double i;
public double j;
public String orgStr = null;
}
Plus类:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class Plus implements Runnable {
public static BlockingQueue<Msg> bq = new LinkedBlockingQueue<Msg>();
@Override
public void run() {
while (true){
try {
Msg msg = bq.take();
msg.j = msg.i + msg.j;
Multiply.bq.add(msg);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Multiply类:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class Multiply implements Runnable {
public static BlockingQueue<Msg> bq = new LinkedBlockingQueue<Msg>();
@Override
public void run() {
while (true){
try {
Msg msg = bq.take();
msg.i = msg.i * msg.j;
Div.bq.add(msg);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Div类:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class Div implements Runnable {
public static BlockingQueue<Msg> bq = new LinkedBlockingQueue<Msg>();
@Override
public void run() {
while (true){
try {
Msg msg = bq.take();
msg.i = msg.i / 2;
System.out.println(msg.orgStr + "=" + msg.i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
PStreamMain类:
public class PStreamMain {
public static void main(String[] args) {
new Thread(new Plus()).start();
new Thread(new Multiply()).start();
new Thread(new Div()).start();
for(int i = 1; i<=1000; i++){
for(int j = 1; j <= 1000; j++){
Msg msg = new Msg();
msg.i = i;
msg.j = j;
msg.orgStr = "((" + i + "+" + j + ") * " + i + ") / 2";
Plus.bq.add(msg);
}
}
}
}
输出:
5.7 并行搜索
并行搜索代码:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class ParallelSearch {
static ExecutorService pool = Executors.newCachedThreadPool();
static final int Thread_Num = 2;
static AtomicInteger result = new AtomicInteger(-1);
static int[] arr = new int[]{45,213,565,7,324,78,7879,435,43,6768,4,76,32,5,8,54,324,23};
public static int search(int searchValue, int beginPos, int endPos){
int i = 0;
for(i = beginPos; i < endPos; i++){
if(result.get() >= 0){
return result.get();
}
if(arr[i] == searchValue){
if(!result.compareAndSet(-1,i)){
return result.get();
}
return i;
}
}
return -1;
}
public static int pSearch(int searchValue) throws ExecutionException, InterruptedException {
int subArrSize = arr.length / Thread_Num + 1;
List<Future<Integer>> re = new ArrayList<>();
for(int i = 0; i < arr.length; i += subArrSize){
int end = i + subArrSize;
if(end >= arr.length){
end = arr.length;
}
re.add(pool.submit(new SearchTask(searchValue,i,end)));
}
for(Future<Integer> fu:re){
if(fu.get() >= 0){
return fu.get();
}
}
return -1;
}
public static class SearchTask implements Callable<Integer>{
int begin, end, searchValue;
public SearchTask(int searchValue, int begin, int end){
this.begin = begin;
this.end = end;
this.searchValue = searchValue;
}
@Override
public Integer call() throws Exception {
int re = search(searchValue,begin,end);
return re;
}
}
public static void main(String[] args) {
try {
int pos = pSearch(76);
System.out.println(pos);
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
5.8 并行排序
5.8.1 分离数据相关性:奇偶交换排序
串行奇偶交换排序 :
public class OddEvenNumberSort {
public static int[] oddEvenSort(int[] arr){
int exchFlag = 1, start = 0;
while (exchFlag ==1 || start == 1){
exchFlag = 0;
for(int i = start; i < arr.length-1; i += 2){
if(arr[i] > arr[i+1]){
int temp = arr[i];
arr[i] = arr[i+1];
arr[i+1] = temp;
exchFlag = 1;
}
}
if(start == 0){
start = 1;
}else {
start = 0;
}
}
return arr;
}
public static void main(String[] args) {
int[] arr = new int[]{5,67,67,12,3,74};
int[] sort = oddEvenSort(arr);
for (int i = 0; i < arr.length; i++){
System.out.println(sort[i]);
}
}
}
并行奇偶交换排序:
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ParallelOddEvenSwapSort {
static int exchFlag = 1;
static int[] arr = new int[]{312,54,6,2,223,24,65};
static ExecutorService pool = Executors.newCachedThreadPool();
static synchronized void setExchFlag(int v){
exchFlag = v;
}
static synchronized int getExchFlag(){
return exchFlag;
}
public static class OddEvenSortTask implements Runnable{
int i;
CountDownLatch latch;
public OddEvenSortTask(int i, CountDownLatch latch){
this.i = i;
this.latch = latch;
}
@Override
public void run() {
if (arr[i] > arr[i+1]){
int temp = arr[i];
arr[i] = arr[i+1];
arr[i+1] = temp;
setExchFlag(1);
}
latch.countDown();
}
}
public static int[] pOddEvenSort(int[] arr){
int start = 0;
while (getExchFlag() == 1 || start == 1){
setExchFlag(0);
// 偶数的数组长度,当start为1时,只有len/2-1个线程
CountDownLatch latch = new CountDownLatch(arr.length/2-(arr.length%2==0?start:0));
for(int i = start; i < arr.length-1; i += 2){
pool.submit(new OddEvenSortTask(i,latch));
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
if(start == 0){
start = 1;
}else{
start = 0;
}
}
return arr;
}
public static void main(String[] args) {
int[] sort = pOddEvenSort(arr);
for(int i = 0; i < sort.length; i++){
System.out.println(sort[i]);
}
}
}
5.8.2 改进的插入排序:希尔排序
简单的插入排序:
public class InsertSort {
public static int[] insertSort(int[] arr){
int i = 0,j = 0;
for(i = 0; i < arr.length; i++){
int key = i;
for(j = i+1; j < arr.length; j++){
if(arr[key] > arr[j]){
key = j;
}
}
int temp = arr[i];
arr[i] = arr[key];
arr[key] = temp;
}
return arr;
}
public static void main(String[] args) {
int[] arr = new int[]{123,4,123,56,6,32,654,231};
int[] sort = insertSort(arr);
for(int i = 0; i<sort.length; i++){
System.out.println(sort[i]);
}
}
}
由于简单插入排序依赖上一次排序后的序列,无法做到并行化。
希尔排序是将整个数组根据间隔h划分为若干个子数组,这若干个子数组可以做到并行化。
简单希尔排序:
public static int[] shellSort(int[] arr){
int h = arr.length / 3 + 1;
while ( h > 1){
for(int i = h; i < arr.length; i++){
for(int j = i; j < arr.length; j+=h){
if(arr[j] < arr[j-h]){
int temp = arr[j];
arr[j] = arr[j-h];
arr[j-h] = temp;
}
}
}
h = h / 3 + 1;
}
return arr;
}
并行希尔排序:
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ParallelShellSortTask implements Runnable {
int i = 0;
int h = 0;
CountDownLatch latch;
int[] arr;
public ParallelShellSortTask(int i, int h, CountDownLatch latch, int[] arr){
this.i = i;
this.h = h;
this.latch = latch;
this.arr = arr;
}
@Override
public void run() {
if(arr[i] < arr[i-h]){
int temp = arr[i];
int j = i - h;
while (j >= 0 && arr[j] > temp){
arr[j+h] = arr[j];
j -= h;
}
arr[j+h] = temp;
}
latch.countDown();
}
public static void pShellSort(int[] arr){
ExecutorService pool = Executors.newCachedThreadPool();
int h = 1;
CountDownLatch latch = null;
while(h<=arr.length / 3){
h = h * 3 + 1;
}
while (h>0) {
System.out.println("h=" + h);
if (h >= 4) {
latch = new CountDownLatch(arr.length - h);
}
for (int i = h; i < arr.length; i++) {
if (h >= 4) {
pool.submit(new ParallelShellSortTask(i, h, latch, arr));
} else {
if (arr[i] < arr[i - h]) {
int temp = arr[i];
int j = i - h;
while (j >= 0 && arr[j] > temp) {
arr[j + h] = arr[j];
j -= h;
}
arr[j + h] = temp;
}
}
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
h = (h - 1) / 3;
}
}
public static void main(String[] args) {
int[] arr= new int[]{123,34,456,678,32,23,3,56,2,23,645,8,0,432,4,5,7};
pShellSort(arr);
for(int i = 0; i< arr.length; i++){
System.out.println(arr[i]);
}
}
}
5.9 网络NIO
JavaNio是new IO的简称,它是一种可以替代java IO的一套新的IO机制。它提供了一套不同于java标准IO的操作机制。NIO与并发无直接关系,但是使用NIO技术可以大大提高线程使用效率。
NIO中有通道、缓冲区、文件IO和网络IO。
5.9.1 基于Socket的服务端多线程模式
示例代码:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MultiThreadEchoServer {
private static ExecutorService tp = Executors.newCachedThreadPool();
static class HandleMsg implements Runnable{
Socket clientSocket;
public HandleMsg(Socket clientSocket) {
this.clientSocket = clientSocket;
}
@Override
public void run() {
BufferedReader is = null;
PrintWriter os = null;
try {
is = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
os = new PrintWriter(clientSocket.getOutputStream(),true);
String inputLine = null;
long b = System.currentTimeMillis();
while ((inputLine = is.readLine())!=null){
os.println(inputLine);
}
long e = System.currentTimeMillis();
System.out.println("spend:" + (e-b)+"ms");
} catch (IOException e) {
e.printStackTrace();
}finally {
if(is!=null){
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(os!=null){
os.close();
}
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
ServerSocket echoServer = null;
Socket clientSocket = null;
try {
echoServer = new ServerSocket(8000);
} catch (IOException e) {
e.printStackTrace();
}
while (true){
try {
clientSocket = echoServer.accept();
System.out.println(clientSocket.getRemoteSocketAddress() + "connect!");
tp.execute(new HandleMsg(clientSocket));
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
public class ClientPrinter {
public static void main(String[] args) {
Socket client = null;
PrintWriter writer = null;
BufferedReader reader = null;
client = new Socket();
try {
client.connect(new InetSocketAddress("localhost",8000));
writer = new PrintWriter(client.getOutputStream(),true);
writer.println("Hello!");
writer.flush();
reader = new BufferedReader(new InputStreamReader(client.getInputStream()));
System.out.println("from server: " + reader.readLine());
} catch (IOException e) {
e.printStackTrace();
}finally {
if(writer != null){
writer.close();
}
if(reader != null){
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(client!=null){
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
但是这种模式会让CPU进行IO等待。
5.10.2 使用NIO进行网络编程
第 六 章 :java8/9/10与并发