一、什么是多线程
1.1 介绍进程与线程

进程实现多处理非常耗费CPU的资源,而我们引入线程是作为调度和分派的基本单位

所以说:一个进程会有1个或多个线程
• 值得注意的是:多线程的存在,不是提高程序的执行速度。其实是为了提高应用程序的使用率,程序的执行其实都是在抢CPU的资源,CPU的执行权。多个进程是在抢这个资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权
1.2 并行与并发
并行:
并行性是指同一时刻内发生两个或多个事件,
并行是在不同实体上的多个事件
并发:
并发性是指同一时间间隔内发生两个或多个事件,
并发是在同一实体上的多个事件
1.3 Java线程的创建方式
常见的Java线程的4种创建方式分别为:
• 继承Thread类
• 实现Runnable接口
• 通过ExecutorService和Callable< Class>实现有返回值的线程
• 基于线程池
1.4 Java实现多线程需要注意的细节
start( )和run( )方法区别:
• start方法用于启动线程,此时线程处于就绪状态,并没有运行
• run方法也叫线程体,包含了要执行的线程的逻辑代码,在调用run方法后,线程就进入运行状态
sleep( )和wait( )方法区别:
• sleep方法属于Thread类,wait方法属于Object类
• 在调用sleep方法过程中,线程不会释放对象锁
• 在调用wait方法时,线程会放弃对象锁,进入等待此对象的对象锁池,只有针对此对象调用notify方法后,该线程才会进入对象锁池获取对象锁,并进入运行状态
jvm虚拟机的启动是单线程的还是多线程的?
• 是多线程的。不仅仅是启动main线程,还至少会启动垃圾回收线程的,不然谁帮你回收不用的内存~
Thread类既然有两种方式实现多线程,我们使用哪一种?
一般我们使用实现Runnable接口
• 可以避免Java中的单继承的限制
• 应该将并发运行任务和运行机制解耦,因此我们选择实现Runnable接口这种方式
1.5 线程的生命周期:
线程的生命周期分为新建(New)、就绪(Runnable)、运行(Runing)、阻塞(Blocked)、死亡(Dead)
系统运行过程中不断有新的线程被创建,旧的线程在执行完毕后被清理,线程在排队获取共享资源或锁时将被阻塞,因此运行中的线程会在就绪-阻塞-运行状态来回切换

在Java中使用new关键字创建一个线程,新创建的线程将处于新建状态
1.5.2 就绪状态Runnable
新建的线程对象在调用start方法之后将转为就绪状态
1.5.3 运行状态Running
就绪状态的线程竞争到CPU使用权并开始执行run方法的线程执行体时,会转为运行状态
1.5.4 阻塞状态Blocked
运行中的线程主动或被动放弃CPU的使用权并暂停运行,此时该线程转为阻塞状态,直到再次进入可运行状态,才有机会再次竞争到CPU使用权并转为运行状态。阻塞转台分为3种:
• 等待阻塞:运行中的线程调用wait方法时,JVM会把该线程放入等待序列(Waitting Queue)中,线程转为阻塞状态
• 同步阻塞:运行中的线程尝试获取正在被其他线程占用的对象同步锁时,JVM会把该线程放入锁池(Lock Pool)中,线程转为阻塞状态
• 其他阻塞:运行中的线程在执行Thread.sleep(long ms)、Thread.join()或者发送I/O请求时,JVM会把该线程转为阻塞状态
1.5.5 线程死亡Dead
线程以3种方式结束后转为死亡状态
• 线程正常结束:执行完run方法或者call方法
• 线程异常退出:运行中的线程抛出一个Error或未捕获的Exception,线程异常退出
• 手动结束:调用线程对象的stop方法手动结束运行中的线程(该方法会瞬间释放线程占用的同步对象锁,导致所混乱和死锁,不推荐使用)
二、线程类的基本方法
1.1 设置线程名
我们在使用多线程的时候,想要查看线程名是很简单的,调用Thread.currentThread().getName()即可
我们想要为线程起个名字,那也是很简单的。Thread给我们提供了构造方法

