Java并发编程

一,走入并行世界

1.2 必须知道的几个概念
1.2.1 同步(Synchronous)和异步(Asynchronous)

1.2.2 并发(Concurrency)和并行(Parallelism)

1.2.3  临界区
用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。

1.2.4 阻塞(Blocking)和非阻塞(Non-Blocking)
通常用来形容多个线程间的相互影响。比如一个线程占用了临界区资源,那么其他所有需要这个资源的线程就必须在这个临界区中进行等待。等待就会导致线程挂起,这种情况就是阻塞,此时,如果占用资源的线程一直不愿意释放资源,那么其他所有阻塞在这个临界区上的线程都不能工作。非阻塞的意思与之相反,他强调没有一个线程可以妨碍其他线程的执行,所以线程都会尝试不断向前执行。

1.2.5 死锁(Deadlock)、饥饿(Starvation)和活锁(Livelock)
死锁、饥饿和活锁都属于多线程的活跃性问题。如果发现上述几种情况,那么相关线程可能就不再活跃,也就是说他可能很难再继续玩下执行了。

1.3 并发级别
由于临界区的存在,多线程之间的并发必须受到控制。根据控制并发的策略,我们可以把并发的级别进行分类,大致上可以分为 阻塞、无饥饿、无障碍、无锁、无等待几种。

1.3.1 阻塞(Blocking)
一个线程是阻塞的,那么在其他线程释放资源前,当前线程无法继续执行。当我们使用synchronized关键字,或者重入锁时,我们得到的就是阻塞的线程。无论是synchronized或者重入锁,都会试图在执行后续代码前,得到临界区的锁,如果得不到,线程就会被挂起等待,直到占有了所需资源为止。

1.3.2 无饥饿(Starvation-Free)
如果线程之间是有优先级的,那么线程调度的时候总是会倾向于满足高优先级的线程。也就是说,对于同一个资源的分配,是不公平的,对于非公平的锁来说,系统允许高优先级的线程插队。这样有可能导致低优先级线程产生饥饿。但如果锁是公平的,满足先来后到,那么饥饿就不会产生,不管新来的线程优先级多高,想要获得资源,就必须乖乖排队,那么所有的线程都有机会执行。

1.3.3 无障碍(Obstruction-Free)
无障碍是一种最弱的非阻塞调度。两个线程如果是无障碍的执行,那么他们不会因为临界区的问题导致一方被挂起。一旦检测到共同修改了共享数据,把数据破坏了,它就会立刻对自己所做的修改进行回滚,确保数据安全,无障碍的多线程并不一定能顺畅的运行。相对于阻塞的控制方式是悲观策略,无障碍是一种乐观的策略。一种可行的无障碍实现可以依赖一个“一致性“来实现。线程在操作之前,先读取并保存这个标记,在操作完成后,再次读取,检查这个标记是否被更改过,如果两者一致的,则说明资源访问没有冲突,反之 则说明资源可能在自己操作过程中与其他写线程冲突,需要重试操作。而任何对资源有修改操作的线程,在修改数据前,都需要更新这个一致性的标记,表示数据不再安全。

1.3.4 无锁(Lock-Free)
无锁的并行都是无障碍的。在无锁的情况下,所有的线程都能尝试对临界区进行访问。但不同的是,无锁的并发,保证必然有一个线程能够在有限的步骤内完成操作并离开临界区。在无锁的调用中,一个典型的特点是可能会包含一个无穷循环。在这个循环中,线程会不断尝试修改共享变量。如果没有冲突,那么修改成功并程序退出,否则继续尝试修改。但无论如何,无锁的并行总能保证有一个线程是可以胜出的。至于临界区中竞争失败的线程,他们则必须不断重试,知道自己获胜,如果运行不好,总是尝试不成功,则会出现类似饥饿的现象,线程会停止不前。

1.3.5 无等待(Wait-Free)
无锁只要求有一个线程可以在有限步骤内完成操作,而无等待则是在无锁的基础上更进一步进行扩展。他要求所有的线程都必须在有限步骤内完成,这样就不会引起饥饿的问题。如果限制这个步骤的上限,还可以进一步分解为有界无等待和线程数无关的无等待几种,他们之间的区别只是对循环次数的限制不同。

1.5 Java内存模型:JMM
JMM的关键技术点都是围绕着多线程的原子性、可见性和有序性来建立的。

1.5.1 原子性
是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。比如,对于一个静态全局变量 int i,两个线程同时 对他赋值,线程A给他赋值1,线程B给他赋值-1,那么不管这2个线程以何种方式、何种步调工作,i 的值要么是1,要么是 -1.线程A和线程B之间是没有干扰的,这就是原子性的一个特点,不可被中断。

1.5.2 可见性(Visibility)
是指当一个线程修改了某一个共享变量的值,其他线程是否能够立刻知道这个修改。 在并行程序中,线程是分别被多个CPU调度执行,由于编译器优化或者硬件优化的缘故,CPU 1上的线程读取共享变量,并缓存在cache或寄存器里,如果这时CPU2上的线程修改了变量的 实际值,那么CPU1上的线程可能无法意识到这个改动,依然读取cache或寄存器中的数据,因此就产生了可见性的问题。  缓存优化、硬件优化、指令重排(为了提高执行效率而做的指令执行顺序的优化)以及编译器优化,都可能导致一个线程的修改不会立刻被其他线程察觉。

1.5.3 有序性(Ordering)
对于一个线程的执行代码而言,我们总是习惯的认为代码的执行是从先往后,一次执行的,在一个线程内确实表现成这样,但是在多个线程并发执行时,程序的执行顺序可能会出现乱序。写在前面的代码,会在后面执行;这个问题是因为程序在执行时,可能会进行指令重排,重排后的指令与原指令的顺序未必一致。 

1.5.4 哪些指令不能重排:Happen-Before规则
虽然Java虚拟机和执行系统会对指令进行一定的重排,但是有原则的,并非所有的指令都可以随便改变执行的位置,这些原则包括:
程序顺序原则:一个线程内保证语义的串行性
volatile规则:volatile修饰的变量的写,先发生于读,这保证了volatile变量的可见性
锁规则:解锁必然发生在随后的加锁前
传递性:A先于B,B先于C,那么A必然先于C
线程的start()方法先于它的每一个动作
线程的所有操作先于线程终结(Thread.join())
线程的中断(interrupt())先于被中断线程的代码
对象的构造函数执行、结束先于finalize()方法

二,并行程序基础
2.1 线程状态
线程的声明周期:NEW , RUNNABLE , BLOCKED,WAITING,TIMED_WAITING,TERMINATED
NEW表示刚刚创建的线程,这种线程还没有开始执行。等到线程的start()方法调用时,才表示线程开始执行。当线程执行时,处于RUNNABLE状态,表示线程所需要的一切资源都已经准备好了。如果线程在执行过程中遇到了synchronized同步块,就会进入BLOCKED阻塞状态,这时线程就会暂停执行,直到获得请求的锁。WAITING和TIMED_WAITING都表示等待状态,区别是WATING会进入一个无时间限制的等待,TIMED_WAITING会进行一个有时限的等待。那等待的线程究竟在等什么呢?一般来说,WAITING的线程正是在等待一些特殊的事件,比如,通过wait()等待的线程在等待notify()方法,二通过join()方法等待的线程则会等待目标线程的终止。一旦等到了期望的事件,线程就会再次执行,进入RUNNABLE状态。当线程执行完毕后,则进入TERMINATED状态,表示结束。线程状态是不可以返回进入的

