多线程知识点总结
- 1.线程的生命周期和状态
- 2.sleep()方法和wait()方法
- 3.synchronized关键字
- 3.1 synchronized简介
- 3.2 synchronized关键字最主要的三种使用方式
- 3.3 双检锁单例实现
- 4.volatile关键字
- 4.1 java内存模型
- 4.2 synchronized和volatile的区别
- 5.ThreadLocal
- 5.1ThreadLocal简介
- 5.2 ThreadLocal内存泄露问题
- 6 线程池
- 6.1 线程池简介
- 6.2 线程池的创建
- 6.3 ThreadPoolExecutor 构造方法
- 7 AQS
- 7.1 AQS简介
- 7.2 AQS原理概述
- 7.3 Semaphore(信号量)
- 7.4 CountDownLatch(倒计时器)
- 7.5 CyclicBarrier(循环栅栏)
1.线程的生命周期和状态
线程的生命周期包含5个阶段,包括:新建、就绪、运行、阻塞、销毁。
- 初始:就是刚使用new方法,new出来的线程;
- 就绪:就是调用的线程的
start()
方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行; - 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能;
- 阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如join(), wait()之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify()或者notifyAll() 法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态;
- 销毁:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源;
2.sleep()方法和wait()方法
- 两者的共同点是都可以用来暂停线程的执行
- 主要区别在于:
-
sleep()
方法不会释放对象锁,wait()
方法会释放对象锁; -
sleep()
方法属于Thread类而wait()方法属于Object类; -
sleep()
方法调用后会自动苏醒。wait()方法调用后不会自动苏醒,需要通过调用同一对象的notify()
/notifyAll()
方法唤醒。或者可以使用wait(long timeout)
超时后线程会自动苏醒。
3.synchronized关键字
3.1 synchronized简介
synchronized
关键字解决的是多个线程之间访问资源的同步性,synchronized
关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
在 Java 早期版本中,synchronized
属于重量级锁,JDK1.6之后对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。想要更深入了解synchronized
锁升级,可以看下这篇博客。
3.2 synchronized关键字最主要的三种使用方式
- 修饰实例方法: 作用于当前对象实例加锁,进入方法前需要获得当前实例对象的锁。
- 修饰静态方法: 由于静态方法是属于类的,所以会对当前的类加锁,所有对象实例在访问方法时需要获取类的锁。
- 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。推荐使用这种方式,可以减少不必要的阻塞。
3.3 双检锁单例实现
说到synchronized
通常就会提到双检锁实现的单例,下面给出具体实现的代码:
public class Singleton{
//使用volatile修饰,方式jvm在实例对象时进行指令重排
private volatile static Sigleton instance;
private Singleton(){
}
public static Singleton getInstance(){
//判断对象是否实例化,未实例化才进入加锁代码继续判断,可以避免不必要的锁获取
if(instance == null){
synchronized(Sigleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
4.volatile关键字
4.1 java内存模型
在了解volatile
关键字之前,我们需要简单的了解下java的内存模型。在jdk1.2之前,java的内存模型是直接从主存(线程共享) 中进行读写的。但是在当前的java内存模型下,线程是可以把变量保存到本地内存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
要解决这个问题,就需要把变量声明为volatile
,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。
所以说, volatile
关键字的主要作用就是保证变量的可见性(每次读写都直接操作主内存)然后还有一个作用是防止指令重排序。
4.2 synchronized和volatile的区别
-
volatile
只能修饰变量,synchronized
可以修饰方法和代码块 - 多线程访问
volatile
关键字不会发生阻塞,而synchronized
关键字可能会发生阻塞 -
volatile
关键字能保证数据的可见性,但不能保证数据的原子性。synchronized
关键字两者都能保证。
5.ThreadLocal
5.1ThreadLocal简介
ThreadLocal
提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。
ThreadLocal
的静态内部类ThreadLocalMap
为每个Thread都维护了一个数组table,ThreadLocal
确定了一个数组下标,而这个下标就是value存储的对应位置。ThreadLocal
使用方式:
private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
THREAD_LOCAL.set("");//存
THREAD_LOCAL.get();//取
5.2 ThreadLocal内存泄露问题
内存泄漏 :是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。
使用ThreadLocal
产生内存泄露的的主要原因是由于ThreadLocalMap
内部是由一个Entry
数组组成,其中Entry
的key是ThreadLocal
的弱引用,所以jvm在垃圾回收之前会清除Entry
的key,ThreadLocalMap
中就会出现 key 为 null 的 Entry
,就没有办法访问这些 key 为 null 的 Entry 的 value。这些 value 被Entry对象引用,所以value所占内存不会被释放。
为了防止这种情况的发生,当我们在调用 set()
、get()
、remove()
方法的时候,会清理掉 key 为 null 的记录。
6 线程池
6.1 线程池简介
线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。使用线程池的好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
6.2 线程池的创建
我们可以通过ThreadPoolExecutor
的构造方法和通过Executor 框架的工具类Executors
来创建线程池。Executors
为我们提供了三种类型的ThreadPoolExecutor:
- FixedThreadPool:该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
- SingleThreadExecutor:方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
- CachedThreadPool:该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors
去创建,而是通过 ThreadPoolExecutor
的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
Executors 返回线程池对象的弊端如下:
FixedThreadPool和 SingleThreadExecutor: 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致OOM。
CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。
6.3 ThreadPoolExecutor 构造方法
ThreadPoolExecutor
类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
ThreadPoolExecutor
构造方法中的参数解析:
-
corePoolSize
:核心线程数线程数定义了最小可以同时运行的线程数量。 -
maximumPoolSize
:当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 -
workQueue
: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 -
keepAliveTime
:当线程池中的线程数量大于corePoolSize
的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime
才会被回收销毁. -
unit
:keepAliveTime
参数的时间单位。 -
threadFactory
:executor 创建新线程的时候会用到。 -
handler
:饱和策略。关于饱和策略这里就不赘述了,感兴趣的可以去了解一下。
7 AQS
7.1 AQS简介
AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks
包下面。
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。
7.2 AQS原理概述
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
7.3 Semaphore(信号量)
synchronized
和 ReentrantLock
都是一次只允许一个线程访问某个资源,Semaphore
(信号量)可以指定多个线程同时访问某个资源。示例代码如下:
public class SemaphoreExample1 {
// 请求的数量
private static final int threadCount = 550;
public static void main(String[] args) throws InterruptedException {
// 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢)
ExecutorService threadPool = Executors.newFixedThreadPool(300);
// 一次只能允许执行的线程数量。
final Semaphore semaphore = new Semaphore(20);
for (int i = 0; i < threadCount; i++) {
final int threadnum = i;
threadPool.execute(() -> {// Lambda 表达式的运用
try {
semaphore.acquire();// 获取一个许可,所以可运行线程数量为20/1=20
test(threadnum);
semaphore.release();// 释放一个许可
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
threadPool.shutdown();
System.out.println("finish");
}
public static void test(int threadnum) throws InterruptedException {
Thread.sleep(1000);// 模拟请求的耗时操作
System.out.println("threadnum:" + threadnum);
Thread.sleep(1000);// 模拟请求的耗时操作
}
}
执行 acquire()
方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个 release()
方法增加一个许可证,这可能会释放一个阻塞的acquire方法。然而,其实并没有实际的许可证这个对象,Semaphore
只是维持了一个可获得许可证的数量。 Semaphore
经常用于限制获取某种资源的线程数量。
Semaphore 有两种模式,公平模式和非公平模式,可以通过构造方法中的boolean fair
参数来构造。
- 公平模式:调用acquire的顺序就是获取许可证的顺序,遵循FIFO;
- 非公平模式:抢占式。
7.4 CountDownLatch(倒计时器)
CountDownLatch
是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
- CountDownLatch的两种典型用法:
- 某一线程在开始运行前等待n个线程执行完毕。将
CountDownLatch
的计数器初始化为n :new CountDownLatch(n)
,每当一个任务线程执行完毕,就将计数器减1countdownlatch.countDown()
,当计数器的值变为0时,在CountDownLatch
上await()
的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。 - 实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的
CountDownLatch
对象,将其计数器初始化为 1 :new CountDownLatch(1)
,多个线程在开始执行任务前首先coundownlatch.await()
,当主线程调用countDown()
时,计数器变为0,多个线程同时被唤醒。
- CountDownLatch 的使用示例:
public class CountDownLatchExample1 {
// 请求的数量
private static final int threadCount = 550;
public static void main(String[] args) throws InterruptedException {
// 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢)
ExecutorService threadPool = Executors.newFixedThreadPool(300);
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
final int threadnum = i;
threadPool.execute(() -> {// Lambda 表达式的运用
try {
test(threadnum);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
countDownLatch.countDown();// 表示一个请求已经被完成
}
});
}
countDownLatch.await();
threadPool.shutdown();
System.out.println("finish");
}
public static void test(int threadnum) throws InterruptedException {
Thread.sleep(1000);// 模拟请求的耗时操作
System.out.println("threadnum:" + threadnum);
Thread.sleep(1000);// 模拟请求的耗时操作
}
}
7.5 CyclicBarrier(循环栅栏)
CyclicBarrier
和 CountDownLatch
非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch
更加复杂和强大。主要应用场景和 CountDownLatch
类似。
CyclicBarrier 的使用示例:
public class CyclicBarrierExample2 {
// 请求的数量
private static final int threadCount = 550;
// 需要同步的线程数量
private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
public static void main(String[] args) throws InterruptedException {
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
Thread.sleep(1000);
threadPool.execute(() -> {
try {
test(threadNum);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (BrokenBarrierException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
threadPool.shutdown();
}
public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
System.out.println("threadnum:" + threadnum + "is ready");
try {
cyclicBarrier.await(2000, TimeUnit.MILLISECONDS);
} catch (Exception e) {
System.out.println("-----CyclicBarrierException------");
}
System.out.println("threadnum:" + threadnum + "is finish");
}
}
CyclicBarrier和CountDownLatch的区别:
CountDownLatch | CyclicBarrier |
减计数方式 | 加计数方式 |
计算为0时释放所有等待的线程 | 计数达到指定值时释放所有等待线程 |
计数为0时无法重置 | 计数达到指定值时,计数置为0重新开始 |
调用countDown()方法计数器减一,调用await()方法只进行阻塞,对计数器没有任何影响 | 调用await()方法计数器会加一,若加一后的值不等于构造器的值,则线程阻塞 |
不可重复利用 | 可重复利用 |