setName(String name)的方法来改掉线程的名字,我们来看看方法实现:

守护线程是为其他线程服务的,也叫“服务线程” (垃圾回收线程就是经典的守护线程)
守护线程的特点:
• 当别的用户线程执行完了,虚拟机就会退出,守护线程也就会被停止掉了
• 也就是说:守护线程作为一个服务线程,没有用户线程可服务时就没有必要继续运行了
使用线程的时候要注意的地方:
• 在线程启动前设置为守护线程,方法是
setDaemon(true)• 使用守护线程不要访问共享资源(数据库、文件等),因为它可能会在任何时候就挂掉了
• 守护线程中产生的新线程也是守护线程

1.3 优先级线程
线程优先级高仅仅表示线程获取的CPU时间片的几率高,但这不是一个确定的因素
线程的优先级是高度依赖于操作系统的,Windows和Linux就有所区别(Linux下优先级可能就被忽略了)
可以看到的是,Java提供的优先级默认是5,最低是1,最高是10


线程等待:wait方法
调用wait方法的线程会进入WAITING状态,只有等到其他线程的通知或被中断后才返回。值得注意的是,调用wait方法后会释放对象的锁,因此wait方法一般被用于同步方法或同步代码块中
线程睡眠:sleep方法
调用sleep方法会进入计时等待状态,但监控状态依然保持,等时间到了,进入的是就绪状态而不是运行状态。sleep方法不会释放当前占有的锁,会导致线程进入TIMED-WAITTING状态
线程让步:yield方法[jiːld]
调用yield方法会使当前线程让出(释放)CPU执行时间片,与其他线程一起重新竞争CPU时间片。一般情况下优先级高的线程更可能竞争到CPU时间片,但不是绝对的,有的操作系统对线程的优先级不敏感
线程加入:join方法
join方法用于等待其他线程终止,如果在当前线程中调用一个线程的join方法,则当前线程转为阻塞状态,等待另一个线程结束,当前线程再有阻塞状态转为就绪状态,等待获取CPU的使用权

Object类有个notify方法,用于唤醒此对象监视器上等待的一个线程。通常调用其中一个对象的wait方法在对象监视器上等待,直到当前进程放弃此对象上的锁定,才能继续执行被唤醒的线程,类似方法还有notifyAll
线程中断:interrupt方法
我们一般使用的是interrupt来请求终止线程
• interrupt不会真正中断一个正在运行的线程,它仅仅是给这个线程设置了一个中断标志,可以通过此标志安全终止线程
interrupt线程中断还有另外两个方法(检查该线程是否被中断):
• 静态方法interrupted( )-->会清除中断标志位
• 实例方法isInterrupted( )-->不会清除中断标志位
比如在想终止一个线程时,可以先调用该线程的interrupt方法,然后在线程的run方法中根据该线程的isInterrupted方法的返回状态值安全终止线程。
三、使用多线程需要注意的问题
1.1 线程安全问题
在多线程的环境下,线程是交替执行的,一般他们会使用多个线程执行相同的代码。如果在此相同的代码里边有着共享的变量,或者一些组合操作,我们想要的正确结果就很容易出现了问题
1.2 性能问题
使用多线程我们的目的就是为了提高应用程序的使用率,但是如果多线程的代码没有好好设计的话,那未必会提高效率。反而降低了效率,甚至会造成死锁!
最简单的方式:如果我们在方法上加上JDK为我们提供的内置锁synchronized,那么我们就可以实现线程安全
public class UnsafeCountingServlet extends GenericServlet implements Servlet {
private long count = 0;
public long getCount() {
return count;
}
public void synchronized service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
++count;
// To something else...
}
}
但是这会带来很严重的性能问题,每个请求都得等待上一个请求的方法处理了以后才可以完成对应的操作,在使用多线程的时候:更严重的时候还有死锁(程序就卡住不动了)
1.3 对象的发布与逸出
• 发布(publish) 使对象能够在当前作用域之外的代码中使用
• 逸出(escape) 当某个不应该发布的对象被发布了
常见逸出的有下面几种方式:
静态域逸出
public修饰的get方法
方法参数传递
隐式的this
静态域逸出:


把对象传递过去给另外的方法,已经是逸出了
this逸出:

1.4 安全发布对象
安全发布对象有几种常见的方式:
• 在静态域中直接初始化: public static Person = new Person();
--静态初始化由JVM在类的初始化阶段就执行了,JVM内部存在着同步机制,致使这种方式我们可以安全发布对象
• 对应的引用保存到volatile或者AtomicReferance引用中
--保证了该对象的引用的可见性和原子性
• 由final修饰
--该对象是不可变的,那么线程就一定是安全的,所以是安全发布
• 由锁来保护
--发布和使用的时候都需要加锁,这样才保证能够该对象不会逸出
1.5 简述解决线程安全性的办法
在Java中,我们一般会有下面这么几种办法来实现线程安全问题:
• 无状态(没有共享变量)
• 使用final使该引用变量不可变(如果该对象引用也引用了其他的对象,那么无论是发布或者使用时都需要加锁)
• 加锁(内置锁,显示Lock锁)
• 使用JDK为我们提供的类来实现线程安全(此部分的类就很多了)
--原子性(就比如上面的count++操作,可以使用AtomicLong来实现原子性,那么在增加的时候就不会出差错了)
--容器(ConcurrentHashMap等等…)
1.6 原子性和可见性
如果操作的数据是原子性的,那么就可以很大程度上避免了线程安全问题了!
• count++,先读取,后自增,再赋值。如果该操作是原子性的,那么就可以说线程安全了
原子性就是执行某一个操作是不可分割的
--比如上面所说的count++操作,它就不是一个原子性的操作,它是分成了三个步骤的来实现这个操作的
JDK中有atomic包提供给我们实现原子性操作

volatile经典总结:volatile仅仅用来保证该变量对所有线程的可见性,但不保证原子性
使用了volatile修饰的变量保证了三点:
• 一旦你完成写入,任何访问这个字段的线程将会得到最新的值
• 在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存
• volatile可以防止重排序(重排序指的就是:程序执行的时候,CPU、编译器可能会对执行顺序做一些调整,导致执行的顺序并不是从上往下的。从而出现了一些意想不到的效果)。而如果声明了volatile,那么CPU、编译器就会知道这个变量是共享的,不会被缓存在寄存器或者其他不可见的地方
四、synchronized锁和lock锁
(一)、synchronized锁
1.1 synchronized锁是什么?
synchronized是Java的一个关键字,它能够将代码块(方法)锁起来,它使用起来是非常简单的,只要在代码块(方法)添加关键字synchronized,即可以实现同步的功能~

• synchronized是一种内置锁/监视器锁
--Java中每个对象都有一个内置锁(监视器,也可以理解成锁标记),而synchronized就是使用对象的内置锁(监视器)来将代码块(方法)锁定的!
1.2 synchronized用处是什么?
• synchronized保证了线程的原子性
--被保护的代码块是一次被执行的,没有任何线程会同时访问
• synchronized还保证了可见性
--当执行完synchronized之后,修改后的变量对其他的线程是可见的
*Java中的synchronized,通过使用内置锁,来实现对变量的同步操作,进而实现了对变量操作的原子性和其他线程对变量的可见性,从而确保了并发情况下的线程安全。
1.3 synchronized的原理
我们首先来看一段synchronized修饰方法和代码块的代码:

--monitorenter和monitorexit指令实现的
同步方法:(在这看不出来需要看JVM底层实现)
--方法修饰符上的ACC_SYNCHRONIZED实现
*synchronized底层是通过monitor对象,对象存储了很多信息,其中一个信息标示是被哪个线程持有
1.4 synchronized如何使用
synchronized一般我们用来修饰三种东西:
修饰普通方法
修饰代码块
修饰静态方法
1.4.1 修饰普通方法:


public class Java3y {
// 修饰静态方法代码块,静态方法属于类方法,它属于这个类,获取到的锁是属于类的锁(类的字节码文件对象)-->Java3y.class
public static synchronized void test() {
// 关注公众号Java3y
// doSomething
}
}
1.4.4 类锁与对象锁:
synchronized修饰静态方法获取的是类锁(类的字节码文件对象),synchronized修饰普通方法或代码块获取的是对象锁
它俩是不冲突的,也就是说:获取了类锁的线程和获取了对象锁的线程是不冲突的!
1.5 重入锁

这就是内置锁的可重入性。
1.6 释放锁的时机
• 当方法(代码块)执行完毕后会自动释放锁,不需要做任何的操作
• 当一个线程执行的代码出现异常时,其所持有的锁会自动释放。
不会由于异常导致出现死锁现象~
(二)、Lock显式锁
2.1 Lock显式锁简单介绍
Lock显式锁是JDK1.5之后才有的,之前我们都是使用Synchronized锁来使线程安全的~
Lock显式锁是一个接口


• Lock方式来获取锁支持中断、超时不获取、是非阻塞的
• 提高了语义化,哪里加锁,哪里解锁都得写出来
• Lock显式锁可以给我们带来很好的灵活性,但同时我们必须手动释放锁
• 支持Condition条件对象
• 允许多个读线程同时访问共享资源
2.2 synchronized锁和Lock锁使用哪个
Lock锁在刚出来的时候很多性能方面都比Synchronized锁要好,但是从JDK1.6开始Synchronized锁就做了各种的优化(毕竟亲儿子)
• 优化操作:适应自旋锁,锁消除,锁粗化,轻量级锁,偏向锁
*所以,到现在Lock锁和Synchronized锁的性能其实差别不是很大! 而Synchronized锁用起来又特别简单,Lock锁还得顾忌到它的特性,要手动释放锁才行(如果忘了释放,这就是一个隐患)
*所以说,我们绝大部分时候还是会使用Synchronized锁,用到了Lock锁提及的特性,带来的灵活性才会考虑使用Lock显式锁~
2.3 公平锁
公平锁理解起来非常简单:
线程将按照它们发出请求的顺序来获取锁
非公平锁就是:
线程发出请求的时可以“插队”获取锁
Lock和synchronize都是默认使用非公平锁
如果不是必要的情况下,不要使用公平锁,公平锁会来带一些性能的消耗的
五、AQS
(一)、AQS是什么?
首先我们来普及一下juc是什么:juc其实就是包的缩写(java.util.concurrent)
我们可以发现lock包下有三个抽象的类:

我们Lock子类的两个常见的锁都是基于它来实现的:

• AQS其实就是一个可以给我们实现锁的框架
• 内部实现的关键是:先进先出的队列、state状态
• 定义了内部类ConditionObject
• 两种线程模式
--独占模式
--共享模式
• 在Lock包中的相关锁 (常用的有ReentrantLock重入锁[ri:'entrənt]、 ReadWriteLock读写锁)都是基于AQS来构建
• 我们叫AQS为同步器
(二)、简单看看AQS
上面也提到了AQS里边最重要的是状态和队列,我们接下来就看看其源码是怎么样的...
2.1 同步状态
使用volatile修饰实现线程可见性


这个队列被称为:CLH队列,是一个双向队列

获取独占锁的过程就是在acquire定义的,该方法用到了模板设计模式,由子类实现的~

释放独占锁的过程就是在release定义的,该方法也用到了模板设计模式,由子类实现的~

• juc包中很多可阻塞的类都是基于AQS构建的
• AQS可以说是一个给予实现同步锁、同步器的一个框架,很多实现类都在它的的基础上构建的
• 在AQS中实现了对等待队列的默认实现 ,子类只要重写部分的代码即可实现(大量用到了模板代码)
六、ReentrantLock和ReentrantReadWriteLock
ReentrantLock和ReentrantReadWriteLock是Lock锁两个主要的子类
(一)、ReentrantLock锁

比synchronized更有伸缩性(灵活)
支持公平锁(是相对公平的)
使用时最标准用法是在try之前调用lock方法,在finally代码块释放锁


可以很清晰的看到,我们的ReentrantLock锁是支持公平锁和非公平锁的~


尝试获取锁,获取失败的话就调用AQS的
acquire(1)方法

acquire(1)方法我们在AQS时简单看过,其中tryAcquire()是子类来实现的


公平的lock方法其实就多了一个状态条件:





(二)、ReentrantReadWriteLock
我们知道synchronized内置锁和ReentrantLock都是互斥锁(一次只能有一个线程进入到临界区(被锁定的区域))
而ReentrantReadWriteLock是一个读写锁:
• 在读取数据的时候,可以多个线程同时进入到到临界区(被锁定的区域)
• 在写数据的时候,无论是读线程还是写线程都是互斥的
一般来说:我们大多数都是读取数据得多,修改数据得少。所以这个读写锁在这种场景下就很有用了!

于是我们可以总结出读写锁的一些要点了:
读锁不支持条件对象,写锁支持条件对象
读锁不能升级为写锁,写锁可以降级为读锁
读写锁也有公平和非公平模式
读锁支持多个读线程进入临界区,写锁是互斥的
2.1 ReentrantReadWriteLock内部类
ReentrantReadWriteLock比ReentrantLock锁多了两个内部类(都是Lock实现)来维护读锁和写锁,但是主体还是使用Syn:
WriteLock
ReadLock

在ReentrantLock锁上使用的是state来表示同步状态(也可以表示重入的次数),而在ReentrantReadWriteLock是这样代表读写状态的:

主要还是调用syn的
acquire(1):


写锁的获取调用的是
acquireShared(int arg)方法:

:doAcquireShared(arg);方法(实现也是在Syn的)

(三)、最后
• AQS 是ReentrantReadWriteLock和ReentrantLock的基础 ,因为默认的实现都是在内部类Syn中,而Syn是继承AQS的~
• ReentrantReadWriteLock和ReentrantLock都支持公平和非公平模式 ,公平模式下会去看FIFO队列线程是否是在队头,而非公平模式下是没有的
• ReentrantReadWriteLock是一个读写锁,如果读的线程比写的线程要多很多的话,那可以考虑使用它。它使用state的变量高16位是读锁,低16位是写锁
• 写锁可以降级为读锁,读锁不能升级为写锁
• 写锁是互斥的,读锁是共享的
七、线程池
线程池可以看做是线程的集合。在没有任务时线程处于空闲状态,当请求到来:线程池给这个请求分配一个空闲的线程,任务完成后回到线程池中等待下次任务(而不是销毁),这样就实现了线程的重用。
我们来看看如果没有使用线程池,为每个请求都新开一个线程理论上是可以,但是会有缺点:
线程生命周期的开销非常高。每个线程都有自己的生命周期,创建和销毁线程所花费的时间和资源可能比处理客户端的任务花费的时间和资源更多,并且还会有某些空闲线程也会占用资源。
程序的稳定性和健壮性会下降,每个请求开一个线程。如果受到了恶意攻击或者请求过多(内存不足),程序很容易就奔溃掉了
*所以说:我们的线程最好是交由线程池来管理,这样可以减少对线程生命周期的管理,一定程度上提高性能
(二)、JDK提供的线程池API
JDK给我们提供了Excutor框架[ɪɡˈzekjətə(r)]来使用线程池,它是线程池的基础
Executor提供了一种将“任务提交”与“任务执行” 分离开来的机制(解耦)

除了ScheduledThreadPoolExecutor线程池和ThreadPoolExecutor类线程池以外,还有一个是JDK1.7新增的线程池:ForkJoinPool线程池[puːl]

2.2 补充:Callable和Future
学到了线程池,我们可以很容易地发现:很多的API都有Callable[ˈkɔːləbl]和Future这么两个东西
我们可以简单认为:Callable就是Runnable的扩展,Runnable没有返回值,不能抛出受检查的异常,而Callable可以

Future一般我们认为是Callable的返回值,但Future其实代表的是任务的生命周期

public class CallableDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 创建线程池对象
ExecutorService pool = Executors.newFixedThreadPool(2);
// 可以执行Runnable对象或者Callable对象代表的线程
Future<Integer> f1 = pool.submit(new MyCallable(100));
Future<Integer> f2 = pool.submit(new MyCallable(200));
// V get()
Integer i1 = f1.get();
Integer i2 = f2.get();
System.out.println(i1);
System.out.println(i2);
// 结束
pool.shutdown();
}
}
Callable任务:
public class MyCallable implements Callable<Integer> {
private int number;
public MyCallable(int number) {
this.number = number;
}
@Override
public Integer call() throws Exception {
int sum = 0;
for (int x = 1; x <= number; x++) {
sum += x;
}
return sum;
}
}