2.2 线程的基本操作,
了解如何新建并启动线程,如何终止线程,中断线程等。并行操作比串行操作复杂得多。

2.2.1新建线程
方法1:继承Thread类并重载run方法 (不常用)
class xxx extends Thread(){
   @override
   public void run(){
         // 做一些事情
    }
}
xxx t1 = new xxx();
t1.start()

start方法会新建一个线程并自动让这个线程执行线程的run方法,不能直接调用run方法,如果直接调用,只是作为一个普通的方法调用。默认run方法什么也没做,如果想让线程干点事情,必须重载run方法

方法2:对象实现Runnable接口,并重载run方法,将对象的实例放入到Thread构造器中(常用)
Thread t1 = new Thread(new xxx());
t1.start()
方法3:实现Callable接口,重载call方法,将对象的实例放入到Thread构造器中(常用)
Thread t1 = new Thread(new xxx());
t1.start()

2.2.2 终止线程
线程运行完毕后,一般就终止了,但是有一些线程中的run方法有无限大循环的操作,可以使用stop方法,但这个方法是一个废弃的方法,stop方法太暴力了,有可能会把运行到一半的线程终止了,可能会引起一些数据不一致的问题。

解决办法也很简单,在线程的run方法中,根据一些条件,在对数据做修改的操作前执行bread的操作。

2.2.3线程中断
提供让线程停止更为强大的支持。线程中断并不会使线程立刻退出,而是给线程发送一个通知,告诉目标线程,有人希望你退出,至于目标线程接到通知后如何处理,则完全有目标线程自己决定。这点很重要,如果中断后,线程立刻无条件退出,又会遇到stop方法的问题。

与线程中断有关的三个方法:
public void Thread.interrupt() :中断线程,run方法中必须做是否中断的判断,然后做相关的操作和break来真实中断

public boolean Thread.isInterrupted():判断是否被中断

public static boolean Thread.interrupted() :判断是否被中断,并清除当前 中断状态

interrupt是一个实例方法,他通知目标线程中断(设置中断标志位,中断标志位表示当前线程已经被中断了),isInterrupted也是实例方法,他判断当前线程是否被中断(通过检查中断标志位),最后是静态方法 interrupted 也是用来判断当前线程的中断状态,但同时会清除当前线程的中断标志位状态。run方法中的wait()、sleep()等会引起中断

2.2.4等待(wait)和通知(notify)
为了支持多线程之间的协作,JDK提供了两个非常重要的接口:线程等待 wait() 方法和 线程通知 notify() / nodifyAll() 方法,此2个方法是属于Object基类的,意味着任何对象都可以调用这2个方法。
当在一个对象实例上调用了wait方法后,调用线程就会在这个对象上等待(调用线程进入对象的等待队列,这个队列可能会有多个线程,因为系统运行了多个线程,同时等待这个对象实例),会一直等到其他线程调用这个实例对象的notify方法为止(个人理解:必须是相同的对象实例去调用wait和notify)。这时系统会从这个队列中,随机选择一个线程,并将其唤醒(这个选择是不公平的,并不是先等待的线程会优先被选择,这个选择完全是随机的);wait方法必须被包含在对应的synchronized语句中;
个人理解:
A线程通过 synchronized 获得锁(监视器),其他线程等待,A线程执行自己的业务逻辑,当执行到wait方法时进行等待,并释放锁(监视器),其他线程获得锁(监视器),继续执行自己的业务逻辑,当执行到notify方法时,唤醒A线程,注意唤醒后的A线程不是立刻执行后续代码,后是尝试获取锁(监视器),获取成功后再执行后续代码(但是如果其他线程也获取了锁,并且没有释放锁,这时A线程还是无法获取到锁,也就无法继续执行wait方法后的业务逻辑了;当然其他线程有可能不需要获取锁,那么执行到notify方法后,A线程可以立刻获取到锁,并继续执行wait后的代码)

2.2.5 挂起(suspend)和继续执行(resume)线程
不推荐使用suspend去挂起线程的原因,是因为suspend在导致线程暂停的同时,并不会去释放任何锁资源。此时,其他任何线程想要访问被他占用的锁时,都会被牵连,导致无法正常继续运行。直到对应的线程上进行了resume操作,被挂起的线程才能继续,从而其他所有阻塞在相关锁上的线程也可以继续执行。但是如果resume操作意外地在suspend前就执行了,那么被挂起的线程可能很难有机会被继续执行,他所占用的锁不会被释放,可能会导致整个系统工作不正常。

2.2.6 等待线程结束(join)和谦让(yield)
很多时候一个线程的输入可能非常依赖于另外一个或多个线程的输出,此时这个线程就需要等待依赖线程执行完毕,才能继续执行,join操作来实现这个功能;join本质是让调用线程wait在当前线程对象实例上(个人理解:wait就是释放锁,其他线程获取锁),当线程执行完毕后,被等待的线程会在退出前调用notifyAll通知所有的等待线程继续执行。

yield方法一旦执行,他会使当前线程让出CPU,注意让出CPU并不表示当前线程不执行了。当前线程在让出CPU后,还会进行CPU资源的争夺,但是是否能够再次被分配到,就不一定了。对yield调用就好像在说:我已经完成一些最重要的工作了,我应该是可以休息一会了,可以给其他线程一些工作机会啦;如果你觉得一个线程不那么重要,或者优先级非常低,而且又害怕他会占用太多的CPU资源,那么可以在适当的时候调用Thread.yield() ,给予其他重要线程更多的工作机会。

2.3 volatile 与 Java内存模型 JMM
为了在适当的场合,确保线程间的有序性、可见性和原子性,Java使用了一些特殊的操作或关键字来申明、告诉jvm虚拟机,在这个地方,要尤其注意,不能随意变动优化目标指令。关键字volatile就是其中之一。当你去用volatile去申明一个变量时,就等于告诉虚拟机,这个变量极有可能会被某些程序或者线程修改,为了确保这个变量被修改后,应用程序范围内的所有线程都能够“看到”这个改动,虚拟机就必须采用一些特殊的手段,保证这个变量的可见性等特点。根据编译器的优化规则,如果不使用volatile申请变量,那么这个变量被修改后,其他线程可能并不会被通知到,甚至在别的线程中,看到变量的修改顺序都会是反的。但一旦使用volatile,虚拟机就会特别小心地处理这种情况。 但要注意的是,volatile对于保证操作的原子性是有非常大的帮助,但是并不能代替锁,他也无法保证一些复杂操作的原子性。此外,volatile也能保证数据的可见性和有序性。volatile并不能真正的保证线程安全,他只能确保一个线程修改了数据后,其他线程能够看到这个改动,但当两个线程同时修改某一个数据时,却依然会产生冲突。

