一、同步控制
1、synchronized:重入锁
重入锁使用java.util.concurrent.locks.ReentranLock类实现。与synchronized相比,重入锁有着显示的操作过程。必须手动指定何时加锁,何时释放锁。也正因为这样,重入锁对逻辑控制的灵活性要远好于synchronized。当退出临界区时,必须记得释放锁,否则其他线程就没有机会再访问临界区了。
实例:
public class ReentranLockThread implements Runnable{
private static ReentrantLock reentrantLock=new ReentrantLock();
static Integer i=0;
@Override
public void run() {
try {
reentrantLock.lock();
for (int j = 0; j < 1000; j++) {
i++;
}
}finally {
reentrantLock.unlock();
}
}
}
@Test
public void ReentranLockThreadTest() throws InterruptedException {
ReentranLockThread reentranLockThread=new ReentranLockThread();
Thread t1=new Thread(reentranLockThread);
Thread t2=new Thread(reentranLockThread);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(ReentranLockThread.i);
}
中断响应
中断提供了一套机制。如果一个线程正在等待锁,那么他依然可以收到一个通知,被告知无须再等待,可以停止工作了。
例子:执行下方代码后,两个线程会分别锁住l1和l2,然后再去抢l2和l1,他们就会占用了对方需要的锁资源,同时也在等对方释放锁资源,这样就造成了死锁。然后过了一秒后,线程t2主动释放锁资源,t1则成功拿到l2的锁资源继续执行代码。
解释:interrupt():中断标志位,中断线程。
lockInterruptibly():获取某个锁,如果没有获取到,则进入等待,可以响应中断。
isHeldByCurrentThread():判断当前锁状态
@Data
public class IntLock implements Runnable{
private static ReentrantLock l1=new ReentrantLock();
private static ReentrantLock l2=new ReentrantLock();
private Integer i=1;
@Override
public void run() {
try{
if(i==1){
l1.lockInterruptibly();
System.out.println("l1锁住");
Thread.sleep(500);
l2.lockInterruptibly();
System.out.println("l2锁住");
}else {
l2.lockInterruptibly();
System.out.println("l2锁住");
Thread.sleep(500);
l1.lockInterruptibly();
System.out.println("l1锁住");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//判断锁状态
if(l1.isHeldByCurrentThread()){
l1.unlock();
System.out.println("l1解锁");
}
if(l2.isHeldByCurrentThread()){
l2.unlock();
System.out.println("l2解锁");
}
}
}
}
@Test
public void IntLockTest() throws InterruptedException {
IntLock reentranLockThread1=new IntLock();
IntLock reentranLockThread2=new IntLock();
reentranLockThread1.setI(1);
reentranLockThread2.setI(2);
Thread t1=new Thread(reentranLockThread1);
Thread t2=new Thread(reentranLockThread2);
t1.start();
t2.start();
Thread.sleep(1000);
t2.interrupt();
}
锁申请等待限时
避免死锁还有一种方式就是限时等待。给定一个等待时间,让线程自动放弃。可以使用tryLock()方法进行一次现时等待。
tryLock()接收两个参数,一个是时长,一个是时间单位。此方法也可以不设置参数,那他就没有等待时间,他会立即返回是否得到锁资源的结果。
例子:两个线程执行,去争抢锁资源,第一个拿到所资源会执行”获取到锁“那一部分代码,没有抢到锁资源会等待两秒,如果两秒后还没有获取到则直接放弃。
public class TryLock implements Runnable{
private static ReentrantLock l1=new ReentrantLock();
@Override
public void run() {
try{
if(l1.tryLock(2, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName()+"获取到");
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName()+"执行完毕");
}else {
System.out.println(Thread.currentThread().getName()+"没有获取锁资源");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
if(l1.isHeldByCurrentThread()){
l1.unlock();
System.out.println(Thread.currentThread().getName()+"释放锁");
}
}
}
}
@Test
public void TryLockTest() throws InterruptedException {
TryLock reentranLockThread1=new TryLock();
TryLock reentranLockThread2=new TryLock();
Thread t1=new Thread(reentranLockThread1);
Thread t2=new Thread(reentranLockThread2);
t1.start();
t2.start();
t1.join();
t2.join();
}
公平锁
公平锁它会按照时间的先后顺序,保证先到先得。他不会产生饥饿。只要你排队,最终还是可以等到资源的。
在创建ReentrantLock对象是带入参数true,则表示设置锁为公平锁。要实现公平锁必然要系统维护一个有序队列,因此公平锁的实现成本比较高,性能相对也非常低下,因此默认情况下,锁匙非公平的。
例子:在创建ReentrantLock时分别设置true和不设置参数,然后使用两个线程分别进行打印输出。然后会发现设置了true的会依次按照创建线程的顺序来执行,而没有设置参数则会乱序打印。
public class FairLock implements Runnable{
private static ReentrantLock l1=new ReentrantLock(true);
@Override
public void run() {
for (int i = 0; i < 50; i++) {
l1.lock();
System.out.println(Thread.currentThread().getName());
l1.unlock();
}
}
}
@Test
public void FairLockTest() throws InterruptedException {
FairLock reentranLockThread1=new FairLock();
Thread t1=new Thread(reentranLockThread1);
Thread t2=new Thread(reentranLockThread1);
t1.start();
t2.start();
t1.join();
t2.join();
}
总结
方法总结:
lock():获得锁,如果锁已经被占用,则等待。
lockInterruptibly():获得锁,但优先响应中断。
tryLock():尝试获得锁,如果成功返回true,失败返回false。不会等待,直接返回。
tryLock(Long time,TimeUnit unit):在给的定时间获取锁,没有得到则返回false。
unlock():释放锁。
2、Condition条件
Condition与wait()和notify()方法类似。通过Lock接口的Condition newCondition()方法就可以生成一个与当前重入锁绑定的Condition实例。
方法解释:
await():使当前线程等待,同时释放当前锁,当其他线程中使用signal()或signalAll()方法时,线程会重新获取锁并继续执行。或者当前线程被中断时,也能跳出等待。
awaitUninterruptibly()与await()方法相同,不过他不能在等待过程中响应中断。
signal()方法用于唤醒一个在等待中的线程。相对的signalAll()会唤醒所有在等待中的线程。
例子:ConditionThread在执行run方法时会进入等待,知道主线程这边去唤醒他。
注:在await()方法和signal()方法在执行时,都需要获取到相关的锁。可以注意到测试方法中的唤醒前后的加锁和释放锁。
public class ConditionThread implements Runnable{
public static ReentrantLock reentrantLock=new ReentrantLock();
public static Condition condition= reentrantLock.newCondition();
@Override
public void run() {
try{
reentrantLock.lock();
System.out.println("获取资源,开始等待");
condition.await();
System.out.println("继续执行");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
reentrantLock.unlock();
}
}
}
@Test
public void ConditionThreadTest() throws InterruptedException {
ConditionThread reentranLockThread1=new ConditionThread();
Thread t1=new Thread(reentranLockThread1);
t1.start();
Thread.sleep(1000);
ConditionThread.reentrantLock.lock();
ConditionThread.condition.signal();
ConditionThread.reentrantLock.unlock();
}
3、信号量
内部锁synchronized或者重入锁ReentrantLock,一次都只允许一个线程访问一个资源,而信号量可以指定多个线程,同时访问某一个资源。
主要构造函数:
public Semaphore(int permts);
public Semaphore(int permts,boolean faiir); 第二个参数表示是否公平
构造信号量对象时,必须要指定信号量的准入数,即同时能申请多少个许可。
信号量主要方法:
void acquire():尝试获得一个准入的许可。若无法获得,则线程会等待,直到有线程释放一
个许可或当前线程被中断。void acquireUninterruptibly():与acquire类似,但是他不响应中断。
boolean tryAcquire():尝试获得一个许可,成功true,失败false,立即返回结果。
boolean tryAcquire(long timeout, TimeUnit unit):与tryAcquire类似,不过是有最大等待时间
void release():用于在线程访问资源结束后,释放一个许可,以使得其他等待许可的线程可
以进行资源访问。
例子:利用线程池创建初始为20的线程池,通过for循环进行执行子线程代码,实现类创建一个最大线程5的信号量对象。
public class SemaphoreThread implements Runnable{
final Semaphore semaphore=new Semaphore(5);
@Override
public void run(){
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"开始执行");
Thread.sleep(1000);
semaphore.release();
Thread.currentThread().interrupt();
System.out.println(Thread.currentThread().getName()+"执行完毕");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
@Test
public void SemaphoreThreadTest() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(20);
SemaphoreThread reentranLockThread1=new SemaphoreThread();
for (int i = 0; i < 20; i++) {
executorService.execute(reentranLockThread1);
}
//关闭线程池
executorService.shutdown();
//子线程全部执行完则退出程序
while (!executorService.isTerminated()){
}
}
4、读写锁
ReadWriteLock是JDK5中提供的读写分离锁。读写分离锁可以有效的减少锁竞争,提升系统性能。读写锁允许多个线程同时读,但是写写操作和读写操作之间依然需要相互等待和持有锁。也就是只有读与读之间才不阻塞。
例子:ReadWritLockDemo:创建读写方法,并且枷锁。
ChangeThread:子线程进行调用,调用读写方法,根据传入参数判断。
测试类:在交叉进行读写操作,看输出结果,
结果:读操作没有进行阻塞,读与读之间会交替进行,但是写会是一个完整且单独的
执行。
public class ReadWritLockDemo {
public static ReentrantReadWriteLock reentrantReadWriteLock= new ReentrantReadWriteLock();
public static Lock readLock=reentrantReadWriteLock.readLock();
public static Lock writeLock=reentrantReadWriteLock.writeLock();
public static Integer num=0;
public static void read(){
try {
readLock.lock();
System.out.println(Thread.currentThread().getName()+"读");
Thread.sleep(1000);
System.out.println(num);
System.out.println(Thread.currentThread().getName()+"读完成");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
readLock.unlock();
}
}
public static void write(){
try {
writeLock.lock();
System.out.println(Thread.currentThread().getName()+"写");
Thread.sleep(1000);
num++;
System.out.println(Thread.currentThread().getName()+"写完成");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
writeLock.unlock();
}
}
}
@AllArgsConstructor
public class ChangeThread implements Runnable{
private Boolean isRead=true;
@Override
public void run() {
if(isRead){
ReadWritLockDemo.read();
}else{
ReadWritLockDemo.write();
}
}
}
@Test
public void ReadWritLockDemoTest() throws InterruptedException {
ChangeThread read=new ChangeThread(true);
ChangeThread write=new ChangeThread(false);
for (int i = 0; i < 20; i++) {
if(i%3==0){
Thread thread = new Thread(write);
thread.start();
}else{
Thread thread = new Thread(read);
thread.start();
}
}
Thread.sleep(20000);
}
5、倒计时器
CountDownLatch是一个多线程控制工具类。通常用来控制线程等待,他可以让某个一个线程等待直到倒计时结束,在开始执行。
使用场景:可以让子线程执行到多少条,主线程才继续往下走。
多个线程同时执行一个任务。
他的构造函数接受一个整数为参数,即这个计数个数(线程个数)。
public CountDownLatch(int count);
方法解释
countDown:没调用一次计数器值减一
getCount:获取当前计数值
await:等待计数器清空,等待其他线程执行完毕
例子:_01_05_CountDownLatch:设置计数线程为5。
测试类:运行5个线程,触发主线程继续执行。
public class _01_05_CountDownLatch extends Thread{
static CountDownLatch countDownLatch=new CountDownLatch(5);
@Override
public void run(){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"准备好了");
countDownLatch.countDown();
}
}
@Test
public void _01_05_CountDownLatchTest() throws InterruptedException {
for (int i = 0; i < 4; i++) {
_01_05_CountDownLatch downLatch=new _01_05_CountDownLatch();
downLatch.start();
}
_01_05_CountDownLatch.countDownLatch.await();
System.out.println("全部执行完毕");
}
6、循环栅栏
CyclicBarrier是另一种多线程并发控制实用工具。他可以实现线程间的计数等待。相比于CountDownLatch他可以进行循环等待,一批线程等齐执行了,就继续进行下一批次。
他接受两个参数,第一个:计数总数;第二个:一次计数完成后,系统会执行的动作。
例子:一批子线程执行完后,打印一批执行完的信息,在进入下一批。
CyclicBarrierDemo:传入一个CyclicBarrier用作等待,run方法为子线程执行的主要方
法。
CyclicBarrierLast:此run方法为一批次执行完后执行的方法。
测试类:创建一个线程池和CyclicBarrier,循环调用并start CyclicBarrierDemo,查看
打印台输出
public class CyclicBarrierDemo implements Runnable{
public final CyclicBarrier cyclicBarrier;
public int miao;
public CyclicBarrierDemo(CyclicBarrier cyclicBarrier,int miao) {
this.cyclicBarrier = cyclicBarrier;
this.miao=miao;
}
@Override
public void run() {
try {
System.out.println("正在执行");
Thread.sleep(miao*500);
System.out.println("已执行完毕,等待其他线程执行完毕");
cyclicBarrier.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
}
}
public class CyclicBarrierLast implements Runnable{
@Override
public void run() {
System.out.println("一批执行完毕");
}
}
@Test
public void CyclicBarrierTest() throws InterruptedException, BrokenBarrierException {
CyclicBarrier cyclicBarrier=new CyclicBarrier(5,new CyclicBarrierLast());
ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(5,10,20,TimeUnit.SECONDS,new LinkedBlockingDeque<>());
for (int i = 0; i < 10; i++) {
threadPoolExecutor.submit(new CyclicBarrierDemo(cyclicBarrier,i));
}
threadPoolExecutor.shutdown();
while (!threadPoolExecutor.isTerminated()){
}
System.out.println("全部执行完毕");
}
}
7、线程阻塞工具类
LockSupport是一个线程阻塞工具类,它可以在线程内任意位置让线程阻塞。LockSupport类使用类似信号量的机制。他为每一个线程准备了一个许可,如果许可可用,那么park()函数会立即返会,并消费这个许可,如果许可不可用,就会阻塞。unpark()则使一个许可变为可用。
wait和notify这个两个方法必须获取到同一个对象的锁才能执行。而park和inpark这两个不需要获取到同一个的锁。而且可以先解锁,在后锁(不用担心线程之间执行的顺序)。
例子1:使用wait和notify。子线程在锁住一个对象后,再调用wait方法,然后主线程在调用notify方法进行释放,最后子线程在开始执行。期间必须保持这种执行顺序,不然可能会导致先解锁,然后才加锁。
public class WaitThread extends Thread{
static Object object=new Object();
@Override
public void run(){
synchronized (object){
System.out.println("子线程开始执行,并等待解锁");
try {
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("执行完毕");
}
}
public void selfNotify(){
synchronized (object){
System.out.println("解锁");
object.notify();
System.out.println("解锁完毕");
}
}
}
@Test
public void te() throws InterruptedException {
WaitThread waitThread=new WaitThread();
waitThread.start();
Thread.sleep(1000);
waitThread.selfNotify();
}
例子2:与上方例子差不多,但是把wait和notify改为park和unpark。差别,不用再同一个对象上加锁,没有异常处理。
public class LockSupportThread extends Thread{
static Object object=new Object();
@Override
public void run(){
synchronized (object){
System.out.println("子线程开始执行,并等待解锁");
LockSupport.park();
System.out.println("执行完毕");
}
}
}
@Test
public void te2() throws InterruptedException {
LockSupportThread lockSupportThread=new LockSupportThread();
lockSupportThread.start();
System.out.println("主线程解锁");
LockSupport.unpark(lockSupportThread);
System.out.println("主线程解锁完毕");
}
二、线程池
1、什么是线程池
为了避免系统频繁的创建和销毁线程,可以让创建的线程进行复用。可以节约创建和销毁对象的时间。
线程Executor主要工厂方法:
newFixedThreadPool():该方法返回一个固定线程数量的线程池。该线程池中的线程始
终不变。当有一个新的任务提交时,线程中若有空闲线程,则
立即执行。若没有,则新的任务会被暂存在一个任务队列中,
待有线程空闲时,便处理在任务队列中的任务。
newSingleThreadExecutor():只有一个线程的线程池。多余的线程任务进入队列排队,
后续会顺序执行。
newCachedThreadPool():返回一个可根据实际情况调整的线程数量的线程池。线程数
量不确定,但若有空闲线程可以复用,则会优先使用可复用的
线程。若所有线程均有任务,又有新的任务,则会创建新的线
程处理任务。所有线程在当年前任务执行完毕后,将返回线程
池进行复用。
newSingleThreadScheduledExecutor():返回一个ScheduleExecutorService对象,线程
池大小为1。这个对象添加了指定时间,可以用于周期性、延
时等任务。
newScheduledThreadPool():与上方想同,不过可以指定线程数量。
2、内部实现
对于上述工厂方法中的前三个方法,他们的内部都是使用了ThreadPoolExecutor实现的。都是通过new ThreadPoolExecutor()这个方法设置不同的参数实现。
ThreadPoolExecutor这个方法的参数解释(按照顺序):
corePoolSize:指定线程池中的线程数量。(核心线程数)
maximumPoolSize:指定了线程池中的最大线程数量。
keepAliveTime:当线程池数量超过corePoolSize时,多余的空闲线程的存活时
间。多余的线程,在多长时间后会被销毁。
unit:keepAliveTime的单位。
workQueue:任务队列,被提交但尚未被执行的任务。
threadFactory:线程工厂,用于线程创建。
handler:拒绝策略。当任务太多来不及处理,如何拒绝任务。
参数workQueue指被提交但未执行的任务队列,是一个BlockingQueue接口的对象,仅用于存放Runnable对象。可以使用下列几个队列:
直接提交的队列
SynchronousQueue没有容量,每一个任务的新建都要等待一个相应的结束任务,同时每一个结束任务都对应一个新建任务。使用这个队列提交的任务不会被真实的保存,而总是将新任务提交给线程执行,如果没有空闲的线程,则尝试创建新的线程,如果线程数量已经到达最大值,则执行拒绝策略。因此使用这个队列,通常会设置很大的maximumPoolSize,否则容易执行拒绝策略。有界的任务队列
ArrayBlockingQueue有一个参数,表示该队列的最大容量。这个队列的执行顺序:实际线程数小于corePoolSize,创建新线程,否则加入等待队列,等待队列满了则创建小于maximumPoolSize的线程数量,如果大于maximumPoolSize的任务执行拒绝策略。无界的任务队列
LinkedBlockingQueue。与有界相似,线程数在大于corePoolSize后,新的任务会一直放到队列里面。直到系统内存耗尽。优先任务队列
PriorityBlockingQueue。带有执行优先级的队列。可以控制任务的执行先后顺序。是一个特殊的无界队列。无论是无界还是有界都是按照先进先出的顺序执行的。
回顾一下前三个工厂线程创建:
newFixedThreadPool()
他的corePoolSize和maximumPoolSize大小一样,并且使用LinkedBlockingQueue任务队列的线程池。因为对于固定大小的线程池,不存在现场数量的动态变化。他使用无界队列存放无法立即执行的任务,当任务在短时间内大量添加,可能导致内存耗尽
newSingleThreadExecutor()
与newFixedThreadPool一样,不过他把线程数量设置为了1。
newCachedThreadPool()
corePoolSize为0,maximumPoolSize为Integer的最大值。在没有任务时,线程池内部没有任何线程。当任务提交时,有空闲线程直接使用,没有会进入SynchronousQueue队列,这个队列是没有容量的,他会直接提交,迫使创建一个新的线程执行,线程执行完会在60秒后销毁
3、拒绝策略
ThreadPoolExecutor的最后一个参数指定了拒绝策略。JDK提供了四种拒绝策略。
AbortPolicy
直接抛出异常,阻止系统正常工作。
CallerRunsPolicy
只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。这会导致任务提交的线程性能降低。
DiscardOledestPolicy
丢弃最老的一个请求,并尝试再次提交当前任务。
DiscardPolicy
丢弃无法处理的任务,不予任何处理。
上述四种方式都实现了RejectedExecutionHandler接口,可以去实现这个接口进行自定义的拒绝策略。
例子:RejectedExecutionHandlerDeamo:实现上述接口,复写方法。编写拒绝逻辑
RejectedExecutionHandlerThread:正常线程执行的代码。
public class RejectedExecutionHandlerDeamo implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println(r.toString()+"被拒绝了");
}
}
public class RejectedExecutionHandlerThread extends Thread{
@Override
public void run(){
System.out.println(Thread.currentThread().getName()+"线程执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"执行完毕");
}
}
@Test
public void _030203Test(){
RejectedExecutionHandlerThread rejectedExecutionHandlerThread=new RejectedExecutionHandlerThread();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 2, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1), Executors.defaultThreadFactory(), new RejectedExecutionHandlerDeamo());
for (int i = 0; i < 20; i++) {
threadPoolExecutor.execute(rejectedExecutionHandlerThread);
}
threadPoolExecutor.shutdown();
while (!threadPoolExecutor.isTerminated()){
}
}
4、自定义线程创建
线程池的主要作用是线程复用。最开始的线程都是ThreadFactory创建的。线程池会调用ThreadFactory中的new Thread(Runnable r)类来创建。
我们可以通过实现这个接口并复写其方法,就可以实现一些自定义的线程执行条件。复写方法只会在创建线程时执行,第一次使用这个线程时调用。
例子:ThreadFactoryDeamo:实现工厂并复写其方法,自定义了一些执行代码。
ThreadDeamo:子线程执行的代码。
public class ThreadFactoryDeamo implements ThreadFactory {
@Override
public Thread newThread(Runnable r) {
Thread thread=new Thread(r);
System.out.println(thread.getName()+"正在执行");
return thread;
}
}
public class ThreadDeamo extends Thread {
@Override
public void run(){
System.out.println("子线程正在执行");
}
}
@Test
public void run(){
ThreadDeamo threadDeamo=new ThreadDeamo();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 2, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10), new ThreadFactoryDeamo());
for (int i = 0; i < 10; i++) {
threadPoolExecutor.execute(threadDeamo);
}
threadPoolExecutor.shutdown();
while (!threadPoolExecutor.isTerminated()){
}
}
5、扩展线程池
如果需要对一些线程池做一些扩展,比如每个任务的开始时间和结束时间。ThreadPoolExecutor是可以做扩展的线程池。他提供了beforeExecute()、afterExecute()和terminated()三个接口对线程池进行控制。
在ThreadPoolExecutor.Worker.runTask()方法内部提供了这样的实现。
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
Worker是ThreadPoolExecutor的内部类,他实现了Runnable接口。ThreadPoolExecutor线程池中的工作线程也正是Worker实例。Worker.runTask()方法会被线程池以多线程异步调用,即Worker.runTask()会同时被多个线程访问。因此内部的beforeExecute、afterExecute也会同时被多个线程访问。
例子:KuoZhanDeamo:继承ThreadPoolExecutor并复写beforeExecute、afterExecute。
ThreadDeamo:普通的Thread继承类。
public class KuoZhanDeamo extends ThreadPoolExecutor{
public KuoZhanDeamo(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
protected void beforeExecute(Thread t, Runnable r) {
System.out.println("线程"+t.getName()+"正在执行"+r.hashCode());
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
System.out.println("执行完毕"+r.hashCode());
}
}
@Test
public void run(){
ExecutorService executorService=new KuoZhanDeamo(5,5,5, TimeUnit.SECONDS,new ArrayBlockingQueue<>(5));
for (int i = 0; i < 10; i++) {
ThreadDeamo threadDeamo=new ThreadDeamo();
executorService.execute(threadDeamo);
}
executorService.shutdown();
while (!executorService.isTerminated()){}
}
6、优化线程池线程数量
线程池的大小对系统的性能有一定的影响。过大或过小的线程数量都无法发挥最优的系统性能,但线程池的大小的确定也不需要做的非常精确,因为只要避免太大和太小两个极端就行。
线程的计算公式:
a=CPU数量
b=目标CPU使用率,0<=b<=1
c=等待时间与计算时间的比率
最优大小=a*b*(1+c)
7、在线程池中寻找堆栈
例子:在线程继承类中,实现一个获取两个数的商的函数。在测试类中使用线程池进行调用测试,并且线程池提交执行线程使用submit方法。
@AllArgsConstructor
public class TestThread extends Thread{
private Integer a;
private Integer b;
@Override
public void run(){
System.out.println(a/b);
}
}
@Test
public void run(){
ExecutorService executorService=new ThreadPoolExecutor(5,5,1, TimeUnit.SECONDS,new ArrayBlockingQueue<>(5));
for (int i = 0; i < 5; i++) {
executorService.execute(new TestThread(100,i));
}
executorService.shutdown();
while (!executorService.isTerminated()){}
}
结果:
总结:打印输出的结果应有5条记录,但实际只有4条,这是因为第一条的被除数为0,执行时报错了,就没有打印输出。同时控制台没有进行错误信息输出。
解决方式:1、把submit改为execute。
2、submit执行时,把返回值接收到,并使用Future的get方法获取异常信息。
3、扩展ThreadPoolExecutor线程池,让它在调度任务之前,保存提交任务线程的堆栈
信息。
8、Fork/Join框架
fork函数用来创建子线程,使系统进程可以多一个执行分支。join表示等待,在使用fork函数后系统多了一个执行分支,所以需要等待这个执行分支执行完毕,才能得到最终结果。
ForkJoinPool中的ForkJoinTask任务就是支持fork分解以及join等待的任务。ForkJoinTask有两个重要的子类,RecursiveAction和RecursiveTask,他们表示没有返回值的任务和可携带返回值的任务。
例子:计算1-20000的累加。
ForkThread:继承RecursiveTask,复写compute方法,此方法完成计算工作。
@AllArgsConstructor
public class ForkThread extends RecursiveTask<Long> {
private Long start;
private Long end;
@Override
protected Long compute() {
long l = end - start;
Long sum=0l;
//小于10000不分线程累加
if(l<10000){
for (Long i = start; i <= end; i++) {
sum+=i;
}
}else{
//大于10000则分100个小线程进行计算
//子线程list
ArrayList<ForkThread> forkThreads=new ArrayList<>();
long l1 = (l / 100)+1;
Long first=start;
for (int i = 0; i < 100; i++) {
Long last=first+l1;
if(last>end){
last=end;
}
//创建子线程对象
ForkThread forkThread=new ForkThread(first,last);
first+=l1+1;
forkThreads.add(forkThread);
forkThread.fork();
}
for (ForkThread forkThread : forkThreads) {
sum+=forkThread.join();
}
}
return sum;
}
}
@Test
public void run() throws ExecutionException, InterruptedException {
ForkJoinPool forkJoinPool=new ForkJoinPool();
ForkThread forkThread=new ForkThread(0l,100002l);
ForkJoinTask<Long> submit = forkJoinPool.submit(forkThread);
Long aLong = submit.get();
System.out.println(aLong);
}
三、JDK的并发容器
1、并发集合简介
ConcurrentHashMap:高效的并发HashMap。一种线程安全的HashMap。
CopyOnWriteArrayList:在读多写少的场合,这个List的性能非常好,远远好于Vector。
ConcurrentLinkedQueue:高效的并发队列,使用链表实现。可以看作一个线程安全的
LinkedList。
BlockingQueue:这是一个接口,JDK内部通过链表、数组等方式实现了这个接口。表示阻塞
队列,非常适合用于作为数据共享的通道。
ConcurrentSkipListMap:跳表的实现。这是一个Map,使用跳表的数据结构进行快速查找。
2、线程安全的HashMap
ConcurrentHashMap组成。在1.7由Segment数组和HashEntry组成,数组加链表;1.8由CAS和Synchronize组成,存放数据的HashEntry改为了Node节点,他的val和next都使用了volatile修饰保证了可见性。
8以后采用了红黑树保证了查询的效率,取消了ReentrantLock改为了Synchronize,因为8以后对Synchronize做了很大的优化。
安全的原理。使用了分段锁技术,将数据一段一段存储,然后每一段数据配一把锁,当一个线程占用了锁访问了其中一段数据,其他段的数据可以被其他线程访问。
3、ConcurrentLinkedQueue
他是基于链表的队列,在高并发的环境下性能最好的队列。
他在添加元素是没有任何锁的操作,线程完全是由CAS操作和队列算法保证的。整个核心就是使用一个没有出口的for循环,直到添加成功才结束。
4、CopyOnWriteArrayList
他在读与读、读与写之间不会加锁。他在进行修改时,并不会修改原有list,而是对原始数据进行一次复制,将修改内容写入副本中。写完之后,再将修改后的数据替换原来的数据。
读操作:读取的代码没有任何的同步操作和锁操作,理由是内部数组array不会发生修改,只会被另外一个array替换。
写操作:写操作使用锁,这个所仅限于写-写操作。他会先从原有数据复制一个新的数组,然后更改这个新的数组,最后把这个新的数组赋值给旧的数组。
5、BlockingQueue
BlockingQueue是一个接口,主要实现的类有ArrayBlockingQueue和LingkedBlockingQueue。他们分别基于数组和链表实现。所以ArrayBlockingQueue适合做有界队列,而LingkedBlockingQueue适合做无界队列。
BlockingQueue之所以适合作为数据共享的通道,其关键还在于Blocking上。当服务线程处理完成队列中的消息后,他如何知道下一条消息何时来呢?
以ArrayBlockingQueue为例,这个类中的获取消息和插入消息的方法中有Condition对象,在消息为空时,会掉用Condition的await方法,如果这时有新的消息进来,就会调用Condition的signal方法进行线程唤醒。
6、跳表(SkipList)
跳表是一种可以用来快速查找的数据结构,类似于平衡树。他们都可以对元素进行快速的查找。区别:对于跳表的插入和删除只需要对整个数据结构的局部进行操作即可。所以在高并发的情况下只需要锁部分数据即可。
跳表的算法是随机的。他的本质就是维护了多个链表,并且链表是分层的。
最底层的链表维护了所有的数据,上一层的链表都是下一层的子集,一个元素插入那些层是完全随机的。
跳表内的所有链表的元素都是有序的。查找时可以从顶层链表开始,一旦发现被查找元素大于当前链表中的取值,就会转入下一层链表继续查找。比如找5的顺序,3(顶层)---3(二层)---5(低层)。
跳表是一种使用空间换时间的算法。