目录
进程和线程的区别,进程间是如何通信的
什么是线程上下文切换
什么是死锁
死锁的必要条件
Synchronized和lock的区别
什么是AQS锁
为什么AQS使用的是一个双向链表
有哪些常见的AQS锁
sleep()和wait()的区别
yield()和join的区别
线程池的七大参数
Java内存模型(JMM)
保证并发安全的三大特性?
volatile关键字
单例模式双重校验锁变量为什么使用 volatile 修饰?
线程使用方式
ThreadLocal原理
什么是CAS锁
Synchronized锁的原理和优化
如何根据CPU核心数来设计线程池线程数量
cpu密集型(涉及到计算操作时)
IO密集型(涉及到一些读取文件等操作)
AutomaticInteger的使用场景
进程:系统运行的基本单位,进程在运行过程中是相互独立的,但是线程之间运行可以互相影响
线程:独立运行的最小单位,一个进程包含多个线程并且它们共享同一进程内的系统资源
进程之间通过管道、共享内存、信号量机制、消息队列来进行通信
当一个线程被剥夺CPU的使用权时,切换到另外一个线程执行
死锁指的是多个线程在执行过程中,因资源竞争造成的相互等待的情况
死锁需要满足以下四个条件
想要预防死锁,只需要破坏其中一个条件即可,比如使用定时锁、尽量让线程用相同的加锁顺序,还可以用银行家算法可以预防死锁
(1)synchronized是一个关键字,lock是一个类
(2)synchronized在发生异常的时候可以自动释放掉锁,lock则需要手动释放锁
(3)synchronized是可重入锁、非公平锁、不可中断锁
lock的ReentrantLock是可重入锁、可中断锁,(可以是公平锁,也可以设定为不公平锁)
(4)synchronized是JVM通过监视器实现的,Lock是通过AQS实现的
AQS(AbstractQueuedSynchronizer)是Java并发编程中的一个抽象类,位于java.util.concurrent.locks包中。AQS提供了一种实现锁和同步器的框架,可用于构建各种类型的同步工具,如ReentrantLock、CountDownLatch、Semaphore等。
AQS内部通过一个FIFO(先进先出)的队列来管理等待获取同步状态的线程,并提供了一组基本方法来支持子类实现对共享资源的安全访问控制。AQS的核心思想是使用一个整型变量(state)表示共享资源的状态,并通过CAS(Compare and Swap)操作来保证对该变量的原子性操作。
AQS主要提供了以下几个关键方法供子类实现:
acquire(int arg):尝试获取同步状态,如果获取成功则直接返回,否则将当前线程加入等待队列中。
release(int arg):释放同步状态,如果释放后唤醒了等待队列中的其他线程,则通知它们重新尝试获取同步状态。
tryAcquire(int arg):尝试独占方式获取同步状态,成功返回true,失败返回false。
tryRelease(int arg):尝试独占方式释放同步状态,成功返回true,失败返回false。
因为有一些线程可能会出现中断的情况,出现这种情况之后就需要从同步阻塞队列中删除掉,这个时候使用双向链表方便删除中间的节点
AQS分为独占锁和共享锁
ReentrantLock(独占锁):可重入(一个线程在持有锁的情况下,可以再次获取同一个锁而不会发生死锁或其他异常情况——就是可以在一个实现功能的递归函数中重复获取同一个锁),可中断,可以是公平锁也可以是非公平锁,非公平锁就是会通过两次CAS去抢占锁,公平锁会按队列顺序排队
Semaphore(信号量):设定一个信号量,当调用acquire()时判断是否还有信号,有就获取一个信号量,没有就阻塞等待其他线程释放信号量,当调用release()时释放一个信号量,唤醒阻塞线程。
应用场景:允许多个线程访问某个临界资源时,如上下车,买卖票
CountDownLatch(倒计数器):给计数器设置一个初始值,当调用CountDown()时计数器减一,当调用await() 时判断计数器是否归0,不为0就阻塞,直到计数器为0。(等待多个子线程完成某个任务,然后再继续执行主线程的下一步操作)
应用场景:启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行
CyclicBarrier(循环栅栏):给计数器设置一个目标值,当调用await() 时会计数+1并判断计数器是否达到目标值,未达到就阻塞,直到计数器达到目标值(一组线程互相等待,直到所有线程都达到某个公共屏障点,然后继续执行下一步操作。)
(1)wait()是Object的方法,sleep()是Thread类的方法
(2)wait()会释放掉锁,sleep()不会释放掉锁
(3)wait()要在同步方法或同步代码块中执行,sleep()方法没有限制
(4)wait()要用notify()或notifyAll()唤醒,sleep()自动唤醒
yield()调用之后线程进入就绪状态
A线程中调用B线程的join(),则B执行完之前A进入阻塞状态
corePoolSize
(核心线程数):线程池中最小的线程数量。即使没有任务需要执行,核心线程也会一直存在。核心线程数通常是线程池能够同时执行的最大任务数。
maximumPoolSize
(最大线程数):线程池中允许存在的最大线程数量。当任务数量超过核心线程数,并且工作队列已满时,线程池可以创建新的线程,但不会超过最大线程数。
keepAliveTime
(线程空闲时间):当线程池中的线程数量超过核心线程数时,多余的空闲线程在被终止之前等待新任务的最长时间。超过这个时间后,多余的线程将被终止,从而减少资源消耗。
unit
(时间单位):用于指定keepAliveTime
的时间单位,例如毫秒、秒、分钟等。
workQueue
(工作队列):用于存储提交的任务的队列。当线程池中的线程都在执行任务时,新任务将被添加到工作队列中等待执行。
threadFactory
(线程工厂):用于创建新线程的工厂。可以自定义线程的名称、优先级、是否守护线程等属性。
handler
(拒绝策略):当线程池已经达到最大线程数并且工作队列已满时,新任务无法加入线程池的处理方式。常见的拒绝策略有直接抛出异常、丢弃最早的任务、丢弃最新的任务以及在调用者线程中执行任务。
Java内存模型屏蔽了各种硬件和操作系统的内存访问差异,实现java程序在各平台之下都能达到一致的内存访问效果,它定义了JVM如何将程序中的变量到主存中读取
具体定义为:所有变量都存在主存中,主存是线程共享区域;每个线程都有自己独有的工作内存,线程想要操作变量必须从主从中copy变量到自己的工作区,每个线程的工作内存是相互隔离的
由于主存与工作内存之间有读写延迟,且读写不是原子性操作,所以会有线程安全问题
原子性:一次或多次操作在执行期间不被其他线程影响
可见性:当一个线程在工作内存修改了变量,其他线程能立刻知道
有序性:JVM对指令的优化会让指令执行顺序改变,有序性是禁止指令重排
保证变量的可见性和有序性,不保证原子性。使用了 volatile 修饰变量后,在变量修改后会立即同步到主存中,每次用这个变量前会从主存刷新。
当使用双重校验锁(double-checked locking)实现单例模式时,我们需要使用volatile
修饰变量。这是因为在多线程环境下,如果不使用volatile
修饰变量,可能会导致一个未完全初始化的实例对象被返回。
具体来说,当一个线程第一次访问单例对象时,如果实例对象还没有被初始化,那么该线程会进入同步块,然后再次检查实例对象是否已经被初始化。但是由于JVM存在指令重排序的优化机制,可能会出现以下情况:
这样,线程B拿到的实例对象是未完全初始化的,可能会导致错误的结果。
通过使用volatile
修饰变量,可以禁止指令重排序,保证在第2步之前,实例对象已经完全初始化。这样,在线程B访问单例对象时,能够获取到一个正确初始化的实例对象。
(1)继承Thread类
public class MyThread extends Thread {
@Override
public void run() {
// 线程执行的代码逻辑
}
}
// 创建并启动线程
MyThread thread = new MyThread();
thread.start();
(2)实现Runnable接口
public class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行的代码逻辑
}
}
// 创建并启动线程
Thread thread = new Thread(new MyRunnable());
thread.start();
(3)实现 Callable 接口(带有返回值):
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class MyCallable implements Callable {
@Override
public String call() throws Exception {
// 线程执行的代码逻辑,可以返回一个结果
return "Hello, World!";
}
}
// 创建并启动线程
FutureTask task = new FutureTask<>(new MyCallable());
Thread thread = new Thread(task);
thread.start();
// 获取线程的返回结果
String result = task.get();
(4)使用线程池创建线程:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行的代码逻辑
}
}
// 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交任务给线程池执行
executor.execute(new MyRunnable());
// 关闭线程池
executor.shutdown();
原理是为每个线程创建变量副本,不同线程之间不可见,保证线程安全。每个线程内部都维护了一个Map,key为ThreadLocal的实例,value为要保存的副本。
但是使用ThreadLocal会存在内存泄露的问题,因为可以为弱引用,value为强引用,每次gc的时候key都会回收,而value不会被回收。所以为了解决内存泄露的问题,可以每次使用完后删除value或者使用static修饰ThreadLocal,可以随时获取value
CAS锁是一种乐观锁机制,一个线程在修改变量的时候,需要去判断当前值是否与预期值是一致的,如果与期望值一致则修改成功,不一致则证明其他线程做出了修改,则当前线程的修改失败。
CAS操作包括三个参数:内存地址(变量)、期望值和新值。CAS指令执行时,将内存地址的值与期望值进行比较,如果相等,则使用新值更新内存地址的值;如果不相等,则操作失败,不做任何修改。CAS操作是以原子方式完成的,即在执行过程中不会被其他线程中断。
CAS锁可以保证原子性,思想是更新内存时会判断内存值是否被别人修改过,如果没有就直接更新。如果被修改,就重新获取值,直到更新完成为止。这样的缺点是
(1)只能支持一个变量的原子操作,不能保证整个代码块的原子操作
(2)CAS频繁失败导致CPU开销大
(3)ABA问题:线程1和线程2同时去修改一个变量,将值从A改为B,但线程1突然阻塞,此时线程2将A改为B,然后线程3又将B改成A,此时线程1将A又改为B,这个过程线程2是不知道的,这就是ABA问题,可以通过版本号或时间戳解决
Synchronized是通过对象头的markword来标明监视器的,监视器本质是依赖操作系统的互斥锁实现的。操作系统实现线程切换要从用户态切换为核心态,性能开销成本比较高,这种锁也就叫做重量级锁。
在JDK1.6之后也引入了偏向锁、轻量级锁来对其优化
偏向锁:当一段代码没有别的线程访问,此时线程去访问会直接获取偏向锁
轻量级锁:当锁是偏向锁时,有另外一个线程来访问,会升级为轻量级锁。线程会通过CAS方式获取锁,不会阻塞,提高性能,
重量级锁:轻量级锁自旋一段时间后线程还没有获取到锁,会升级为重量级锁,重量级锁时,来竞争锁的所有线程都会阻塞,性能降低
注意,锁只能升级不能降级
IO密集型是指在处理任务时,IO过程所占用的时间较多,在这种情况下,线程数的计算方法可以分为两种:
AutomaticInteger是一个提供原子操作的Integer类,使用CAS+volatile实现线程安全的数值操作
因为volatile禁止了jvm的排序优化,所以它不适合在并发量小的时候使用,只适合在一些高并发的场景中使用。