2.4 分门别类的关联:线程组
线程太多时,可以把相似的线程放入线程组: 
ThreadGroup tg  = new ThreadGroup("GroupName");
Thread t1 = new Thread(tg, new ThreadGroupName(), "T1");
Thread t2 = new Thread(tg, new ThreadGroupName(), "T2");
t1.start():
t2.start();
tg.activeCount();
tg.list();

2.5 守护线程(Daemon)
守护线程必须在线程start之前设置,否则会被当成用户线程使用,而不是守护线程。
Thread t =  new DaemodT():
t.setDaemon(true);
t.start();

2.6 线程优先级
优先级高的线程在竞争资源时会更有优势,更可能抢占资源,但这是一个概率问题,如果运气不好,高优先级线程可能会抢占失败。
在Java中,使用1到10表示线程优先级,数字越大则优先级越高,一般可以使用内置的三个静态标量表示:
public final staitc int MIN_PRIORITY = 1;
public final staitc int NORM_PRIORITY = 5;
public final staitc int MAX_PRIORITY = 10;

2.7 线程安全的概念与synchronized
线程安全是并行程序的根本和根基;volatile并不能真正的保证线程安全,他只能确保一个线程修改了数据后,其他线程能够看到这个改动,但当两个线程同时修改某一个数据时,却依然会产生冲突。这时可以在修改数据的代码块,添加synchronized关键字,表示每一次,只能有一个线程进入同步块,从而保证线程间的安全性。

synchronized可以有多种用法:
指定加锁对象:对象锁
直接作用于实例方法:实例锁
直接作用于静态方法:类锁

2.8 隐蔽的错误
2.8.1 无提示的错误案例,例如变量类型溢出
2.8.2 并发下的ArrayList异常(不是线程安全的)
2.8.3 并发下的HashMap(不是线程安全的)
2.8.4 初学者常见问题:错误的加锁
对全局变量Integer i加synchronized同步代码块,做 i++操作,这种是错误的,运维Integer在java中属于不变对象,这里i++操作实质上是新建一个Integer对象,并把旧Integer的值加上1,赋值给这个新的Integer对象,所以这里的锁实质上已经发生变化。

三,JDK并发包
介绍三大部分:同步控制的工具(多线程控制方法)、线程池(提高线程调度的性能)、并发容器(专为并发访问设计,高效、安全、稳定的实用工具)

3.1 多线程的团队协作:同步控制
介绍synchronized,wait 和 notify方法的增强版

3.1.1 synchronized内部锁的功能扩展:重入锁
重入锁使用 java.util.concurrent.locks.ReetrantLock类来实现,开发人员必须手动指定何时加锁 (lock),何时释放锁(unlock),也正因为这样,重入锁对逻辑控制得灵活性要远远好于synchronized内部锁,但值得注意的是,在退出临界区时,必须记得释放锁,否则其他线程就没有机会再访问临界区了。重入锁是可以反复进入的,一个线程连续两次获得同一把锁是允许的。

中断响应:对于synchronized来说,如果一个线程在等待锁,那么结果只有两种情况,要么他获得这把锁继续执行,要么他就保持等待;而使用重入锁,则提供另外一种可能,那就是线程可以被中断:lockInterruptibly() / unlock(),lockInterruptibly方法是一个可以对中断进行响应的锁申请动作,即在等待锁的过程中,可以响应中断。

锁申请等待时限:使用tryLock方法进行一次限时的等待,如果超过时间仍然无法获取锁,则返回false,反之则返回true,这里tryLock方法有2个参数(第1个时间,第2个时间单位),也可用用无参数的tryLock方法,则表示当前线程会尝试获得锁,如果锁没有被其他线程占用,则申请锁会成功,并立刻返回true,反之立刻返回false,这种模式不会引起线程等待,也不会产生死锁。

公平锁:大多数情况下,锁的申请都是非公平的。当锁可用时,系统会从等待队列中随机挑选一个线程来获取锁;而公平锁的特点是:他不会产生饥饿,只要你排队,最终还是可以等到资源的;如果我们用synchronized关键字进行锁控制,那么产生的锁就是非公平的,而重入锁允许我们对公平性进行设置:
public ReetrantLock(boolean fair)
参数fair为true时,表示锁是公平的;但公平锁要维护一个有序队列,实现成本比较高,性能相对也非常低下;默认情况锁是非公平的;

在重入锁的实现中,主要包含三个要素:
第一,是原子性
第二,是等待队列
第三,是阻塞原语park和unpark,用来挂起和恢复线程。没有得到锁的线程将会被挂起

3.1.2 重入锁的好搭档:Condition条件
重入锁的  await和signal方法,相对应于 synchronized的wait 和 notify方法,用于线程之间的等待和唤醒:
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition(); 

3.1.3 允许多个线程同时访问:信号量(Semaphore)
信号量是对锁的扩展,无论是synchronized还是重入锁ReentrantLock,一次都只允许一个线程访问一个资源,二信号量却可以指定多个线程,同时访问某一个资源,有如下构造器:
Semaphore(int permits);第1个参数指定能申请多少个线程的许可
Semaphore(int permits, boolean fair) :第2个参数 指定是否为公平

信号量的主要逻辑方法有:
void acquire():尝试获得一个准入的许可,若无法获得,则线程会等待,直到有线程释放一个许可或者当前线程被中断
void acquireUninterruptibly():和acquire相似,但不响应中断
boolean tryAcquire():尝试获得一个许可,如果成功返回true,反之返回false,他不会进行等待
boolean tryAcquire(loang timeout, TimeUnit unit)
void release():用于在线程访问资源结束后,释放一个许可,以使其他等待许可的线程可以进行资源访问

Semaphore  semp = new Semaphore(5)// 申明了一个包含5个(线程)许可的信号量
semp.acquire() // 申请信号量

3.1.4 ReadWriteLock 读写锁
读写锁允许多个线程同时读,写写操作和读写操作间依然是需要相互等待和持有锁。
读 与 读:非阻塞
读 与 写:阻塞
写 与 读:阻塞
写 与 写:阻塞

如果系统中,读操作次数远远大于写操作,则读写锁就可以发挥最大的功效,提升系统的性能。
ReentrantReadWriteLock reaWriteLock = new ReentrantReadWriteLock();
Lock readLock = reaWriteLock.readLock();  // 读锁,多个线程并行执行
Lock writeLock = reaWriteLock.writeLock();  // 写锁,多个线程串行执行

3.1.5 倒计时器:CountDownLatch
CountDownLatch是一个非常实用的多线程控制工具。这个工具通常用来控制线程等待,他可以让某一个线程等待直到 倒计时结束,再开始执行:
public CountDownLatch(int count); // 接收一个整数做为参数,即当前这个计数器的计数个数,表示需要多少个线程完成任务
public void await();   //调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public boolean await(long timeout, TimeUnit unit);  //和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public void countDown() { };//将count值减1

3.1.6 循环栅栏:CyclicBarrier (有点复杂,好像不需要执行countDown的方法对计数器减1,计数器内置??)
CyclicBarrier是另外一种多线程并发控制实用工具,和CountDownLatch非常相似,他也可以实现线程间的计数等待,但他的功能比CountDownLatch更加复杂且强大,计数器可以循环反复使用:
public CyclicBarrier(int parties, Runnable barrierAction)// parties为计数器的个数,barrierAction指当计数器一次计数完成后,系统会执行的动作。
举例:司令命令10个士兵统一集结完毕(使用await),10个士兵统一完成任务(使用await),然后司令说 任务完成