(三)、ThreadPoolExecutor详解
这是用得最多的线程池


RUNNING:线程池能够接受新任务,以及对新添加的任务进行处理
SHUTDOWN:线程池不可以接受新任务,但是可以对已添加的任务进行处理
STOP:线程池不接收新任务,不处理已添加的任务,并且会中断正在处理的任务
TIDYING:[ˈtaɪdiɪŋ]当所有的任务已终止,ctl记录的"任务数量"为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,
会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,
若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现
TERMINATED:线程池彻底终止的状态


下面列举三个比较常见的实现池:
newFixedThreadPool
newCachedThreadPool
SingleThreadExecutor
3.2.1 newFixedThreadPool
一个固定线程数的线程池,它将返回一个corePoolSize和maximumPoolSize相等的线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
3.2.2 newCachedThreadPool
非常有弹性的线程池,对于新的任务,如果此时线程池里没有空闲线程,线程池会毫不犹豫的创建一条新的线程去处理这个任务
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
3.2.3 SingleThreadExecutor
使用单个worker线程的Executor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
3.3 构造方法 构造方法可以让我们自定义(扩展)线程池

指定核心线程数量
指定最大线程数量
允许线程空闲时间
时间对象
阻塞队列
线程工厂
任务拒绝策略
总结这些参数的要点
线程数量:
如果运行线程的数量少于核心线程数量,则创建新的线程处理请求
如果运行线程的数量大于核心线程数量,小于最大线程数量,则当队列满的时候才创建新的线程
如果核心线程数量等于最大线程数量,那么将创建固定大小的连接池
如果设置了最大线程数量为无穷,那么允许线程池适合任意的并发数量
线程空闲时间要点:
当前线程数大于核心线程数,如果空闲时间已经超过了,那该线程会销毁
排队策略要点:
同步移交:不会放到队列中,而是等待线程执行它。如果当前线程没有执行,很可能会新开一个线程执行
无界限策略:如果核心线程都在工作,该线程会放到队列中。所以线程数不会超过核心线程数
有界限策略:可以避免资源耗尽,但是一定程度上减低了吞吐量
当线程关闭或者线程数量满了和队列饱和了,就有拒绝任务的情况了,
拒绝任务策略:
直接抛出异常
使用调用者的线程来处理
直接丢掉这个任务
丢掉最老的任务
(四)、execute执行方法
execute[ˈeksɪkjuːt]执行方法分了三步,以注释的方式写在代码上了~
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//如果线程池中运行的线程数量<corePoolSize,则创建新线程来处理请求,即使其他辅助线程是空闲的。
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//如果线程池中运行的线程数量>=corePoolSize,且线程池处于RUNNING状态,且把提交的任务成功放入阻塞队列中,就再次检查线程池的状态,
// 1.如果线程池不是RUNNING状态,且成功从阻塞队列中删除任务,则该任务由当前 RejectedExecutionHandler 处理。
// 2.否则如果线程池中运行的线程数量为0,则通过addWorker(null, false)尝试新建一个线程,新建线程对应的任务为null。
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 如果以上两种case不成立,即没能将任务成功放入阻塞队列中,且addWoker新建线程失败,则该任务由当前 RejectedExecutionHandler 处理。
else if (!addWorker(command, false))
reject(command);
}
(五)、线程池关闭
ThreadPoolExecutor提供了shutdown()和shutdownNow()两个方法来关闭线程池
shutdown( ) :


