多线程
1、多线程概念
进程和线程
- 进程:程序的一次执行过程,是系统运行的基本单位。当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并具有一定独立功能。
- 线程:线程是进程中的一个执行单元,负责当前进程程序的执行,一个进程至少有一个线程,可以包含多个线程。Java默认两个线程:main、GC
并发和并行
- 并发:CPU一核,多线程操作同一个资源
- 并行:CPU多核,多个线程可以同时执行
程序运行原理
- 分时调度:所有线程轮流使用CPU的使用权
- 抢占式调度:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么CPU会随机选择一个执行,Java使用的为抢占式调度。不提高程序的运行速度,但能够提高程序的运行效率
什么是JUC
JUC是并发工具类
- java.util.concurrent
- java.util.concurrent.atomic
- java.util.concurrent.locks
Thread类
- 构造方法
Thread(); // 分配新的Thread对象
Thread(String name); // 分配新的Thread对象,将name指定为线程名称
Thread(Runnable target); // 分配新的Thread对象,传入Runnable对象
Thread(Runnable target,String name);// 分配新的Thread对象,传入Runnable对象,将name指定为线程名称
- 常用方法
start(); // Java虚拟机调用该线程的run方法,启动线程
run(); // 该线程要执行的操作
sleep(long millis); // 指定毫秒数内当前执行的线程休眠
- 获取线程名称
currentTread(); // 静态方法,返回当前正在执行的线程对象的引用(Thread)
getName(); // 返回该线程的名称(String)
Thread.currentThread() //获取当前线程对象
Thread.currentThread().getName() //获取当前线程对象的名称
创建线程方式
- 将类声明为Thread的子类,该子类重写Thread类的run方法。创建该类的实例,调用start()方法开启线程。
public class Demo1 {
public static void main(String[] args) {
//获取当前线程名称
System.out.println(Thread.currentThread().getName());
MyThread thread = new MyThread();
thread.start();
}
}
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(this.getName() + "开启线程");
}
}
- 声明一个实现Runnable接口的类,实现run方法,然后创建Runnable子类的实例对象,传入某个线程的构造方法中,开启线程。
public class Demo1 {
public static void main(String[] args) {
//获取当前线程名称
System.out.println(Thread.currentThread().getName());
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread() + "启动线程");
}
},"线程1").start();
//使用lambda表达式
new Thread(()->{
System.out.println(Thread.currentThread() + "启动线程")
}).start();
}
}
- 使用实现Runnable接口避免单继承的局限性,将线程分为两部分,一部分线程对象、一部分线程任务,实现线程解耦。
- 通过实现Runnable接口来实现多线程时,要获取当前线程对象只能通过Thread.currentThread()方法,而不能通过this关键字获取;
- 从JAVA8开始,Runnable接口使用了@FunctionlInterface修饰,也就是说Runnable接口是函数式接口,可使用lambda表达式创建对象,使用lambda表达式就可以不像上述代码一样还要创建一个实现Runnable接口的类,然后再创建类的实例。
- Callable和Future接口创建
这种方式创建并启动多线程的步骤如下:
1、创建Callable接口实现类,并实现call()方法,该方法将作为线程执行体,且该方法有返回值,再创建Callable实现类的实例;
2、使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值;
3、使用FutureTask对象作为Thread对象的target创建并启动新线程;
4、调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
public class Demo2 {
public static void main(String[] args) {
FutureTask<Integer> task = new FutureTask<>((Callable<Integer>) ()->{
int i = 0;
for (; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + "线程的i值为" + i);
}
return i;
});
for(int j = 0; j < 50; j++){
System.out.println(Thread.currentThread().getName() + "循环变量j的值" + j);
if(j == 20){
new Thread(task, "有返回值的线程").start();
}
}
try {
System.out.println("子线程的返回值" + task.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
线程六个状态
线程基本状态:
NEW:初始状态,线程被创建,但还没有调用start()方法
RUNNABLE:运行状态,在Java线程中将操作系统中的就绪和运行两种状态称为"运行中"
BLOCKED:阻塞状态,表示线程阻塞于锁
WAITING:等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)
TIMED_WAITING:超时等待状态
TERMINATED:终止状态,表示当前线程已经执行完毕。
public enum State {
// 初始状态
NEW,
// 运行
RUNNABLE,
// 阻塞
BLOCKED,
// 等待,无限期等待
WAITING,
// 超时等待
TIMED_WAITING,
// 终止
TERMINATED;
}
wait和sleep区别
- wait来自Object类,sleep来自Thread类
- wait会释放锁,sleep不会
- 使用范围不同,wait必须在同步代码块中,而sleep可以在程序的任何位置
2、锁
Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。
几个概念:[参考](https://www.cnblogs.com/1013wang/p/11691466.html)
原子性:在一个操作中要么指向完成,要么不执行
可见性:当一个线程修改了线程共享变量的值,其他线程能够立即得知修改
有序性:禁止指令重排序
2.1 synchronized
在Java中synchronized关键字用于维护数据的一致性
。具有原子性和可见性。
synchronized机制是给共享资源上锁,只有拿到锁的线程才能访问共享资源,这样就可以强制对共享资源的访问都是顺序的。
可以修饰需要同步的类、方法、代码块。
synchronized (obj) {
//方法
……
}
注意:当线程通过synchronized等待锁时是不能被Thread.interrupt()中断的。
买票实例
public class saleTicket {
public static void main(String[] args) {
// 并发:多线程操作同一个资源类
Ticket ticket = new Ticket();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
ticket.sale();
}
}
},"a").start();
// Rannable为@FunctionalInterface 函数式接口,可以使用lambda表达式
new Thread(()->{
for (int i = 0; i < 10; i++) {
ticket.sale();
}
},"b").start();
new Thread(()->{
for (int i = 0; i < 10; i++)
ticket.sale();
},"c").start();
}
}
class Ticket{
private int num = 30;
// synchronized 本质: 队列,锁
public synchronized void sale(){
if(num > 0){
System.out.println(Thread.currentThread().getName() + "卖出了" +(num--) + "票,剩余" + num);
}
}
}
2.2 Lock接口
所有已知实现类:
ReentrantLock , ReentrantReadWriteLock.ReadLock , ReentrantReadWriteLock.WriteLock
2.2.1 ReentrantLock
可重入锁, 这个锁可以被线程多次重复进入进行获取操作。
ReentranLock源码
// 非公平锁(默认)
public ReentrantLock() {
this.sync = new ReentrantLock.NonfairSync();
}
// 传入一个boolean值,true公平锁、false非公平锁
public ReentrantLock(boolean fair) {
this.sync = (ReentrantLock.Sync)(fair ? new ReentrantLock.FairSync() : new ReentrantLock.NonfairSync());
}
公平锁和非公平锁
- 公平锁:锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁
- 非公平锁:锁的分配机制是不公平的,JVM按随机、就近原则分配锁的机制,默认使用非公平锁
使用方式
ReentrantLock必须在finally控制块中进行解锁操作,保证锁能够解锁。
Lock lock = new ReentrantLock();
try{
lock.lock();
// 任务操作
} finally {
lock.unlock();
}
sychronized和Lock区别
1、Synchronized 内置的Java关键字, Lock 是一个Java类
2、Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁
3、Synchronized 会自动释放锁,lock 必须要手动释放锁!如果不释放锁,会导致死锁
4、Synchronized 线程获得锁,会阻塞;Lock锁就不一定会等待下去;
5、Synchronized 可重入锁,不可以中断的,非公平;Lock ,可重入锁,可以 判断锁,默认使用非公平锁;
6、Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码!
2.2.2 读写锁(ReadWriteLock)
已知实现类ReentrantReadWriteLock
有一对关联Locks,一个用于只读操作,一个用于写入操作。读可以被多线程同时读,写只能一个线程写。
- ReentrantReadWriteLock.readLock
- ReentrantReadWriteLock.writeLock
独占锁(写锁):一次只能被一个线程占有
共享锁(读锁):多个线程可以同时占有
2.3 Volatile
Java虚拟机提供轻量级的同步机制。
保证可见性、禁止指令重排、不保证原子性
指令重排
源代码–>编译器优化的重排–> 指令并行也可能会重排–> 内存系统也会重排—> 执行
单例模式
双重检测模式的懒汉式单例
注意:可以使用反射,破坏单例。
public class LazySingleton{
private volatile static LazySingleton lazySingleton;
private LazySingleton(){
}
public static LazySingleton getInstance(){
if(lazySingleton == null){
synchronized(LazySingleton.class){
if(lazySingleton == null){
lazySingleton = new LazyLazySingleton();
}
}
}
return lazySingleton;
}
}
使用枚举,不会被反射破坏。
public EnumSingle{
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}
2.4 自旋锁
互斥同步进入阻塞状态(线程上下文切换)的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。
自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。
在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。
2.5 CAS 比较并交换
乐观锁需要操作和冲突检测这两个步骤具备原子性,硬件支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)。CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。
2.6 死锁
排查问题
1、使用jps -l
定位进程号
2、使用jstack 进程号
找到死锁问题
2.6 线程8锁,判断锁的是谁
- synchronized修饰的方法,锁的对象是方法的调用者(对象实例)
- synchronized和static修饰的方法,锁的对象是类的class对象
3、常用辅助类
CountDownLatch
用来控制一个或者多个线程等待多个线程。
维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒。
CyclicBarrier
用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。
和 CountdownLatch 相似,都是通过维护计数器来实现的。线程执行 await() 方法之后计数器会减 1,并进行等待,直到计数器为 0,所有调用 await() 方法而在等待的线程才能继续执行。
CyclicBarrier 和 CountdownLatch 的一个区别是,CyclicBarrier 的计数器通过调用 reset() 方法可以循环使用,所以它才叫做循环屏障。
CyclicBarrier 有两个构造函数,其中 parties 指示计数器的初始值,barrierAction 在所有线程都到达屏障的时候会执行一次。
Semaphore
Semaphore 类似于操作系统中的信号量,可以控制对互斥资源的访问线程数。
4、阻塞队列
Interface BlockingQueue接口有以下阻塞队列的实现:
- FIFO 队列 :LinkedBlockingQueue、ArrayBlockingQueue(固定长度)
- 优先级队列 :PriorityBlockingQueue
方式 | 抛出异常 | 有返回值,不抛出异常 | 阻塞等待 | 超时等待 |
---|---|---|---|---|
添加 | add() | offer() | put() | offer(,) |
移除 | remove() | poll() | take() | poll(,) |
检测队首元素 | element() | peek | – | – |
5、ForkJoin
主要用于并行计算中,和 MapReduce 原理类似,都是把大的计算任务拆分成多个小任务并行计算。
ForkJoinPool 实现了工作窃取算法来提高 CPU 的利用率。每个线程都维护了一个双端队列,用来存储需要执行的任务。工作窃取算法允许空闲的线程从其它线程的双端队列中窃取一个任务来执行。窃取的任务必须是最晚的任务,避免和队列所属线程发生竞争。
6、线程池
(1)降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
(2)提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
(3)方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
(4)提供更强大的功能,延时定时线程池。
池化技术
池化技术能够减少资源对象的创建次数,提高程序的性能,特别是在高并发下这种提高更加明显。使用池化技术缓存的资源对象有如下共同特点:
-
对象创建时间长
-
对象创建需要大量资源
-
对象创建后可被重复使用
三大方法
使用Executors工具类创建
// Executors 工具类、3大方法
ExecutorService threadPool = Executors.newSingleThreadExecutor();// 创建一个单线程的线程池,适用于需要报顺序执行的各个任务。
ExecutorService threadPool = Executors.newFixedThreadPool(5); // 创建一个固定大小的线程池。
ExecutorService threadPool = Executors.newCachedThreadPool(); // 用于创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。
// 使用了线程池之后,使用线程池来创建线程
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+" ok");
});
线程池不允许使用工具类Executors(底层也是后面的类)创建,而是通过ThreadPoolExcutor的方式创建线程池,避免资源耗尽的风险。
注意:使用Executors创建返回的线程池对象的弊端:
-
FixedThreadPool和SingleThreadPool: 允许请求队列长度为Integer.MAX_VALUE,可能会堆积大量请求,导致OOM
-
CacheThreadPool和ScheduledThreadPool:允许创建线程数量为Integer.MAX_VALUE,可能创建大量的线程,导致OOM
ThreadPoolExecutor源码
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handle) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null : AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
7大参数
int corePoolSize, // 核心线程池大小
int maximumPoolSize, // 最大核心线程池大小
long keepAliveTime, // 线程存活保持时间
TimeUnit unit, // 超时单位
BlockingQueue<Runnable> workQueue, // 阻塞队列
ThreadFactory threadFactory, // 线程工厂:用于创建新线程
RejectedExecutionHandler handle // 拒绝策略,当线程池和队列满了,再加入线程会执行此策略
4种拒绝策略
当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize时,如果还有任务到来就会采取任务拒绝策略
new ThreadPoolExecutor.AbortPolicy() // 丢弃任务,抛出RejectedExecutionException异常(默认)
new ThreadPoolExecutor.CallerRunsPolicy() // 由调用线程处理该任务
new ThreadPoolExecutor.DiscardPolicy() //丢弃任务,但是不抛出异常
new ThreadPoolExecutor.DiscardOldestPolicy() //丢弃队列最前面的任务,然后重新提交被拒绝的任务
线程池优化
-
池最大大小设置
-
IO密集型、CPU密集型
- IO密集型:可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。
- CPU密集型:尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
7、JMM,Java内存模型
Java 内存模型试图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。
7.1 工作内存和主内存
所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。
线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成。
7.2 内存间交互操作
Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作。
- read:把一个变量的值从主内存传输到工作内存中
- load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中
- use:把工作内存中一个变量的值传递给执行引擎
- assign:把一个从执行引擎接收到的值赋给工作内存的变量
- store:把工作内存的一个变量的值传送到主内存中
- write:在 store 之后执行,把 store 得到的值放入主内存的变量中
- lock:作用于主内存的变量
- unlock:释放锁
7.3 内存模型的三大特性
原子性、可见性、有序性
8、深入理解CAS
CAS(compareAndSet) : 比较和交换,它将内存位置的内容与期望值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值
//新建一个原子类,设置期望值为2020
AtomicInteger atomicInteger = new AtomicInteger(2020);
//设置值,如果期望值,则更新值
atomicInteger.compareAndSet(2020, 2021);
自旋锁
比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就一直循环
CAS中的ABA问题
如线程1从内存X中取出A,这时候另一个线程2也从内存X中取出A,并且线程2进行了一些操作将内存X中的值变成了B,然后线程2又将内存X中的数据变成A,这时候线程1进行CAS操作发现内存X中仍然是A,然后线程1操作成功。虽然线程1的CAS操作成功,但是整个过程就是有问题的。比如链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。
所以JAVA中提供了AtomicStampedReference/AtomicMarkableReference
来处理会发生ABA问题的场景,主要是在对象中额外再增加一个标记来标识对象是否有过变更。
9、生产者和消费者
消费者生产者实例
public class ConsumerDemo {
public static void main(String[] args) {
Producer producer = new Producer();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
producer.increment();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"A").start();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
producer.decrement();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"B").start();
}
}
class Producer{
private int num = 0;
public synchronized void increment() throws InterruptedException {
if(num != 0){
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName()+"->"+num);
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
if (num == 0){
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName()+"->"+num);
this.notifyAll();
}
}
线程也可以唤醒,而不会被通知,中断或超时,导致虚假唤醒。可以使用while()替换if判断来解决问题。