3.1.7 线程阻塞工具类:LockSupport
LockSupport是一个非常方便实用的线程阻塞工具,他可以在线程内任意位置让线程阻塞,和wait方法比较,他不需要先获取某个对象的锁,也不会抛出InterruptedException异常。LockSupport的静态方法part()可以阻塞当前线程,类似还有 partNanos() / parkUntil()等方法,他们实现了一个限时的等待,他弥补了suspend/resume执行顺序变乱后导致的问题,与之对应的方法是 part / unpark,不会由于unpark在park之前执行,导致挂起线程,使用了类似信号量的许可技术。

3.2 线程复用:线程池
多线程的软件设计方法确实可以最大限度地发挥现代多核处理器的计算能力,提高生产系统的吞吐量和性能,但是,若不加控制和管理的随意使用线程,对系统的性能反而会产生不利的影响。

3.2.1 什么是线程池
为了避免系统频繁创建和销毁线程,我们可以让创建的线程进行复用。在使用线程池后,创建线程线程变成了从线程池获得空闲的线程,关闭线程变成了向池子归还线程。

3.2.2 JDK对线程池的支持
为了能够更好地控制多线程,JDK提供了一套Executor框架,帮助开发人员有效地进行线程控制,其本质就是一个线程池。
ThreadPoolExecutor表示一个线程池,Executors类则扮演了线程池工厂的角色,通过Executors可以取得一个拥有特定功能的线程池,ThreadPoolExecutor类实现了Executor接口,任何Runnable的对象都可以被ThreadPoolExecutor线程池调度。
Executor框架提供了各种类型的线程池,主要有以下工厂方法:

// 返回一个固定线程数量的线程池,该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立刻执行,若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
public static ExecutorService newFixedThreadPool(int nThreads)
{
    return new ThreadPoolSize(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())
}

//返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务
public static ExecutorService newSingleThreadExecutor()
{
    return new ThreadPoolSize(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())
}

//返回一个可根据实际情况调整线程数量的线程池,线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程,若所有线程均工作,又有新的任务提交,则会创建新的线程 处理任务,所有线程在当前任务执行完毕后,将返回线程池进行复用。
public static ExecutorService newCachedThreadPool()
{
    return new ThreadPoolSize(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronouseQueue<Runnable>())
}

public static ScheduleExecutorService newSingleThreadScheduledExecutor()
返回一个ScheduleExecutorService 对象,线程池大小为1,ScheduleExecutorService 接口在ExecutorService接口之上扩展了在给定时间执行某任务的功能,如在某个固定的延时之后执行,或者周期性执行某个任务。 

public static ScheduleExecutorService newScheduledThreadPool(int corePoolSize)
也返回一个ScheduleExecutorService对象,但该线程可以指定线程数量,和newSingleThreadScheduledExecutor相同,并不一定会立刻安排执行任务,他其实是起到了计划任务的作用,有如下几个方法:

/ /在给定时间,对任务进行一次调度
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit); 

//对任务进行周期性调度,调度算法:下一个调度时间 = initialDelay + 1 * period,下下一个调度= initialDelay + 2 * period,后续的调度并不关心前一个调度是否完成,只以前一个调度的开始 + period时间来计算,相对于第一次调度时间来说是固定的
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)

//对任务进行周期性调度,调度算法:下一个调度时间 = 上一个任务的结束时间 到 下一个任务的调度时间,关心前一个调度的结束时间,相对于第一次调度时间来说是动态的
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) 

3.2.3 核心线程池的内部实现
newFixedThreadPool、newSingleThreadExecutor 、newCachedThreadPool 内部实现均使用了ThreadPoolExecutor实现,都只是ThreadPoolExecutor类的封装:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, Timeunit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)

参数含义:
corePoolSize:指定了线程池中的线程数量
maximumPoolSize:指定了线程池中的最大线程数量
keepAliveTime:当线程池线程数量超过corePoolSize时,多余的空闲线程的存活时间,即,超过corePoolSize的空闲线程,在多长时间内,会被销毁
unit:keepAliveTime的单位
workQueue:任务队列,被提交但尚未被执行的任务队列,他是一个BlockingQueue接口的对象,仅仅用来存放Runnable对象,根据功能分类,可以使用以下几种BlockingQueue:

  • 直接提交的队列:SynchronousQueue,没有容量,每一个插入操作都要等待一个相应的删除操作,反之,每一个删除操作都要等待对应的插入操作,使用它时,提交的任务不会被真实的保存,而总是将新任务提交给线程执行,如果没有空闲的线程,则尝试创建新的线程(它总是会迫使线程池增加新的线程执行任务),如果线程数量达到了最大值,则执行拒绝策略,因此使用它时,通常要设置很大的maxinumPoolSize值,否则很容易执行拒绝策略。
  •  
  • 有界的任务队列:可以使用ArrayBlockingQueue实现,ArrayBlockingQueue的构造函数必须带有一个容量参数,表示该队列的最大容量,当使用它时,若有新的任务需要执行,如果线程池的实际线程数小于corePoolSize,则会优先创建新的线程,若大于corePoolSize,则会将新任务加入等待队列,若等待队列已满,无法加入,则在总线程数不大于maximumPoolSize的前提下,创建新的线程执行任务,若大于maximumPoolSize,则执行拒绝策略
  •  
  • 无界的任务队列:可以通过LinkedBlockingQueue类实现,与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况,当有新的任务到来,系统的线程数小于corePoolSize时,线程池会产生新的线程执行任务,但当系统的线程数达到corePoolSize后,就不会继续增加(个人理解:此新任务放入到等待队列),若后续仍有新的任务加入,而又没有空闲的线程资源,则任务直接进入等待队列等待,若任务创建和处理的速度差异很大,无界队列会保持快速增长,直到耗尽系统内存。
  •  
  • 优先任务队列:是带有执行优先级的队列,他通过ProrityBlockingQueue实现,可以控制任务的执行先后顺序,他是一个特殊的无界队列,无论是有界队列ArrayBlockingQueue,还是未指定大小的无界队列LinkedBlockingQueue,都是按照先进先出算法处理任务的,而ProrityBlockingQueue则可以根据任务自身的优先级顺序先后执行,在确保系统性能的同时,也能有很好的质量保证(总是确保高优先级的任务先执行)


threadFactory:线程工厂,用于创建线程,一般用默认的即可
handler:拒绝策略,当任务太多来不及处理,如何拒绝任务

3.2.4 超负载ile怎么办:拒绝策略 (RejectedExecutionHandler )
实现了RejectedExecutionHandler 接口的四种内置的拒绝策略:
AbortPolicy:该策略会直接抛出异常,阻止系统正常工作
CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务,这样做不会真的丢弃任务,但是任务提交线程的性能可能会急剧下降
DiscardOledestPolicy:该策略将丢弃最老的一个请求,也就是即将被执行的一个任务(先进入队列,先出来),并尝试再次提交当前任务
DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理,如果允许任务丢失,这可能是最好的一种方案(例如传输不太重要的系统日志等)