• 调用shutdown( )后,线程池状态立刻变为SHUTDOWN,而调用shutdownNow( ),线程池状态立刻变为STOP
• shutdown( )等待任务执行完才中断线程,而shutdownNow( )不等任务执行完就中断了线程
八、死锁
(一)、死锁讲解
在Java中使用多线程,就会有可能导致死锁问题。死锁会让程序一直卡住,不再程序往下执行。我们只能通过中止并重启的方式来让程序重新执行。
造成死锁的原因可以概括成三句话:
当前线程拥有其他线程需要的资源
当前线程等待其他线程已拥有的资源
都不放弃自己拥有的资源
1.1 锁顺序死锁
上锁的时候,没有规定顺序

线程A调用leftRight()方法,得到left锁
同时线程B调用rightLeft()方法,得到right锁
线程A和线程B都继续执行,此时线程A需要right锁才能继续往下执行。此时线程B需要left锁才能继续往下执行。
但是:线程A的left锁并没有释放,线程B的right锁也没有释放
所以他们都只能等待,而这种等待是无期限的-->永久等待-->死锁
1.2 动态锁顺序死锁
调用时参数位置不同,就可能发生死锁

如果两个线程同时调用transferMoney()
线程A从X账户向Y账户转账
线程B从账户Y向账户X转账
那么就会发生死锁
1.3 协作对象之间发生死锁
互相调用方法时,有可能发生死锁
(二)、避免死锁的方法
避免死锁可以概括成三种方法:
• 固定加锁的顺序 (针对锁顺序死锁)
• 开放调用(针对对象之间协作造成的死锁)
• 使用定时锁-->tryLock( )
----如果等待获取锁时间超时,则抛出异常而不是一直等待,使用tryLock()能够有效避免死锁问题
2.1 死锁检测
JDK提供了两种方式来给我们检测:
JconsoleJDK自带的图形化界面工具,使用JDK给我们的的工具JConsole
Jstack是JDK自带的命令行工具,主要用于线程Dump分析
九、线程常用的工具类(Java并发关键字)
Java为我们提供了三个同步工具类:
CountDownLatch(闭锁)[ˈkaʊntdaʊnlætʃ]
CyclicBarrier(循环屏障)[ˈsaɪklɪkˈbæriər]
Semaphore(信号量)['seməfɔ:(r)]
这几个工具类其实说白了就是为了能够更好控制线程之间的通讯问题
(一)、CountDownLatch
1.1 CountDownLatch简介
CountDownLatch类位于java.util.concurrent包下,是一个是一个同步工具类,允许一个或多个线程一直等待其它线程的操作执行完后再执行相关操作
它常用的API其实就两个:await()和countDown()

使用说明:
count初始化CountDownLatch,然后需要等待的线程调用await方法。await方法会一直受阻塞,
直到count=0。而其它线程完成自己的操作后,调用countDown()使计数器count减1。当count减到0时,
所有在等待的线程均会被释放
说白了,就是通过count变量来控制等待,如果count值为0了(其他线程的任务都完成了),那就可以继续执行
1.2 CountDownLatch例子 例子:其他的员工还没下班,3y不好意思先走,等其他的员工都走光了,3y再走


(二)、CyclicBarrier
2.1 CyclicBarrier简介
CyclicBarrier是一个同步工具,可以实现让一组线程互相等待,直至某个状态之后再全部同时执行。叫做cyclic是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用(对比于CountDownLatch是不能重用的)
使用说明:
CountDownLatch注重的是等待其他线程完成,CyclicBarrier注重的是:当线程到达某个状态后,
暂停下来等待其他线程,所有线程均到达以后,继续执行。
CyclicBarrier中最重要的方法是await方法,它有两种实现:
public int await();挂起当前线程直到所有线程都为Barrier状态再同时执行后续任务
public int await(long timeout,TimeUnit unit);设置一个超时时间,在超时时间过后,如果还有线程未达到Barrier状态,
则不再等待,让达到Barrier状态的线程继续执行后续的任务
2.2 CyclicBarrier例子
例子:3y和女朋友约了去广州夜上海吃东西,由于3y和3y女朋友住的地方不同,自然去的路径也就不一样了。于是他俩约定在体育西路地铁站集合,约定等到相互见面的时候就发一条朋友圈


(三)、Semaphore
3.1 Semaphore简介
Semaphore指信号量,用于控制同时访问某些资源的线程个数,具体做法为通过调用acquire()获取一个许可,如果没有许可,则阻塞等待,在许可证使用完毕后通过release()释放该许可,以便其他线程使用。
Semaphore常被用于多个线程需要共享有限资源的情况
3.2 Semaphore例子
3y女朋友开了一间卖酸奶的小店,小店一次只能容纳5个顾客挑选购买,超过5个就需要排队啦


(四)、volatile关键字
volatile具备2种特性:
• 一种是保证变量对所有线程可见,在一个线程修改变量值后,新的值对于其他线程是可以立即获取的;
• 一种是volatile禁止指令重排,即volatile变量不会被缓存在寄存器中或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
volatile主要适用于一个变量被多个线程共享,多个线程均可针对这个变量执行赋值或者读取的操作
volatile关键字使用方法很简单,直接在变量上加上volatile关键字即可:
volatile boolean flag=false;
十、Atomic[əˈtɒmɪk]
我们知道,在多线程程序中,诸如++i或i++等运算不具有原子性,i++需要经过读取--修改--写入三个步骤,因此不是安全的线程操作。我们通过synchronized或者ReentrantLock将该操作变成原子操作,但synchronized和ReentrantLock均属于重量级锁 ,只做一个++这么简单的操作,都用到了synchronized锁,未免有点小题大做了
因此JVM为此类原子操作提供了一些原子操作同步类 ,使得同步操作(线程安全操作)更方便高效。原子变量类在java.util.concurrent.atomic包下,总体来看有这么多个:

• 基本类型:
AtomicBoolean:布尔型
AtomicInteger:整型
AtomicLong:长整型
• 数组:
AtomicIntegerArray:数组里的整型
AtomicLongArray:数组里的长整型
AtomicReferenceArray:数组里的引用类型
• 引用类型:
AtomicReference:引用类型
AtomicStampedReference:带有版本号的引用类型
AtomicMarkableReference:带有标记位的引用类型
• 对象的属性:
AtomicIntegerFieldUpdater:对象的属性是整型
AtomicLongFieldUpdater:对象的属性是长整型
AtomicReferenceFieldUpdater:对象的属性是引用类型
• JDK8新增DoubleAccumulator、LongAccumulator、DoubleAdder、LongAdder
--是对AtomicLong等类的改进。比如LongAccumulator与LongAdder在高并发环境下比AtomicLong更高效
十一、ThreadLocal
(一)、什么是ThreadLocal
ThreadLocal提供了线程的局部变量,每个线程都可以通过set()和get()来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离~
简要言之:往ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的
(二)、为什么要学习ThreadLocal?
管理Connection和避免一些参数传递
总结:
ThreadLocal设计的目的就是为了能够在当前线程中有属于自己的变量,并不是为了解决并发或者共享变量的问题