目录
基础知识
并发编程的优缺点
为什么要使用并发编程(并发编程的优点)
并发编程有什么缺点
并发编程三要素是什么?在 Java 程序中怎么保证多线程的运行安全?
并行和并发有什么区别?
什么是多线程,多线程的优劣?
线程和进程区别
什么是线程和进程?
进程与线程的区别
什么是上下文切换,线程调度策略(切换原因)?
守护线程和用户线程有什么区别呢?
如何在 Windows 和 Linux 上查找哪个线程和进程?
什么是线程死锁
形成死锁的四个必要条件是什么
线程活跃性问题:活锁,死锁,饥饿
创建线程的四种方式
创建线程有哪几种方式?
说一下 runnable 和 callable 有什么区别?
线程的 run()和 start()有什么区别?
为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
什么是 Callable 和 Future?
什么是 FutureTask
线程的状态和基本操作
说说线程的生命周期及五种基本状态?
Java 中用到的线程调度算法是什么?
什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing )?
JAVA线程的常用方法
sleep() 和 wait() 有什么区别
你是如何调用 wait() 方法的?使用 if 块还是循环?为什么?
为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里?
为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用?
为什么 Thread 类的 sleep()和 yield ()方法是静态的?
如何停止一个正在运行的线程?
notify() 和 notifyAll() 有什么区别?
Java 如何实现多线程之间的通讯和协作?
同步方法和同步块,哪个是更好的选择?
如果你提交任务时,线程池队列已满,这时会发生什么
什么叫线程安全?servlet 是线程安全吗?
在 Java 程序中怎么保证多线程的运行安全?
你对线程优先级的理解是什么?
线程类的构造方法、静态块是被哪个线程调用的
Java 中怎么获取一份线程 dump 文件?你如何在 Java 中获取线程堆栈?
一个线程运行时发生异常会怎样?
Java 线程数过多会造成什么异常?
并发理论
Java内存模型
Java中垃圾回收有什么目的?什么时候进行垃圾回收?
如果对象的引用被置为null,垃圾收集器是否会立即释放对象占用的内存?
finalize()方法什么时候被调用?析构函数(finalization)的目的是什么?
重排序与数据依赖性
为什么代码会重排序?
as-if-serial规则和happens-before规则的区别
并发关键字
synchronized
synchronized 的作用?
说说自己是怎么使用 synchronized 关键字,在项目中用到了吗
说一下 synchronized 底层实现原理?
说一下对象头
说一下锁
什么是自旋
多线程中 synchronized 锁升级的原理是什么?
线程 B 怎么知道线程 A 修改了变量
当一个线程进入一个对象的 synchronized 方法 A 之后,其它线程是否可进入此对象的 synchronized 方法 B?
synchronized、volatile、CAS 比较
synchronized 和 Lock 有什么区别?
synchronized 和 ReentrantLock 区别是什么?
volatile
volatile 关键字的作用
Java 中能创建 volatile 数组吗?
volatile 变量和 atomic 变量有什么不同?
volatile 能使得一个非原子操作变成原子操作吗?
volatile如何保证有序性
volatile如何保证可见性
final
什么是不可变对象,它对写并发应用有什么帮助?
JUC概览
Lock体系
Lock简介与初识AQS
Java Concurrency API 中的 Lock 接口(Lock interface)是什么?对比同步它有什么优势?
乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
什么是 CAS
CAS 的会产生什么问题?
AQS(AbstractQueuedSynchronizer)详解与源码分析
ReentrantLock(重入锁)实现原理与公平锁非公平锁区别
ReentrantReadWriteLock读写锁
LockSupport工具类
JUC原子类
什么是原子操作?在 Java Concurrency API 中有哪些原子类(atomic classes)?
说一下 atomic 的原理?
原子整数:AtomicInteger, AtomicLong, AtomicBoolean
原子引用:AtomicReference(存在ABA问题) AtomicStampedReference, AtomicMarkableReference
原子数组:AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray
字段更新器:AtomicReferenceFieldUpdater, AtomicIntegerFieldUpdater, AtomicLongFieldUpdater
原子累加器:LongAdder,LongAccmulator
Unsafe对象
线程池
什么是线程池?有哪几种创建方式?
线程池有什么优点?
继承关系
内部重要属性和方法
线程池状态(ctl属性)
构造方法和参数
提交任务方法
关闭线程池
Worker类
Excutors工具类:jdk提供的线程池
在 Java 中 Executor 和 Executors 的区别?
在 Java 中 Executor 和 Executors 的区别?
线程池中 submit() 和 execute() 方法有什么区别
Executors和ThreaPoolExecutor创建线程池的区别
并发工具
Excutors工具包
CountDownLatch 倒计时锁
CountDownLatch 原理
CyclicBarrier 回环屏障
Semaphore信号量
常用的并发工具类有哪些?
并发容器
并发容器之ConcurrentHashMap详解(JDK1.8版本)与源码分析
什么是ConcurrentHashMap?
Java 中 ConcurrentHashMap 的并发度是什么?
什么是并发容器的实现?
SynchronizedMap 和 ConcurrentHashMap 有什么区别?
并发容器之CopyOnWriteArrayList详解
CopyOnWriteArrayList 是什么,可以用于什么应用场景?有哪些优缺点?
并发容器之ThreadLocal详解
ThreadLocal 是什么?有哪些使用场景?
使用例子
调用关系
ThreadLocal类中的方法
ThreadLocalMaps延迟构造
ThreadLocal造成内存泄漏的原因?
ThreadLocal内存泄漏解决方案?
并发容器之BlockingQueue详解
什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现生产者-消费者模型?
并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如**:内存泄漏、上下文切换、线程安全、死锁**等问题。
并发编程三要素(线程的安全性问题体现在哪) | 出现线程安全问题的原因 | 解决办法 |
原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。 | 线程切换带来的原子性问题 | JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题 |
可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile) | 缓存导致的可见性问题 | synchronized、volatile、LOCK,可以解决可见性问题 |
有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序) | 编译优化带来的有序性问题 | Happens-Before 规则、volatile可以解决有序性问题 |
做一个形象的比喻:
并发 = 两个队列和一台咖啡机。
并行 = 两个队列和两台咖啡机。
串行 = 一个队列和一台咖啡机。
多线程:多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务。
多线程的好处:
可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可以运行其它的线程而不是等待,这样就大大提高了程序的效率。也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
多线程的劣势:
线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
多线程需要协调和管理,所以需要 CPU 时间跟踪线程;
线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。
进程:一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。
线程:进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。
线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。
根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行
线程调度策略(切换原因):
1.线程CPU的时间片用完
2.垃圾回收
3.有更高级的线程需要运行
4.自己调用了sleep(n), yield(), Object.wait(), join(), park(), synchronized, lock方法
守护线程和用户线程
main 函数所在的线程就是一个用户线程啊,main 函数启动的同时在 JVM 内部同时还启动了好多守护线程,比如垃圾回收线程。
比较明显的区别之一是用户线程结束,JVM 退出,不管这个时候有没有守护线程运行。而守护线程不会影响 JVM 的退出。
注意事项:
windows:任务管理器/ cmd中tasklist查看进程;taskkill杀死进程
linux:
//查看进程
查看所有进程信息:ps -fe -f:全部列出 -e:显示所有进程(等价于-A)
查看指定进程信息:ps -fe | grep xxxxx
查看java进程:jps
杀死进程: kill 进程id
查看动态进程信息:top
//查看线程
1.先用top指令按下shift+p查出CPU利用率最高的pid号
2.查看某进程中线程信息:top -H -p pid
3.将获取到的线程号转换成16进制,去百度转换一下就行
1.查看java中某一刻线程信息:jstack 进程id
2.eg.使用jstack工具将进程信息打印输出,jstack pid号 > /tmp/t.dat
1.图形化界面展示:jconsole
死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》):
public class DeadLockDemo {
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "线程 2").start();
}
}
输出结果:
Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1
线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过Thread.sleep(1000)
;让线程 A 休眠 1s 为的是让线程 B 得到CPU执行权,然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。
破坏死锁的方法:我们只要破坏产生死锁的四个条件中的其中一个就可以了。
四个必要条件 |
解释 | 破坏方法 |
互斥条件: |
线程(进程)对于所分配到的资源具有排它性,即一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放 | 这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。 |
请求与保持条件: |
一个线程(进程)因请求被占用资源而发生阻塞时,对已获得的资源保持不放。 | 一次性申请所有的资源。 |
不剥夺条件: |
线程(进程)已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。 | 占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。 |
循环等待条件: |
当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞 | 靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。 |
我们对线程 2 的代码修改成下面这样就不会产生死锁了。
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 2").start();
输出结果:
Thread[线程 1,5,main]get resource1
Thread[线程 1,5,main]waiting get resource2
Thread[线程 1,5,main]get resource2
Thread[线程 2,5,main]get resource1
Thread[线程 2,5,main]waiting get resource2
Thread[线程 2,5,main]get resource2
我们分析一下上面的代码为什么避免了死锁的发生?
线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。
1)死锁:一个线程同时需要获取多把锁,容易产生死锁
eg1. Thread1已经获得了A对象锁,接下来想获得B对象锁
Thread2已经获得了B对象锁,接下来想获得A对象锁
此时发生死锁
eg2. 哲学家就餐问题
定位死锁方法:jconsole工具或者先用jps定位线程ID,再用jstack定位死锁
(2)活锁:两个线程互相改变对方的结束条件,最后谁也无法结束
static volatile int count = 10;
new Thread(() -> {
while(count > 0){
count--;
}
}).start();
new Thread(() -> {
while(count < 20){
count++;
}
}).start();
(3)饥饿:一个线程由于优先级太低,始终得不到CPU调度执行,无法结束
创建线程有四种方式
优缺点 | 其他 | |
继承Thread类,重写run()方法 | java不支持多继承,无法继承其他类;此外代码与任务没有分离 | |
实现Runnable接口,重写run()方法,把runnable作为参数传递给thread() | 没有返回值 | 本质都是调用run()方法,如果传入了runnable对象,则会赋值给target,优先使用runnable中的run()方法。 |
继承Callable类,重写里面的call()方法。创建FutureTask类,传入Callable参数 | 本质上FutureTask类实现了Runnable和Callable。它与Runnable的区别在于它可以得到线程的返回值,适用于线程间通信。 |
实现 Callable 接口继承 Thread 类
步骤:
//1.重写Thread子类的run()方法
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " run()方法正在执行...");
}
}
public class TheadTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
System.out.println(Thread.currentThread().getName() + " main()方法执行结束");
}
}
//2.快速构造
Thread t1 = new Thread(){
public void run(){
//线程方法
}
};
t1.start();
实现 Runnable 接口
步骤:
//1.
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " run()方法执行中...");
}
}
public class RunnableTest {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
}
}
//2.
Runnable run = new Runnable(){
public void run(){
//代码
}
};
Thread t = new Thread(run);
t.start();
实现 Callable 接口
步骤
//1.
public class MyCallable implements Callable {
@Override
public Integer call() {
System.out.println(Thread.currentThread().getName() + " call()方法执行中...");
return 1;
}
}
public class CallableTest {
public static void main(String[] args) {
FutureTask futureTask = new FutureTask(new MyCallable());
Thread thread = new Thread(futureTask);
thread.start();
try {
Thread.sleep(1000);
System.out.println("返回结果 " + futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
}
}
//2.
FutureTask task = new FutureTask(new Callable<>()){
public Integer call(){
//代码
return 100;
}
};
Thread t = new Thread(task);
t.start();
Integer result = t.get();
使用 Executors 工具类创建线程池
Executors提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。
主要有newFixedThreadPool,newCachedThreadPool,newSingleThreadExecutor,newScheduledThreadPool,后续详细介绍这四种线程池
相同点
都是接口
都可以编写多线程程序
都采用Thread.start()启动线程
主要区别
注:Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。
start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。
start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。
run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。
这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!
new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。
而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。
Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。
Future 接口表示异步任务,是一个可能还没有完成的异步任务的结果。所以说 Callable用于产生结果,Future 用于获取结果。
FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。
从操作系统层面:有五种
从java API层面:根据Thread.State枚举,分为六种(NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED)
1 | thread1.start() |
2 | thread1线程获得了锁对象之后 调用object.wait() :thread1从RUNNABLE变成WAITING 调用object.notify(),object.notifyAll(),thread2.interrupt(): 锁竞争成功:thread1线程从 WAITING变成RUNNABLE 锁竞争失败:thread1线程从 WAITING变成BLOCKED |
3 | thread2.join():thread1从RUNNABLE变成WAITING,直到thread2结束,thread1再变回RUNNABLE |
4 | LockSupport.park():让当前线程从RUNNABLE变成WAITING LockSupport.unpart(目标线程):让目标线程从WAITING变回RUNNABLE |
5 | thread1线程获得了锁对象之后:调用object.wait(时间) |
6 | thread2.join(时间) |
7 | thread1.sleep(时间) |
8 | LockSupport.parkNanos(时间) 或者 LockSupport.parkUntil(时间) |
9 | thread1线程获取锁对象失败会进入BLOCKED。 如果锁被释放,会通知该对象上所有BLOCKED的线程重新竞争 |
10 | 代码执行完毕 |
计算机通常只有一个 CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU 的使用权才能执行指令。所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得 CPU 的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待 CPU,JAVA 虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配 CPU 的使用权。
有两种调度模型:分时调度模型和抢占式调度模型。
分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU 的时间片这个也比较好理解。
Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。
线程调度器是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。
时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配 CPU 时间可以基于线程优先级或者线程等待的时间。
线程调度并不受到 Java 虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。
名称 | 介绍 |
thread.start() thread.run() |
run()仅仅是调用方法,start()开启并运行新线程 start()只能调用一次,多次调用会illegalThreadStateException |
thread.sleep(n) | eg. 在该线程中调用Thread.sleep(1000); 让线程从Runnable(Running)进入Time Waiting状态; 如果该线程持有了共享锁,不会释放; 其他线程可以通过interrupt()方法打断在睡眠的线程,这时sleep方法会抛出InterruptedException; 睡眠结束了进入Runnable状态等待被调用,未必会立即被调用,等待CPU时间片; 建议使用juc包下的TimeUnit类的sleep方法代替Thread下的sleep方法,可读性更好。 |
thread.yield() | 线程礼让,静态方法 eg. 在该线程中调用Thread.yield(); 让当前线程从Running进入Runnable,重新分配(但有可能重新调度回自己) |
thread.getState() | 获取线程的状态 java中共6种:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED |
thread.setPriority() | 设计优先级1-10,默认5。 然还还是主要根据操作系统分配 |
thread.join() thread.join(long n) |
等待调用join的线程运行结束/限制最大等待时间,该线程会先进入阻塞状态,底层原理与wait()相同。 eg.在主线程中t1.join(),会等待t1运行结束再执行下面代码
底层:保护性暂停模式,底层调用wait(),当线程执行结束,再notifyAll() |
thread.interrupt() thread.isInterrupted() thread.interrupted() |
1.thread.interrupt():打断其他线程 eg1. 主线程中打断t1线程,t1.interrupt(); 2.thread.isInterrupted():判断线程是否被打断 eg2. 主线程中判断t1是否被打断,t1.isInterrupted(); eg3. t1中判断自己是否被打断,Thread.currentThread().isInterrupted(); 3.thread.interrupted() 获取中打断标记重置打断标记为fals
注: 1.对于sleep, wait, join中的线程,如t1: 调用t1.interrupt()会让其重新回到RUNNABLE状态 同时t1会有InterruptedException异常,需要捕获。 打断后,调用t1.isInterrupted()方法会返回false(标记清除) 2.对于正在运行态RUNNABLE的线程,如t1: 调用t1.interrupt()方法,并不会对t1有影响,t1仍然会继续执行。 不会有异常 但是,t1的打断标记会变true,即t1.isinterrupted() = true; 因此,对于正在运行的线程,需要配合打断标记一起使用 3.对于处于park的线程 调用t1.interrupt()会让其重新进入RUNNABLE状态 不会报异常 t1的打断标记会变true,即t1.isinterrupted() = true; |
thread.setDaemon(true) | 设置守护线程 eg. 在主线程中t1.setDaemon(true)会让t1成为守护线程,主线程如果运行结束,强制t1结束。 注:t1.setDaemon(true)需要用在t1.start()之前 常见的守护线程:gc线程 注:当最后一个非守护线程(用户线程)结束时,JVM会正常退出;守护线程是否正常结束不影响JVM的退出下。 main 线程运行结束后, JVM 会自动启动 个叫作 DestroyJavaVM 的线程,该线程会 等待所有用户线程结束后终止 JVM 程。 总结:如果希望主线程结束后JVM进程立马结束,可以设置其他线程为守护线程 |
stop()/ suspend()/ resume() | 停止线程运行/ 挂起线程/ 恢复线程运行; 都不推荐使用,如果该线程在执行同步代码块被停止,对象锁无法释放,引起线程同步问题。三种方法都已过时 |
Object.wait() Object.wait(n) Object.notify() Object.notifyAll() |
只有持有了对象锁时才能调用这些方法 1. Object.wait();Object.wait(n) 让该线程进入WAITING状态,进入Object锁对象中的WaitSet。 2. Object.notify();Object.notifyAll() |
两者都可以暂停线程的执行
处于等待状态的线程可能会被虚假唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。
wait() 方法应该在循环调用,因为当线程获取到 CPU 开始执行的时候,其他条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好。下面是一段标准的使用 wait 和 notify 方法的代码:
synchronized(obj){
//如果条件不满足,可能是被虚假唤醒,让出锁对象继续WAITING
while(条件不满足){
obj.wait();
}
}
经典消费者生产者例子:
//生产者
synchronized(queue){
//如果生产队列满了,继续回去等待
while(queue.size() == MAX_VALUE){
queue.wait();
}
queue.add(element);
//通知消费者线程
queue.notifyAll();
}
//消费者
synchronized(queue){
//如果生产队列满了,继续回去等待
while(queue.size() == 0){
queue.wait();
}
queue.add(element);
//通知生产者线程
queue.notifyAll();
}
Java中,任何对象都可以作为锁,并且 wait(),notify()等方法用于等待对象的锁或者唤醒线程,在 Java 的线程中并没有可供任何对象使用的锁,所以任意对象调用方法一定定义在Object类中。
wait(), notify()和 notifyAll()这些方法在同步代码块中调用
有的人会说,既然是线程放弃对象锁,那也可以把wait()定义在Thread类里面啊,新定义的线程继承于Thread类,也不需要重新定义wait()方法的实现。然而,这样做有一个非常大的问题,一个线程完全可以持有很多锁,你一个线程放弃锁的时候,到底要放弃哪个锁?当然了,这种设计并不是不能实现,只是管理起来更加复杂。
综上所述,wait()、notify()和notifyAll()方法要定义在Object类中。
当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 notify()方法。同样的,当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。
Thread 类的 sleep()和 yield()方法将在当前正在执行的线程上运行。所以在其他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。
在java中有以下3种方法可以终止正在运行的线程:
如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
notifyAll() 会唤醒所有的线程,notify() 只会唤醒一个线程。
notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。
Java中线程通信协作的最常见的两种方式:
一.syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll()
二.ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()
线程间直接的数据交换:
三.通过管道进行线程间通信:1)字节流;2)字符流
同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁。
同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,这样从侧面来说也可以避免死锁。
请知道一条原则:同步的范围越小越好。
//1.锁住代码块
synchronized(Object){
//临界区
}
//2.加在方法上
public synchronized void method1(){} //非静态方法,本质是锁住该类实例对象this
public synchronized static void method2(){} //静态方法,本质是锁住类对象this.class
这里区分一下:
(1)如果使用的是无界队列 LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务
(2)如果使用的是有界队列比如 ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy
线程安全是编程中的术语,指某个方法在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。
Servlet 不是线程安全的,servlet 是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。
Struts2 的 action 是多实例多线程的,是线程安全的,每个请求过来都会 new 一个新的 action 分配给这个请求,请求完成后销毁。
SpringMVC 的 Controller 是线程安全的吗?不是的,和 Servlet 类似的处理流程。
Struts2 好处是不用考虑线程安全问题;Servlet 和 SpringMVC 需要考虑线程安全问题,但是性能可以提升不用处理太多的 gc,可以使用 ThreadLocal 来处理多线程的问题。
每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个 int 变量(从 1-10),1 代表最低优先级,10 代表最高优先级。
Java 的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,如非特别需要,一般无需设置线程优先级。
这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被 new这个线程类所在的线程所调用的,而 run 方法里面的代码才是被线程自身所调用的。
如果说上面的说法让你感到困惑,那么我举个例子,假设 Thread2 中 new 了Thread1,main 函数中 new 了 Thread2,那么:
(1)Thread2 的构造方法、静态块是 main 线程调用的,Thread2 的 run()方法是Thread2 自己调用的
(2)Thread1 的构造方法、静态块是 Thread2 调用的,Thread1 的 run()方法是Thread1 自己调用的
Dump文件是进程的内存镜像。可以把程序的执行状态通过调试器保存到dump文件中。
在 Linux 下,你可以通过命令 kill -3 PID (Java 进程的进程 ID)来获取 Java应用的 dump 文件。
在 Windows 下,你可以按下 Ctrl + Break 来获取。这样 JVM 就会将线程的 dump 文件打印到标准输出或错误文件中,它可能打印在控制台或者日志文件中,具体位置依赖应用的配置。
如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候,JVM 会使用 Thread.getUncaughtExceptionHandler()来查询线程的 UncaughtExceptionHandler 并将线程和异常作为参数传递给 handler 的 uncaughtException()方法进行处理。
线程的生命周期开销非常高
消耗过多的 CPU
资源如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争 CPU资源时还将产生其他性能的开销。
降低稳定性JVM
在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError 异常。
垃圾回收是在内存中存在没有引用的对象或超过作用域的对象时进行的。
垃圾回收的目的是识别并且丢弃应用不再使用的对象来释放和重用资源。
不会,在下一个垃圾回调周期中,这个对象将是被可回收的。
也就是说并不会立即被垃圾收集器立刻回收,而是在下一次垃圾回收时才会释放其占用的内存。
1)垃圾回收器(garbage colector)决定回收某对象时,就会运行该对象的finalize()方法;
finalize是Object类的一个方法,该方法在Object类中的声明protected void finalize() throws Throwable { }
在垃圾回收器执行时会调用被回收对象的finalize()方法,可以覆盖此方法来实现对其资源的回收。注意:一旦垃圾回收器准备释放对象占用的内存,将首先调用该对象的finalize()方法,并且下一次垃圾回收动作发生时,才真正回收对象占用的内存空间
2)GC本来就是内存回收了,应用还需要在finalization做什么呢? 答案是大部分时候,什么都不用做(也就是不需要重载)。只有在某些很特殊的情况下,比如你调用了一些native的方法(一般是C写的),可以要在finaliztion里去调用C的释放函数。
在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:
在单线程环境下不能改变程序运行的结果;
存在数据依赖关系的不允许重排序
需要注意的是:重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义。
as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。
在 Java 中,synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行。synchronized 可以修饰类、方法、变量。
另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
synchronized关键字最主要的三种使用方式:
总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!
下面我以一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。
面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!”
双重校验锁实现对象单例(线程安全)
public Class Singleton{
private volatile static Singleton INSTANCE;
private Singleton(){}
public static Singleton getInstance(){
if(INSTANCE == null){
synchronized(Singleton.class){
if(INSTANCE == null){
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。
uniqueInstance 采用 volatile 关键字修饰也是很有必要的, INSTANCE= new Singleton(); 这段代码其实是分为三步执行:
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
synchronized是Java中的一个关键字,在使用的过程中并没有看到显示的加锁和解锁过程。因此有必要通过javap命令,查看相应的字节码文件。
通过JDK 反汇编指令 javap
可以看出在执行同步代码块之前之后都有一个monitor字样,其中前面的是monitorenter,后面的是离开monitorexit,不难想象一个线程也执行同步代码块,首先要获取锁,而获取锁的过程就是monitorenter ,在执行完代码块之后,要释放锁,释放锁就是执行monitorexit指令。
为什么会有两个monitorexit呢?
这个主要是防止在同步代码块中线程因异常退出,而锁没有得到释放,这必然会造成死锁(等待的线程永远获取不到锁)。因此最后一个monitorexit是保证在异常情况下,锁也可以得到释放,避免死锁。
仅有ACC_SYNCHRONIZED这么一个标志,该标记表明线程进入该方法时,需要monitorenter,退出该方法时需要monitorexit。
synchronized可重入的原理
重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁。
每个对象都有对象头:
eg. Integer对象大小为:8byte(对象头)+4byte(数据)+4byte(自动补齐) = 16字节
int大小为:4byte = 4字节
对象头:以32位虚拟机为例。MarkWord:存储该对象的信息,KlassWord:指针指向该对象的类型
普通对象:MarkWord(32bits) + KlassWord(32bits) 共64bits8个字节
数组对象:MarkWord(32bits) + KlassWord(32bits) + arraylength(32bits)
MarkWord具体细节(下图):以Normal为例,分别记录:hashcode值,垃圾回收分代年龄,是否是偏向锁,锁标志位
https://www.cnblogs.com/paddix/p/5405678.html
(1)轻量级锁
使用场景:多线程的访问时间是错开的(无竞争)
步骤:
首先栈帧中会创建锁记录(Lock Record)对象,锁记录中包含:该对象的地址 和 锁记录
加锁时,会把引用地址指向该对象,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如下图,该对象由的锁标志位由01(无锁)变成00(轻量级锁),表示已经加锁。
如果CAS失败,有两种情况:
如果其他线程已经持有了该Object的轻量级锁,表示有竞争,进入锁膨胀
如果是自己执行了synchronized重入,那么再添加一条锁记录作为锁重入的记数。
锁重入时会再添加一条锁记录(因为是自己调用,在同一栈帧中),内容为null。解锁时会通过是否为null判断是否结束,如果是null则表示有锁重入,继续解锁;如果不是null则需将MarkWord再恢复给对象头。(成功:说明解锁成功;失败:轻量级锁已经锁膨胀了,需要重量级锁解锁)
(2)锁膨胀
有竞争时会锁膨胀,即加锁时发现对象头的Markword已经变成00。如下图,Thead-1尝试加锁,发现锁已经被Thread-0占用。
Thead-1加锁失败,进行锁膨胀:首先thead-1会为该Object对象申请Monitor锁,让Object指向重量级锁地址(即Monitor对象),Monitor会记录原来该对象的信息,然后自己进入Monitor的EntryList BLOCKED。
注:此时Markword为 monitor地址(30bits) + 10(2bit)
当Thead-0执行完退出时,会执行解锁。但发现此时Object内的锁标志位不再是00(轻量级),而是10(重量级)。因此会进入重量级解锁流程,先找到Monitor对象,设置Owner为null,唤醒EntryList中的BLOCKED线程。
(3)自旋优化
在重量级锁竞争时,可以用自旋进行优化。当新线程Thead-2想继续加锁时,发现对象头的锁标志位是10(重量级),它会先尝试自旋而不是立即阻塞。
注:JAVA6之后自旋是自适应的,即如果刚刚自旋成功,则会认为现阶段自旋成功可能性高,多自旋几次。反之少自旋。
成功情况:
失败情况:
(4)偏向锁
在轻量级锁加锁的过程中,每次锁重入仍然需要执行CAS操作。即尝试用锁记录去替换MarkWord,但是发现原先记录属于同一个线程,因此替换失败。(尽管失败,仍然每次都会开销)。因此在JAVA6中引入偏向锁对轻量级锁的锁重入进行优化。
偏向锁:第一次使用CAS将该线程ID记录到对象的MarkWord中(取代了原来的锁记录)。
优化前:
优化后:
总结:
无竞争时,如果开启了偏向锁,会优先加偏向锁;当锁未释放其他线程来竞争时,会撤销偏向锁变成重量级锁。当锁已释放其他线程来加锁时,会撤销偏向锁变成轻量级锁。
无竞争时,如果没开启偏向锁,会加轻量级锁;当其他线程来竞争时,轻量级锁变成重量级锁
偏向锁撤销情况:
hashcode():如果开启了偏向锁,一开始的hashcode和age不会存在(这些位都用来记录threadID了)。此时如果调用hashcode方法,会撤销偏向锁
其他线程对该对象加锁:如果此时已经解锁则转轻量级锁;如果此时没有解锁存在竞争转重量级锁。
wait/notify方法:只有重量级锁才有,会转重量级锁
很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然 synchronized 里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized 的边界做忙循环,这就是自旋。如果做了多次循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。
synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
(1)volatile 修饰变量
(2)synchronized 修饰修改变量的方法
(3)wait/notify
(4)while 轮询
不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静态方法上的 synchronized 修饰符要求执行方法时要获得对象的锁,如果已经进入A 方法说明对象锁已经被取走,那么试图进入 B 方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁。
(1)synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。
(2)volatile 提供多线程共享变量可见性和禁止指令重排序优化。
(3)CAS 是基于冲突检测的乐观锁(非阻塞)
相对于Synchronized的优点:可中断,支持锁超时,支持公平锁,支持多个条件变量;同时与synchronized一样都支持可重入
主要区别如下:
基本语法:lock()加锁, unlock()解锁
reentrantLock.lock(); //获取锁
try{
//临界区
}finally{
reentrantLock.unlock(); //释放锁
}
(1)可重入:指同一个线程可重复递归调用锁。
(2)可打断:在阻塞队列等待的过程中可以被其他线程调用interrupt()方法打断。
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args){
new Thread(() -> {
try{
//如果没有竞争,此方法会获取Lock对象的锁
//如果有竞争,会进入阻塞队列,可以被其他线程用interrupt方法打断
lock.lockInterruptibly();
}catch(InterruptedException e){
e.printStackTrace();
}
}).start();
}
(3)锁超时:设置线程等待时间。tryLock(等待时间,时间单位)或tryLock()方法
(4)公平性:synchronized会在阻塞队列中随机选取一个,而不是先来先到顺序,因此是不公平锁。
ReentrantLock默认也是不公平锁,但可以设置成公平锁:ReentrantLock lock = new ReentrantLock(true)
(5)条件变量:synchronized中也有条件变量:线程满足条件时继续执行,不满足条件时进入waitList等待
ReentrantLock支持多个条件变量(waitList)
static ReentrantLock lock = new ReentrantLock();
public void static main(String[] args){
Condition condition1 = lock.newCondition(); //创建两个条件变量
Condition condition2 = lock.newCondition();
lock.lock();
condition1.await(); //效果等同于wait()
}
其他线程可以用condition1.signal()或者condition1.signalAll()方法唤醒
对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。 volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。
volatile 常用于多线程环境下的单次操作(单次读或者单次写)。
能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。意思是,如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了。
volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用 volatile 修饰 count 变量,那么 count++ 操作就不是原子性的。
而 AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。
关键字volatile的主要作用是使变量在多个线程间可见,但无法保证原子性,对于多个线程访问同一个实例变量需要加锁进行同步。
虽然volatile只能保证可见性不能保证原子性,但用volatile修饰long和double可以保证其操作原子性。
所以从Oracle Java Spec里面可以看到:
保证有序性:写屏障之前的代码不会被重排到写屏障后,读屏障之后的代码不会重排到屏障之前
底层实现原理是内存屏障, Memory Barrier
对volatile读指令前加入读屏障,对volatile写指令后会加入写屏障。读写屏障保证有序性和可见性
synchronized和volatile内存语义:
1.进入synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变时就不会从线程的工作内存中获取,而是 直接从主内存中获取。
退出synchronized块的内存语义是把在synchronized块内对共享变量修改刷新到主内存
2.volatile的内存语义和synchronized相似之处,具体来说就是,当线程写入了volatile变量值时就等价于线程退出synchronized同步块(把写入工作内存的变量值同步到主内存),读取volatile变量的值时就相当于进入同步块(先清空本地内存变量值,再从主内存获取最新值)
不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(Mutable Objects)。
不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多不可变类,如 String、基本类型的包装类、BigInteger 和 BigDecimal 等。
只有满足如下状态,一个对象才是不可变的;
它的状态不能在创建后再被修改;
所有域都是 final 类型;并且,它被正确创建(创建期间没有发生 this 引用的逸出)。
不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。
Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。
它的优势有:
(1)可以使锁更公平
(2)可以使线程在等待锁的时候响应中断
(3)可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
(4)可以在不同的范围,以不同的顺序获取和释放锁
整体上来说 Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操作。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。
乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。
乐观锁的实现方式:
1、使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。
2、java 中的 Compare and Swap 即 CAS ,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。 CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置 V 的值与预期原值 A 相匹配,那么处理器会自动将该位置值更新为新值 B。否则处理器不做任何操作。
CAS 是 compare and swap 的缩写,即我们所说的比较交换。
cas 是一种基于锁的操作,而且是乐观锁。在 java 中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加 version 来获取数据,性能较悲观锁有很大的提高。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和 A 的值是一样的,那么就将内存里面的值更新成 B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a 线程获取地址里面的值被b 线程修改了,那么 a 线程需要自旋,到下次循环才有可能机会执行。
java.util.concurrent.atomic 包下的类大多是使用 CAS 操作来实现的(AtomicInte
ger,AtomicBoolean,AtomicLong)。
1、ABA 问题:
比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。从 Java1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。
2、循环时间长开销大:
对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。
3、只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。
AQS 介绍
AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。
AQS 原理分析
https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html
1. 成员变量
内部是一个FIFO的双向队列,本质是CLH单项队列的改进
重要的属性包括:state;Node head指向第一个被挂起的线程; Node tail指向随后一个被挂起的线程
AQS是个抽象类(需要被继承实现),中间有ConditionObject和Node两个静态内部类
1.state:表示资源的状态:
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
ReentrantLock:当前线程获得锁的可重入次数(=0表示可获得锁,>0表示锁已被占用,需要判断是否是自身锁)
ReentrantReadWriteLock:state高16位表示读状态(获取读锁的次数),低16位表示获取写锁的线程可重入次数
semaphore:state表示当前可用信号的个数
CountDownlatch:state表示计数器当前的值
state方法:1.getState:获取state状态;2. setState:设置state状态;3. compareAndSetState:CAS设置state状态
//返回同步状态的当前值
protected final int getState() {
return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
2.内部类Node
SHARED:用来标记该线程是获取共享资源时被阻挂起后放入AQS 队列的
EXCLUSIVE:用来标记线程是获取独占资源时被挂起后放入 AQS 队列的
waitStatus:记录当前线程等待状态,可以为 CANCELLED (线程被取消了),SIGNAL(线程需要被唤醒),CONDITION(线程在条件队列里面等待),PROPAGATE(释放共享资源时需要通知其他节点)
prev 记录当前节点的前驱节点,next 记录当前节点的后继节点
3.内部类ConditionObject
每个ConditionObject是一个单项链表队列(条件队列),其用来存放调用条件变量的await 方法后被阻塞的线程
这个条件队列的头、尾元素分别为 firstWaiter 和 lastWaiter。
4. 介绍和使用
本身是一个抽象类,主要通过继承的方式使用,分为独占模式和共享模式:
独占模式:只有一个线程可以访问资源;如ReentrantLock;
独占模式方法有:void acquire( int arg);void acquirelnterruptibly(int arg);boolean release(int arg);
共享模式:多个线程可以访问资源;如ReantrantReadWriteLock
共享模式方法有:void acquireShared(int arg);void acquireSharedinterruptibly(int arg);boolean releaseShared(int arg);
子类需要重写的方法:
1.tryAcquire() / tryAcquireShared():尝试加 独占 / 共享 锁
2. tryRelease() / tryReleaseShared():尝试解锁 独占 / 共享 锁
3. isHeldExclusively:判断锁是被独占还是共享
4.newCondition();new一个ConditionObject对象
下面实现了自定义锁,其中MySync继承了AQS,在加锁时只需在lock方法中调用mysync.acquire()和mysync.release()方法。
这些方法AQS都已经提供无需重写。需要在MySync中重写的是tryAcquire()和tryRelease()方法。
原理如上:acquire()和release()中调用了tryAcquire()和tryRelease()
//自定义锁,不可重入锁
class MyLock implements Lock{
//独占锁
class Mysync entends AbstractQueuedSynchronizer {
@Override //尝试获取锁
protected boolean tryAcquire(int arg){
if(compareAndSetState(0, 1)){
//加锁成功
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override //尝试释放锁
protected boolean tryRelease(int arg){
setExclusiveOwnerThread(null);
setState(0);
return true;
}
@Override //是否持有独占锁,该方法重写AOS,AOS是AQS父类
protected boolean isHeldExclusively(){
return getState() == 1;
}
public Condition newCondition(){
return new ConditionObject();
}
}
private Mysync sync = new Mysync();
@Override //加锁(加锁失败后会进入等待队列等待)
public void lock(){
sync.acquire(1);
}
@Override //加锁,可打断
public void lockInterruptibly() throws InterruptedException{
sync.lockInterruptibly(1);
}
@Override //尝试加锁(加锁失败只会返回false,)
public boolean tryLock(){
sync.tryAcquire(1);
}
@Override //尝试加锁带超时(加锁失败只会返回false)
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException{
sync.tryAcquireNanos(1, unit.toNanos(time))
}
@Override //解锁
public void unlock(){
sync.release(1);
}
@Override //创建条件变量
public Condition newCondition(){
return sync.newCondition();
}
5. AQS底层使用了模板方法模式
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):
这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。
AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
默认情况下,每个方法都抛出 UnsupportedOperationException
。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease
、tryAcquireShared-tryReleaseShared
中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock
。
ReentrantLock内部的Sync锁同步器,继承了AQS,分为公平锁和非公平锁
1.非公平锁实现原理
构造器默认为非公平锁
//NonefairSync继承了AQS
public ReentrantLock(){
sync = new NonfairSync();
}
2.可重入锁原理
判断state==0时:表示锁可以被占有
state!=0时:如果当前线程==占有锁的线程:state+1;锁重入成功
如果当前线程!=占有锁的线程:枷锁失败
要想支持重入性,就要解决两个问题:1. 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;2. 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。
ReentrantLock支持两种锁:公平锁和非公平锁。何谓公平性,是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO。
if(state为0){
CAS加锁成功
return true;
}
else{ //state不为0
if(当前线程=Owner中的线程){
state += 1;锁重入
return true;
}
}
ReadWriteLock 是什么
首先明确一下,不是说 ReentrantLock 不好,只是 ReentrantLock 某些时候有局限。如果使用 ReentrantLock,可能本身是为了防止线程 A 在写数据、线程 B 在读数据造成的数据不一致,但这样,如果线程 C 在读数据、线程 D 也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。因为这个,才诞生了读写锁 ReadWriteLock。
ReadWriteLock 是一个读写锁接口,读写锁是用来提升并发程序性能的锁分离技术,ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。
而读写锁有以下三个重要的特性:
(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
(2)重进入:读锁和写锁都支持线程重进入。
(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。
class DataContainer{
private Object data;
private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock r = rw.readLock();
private ReentrantReadWriteLock.WriteLock w = rw.writeLock();
public Object read(){
r.lock();
try{
return data;
}finally{
r.unlock();
}
}
public Object write(){
w.lock();
try{
//修改data;
return data;
}finally{
w.unlock();
}
}
}
主要作用是挂起和唤醒线程,底层是由Unsafe类实现。
void park() | / |
void unpark(Thread thread | / |
void parkNanos(long nanos) | / |
park(Object blocker) | Thread 类里面有个变量 volatile Object parkBlocker 用来存放 park 方法传递的 block 对象,也就是把blocker 变量存放到了调用 park 方法的线程的成员变量里面。 |
原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。
处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。在 Java 中可以通过锁和循环 CAS 的方式来实现原子操作。 CAS 操作——Compare & Set,或是 Compare & Swap,现在几乎所有的 CPU 指令都支持 CAS 的原子操作。
原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境下避免数据不一致必须的手段。
int++并不是一个原子操作,所以当一个线程读取它的值并加 1 时,另外一个线程有可能会读到之前的值,这就会引发错误。
为了解决这个问题,必须保证增加操作是原子的,在 JDK1.5 之前我们可以使用同步技术来做到这一点。到 JDK1.5,java.util.concurrent.atomic 包提供了 int 和long 类型的原子包装类,它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。
java.util.concurrent 这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由 JVM 从等待队列中选择另一个线程进入,这只是一种逻辑上的理解。
原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
原子数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
原子属性更新器:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
解决 ABA 问题的原子类:AtomicMarkableReference(通过引入一个 boolean来反映中间有没有变过),AtomicStampedReference(通过引入一个 int 来累加来反映中间有没有变过)
AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
底层原理:
//AtomicInteger中有个volatile修饰的value
private volatile int value;
方法:
AtomicInteger j = new AtomicInteger(); AtomicInteger i = new AtomicInteger(0); |
构造器 注:无参构造默认为0 |
i.incrementAndGet(); j.getAndIncrement(); |
//++i, 返回1 //j++, 返回0, 先获取 |
i.decrementAndGet(); j.getAndDecrement(); |
//i--; //--j; |
i.getAndAdd(4); j.addAndGet(-5); |
//先获取,再+4 //先-5,再获取 |
i.get(); | //返回最新值 |
i.updateAndGet(函数); i.getAndUpdate(函数); |
eg.i.updataAndGet( x -> x*10) |
i.compareAndSet(pre, next) | 通常与while一起使用
|
使用方法:
private AtomicReference balance;
public Constructor(BigDecimal balance){
return new AtomicReference<>(balance);
}
然而,AtomicReference存在ABA问题,AtomicStampedReference会根据版本号判断出ABA问题,使用方法如下
//赋初值“A”,并设置版本号为0
static AtomicStampedReference ref = new AtomicStampedReference<>("A",0);
public static void main(String args[]){
String pre = ref.getReference();
int stamp = ref.getStamp;
//下面这行代码不仅判断pre=="A",还会判断 stamp与最新stamp是否相同,相同则更新版本号和“C”
ref.compareAndSet(pre, "C", stamp, stamp +1)
}
有时候,并不关心变量被改了几次,只是关心变量是否被改过,因此可用AtomicMarkableReference。
AtmoicMarkableReference ref = new AtomicMarkableReference<>("A", true);
String pre = ref.getReference();
//判断当前标记是否为true且当前值是否还是pre,如果都满足表示没有被改过,则更变为"C"和false
ref.compareAndSet(pre, "C", true, false);
保护数组中的值,而并非数组中的引用
LongAdder:jdk1.8后专门用来作原子累加的类,性能较之前高
源码:
原理:给cells数组加上Contended注解:给每个cell加128字节的padding,防止缓存行失效。
eg.如果不加Contended:线程1更新cell[0]时,线程2工作缓存中的cell[1]失效(由于每次都是以128字节的缓存行为单位更新),加了该注释可以防止伪共享
LongAdder原理:缓存一致性,防止伪共享
底层native方法,很多JUC下都用到了这个对象:比如AtomicInteger中的getAndIncrement()方法;LockSupport的park()方法。
获取方法:暴力反射
在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在 Java 中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是”池化资源”技术产生的原因。
线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。Java 5+中的 Executor 接口定义一个执行线程的工具。它的子类型即线程池接口是 ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,因此在工具类 Executors 面提供了一些静态工厂方法,生成一些常用的线程池,如下所示:
(1)newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
(2)newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。如果希望在服务器上使用线程池,建议使用 newFixedThreadPool方法来创建线程池,这样能获得更好的性能。
(3) newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
(4)newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
降低资源消耗:重用存在的线程,减少对象创建销毁的开销。
提高响应速度。可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
附加功能:提供定时执行、定期执行、单线程、并发数控制等功能。
综上所述使用线程池框架 Executor 能更好的管理线程、提供系统资源使用率。
类型 | 参数 | 介绍 |
与构造器相关 | private volatile int corePoolSize; //Timeunit unit 非该类下 private volatile ThreadFactory threadFactory; |
见构造器 |
内部成员 | |
状态控制属性:高3位表示线程池的运行状态,剩下的29位表示当前有效的线程数量 |
内部成员 | |
独占锁,控制新增Worker操作的原子性 |
私有内部类 | |
继承AQS,内部实现互斥锁,控制线程的中断状态 |
静态内部类 | |
都实现了RejectedExecutionHandler,提供饱和策略 |
重要方法 | |
见后面 |
ThreadPoolExecutor使用AtomicInteger ctl高3位来表示线程池状态,低29位表示线程池数量
状态名 | 高3位 | 接受新任务 | 处理阻塞队列任务 | 说明 |
RUNNING | 111 | Y | Y | 刚创建线程池默认状态 |
SHUTDOWN | 000 | N | Y | 不会接受新任务,但会处理阻塞队列剩余任务 |
STOP | 001 | N | N | 会中断正在执行的任务,并丢弃阻塞队列中的任务 |
TIDYING | 010 | / | / | 任务全部执行完毕,活动线程为0即进入终结 |
TERMINATED | 011 | / | / | 终结状态 |
从数字上比较TERMINATED(3)>TIDYING(2)>STOP(1)>SHUTDOWN(0)>RUNNING(-1)
为何要把线程池状态(3bits)和数量(29bits)合并在一起:保证原子性,即可以只用1次CAS操作进行赋值
public ThreadPoolExecutor( int corePoolSize, //核心线程数目(最多保留的线程数)
int maxmumPoolSize, //最大线程数
long keepAliveTime, //生存时间-针对救急线程
TimeUnit unit, //时间单位-针对救急线程
BlockingQueue workQueue, //阻塞队列
ThreadFactory threadfactory, //线程工厂-可以为线程取个好名字
RejectedExecutionHandler handler)
(1)线程池刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。
(2)当线程数达到corePoolSize并且没有线程空闲,这时再加入新任务,新任务会进入workQueue进行排队,直到有空闲线程。
(3)如果队列选择了有界队列,那么任务超过了队列大小时候,会创建maximumPoolSize- corePoolSize数目的线程来救急
(4)如果线程到达maximumPoolSize仍然有新任务,这时会进行拒绝策略,jdk提供了4种实现
AbortPolicy:让调用者抛出RejectedExecutionException异常,默认策略
CallerRunsPolicy:让调用者执行任务
DiscardPolicy:放弃本次任务
DiscardOldestPolicy:放弃队列中最早的任务,本任务取代之
(5)当高峰过去后,超过corePoolSize的急救线程如果一段时间没有任务,需要结束节省资源,时间由keepAliveTime和unit控制
execute | void execute(Runnable command); | 执行任务 |
submit | Future |
提交任务,用返回值Future获得任务结果 |
invokeAll | List List |
提交tasks中所有任务 |
invokeAny | T invokeAny(Collection<继承Callable T invokeAny(Collection<继承Callable |
提交tasks中所有任务,哪个先执行完哪个先返回结果,其他任务取消 |
submit示例
ExecutorService pool = Executors.nexFixedThreadPool(2);
//实例1:submit方法中提供Callable类
Future future = pool.submit(new Callable(){
@Override
public String call() throws Exception{
Thread.sleep(1);
return "123456789";
}
});
//实例2:利用lambda函数
Future future1 = pool.submit(()->{
Thread.sleep(1);
return "987654321";
});
//利用future.get()方法获取返回值
String ans = future.get();
String ans1 = future1.get();
invokeAll()示例
ExecutorService pool = Executors.newFixedThreadPool(2);
List> futures = pool.invokeAll(Arrays.asList(
() -> {
Thread.sleep(1);
return "线程1";
},
() -> {
Thread.sleep(1);
return "线程2";
},
() -> {
Thread.sleep(1);
return "线程3";
}
));
for(Future f: futures){
try{
System.out.println(f.get());
}catch(InterruptedException | ExecutionException e){
e.printStackTrace();
}
}
invokeAny()示例:
ExecutorService pool = Executors.newFixedThreadPool(3);
String str = pool.invokeAny(Arrays.asList(
() -> {
Thread.sleep(3);
return "线程1";
},
() -> {
Thread.sleep(2);
return "线程2";
},
() -> {
Thread.sleep(1);
return "线程3";
}
));
void shutdown(); | 线程池状态变为SHUTDOWN 不会接受新任务,但已经提交的任务会执行完 此方法不会阻塞调用线程的执行(即如果主线程调用该方法,主线程会继续执行下去,不会等该方法执行完再继续运行,异步) |
void shutdownNow(); | 线程池状态变为STOP 不会接受新任务,并将队列中的任务返回 用interrupt方法中断正在执行的任务 |
boolean isShutdown(); | 只要不处于RUNNING的线程,都返回true |
boolean isInterrupted() |
线程池状态是否属于TERMINATED |
1.nexFixedThreadPool:固定大小的线程池
public static ExecutorService newFixedThreadPool(int nThreads){
return new ThreadPoolExecutor( nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.executor(()->{
//任务1
});
pool.executor(()->{
//任务2
});
pool.executor(()->{
//任务3
特点:核心线程数=最大线程数,即没有救急线程,因此也无需超时时间
阻塞队列是无界的,可以放任意数量的任务
评价:适用于任务数量已知,相对耗时的任务
2.newCachedThreadPool:带缓冲池的线程池
public static ExecutorService newCachedThreadPool(){
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
特点:
核心线程数是0,最大线程数MAX_VALUE,即所有创建的线程都是救急线程(60s后可以回收),救急线程可以无限创建。
队列采用了SychronousQueue,实现特点是:没有容量,没有线程来取得时候是放不进去的。
评价:
整个线程池表现为线程数会根据任务量不断增长,没有上限。当任务执行完毕,空闲线程1分钟后会释放。
适合任务数较为密集,但每个任务执行时间较短的情况
3.newSingleThreadExecutor单线程线程池
public static ExecutorService newSingleThreadExecutor(){
return new FinalizableDelegatedExecutorService
( new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
特点:希望多个任务排队执行。线程数固定为1,当任务大于1时,会进入无界队列排队。执行完毕后,这唯一的线程也不会释放
区别:
自己创建的串行执行的单线程任务,如果任务失败会终止。而该线程池会继续往下执行。
Executors.newSingleThreadExecutor()的线程个数始终为1,不能修改。
它运用了装饰器模式,返回的是FinalizableDelegatedExecutorService对象不能调用ThreadPoolExecutor中特有的方法。
对比Executors.newFixedThreadPool(1),可以用setCorePoolSize来修改数量。
Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
Executor 接口对象能执行我们的线程任务。
ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。
使用 ThreadPoolExecutor 可以创建自定义线程池。
Future 表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的完成,并可以使用 get()方法获取计算的结果。
接收参数:execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable 和 Callable 类型的任务。
返回值:submit()方法可以返回持有计算结果的 Future 对象,而execute()没有
异常处理:submit()方便Exception处理
《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
Executors 各个方法的弊端:
newFixedThreadPool 和 newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。
newCachedThreadPool 和 newScheduledThreadPool:
主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。
ThreaPoolExecutor创建线程池方式只有一种,就是走它的构造函数,参数自己指定
用来进行线程同步协作,让一个线程等待其他所有线程完成倒计时后再恢复运行(在其之前一般都使用join(),但是join()针对单个不够灵活)
用法:
构造器: CountDownLatch latch = new CountDownLatch(n);
主线程:latch.await()
其他线程:latch.countDown()
需要被恢复的线程执行await(),其他线程执行完后countDown();
public static void main(String[] args) throws InterruptedException {
//创建
CountDownLatch latch = new CountDownLatch(2);
//执行thread0
Thread thread0 = new Thread(()->{
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread() + " thread0 start...");
//会让latch的计数器-1;
latch.countDown();
System.out.println(Thread.currentThread() +" thread0 end...");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//执行thread1
Thread thread1 = new Thread(()->{
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread() + " thread1 start...");
//会让latch的计数器-1;
latch.countDown();
System.out.println(Thread.currentThread() + " thread1 end...");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread0.start();
thread1.start();
System.out.println(Thread.currentThread() +" main thread start...");
//进入waiting一直等待到有2个线程结束后再恢复
latch.await();
System.out.println(Thread.currentThread() + " main thread end...");
结果:主线程执行到 latch.await()后被挂起,直到有2个线程都执行到了latch.countDown()后继续进入RUNNING态
内部结构:
内部和ReentrantLock一样Sync继承AQS,用state记录共享模式的个数(初始值为构造器参数count)。重写了tryAcquireShare()和tryReleaseShared()方法
每次子线程执行完毕调用countDownLatch.countDown()方法后让count-1,count=0后主线程才返回。
void await() | 委托Sync调用AQS的doAcquireShareInterruptibly()方法阻塞当前线程; 实际还是调用自己重写的tryAcquireShared()方法 两种情况会恢复运行态: (1)其他线程调用countDown()方法,使得计数器为0 (2)其他线程调用interrupt(),该线程被打断,抛出InterruptedException异常 |
boolean await(long, TimeUnit) | 委托Sync调用AQS的doAcquireShareNanos(1, unit.toNanos(timeout))带超时的方法阻塞当前线程; 实际还是调用自己重写的tryAcquireShared()方法 三种情况恢复运行态: (1)其他线程调用countDown()方法,使得计数器为0,返回true (2)其他线程调用interrupt(),该线程被打断,抛出InterruptedException异常 (3)时间到,返回false |
void countDown() | 委托Sync调用AQS的releaseShared()方法 实际还是调用自己重写的tryReleaseShared()方法 用for(;;)循环进行CAS操作,更新state值减1; 当state减1等于0后就唤醒被阻塞的线程 |
long getCount() | 获取state的值 |
之前CountDownLatch的计数器是一次性的,用完就结束了,count的值不能重置。
使用:
构造器: CyclicBarrier cyc = new CyclicBarrier(n)或者
CyclicBarrier cyc = new CyclicBarrier(n, new Runnable(()->{...}))
其他线程:cyc.await();
底层用ReentrantLock()独占锁实现,parties记录线程个数恒定不变。一开始count=parties,每次线程调用await()方法后count减1,当count=0后唤醒所有线程,然后重置count=parties
int await() | 调用该方法的线程会阻塞 三种情况恢复运行态: (1)有n个线程都调用了await()方法,所有线程都恢复运行 (2)其他线程调用interrupt()方法 (3)与当前屏障点关联的 Generation对象的 broken 标志被设置为 true 时 |
boolean await(long, TimeUnit) | 四种情况恢复运行态: (1)有n个线程都调用了await()方法,所有线程都恢复运行,这时返回true (2)超时,返回false (2)其他线程调用interrupt()方法 (3)与当前屏障点关联的 Generation对象的 broken 标志被设置为 true 时 |
int dowait(boolean timed, long nanos) |
|
信号量,用来限制能同时访问共享资源的线程上限:即共享资源有多个,也允许多个线程访问,但需要限制数量
内部和ReentrantLock一样,不过state为共享模式
构造方法
//构造1:创建最大上限为n的信号量
Semaphore sem = new Semaphore( n )
//构造2:创建最大上限为n的信号量,且为公平
Semaphore sem = new Semaphore( n ,true)
semaphore.acquire()和semaphore.release();
应用:使用Semaphore限流,让请求线程阻塞,高峰过去后再释放许可。可以用它改进数据库连接池
原理:acquire:和ReentrantLock相同,都是根据state值和AQS实现
ConcurrentHashMap是Java中的一个线程安全且高效的HashMap实现。平时涉及高并发如果要用map结构,那第一时间想到的就是它。相对于hashmap来说,ConcurrentHashMap就是线程安全的map,其中利用了锁分段的思想提高了并发度。
那么它到底是如何实现线程安全的?
JDK 1.6版本关键要素:
segment继承了ReentrantLock充当锁的角色,为每一个segment提供了线程安全的保障;
segment维护了哈希散列表的若干个桶,每个桶由HashEntry构成的链表。
JDK1.8后,ConcurrentHashMap抛弃了原有的Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。
ConcurrentHashMap 把实际 map 划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是 ConcurrentHashMap 类构造函数的一个可选参数,默认值为 16,这样在多线程情况下就能避免争用。
在 JDK8 后,它摒弃了 Segment(锁段)的概念,而是启用了一种全新的方式实现,利用 CAS 算法。同时加入了更多的辅助变量来提高并发度,具体内容还是查看源码吧。
何为同步容器:可以简单地理解为通过 synchronized 来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如 Vector,Hashtable,以及 Collections.synchronizedSet,synchronizedList 等方法返回的容器。可以通过查看 Vector,Hashtable 等这些同步容器的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字 synchronized。
并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在 ConcurrentHashMap 中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问 map,并且执行读操作的线程和写操作的线程也可以并发的访问 map,同时允许一定数量的写操作线程并发地修改 map,所以它可以在并发环境下实现更高的吞吐量。
SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为 map。
ConcurrentHashMap 使用分段锁来保证在多线程下的性能。
ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将hash 表分为 16 个桶,诸如 get,put,remove 等常用操作只锁当前需要用到的桶。
这样,原来只能一个线程进入,现在却能同时有 16 个写线程执行,并发性能的提升是显而易见的。
另外 ConcurrentHashMap 使用了一种不同的迭代方式。在这种迭代方式中,当iterator 被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时 new 新的数据从而不影响原有的数据,iterator 完成后再将头指针替换为新的数据 ,这样 iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。
https://juejin.im/post/6844903798607970311
CopyOnWriteArrayList 是一个并发容器。有很多人称它是线程安全的,我认为这句话不严谨,缺少一个前提条件,那就是非复合场景下操作它是线程安全的。
其原理是: 初始化的时候只有一个容器,很长一段时间,这个容器数据、数量等没有发生变化,多个线程都是读取同一个容器中的数据,所以这样大家读到的数据都是唯一、一致、安全的,如果后来有线程往里面增加了一个数据,这个时候CopyOnWriteArrayList 底层实现添加的原理是先copy出一个容器(可以简称副本),再往副本容器里添加这个新的数据,最后把副本容器的引用地址赋值给之前旧的的容器地址,但是在添加这个数据的期间,其他线程如果要去读取数据,仍然是读取到旧的容器里的数据。
CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException。在CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。
CopyOnWriteArrayList 的使用场景
通过源码分析,我们看出它的优缺点比较明显,所以使用场景也就比较明显。就是合适读多写少的场景。
CopyOnWriteArrayList 的缺点
CopyOnWriteArrayList 的设计思想
ThreadLocal 是一个本地线程副本变量工具类,在每个线程中都创建了一个 ThreadLocalMap 对象,简单说 ThreadLocal 就是一种以空间换时间的做法,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。通过这种方式,避免资源在多线程间共享。
原理:线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。
经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。
创建一个ThreadLocal对象,然后再不同线程中使用get()和set()方法获取该共享变量。
public class Test{
static ThreadLocal tl = new ThreadLocal<>();
public static void main(String[] args){
Thread one = new Thread(new Runnable(){
tl.set("111");
});
Thread two= new Thread(new Runnable(){
tl.set("222");
});
one.start();
two.start();
}
}
(1)每个Thread对象里有一个ThreadLocalMap属性,属性名叫threadLocals
(2)ThreadLocalMap是ThreadLocal类中的静态内部类
(3)ThreadLocalMap中维护了一个Entry[]的数组(优点类似Hashmap的底层结构),其中Entry类又是ThreadLocalMap中自定义的静态内部类,key是ThreadLocal类,value是obj类
(4)ThreadLocal类中的常用方法有:get(), set(), remove()
总结:每个线程(Thread)中都维护了一个ThreadLocalMap的表,实际是一个Entry
set()方法:
1. 先获取当前线程t
2.用ThreadLocal中的getMap方法获取线程t中的ThreadLocalMap对象
3.如果ThreadLocalMap没被初始化过,进入5初始化;如果已经被初始化过,进入4添加该threadlocal
4.调用ThreadLocalMap的set()方法添加(该方法类似hashmap中的添加方法)
5.调用ThreadLocal中的createMap()方法,新建一个ThreadLocalMap并添加该数据和value
get()方法:
1. 先获取当前线程t
2.用ThreadLocal中的getMap方法获取线程t中的ThreadLocalMap对象
3.情况1:map已被初始化且该entry
情况2:map已被初始化且该entry
情况3:map没被初始化过——>初始化,把entry
从上面代码可以看出,只有再第一次调用get()和set()方法时,ThreadLocalMap才会被创建,不使用threadlocal则一直为null
ThreadLocalMap
中使用的 key 为 ThreadLocal
的弱引用,而 value 是强引用。所以,如果 ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap
中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 set()
、get()
、remove()
方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal
方法后 最好手动调用remove()
方法
每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。
这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
JDK7 提供了 7 个阻塞队列。分别是:
ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
Java 5 之前实现同步存取时,可以使用普通的一个集合,然后在使用线程的协作和线程同步可以实现生产者,消费者模式,主要的技术就是用好,wait,notify,notifyAll,sychronized 这些关键字。而在 java 5 之后,可以使用阻塞队列来实现,此方式大大简少了代码量,使得多线程编程更加容易,安全方面也有保障。
BlockingQueue 接口是 Queue 的子接口,它的主要用途并不是作为容器,而是作为线程同步的的工具,因此他具有一个很明显的特性,当生产者线程试图向 BlockingQueue 放入元素时,如果队列已满,则线程被阻塞,当消费者线程试图从中取出一个元素时,如果队列为空,则该线程会被阻塞,正是因为它所具有这个特性,所以在程序中多个线程交替向 BlockingQueue 中放入元素,取出元素,它可以很好的控制线程之间的通信。
阻塞队列使用最经典的场景就是 socket 客户端数据的读取和解析,读取数据的线程不断将数据放入队列,然后解析线程不断从队列取数据解析。