3.2.5 自定义线程创建:ThreadFactory
ThreadFactory 是一个接口,他只有一个方法,用来创建线程,当线程池需要新建线程时,就会调用这个方法:
Thread newThread(Runnable r);

自定义线程池可以让我们更加自由地设置池子中所有线程的状态。

3.2.6 扩展线程池
ThreadPoolExecutor是一个可以扩展的线程池,他提供了 beforeExecute() / afterExecute() / terminated() 三个接口对线程池进行控制。在默认的ThreadPoolExecutor实现中,提供了空的beforeExecute() / afterExecute()实现,在实际应用中,可以对其进行扩展来实现对线程池运行状态的跟踪,输出一些有用的信息,以帮助系统故障诊断,这对于多线程程序错误排查是很有帮助的。

3.2.7 优化线程池的线程数量
线程池的大小需要考虑CPU数量、内存大小等因素:
Ncpu = CPU数量
Ucpu = 目标CPU的使用率, 0 <= Ucpu <= 1
W/C = 等待时间与计算时间的比率

最优的池大小等于: Nthreads = Ncpu *  Ucpu  *  (1 + W/C)
Java中获取可用CPU的数量:Runtime.getRuntime().availableProcessors()

3.2.8 在线程池中寻找堆栈
线程池有时可能会“吃”掉程序抛出的异常,导致我们对程序的错误一无所知,一种简单的避免办法是放弃submit(),改用execute;或将submit的返回值赋值给 Future:
Future re = pools.submit(new xxxx());
re.get()

3.2.9 分而治之:Fork / Join 框架
也就是对一个大任务的串行执行,分割成多个小任务进行并行执行,以此来提高执行的效率。在Linux系统中,函数fork用来创建子进程,使得系统进程可以多一个执行分支。Java也沿用了类似的命名方式,join表示等待,也就是使用fork()后系统多了一个执行分支(线程),所以需要等待这个分支执行完毕,才有可能得到最终的结果,因此join就表示等待。对于fork方法并不急着开启线程,而是提交给ForkJoinPool线程池进行处理,以节省系统资源。
由于线程池的优化,提交的任务和线程数量并不是一对一的关系,在绝大多数情况下,一个无聊线程实际上是需要处理多个逻辑任务的,因此,每个线程必然需要拥有一个任务队列。在实际执行过程中,可能遇到一种情况:线程A已经把自己的任务都执行完成了,而线程B还有一堆任务等着处理,这时,线程A会帮助线程B,从线程B的任务队列的底部(避免数据竞争,线程B是从队列顶部拿任务)拿一个任务过来处理,尽可能地达到平衡。可以向ForkJoinPool线程池提交一个ForkJoinTask任务,ForkJoinTask任务就是支持fork分解和join等待的任务。ForkJoinTask有2个重要的子类:
RecursiveAction:没有返回值的任务
RecursiveTask:可以携带返回值的任务,可以重载compute方法,实现自己的业务逻辑

class Xxx extends RecursiveTask<Long> { // 重写compute方法,调用fork执行分支,然后用join等待,得到最终结果 }
ForkJoinPool forkJoinPool = new ForkJoinPool();
Xxx task = new Xxx();
ForkJoinTask<Long> result = forkJoinPool.submit(task);
long res = result.get(); // 得到最终结果,如果任务没有结束,那么主线程就会在get方法时等待
system.out.println(res)

ForkJoinPool线程池使用一个无锁的栈来管理空闲线程,如果一个工作线程暂时取不到可用的任务,则可能会被挂起,挂起的线程将会被压入有线程池维护的栈中,待将来有任务可用时,再从栈中唤醒线程。(个人理解:所以ForkJoinPool线程池并没有使用到任务队列吧??)

3.3 JDK的并发容器
容器就是为大家编写程序而准备好的线程数据结构,可用在里面找到链表、HashMap、队列等,他们都是线程安全的。

3.3.1 并发集合简介

  • ConcurrentHashMap:这是一个高效的并发HashMap,可以理解为一个线程安全的HashMap
  • CopyOnWriteArrayList:这是一个List,和ArrayList是一族,在读多写少的场合,这个List的性能非常好,远远好于Vector
  • ConcurrentLinkedQueue:高效的并发队列,使用链表实现,可以看做一个线程安全的LinkedList,非阻塞队列
  • BlockingQueue:是一个接口,JDK内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为多线程间数据共享的通道(生成消费模型,平衡两边处理速度)。
  • ConcurrentSkipListMap:跳表的实现,这是一个Map,使用跳表的数据结构进行快速查找
  • Vector:一个线程安全的ArrayList,使用synchronized重量级锁实现的线程安全的List,效率不高


另外,Collections工具类可以帮助我们将任意集合,包装成线程安全的集合。

3.3.2 线程安全的哈希(HashMap)
一种可行的办法是使用Collections.synchronizedMap()方法包装我们的HashMap,产生的HashMap就是线程安全的:
public static Map m = Collections.synchronizedMap( new HashMap() );
Collections.synchronizedMap()会生成一个名为SynchronizedMap的Map,他使用委托,将自己所有Map相关的功能交给传入的HasnMap实现,而自己则主要保证线程安全,他的实现原理是内部有一个Object的对象,每次对数据的修改和获取都通过synchronized(object)加锁来实现访问的互斥,效率不高;更高效的办法是使用ConcurrentHashMap,他专门为并发进行了性能优化,更加适合多线程的场合。

3.3.3 线程安全的列表(LinkedList)
类似HashMap的实现,也可以通过Collections包装为线程安全的List:
public static List<String> list = Collections.synchronizedList( new LinkedList<String> );

3.3.4 高效读写的队列:剖析ConcurrentLinkedQueue
ConcurrentLinkedQueue应该是在高并发环境中性能最好的队列,之所以能有很好的性能,是因为其内部复杂的实现;(没有使用锁,而是使用了CAS操作,要求在应用层面保证线程安全,并处理一些可能存在的不一致的问题,大大增加了程序设计的难度,这里需要花点时间看看其实现原理)

3.3.5 高效读取:不变模式下的CopyOnWriteArrayList
在很多场合中,读操作可能会远远大于写操作。由于读操作根本不会修改原有的数据,因此对于每次读取都加锁,其实是一种资源浪费;我们应该允许多个线程同时访问List的内部数据,毕竟读取操作是安全的。根据读写锁的思想,读锁和读锁之间也不冲突,但是读操作会受到写操作的阻碍,当写发生时,读就必须等待,否则可能读到不一致的数据。同理,如果读操作正在进行,程序也不能进行写入。为了将读取的性能发挥到极致,JDK中提供了CopyOnWriteArrayList类,对他来说,读取完全不用加锁,写入也不会阻碍读取操作,只有写入和写入之间需要进行同步等待,这样一来,读操作的性能就会大幅度提升。
所谓CopyOnWrite就是在写入操作时,进行一次自我复制,并不直接修改原有的内容(保证了当前在读线程的数据一致性),而是对原有的数据进行一次复制,将修改的内容写入副本,写完之后,再将修改完的副本替换原来的数据(原来的数据被定为为volatile,保证读取线程可以马上察觉到这个修改),这样就可以保证写操作不会影响读了。

3.3.6 数据共享通道:BlockingQueue
ConcurrentLinkedQueue作为高性能的队列,适用于对全局的集合进行操作的场景(通过CAS无锁实现),但多线程的开发模式还会引入一个问题,那就是如何进行多个线程间的数据共享呢?比如,线程A希望给线程B发一个消息,用什么方式告知线程B是比较合理的呢?
BlockingQueue是一个接口,他的主要实现(通过锁实现):
ArrayBlockingQueue:基于数组实现,适合做有界队列,
LinkedBlockingQueue:基于链表实现,适合做无界队列(实际上也是有界的,只是容量默认为Integer.MAX_VALUE)

BlockingQueue之所以适合作为数据共享的通道,关键在于阻塞,当服务线程(指不断获取队列中的消息,进行处理的线程)处理完成队列中所有消息后(队列为空),他会让服务线程进行等待,当有新的消息进入队列后,自动将线程唤醒。
ArrayBlockingQueue队列如何做到服务线程的等待和唤醒(通过ReentrantLock + Condition实现await和signal):
take获取队列消息: 判断对了是否为空,如果是则await等待,反之则取一个消息
put将消息放入队列:判断队列是否满了,如果是,则等待,反之插入一个消息

3.3.7 随机数据结构:跳表(SkipList)
跳表是一种可以用来快读查找的数据结构,跳表的另外一个特点是随机算法,跳表的本质是同时维护了多个链表,并且链表是分层的;最低层的链表维护了跳表的所有的元素,每上面一层链表都是下面一层的子集,一个元素插入哪些层是完全随机的;跳表内的所有链表的元素都是排序的,查找时,可以从顶级链表开始找,一旦发现被查找的元素大于当前链表中的取值,就会转入下一层链表继续找,查找过程中,搜索是跳跃式的。跳表是一种使用空间换时间的算法。使用ConcurrentSkipListMap会保存元素的顺序,和HashMap不同,对跳表的遍历输出是有序的。ConcurrentSkipListMap包含:
1,Node:每个Node包含KEY和VALUE,同时还包含一个指向下一个Node的next元素,所有对Node的操作,都是使用CAS方法;
2,Index:表示索引,包装了Node,同时增加了向下的引用和向右的引用;
3,HeadIndex:表示链表头部的第一个Index,记录当前处于哪一层;

四,锁的优化及注意事项

4.1 有助于提高锁性能的几点建议
4.1.1 减少锁持有的时间,有助于降低锁冲突的可能性,进而提高系统的并发能力。

4.1.2 减少锁粒度
这种技术典型的使用场景就是ConcurrentHashMap类的实现,对于ConcurrentHashMap,他内部将HashMap进一步细分为若干个小的HashMap,称之为端(Segment),默认情况下,一个ConcurrentHashMap被进一步细分为16个段。如果需要在ConcurrentHashMap中增加一个新的表项,并不是将整个HashMap加锁,而是首先根据hashcode得到该表项应该被存放到哪个段中,然后对该段加锁,并完成put() 操作,在多线程环境中,如果多个线程同时进行put操作,只要被加入的表项不存在同一个段中,则线程间便可以做到真正的并行。由于默认有16个段,理论上ConcurrentHashMap可以同时接受16个线程同时插入(如果都插入到不同的段中),从而大大提供其吞吐量。但是减少粒度会引入一个新的问题,当系统需要获取全局锁时,其消耗的资源会比较多,虽然put方法很好地分离了锁,但是当试图访问ConcurrentHashMap全局size信息时,他将返回ConcurrentHashMap的有效表项的数量,需要对所有段进行加锁,获取总数,再对所有段进行解锁。事实上,size方法会先使用无锁的方式求和,如果失败才会尝试这种加锁的方法。

4.1.3 读写分离锁来替换独占锁
类似之前讲过的读写锁ReadWriteLock可以提高系统的性能

4.1.4 锁分离
如果将读写锁的思想做进一步的延伸,就是锁分离。读写锁根据读写操作功能上的不同,进行了有效的锁分离。使用类似的分离思想,也可以对多占锁进行分离,一个典型案例就是 LinkedBlockingQueue 的实现,在LinkedBlockingQueue的实现中,take 和 put 函数分别实现了从队列取得数据和往队列中增加数据的功能,虽然2个函数都对当前队列进行了操作,但由于LinkedBlockingQueue是基于链表的,因此,两个操作分别作用于队列的前端和尾端,从理论上讲,两者并不冲突。如果使用独占锁,则要求在这2个操作进行时,获取当前队列的独占锁,那么take和put操作就不可能真正的并发,在运算时,他们会彼此等待对方释放锁资源,在这种情况下,锁竞争会相对比较激烈,从而影响程序在高并发时的性能,但在JDK实现中,并没有采用这种方式,取而代之的是用两把不同的锁,分离take 和 put的操作(实现方式类似ArrayBlockingQueue,但请注意与ArrayBlockingQueue的不同点:ArrayBlockingQueue的take和put使用同一把可中断的重入锁,配合2个Condition;而LinkedBlockingQueue的take和put分表使用一把可中断的重入锁,配合各自的Condition,,所以LinkedBlockingQueue比ArrayBlockingQueue的性能要高吧??):

4.1.5 锁粗化
如果对同一个锁不停地进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利用性能优化。为此,虚拟机在遇到一连串连续地对同一锁不断进行请求和释放时,便会把所有的锁操作整合成对锁的一次请求,从而减少对锁的请求同步次数,这个操作叫做锁的粗化。

4.2 Java虚拟机对锁优化所做的努力
JDK内部通过几种内部的“锁”优化策略(个人理解:注意这里是策略,并不是真正的锁实现),来优化并发时的系统吞吐量。

4.2.1 偏向锁
是一种针对加锁操作的优化手段,他的核心思想是:如果一个线程获得了锁,那么锁就进入偏向模式。当这个线程再次请求锁时,无需再做任何同步操作。因此对于几乎没有锁竞争的场合,偏向锁有比较好的优化效果,因为连续多次极有可能是同一线程请求形同的锁。而对于锁竞争比较激烈的场合,偏向锁会失效,其效果不佳,还不如不启用偏向锁。开启方法:-XX:+UseBiasedLocking

4.2.2 轻量锁
如果偏向锁失败,虚拟机并不会立刻挂起锁,他还会使用一种称为轻量级锁的优化手段。轻量锁的操作也很轻便,他只是简单地将对象头部作为指针,指向持有锁的线程堆栈的内部,来判断一个线程是否持有对象锁。如果线程获得轻量锁成功,则可以顺利进入临界区。如果轻量锁加锁失败,则表示其他线程抢先夺到了锁,那么当前线程的锁请求就会膨胀为 重量级锁。

4.2.3 自旋锁
锁膨胀后,虚拟机为了避免线程真实地在操作系统层面挂起,虚拟机还会做最后的努力 ---自旋锁。由于当前线程暂时无法获得锁,但是什么时候可以获得锁是一个未知数,也许在几个CPU时钟周期后,就可以得到锁。如果这样,简单粗暴地挂起线程可能是一种得不偿失的操作,因此,系统会进行一次赌注:他会假设在不久的将来,线程可以得到这把锁,因此,虚拟机会让当前线程做几个空循环(自旋的含义),在经过若干次循环后,如果可以得到锁,那么就顺利进入临界区,如果还是不能获得,才会真实地将线程在操作系统层面挂起。(个人理解:重量锁就是在操作系统层面将线程挂起)

4.2.4 锁消除
锁消除是一种更彻底的锁优化。Java虚拟机在JIT编译时,通过对上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。因为有时程序开发人员在不需要做线程安全的场景下,错误的使用了锁,这时虚拟机会将锁去掉(个人理解:例如变量在方法中定义为Vector线程安全型,但方法是存在于虚拟机的栈帧中,每个线程是独享此空间的资源,没有必要使用线程安全类型去加锁),虚拟机通过逃逸分析判断某一个变量是否会逃出某个作用域(变量是否会被其他线程访问)。逃逸分析必须在-server模式下进行,可以使用:-XX:DoEscapeAnalysis参数打开。

4.3 人手一支笔:ThreadLocal
为了保证所有对象的线程安全,除了使用锁控制资源的访问外,还可以通过ThreadLoal增加资源来保证所有对象的线程安全。

4.3.1 TheadLocal的简单使用
TheadLocal 是一个线程的局部变量,只有当前线程可以访问,自然就是线程安全的(个人理解:TheadLocal是一个线程的变量,不是线程),TheadLocal只是起到简单的容器作用,他并不是线程,程序的线程安全还是需要从应用层面进行保护(个人理解:将临界区的资源放入当前线程才可以访问的TheadLocal私有变量中,这样就可以隔离其他线程的访问,就达到了线程安全的目的)

4.3.2 TheadLocal的实现原理
TheadLocal有如下定义:
ThreadLocal.ThreadLocalMap threadLocals = null;
public void set(T value);
public T get();

set方法是先获得当前线程对象,然后通过getMap拿到当前线程的ThreadLocalMap(threadLocals ),并将值设入到ThreadLocalMap中(key为当前ThreadLocal对象,值为我们要设置的值),而threadLocals 本身就保存了当前自己所在线程的所有“局部变量”,也就是一个ThreadLocal变量的集合;get就是从这个ThreadLocalMap中拿数据出来。get方法是先获取当前线程的ThreadLocalMap(所有的局部变量),然后通过将自己线程对象作为key取得内部的实际数据(个人理解:当前线程对象只有一个,如果在当前线程多次声明ThreadLocal对象并设置值,那只有最后一个设置有效,前一个值会被移除,因为key相同呀??)。
这意味着只要线程不退出,对象的引用将一直存在(在固定大小的线程池中可能会一直存在,线程操作完成后,并不会销毁,而是放入到线程的池子中),如果你希望及时回收对象,最好使用ThreadLocal.remove方法将这个变量移除。

4.3.3 对性能有什么帮助
为每个线程分配一个独立的局部对象,对系统的性能也许是有帮助的。当然也不一定,取决于共享对象的内部逻辑;如果共享对象对于竞争的处理容易引起性能损失,我们还是应该考虑使用ThreadLocal为每个线程分配独立的局部对象;

4.4 无锁
对于并发控制而言,锁是一种悲观的策略,他总是加锁每一次的临界区操作都会产生冲突,因此,必须对每次操作都小心翼翼。如果多个线程同时需要访问临界区资源,就宁可牺牲性能让线程进行等待。所以说锁会阻塞线程的执行。而无锁是一种乐观的策略,他会假设对资源的访问是没有冲突的,自然就不需要等待,所以所有线程都可以在不停顿的状态下持续执行。无锁的策略使用一种叫做比较交换的技术(CAS:Compare And Swap)来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突为止。

4.4.1 与众不同的并发策略:比较交换(CAS)
CAS算法的过程是:他包含三个参数CAS(V,E,N),V 表示要更新的变量,E表示预期的值,N表示新值。仅当V值等于E值时,才会将V值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。CAS操作是抱着乐观的态度进行的,他总是认为自己可以成功完成操作,当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理(个人理解:变量被volatile修饰??)
硬件层面,大部分的现代处理器都已经指出原子化的CAS指令。虚拟机便可以使用这个指令来实现并发操作和并发数据结构。

4.4.2 无锁的线程安全整数:AtomicInteger
AtomicInteger 可以看做是一个Integer,但他是可变的,是线程安全的,对其进行修改等操作,都是使用CAS指令进行的:
public final int get(); 取得当前值
public final void set(int newValue)设置当前值
public final int getAndSet(int newValue)设置新值,并返回旧值
public final boolean compareAndSet(int expect , int u)如果当前值是expect,则设置为u
public final int getAndIncrement()当前值加1,并返回旧值
public final int getAndDecrement()当前值加1,并返回旧值
public final int getAndAdd(int delta)当前值加delta,返回旧值
public final int incrementAndGet()当前值加1,返回新值
public final int decrementAndGet()当前值减1,返回新值
public final int andAndGet(int delta)当前值加delta,返回新值

AtomicInteger 内部实现上,有一个核心字段:
private volatile int value  // 保存了AtomicInteger当前实际取值

private static final long valueOffset; // 保存AtomicInteger对象的偏移量,这个偏移量是实现AtomicInteger的关键

类似 AtomicInteger的类还有:AtomicLong、AtomicBoolean、AtomiReference(表示对象引用)

4.4.3 Java中的指针:Unsafe类
一些底层原生的方法

4.4.4 无锁的对象引用:AtomicReference
AtomicReference 和 AtomicInteger 非常相似,不同之处就在于 AtomicInteger 是对整数的封装,而AtomicReference 则对应普通的对象引用,就是他可以保证你在修改对象引用时的线程安全性

4.4.5 带有时间戳的对象引用:AtomicStampedReference
有时值被改变了状态(修改),但值本身最终是不变的场景下(第一次改变:值 + 1, 第二次改变:值 - 1, 最终值仍然是原始值,但状态改变了),我们只要记录对象在修改过程中的状态值,就可以很好解决对象被反复修改导致线程无法正确判断对象状态的问题,AtomicStampedReference 正是这么做的,他内部不仅维护了对象值,还维护了一个状态值,当 AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新这个状态值,当 AtomicStampedReference 设置对象值时,对象值以及状态值都必须满足期望值,写入才会成功,因此即使对象值被反复读写,写回原值,只要状态发生变化,就能防止不恰当的写入:
public boolean compareAndSet(V 期望值 ,V 新值,int  期望的状态,int  新的状态)

4.4.6 数组也能无锁:AtomicIntegerArray
AtomicIntegerArray (整数数组)、 AtomicLongArray(long型数组)、AtomicReferenceArray(普通的对象数组)
AtomicIntegerArray本质上是对 int[] 类型的封装,使用Unsafe类通过CAS的方式控制 int[] 在多线程下的安全性:
public final int get(int i); 取得数组第 i 个下标的元素
public final int length()获得数组的长度
public final int getAndSet(int i, int newValue)将数组第 i 个下标设置为newValue,并返回旧值
。。。。。。。。。。。。。。。。。


4.4.7 让普通变量也享受原子操作:AtomicIntegerFieldUpdater
有时候,由于初期考虑不周,或者后期的需求变化,一些普通变量可能也会有线程安全的需求(开闭原则 : 系统对功能的增加应该是开发的,而对修改应该是相对保守的);AtomicIntegerFieldUpdater可以让你在不改动(或者极少改动)原有代码的基础上,让普通的变量也享受CAS操作带来的线程安全性,这样可以在极少修改代码的情况下,来获得线程安全的保证。
根据数据类型的不同,有三种Updater:
AtomicIntegerFieldUpdater:
AtomicLongFieldUpdater:
AtomicReferenceFieldUpdater:

注意事项:
1,Updater只能修改他可见范围内的变量,因为Updater使用反射得到这个变量,如果不可见,就会出错
2,为了确保变量被正确的读取,他必须是volatile类型的
3,由于CAS操作会通过对象实例中的偏移量直接进行赋值,因此他不支持static字段

4.4.8 挑战无锁算法:无锁的Vector实现

4.4.9 让线程之间互相帮助:细看SynchronouseQueue的实现

4.5 有关死锁的问题
如果想避免死锁,除了使用无锁的函数外,另一种有效的做法是通过重入锁的中断或者限时等待,也可以有效规避死锁带来的问题。

 

五,并行模式与算法
由于并行程序设计比串行程序复杂得多,建议大家可以熟悉和了解一些常见的设计方法,介绍一些有关并行的设计模式以及算法,这些都是前人的经验总结和智慧的结晶。

5.1 单例模式
单例的好处:
对于频繁使用的对象,可以省略new操作话费的时间,对于那些重量级的对象而言,是非常可观的一笔系统开销;
由于new操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻GC压力,缩短GC停顿的时间

最优的实现方式:
单例模式实现方式有好多种,但大部分都会有多线程环境下的问题;使用内部类可以避免这个问题,因为在多线程环境下,jvm对一个类的初始化会做限制,同一时间只会允许一个线程去初始化一个类,这样就从虚拟机层面避免了大部分单例实现的问题

5.2 不变模式
不变模式天生就是多线程友好的,核心思想是,一个对象一旦被创建,则他的内部状态将永远不会发生改变,没有一个线程可以修改其内部状态和数据,同时其他内部状态也绝不会自行发生Gibson,基于整些特性,对不管对象的多线程操作不需要进行同步控制。

5.3 生产者 - 消费者模式
生产者-消费者模式(BlockingQueue)很好地对生产者线程和消费者线程进行解耦,优化了系统整体结构,同时,由于缓冲区的作用,允许生产者线程和消费者线程存在执行上的性能差异,从一定程度上缓解了性能瓶颈对系统性能的影响。

5.4 高性能的生产者 - 消费者:无锁的实现
BlockingQueue使用有锁技术,并提供等待性方法;ConcurrentLinkedQueue基于CAS的无锁技术,但不提供等待性方法(个人理解:获取数据时也可自定义实现等待,例如Thread.sleep(1000),但性能不好,没有基于锁的await/signal高效);

5.4.1 无锁的缓存框架:Disruptor,一款高效的无锁内存队列,他使用无锁的方式实现了一个环形队列,非常适合于实现生产者和消费者模式

5.4.2 用Disruptor实现生产者 - 消费者案例

5.4.3 提高消费者的响应时间:选择合适的策略

5.4.4 CPU Cache的优化:解决伪共享问题

5.5 Future模式
Future模式是多线程开发中非常常见的一种设计模式,他的核心思想是异步调用;当我们需要调用一个函数方法时,如果这个函数执行很慢,那么我们就要进行等待,但有时候,我们可能并不急着要结果,我们可以让被调者立刻返回,让他在后台慢慢处理这个请求,对于调用者来说,则可以先处理一些其他任务,在真正需要数据的场合再去尝试获得需要的数据。对于Future模式来说,虽然他无法立即给出你需要的数据,但是,他会返回给你一个契约凭据,将来可以凭借这个契约凭据,去重新获取你需要的信息。

5.5.1 Future模式的主要角色
Main:系统启动,调用Client发出请求
Client:返回Data对象,立刻返回FutureData,并开启ClientThread线程装配RealData
Data:返回数据的接口
FutureData:Future数据,构造很快,但是是一个虚幻的数据(契约凭据),需要装配RealData
RealData:真实数据,其构造是比较慢的

5.5.2 Future模式的简单实现

5.5.3 JDK中的Future模式:FutureTask
FutureTask<T> future = new FutureTask<T>(Object implement Callable<T>);
ExecutorService executor = Executors.newFixedThreadPool(1);
executor.submit(future);
//请求完毕
System.out.printlin("数据 = " + future.get())

5.6 并行流水线
并发算法虽然可以充分发挥多核CPU的性能,但并非所有的计算都可以改造成并发的形式,简单来说,执行过程中有数据相关性的运算都是无法完美并行化的,所需数据存在依赖关系。下面通过LinkedBlockingQueue阻塞队列,实现流水化运行(充分使用到阻塞队列take方法的特性和阻塞队列作为多个线程间数据共享的通道):

public class Plus implements Runnable {

    public static LinkedBlockingQueue<Msg> bq = new LinkedBlockingQueue<>();
    
    @Override
    public void run() {
        while(true){
            try {
                Msg msg = bq.take(); //如果为空,阻塞在这里,直到取得数据为止
                msg.j= msg.i + msg.j;
                Ply.bq.add(msg); // 放入到下一个流水线的队列
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Ply implements Runnable {
    static LinkedBlockingQueue<Msg> bq = new LinkedBlockingQueue<>();

    @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();
            }
        }
    }
}

public class Div implements Runnable{
    static LinkedBlockingQueue<Msg> bq = new LinkedBlockingQueue<>();
    
    @Override
    public void run() {
        // TODO Auto-generated method stub
        while(true){
            try {
                Msg msg = bq.take();//如果为空,阻塞在这里,直到取得数据为止
                msg.i = msg.i / 2;
                System.out.println(msg.msg +" = " + msg.i); //最后一个流水线,直接打印结果
            } catch (Exception e) {
            }
        }
    }
}

public class Msg {
    public double i;
    public double j;
    public String msg;
    
    public double getI() {
        return i;
    }
    public void setI(double i) {
        this.i = i;
    }
    public double getJ() {
        return j;
    }
    public void setJ(double j) {
        this.j = j;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
}
 

public class Pmain {
    public static void main(String[] args) {
        new Thread(new Div()).start();
        new Thread(new Plus()).start();
        new Thread(new Ply()).start();
        
        for(int i=1;i<=100;i++){
            for(int j=1;j<=100;j++){
                Msg msg = new Msg();
                msg.i=i;
                msg.j=j;
                msg.msg="((" + i + " + " + j + ") * " + i + ")/ 2";
                Plus.bq.add(msg);
            }
        }
    }
}

5.7 并行搜索(有点像 forkJoinPool 分段的思想)

5.8 并行排序:通过CountDownLatch 开启多个线程计算,倒计时结束后统一响应到主线程

5.9 并行算法:矩阵乘法

5.10 准备好了再通知我:网络NIO

5.11 读完了再通知我:AIO

 

Java 开源商城: http://www.macrozheng.com/ 

自动化运维平台: https://github.com/welliamcao/OpsManage/

傻瓜式免安装-Centos操作系统资源 图形可视化监控工具: https://github.com/hcymysql/os_monitor

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值