Java多线程
线程池
线程池原理
创建方式:newFixedThreadPool (固定数目线程的线程池)、newCachedThreadPool(可缓存线程的线程池)、newSingleThreadExecutor(单线程的线程池)、newScheduledThreadPool(定时及周期执行的线程池)、new ThreadPoolExecutor() (自定义的方式创建)
## 线程池七大参数
- corePoolSize(核心线程数)
线程池当中线程数最基本上的数量:只有当工作任务队列满了才会有新的线程被创建出来,此时线程数才会大于该值
- maximumPoolSize(最大线程数)
线程池中允许的最大线程数:当前任务队列满了并且小于该值的时候线程才会被创建,否则交给拒绝策略
- 最大线程的存活时间:如果当前线程空闲且线程数量大于核心数则线程销毁的超时时间
- unit 时间单位
- 阻塞队列:当核心线程满后,后面来的任务都进入阻塞队列
- 线程工厂:用于生产线程
- 任务拒绝策略:阻塞队列满后,拒绝任务,有四种策略(1)抛异常(2)丢弃任务不抛异常(3)打回任务(4)尝试与最老的线程竞争
## 线程池的好处
1⃣️降低资源消耗
2⃣️提高响应速度
3⃣️使线程便于管理
线程提交后的执行过程
a.如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务!
b.如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。
c.如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这个任务;
d.如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者"我不能再接受任务了"
五种阻塞队列
- ArrayBlockingQueue(有界队列)是一个用数组实现的有界阻塞队列,按FIFO排序量。
- LinkedBlockingQueue(可设置容量队列)基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQuene;newFixedThreadPool线程池使用了这个队列
- DelayQueue(延迟队列)是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool线程池使用了这个队列。
- PriorityBlockingQueue(优先级队列)是具有优先级的无界阻塞队列;
- SynchronousQueue(同步队列)一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene,newCachedThreadPool线程池使用了这个队列。
线程创建方式
- (1)继承 Tread 类(拥有run方法和start方法)
- (2)实现 Runnable 接口(只有一个run方法)
- (3)实现 Callable 接口:拥有一个call方法,带有返回值(在Runnable中,我们无法对run()方法抛出的异常进行任何处理,因为其没有返回值,但在Callable中,自定义的call()方法可以抛出一个checked Exception,并由其执行者Handler进行捕获并处理。)
run和start方法的区别?
1. 定义位置不同
run()方法是Thread类中的一个普通方法,它是线程中实际运行的代码,线程的代码逻辑主要就是在run()方法中实现的。
start()方法是Thread类中的一个启动方法,它会启动一个新的线程,并在新的线程中调用run()方法。
2. 执行方式不同
直接调用run()方法,会像普通方法一样在当前线程中顺序执行run()方法的内容,这并不会启动一个新的线程。
调用start()方法会创建一个新的线程,并在新的线程中并行执行run()方法的内容。
3. 线程状态不同
当我们调用start()方法启动一个新线程时,该线程会进入就绪状态,等待JVM调度它和其他线程的执行顺序。而当我们直接调用run()方法时,则会在当前线程中执行,不会产生新的线程。
线程池的五种状态
1. RUNNING:线程池一旦被创建,就处于 RUNNING 状态,任务数为 0,能够接收新任务,对已排队的任务进行处理。
2. SHUTDOWN:不接收新任务,但能处理已排队的任务。调用线程池的 shutdown() 方法,线程池由 RUNNING 转变为 SHUTDOWN 状态。
3. STOP:不接收新任务,不处理已排队的任务,并且会中断正在处理的任务。调用线程池的 shutdownNow() 方法,线程池由(RUNNING 或 SHUTDOWN ) 转变为 STOP 状态。
4. TIDYING:
SHUTDOWN 状态下,任务数为 0, 其他所有任务已终止,线程池会变为 TIDYING 状态,会执行 terminated() 方法。线程池中的 terminated() 方法是空实现,可以重写该方法进行相应的处理。
线程池在 SHUTDOWN 状态,任务队列为空且执行中任务为空,线程池就会由 SHUTDOWN 转变为 TIDYING 状态。
线程池在 STOP 状态,线程池中执行中任务为空时,就会由 STOP 转变为 TIDYING 状态。
5. TERMINATED:线程池彻底终止。线程池在 TIDYING 状态执行完 terminated() 方法就会由 TIDYING 转变为 TERMINATED 状态。
线程池execute和submit方法的区别?
- 方法返回不同: execute(Runnable command) 接受一个Runnable类型的参数,而 submit(Runnable task) 接受一个Runnable或者Callable类型的参数,因此 submit() 方法可以返回一个结果,如果任务在执行过程中抛出异常, execute()方法不会显示抛出异常,而是将其捕获并记录,而 submit()方法则会将异常包装在Future对象中返回。
- 阻塞行为不同:execute()方法一旦提交任务就立即返回,无法阻塞;而 submit()方法可以选择传递一个超时时间作为参数,如果在指定时间内任务没有完成,则取消任务并抛出异常。
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建线程工厂
ThreadFactory threadFactory = Executors.defaultThreadFactory();
// 创建拒绝策略
RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.AbortPolicy();
// 创建线程池,设置参数
int corePoolSize = 5;
int maxPoolSize = 10;
long keepAliveTime = 60; // 线程空闲时间
TimeUnit unit = TimeUnit.SECONDS; // 时间单位
int queueCapacity = 100; // 任务队列大小
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maxPoolSize,
keepAliveTime,
unit,
new LinkedBlockingQueue<>(queueCapacity),
threadFactory,
rejectedExecutionHandler
);
// 提交任务给线程池
for (int i = 0; i < 10; i++) {
final int taskId = i; // 任务ID(仅用于示例)
executor.execute(new Runnable() {
public void run() {
System.out.println("Task " + taskId + " is executing by " +
Thread.currentThread().getName());
// 执行任务的具体逻辑
// ...
}
});
}
// 关闭线程池
executor.shutdown();
}
}
守护线程
守护线程(daemon thread)是在计算机程序中运行的一种特殊线程。它的主要特点是当所有非守护线程结束时,守护线程会自动退出,而不会等待任务的完成。
守护线程通常被用于执行一些后台任务,如垃圾回收、日志记录等。它们在程序运行过程中默默地执行任务,不会阻塞主线程或其他非守护线程的执行。
与普通线程不同,守护线程的生命周期并不影响整个程序的生命周期。当所有非守护线程结束时,守护线程会被强制退出,无论它的任务是否完成。
需要注意的是,守护线程不能用于执行一些重要的任务,因为它们可能随时被强制退出。此外,守护线程也无法捕获或处理异常。
thread1.setDaemon(true); //设置守护线程
线程结束
结束线程有以下三种方法: (1)设置退出标志,使线程正常退出。 (2)使用interrupt()方法中断线程。 (3)使用stop方法强行终止线程(不推荐使用Thread.stop, 这种终止线程运行的方法已经被废弃,使用它们是极端不安全的!)
线程优先级
线程分为1-10级,其中10最高,默认值为5,优先级的高低不代表线程优先执行,JVM不一定采纳,需要看CPU的情况,一般情况下优先级高的先执行。
多线程并发的3个特性
- 原子性 :即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要 么就都不执行(使用synchronized或者Lock悲观锁,、AtomicXXX原子操作)
- 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即 看得到修改的值 (volitale、synchronized)
- 有序性:程序执行的顺序按照代码的先后顺序执行,(单线程时,为了提高执行效率,程序在编译或者运行的时候会对代码进行指令重排,保证最终一致性,指令重排并不会产生问题,但是在多线程的情况下便可能产生不希望看到的结果。使用volatile修饰的内存,不可以重排序,对volatile修饰变量的读写访问,都不可以换顺序)
ThreadLocal关键字
- 主要用于线程本地化存储,即只有本线程才能对该变量进行查看或者修改
- 使用时需要注意,在不使用该变量的时候,一定要调用
remove
方法删除变量,否则可能会造成内存泄露的问题 - ThreadLocal 是由 ThreadLocalMap 实现
- k 为弱引用,在没有强引用的情况下,经可达性分析算法发现当前对象不可达,经一次 GC 之后 k 就为 null
## Thread、ThreadLocal、ThreadLocalMap的关系
Thread 与 ThreadLocalMap 是 has a 的关系。初始时,Thread 中的 threadLocals 为空,只有在当前线程中创建了 ThreadLocal 变量并且设置了变量值,才会创建 ThreadLocalMap 实例。
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal 类中定义了静态内部类 ThreadLocalMap,把它当作伪 Map 即可,就是存储key-value 键值对
ThreadLocalMap 类中又定义了 Entry 静态内部类,该类定义了一个 Entry 类型的数组 table。为什么要自定义一个 Entry 呢,因为现有不满足要定制,Entry 的 k 为 ThreaLocal 实例,v 为变量值
使用注意:
volatile关键字
- 可见性:使用volatile关键字会强制将修改的值立即写入主存;
原理:
线程写Volatile变量的过程:
- 改变线程本地内存中Volatile变量副本的值;
- 将改变后的副本的值从本地内存刷新到主内存
线程读Volatile变量的过程:
- 从主内存中读取Volatile变量的最新值到线程的本地内存中
- 从本地内存中读取Volatile变量的副本
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;
- 确保有序性:利用volatile的变量保证线程的执行顺序
volatile boolean inited = false;
//线程1:
context = loadContext();
inited = true;
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
## 问题:volatile 能够保证线程安全问题吗?为什么?
不能,volatile 只能保证可见性和顺序性,不能保证原子性。
线程状态
Java线程具有以下几种基本状态:
- NEW(新建):线程刚刚被创建,但尚未调用
start()
方法以开始其执行。此时,线程已分配内存空间并已完成初始化,但它还没有开始执行代码。 - RUNNABLE(可运行):线程已被调用
start()
方法,正处于就绪状态,它已经准备好运行。 - BLOCKED(阻塞):线程因等待某个锁、被其他线程调用
wait()
方法、等待 I/O 操作完成等原因而暂时无法继续执行。 - TIMED_WAITING(计时等待):与普通等待状态相似,但在计时等待状态下,如果等待时间超过设定的超时时间,线程将自动转换为就绪状态。这通常发生在使用了
wait()
方法且设置了超时参数的情况下。(无限等待状态(植物人状态):线程调用了wait()方法并且没有传参数 → 自己醒不过来,只能调用notify()方法来唤醒无限等待状态) - TERMINATED(终止):线程完成了所有预定的任务并且已经结束了。这种情况下,线程不再存在于系统中,不能被再次激活。(run()方法执行结束,或者调用了stop(),或者发生了异常,线程就死亡了.)
此外,还有两种特殊状态:
- WAITING(等待):线程正在等待其他线程的通知以便它可以继续执行。这可能是因为等待
notifyAll()
或join()
方法的执行结果。 - DEAD(死亡):线程的
run()
方法已经执行完毕,或者被中断或异常退出,导致线程进入死亡状态。在这种状态下,线程不能再作为独立执行的线程对待。
/*顾客去买包子,包子可能没有要现做,
但是不知道要多久才能做好,因为不知道顾客要等待的时间,所以调用了wait()方法,此时顾客属于无限等待状态
包子做好了,老板通知顾客包子好了,notify()一下*/
public class WaitAndNotify {
public static void main(String[] args) {
//锁对象必须是唯一的
final Object obj = new Object();
//创建一个顾客线程
new Thread(){
@Override
public void run() {
while (true){
//老板和顾客线程要用同步代码块包裹,保证只能实行其中的一个
synchronized (obj){
System.out.println("顾客说要1个素馅的1个肉馅的包子");
//无限等待
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒之后的代码
System.out.println("包子已经做好了,开吃");
System.out.println("______________________________");
}
}
}
}.start();
//创建一个老板线程
new Thread(){
@Override
public void run() {
while(true){
//花了3S做包子
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//老板和顾客线程要用同步代码块包裹,保证只能实行其中的一个
synchronized (obj){
System.out.println("老板花3S做好了包子");
obj.notify();
}
}
}
}.start();
}
}
notify和notifyAll的区别?
notify()是随机唤醒一个线程,notifyAll()唤醒所有线程,都是让线程变为就绪状态
sleep和wait的区别?
- 语法和使用:
sleep
直接作用于Thread
类,不需要与synchronized
关键字一起使用。wait
则是Object
类的方法,通常需要配合synchronized
使用以确保正确性。 - 唤醒机制:
sleep
会自动在指定的时间后唤醒线程,如果未设置超时时间,则会无限期地等待下去。wait
不一定需要传递超时时间参数。如果不传递任何参数,表示永久休眠;若传递超时时间,则在超时后唤醒。 - 锁释放:
sleep
不会释放任何锁资源。wait
会释放所持锁资源,以便其他线程能够访问同步控制块或方法。 - 使用:
sleep
通常用于使整个应用程序暂停执行,而不是特定的同步控制块。wait
更适合于在特定同步控制块内部暂停线程,以便其他线程有机会处理该控制块的资源。
JAVA的锁
CAS锁(Compare and Swap)
CAS是一种无锁算法 (乐观锁),CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
- 首先,每个线程都会先获取当前的值。接着走一个原子的CAS操作,原子的意思就是这个CAS操作一定是自己完整执行完的,不会被别人打断。
- 然后CAS操作里,会比较一下,现在你的值是不是刚才我获取到的那个值。如果是,说明没人改过这个值,那你给我设置成累加1之后的一个值。
- 同理,如果有人在执行CAS的时候,发现自己之前获取的值跟当前的值不一样,会导致CAS失败,失败之后,进入一个无限循环,再次获取值,接着执行CAS操作。
缺点:
1.可能cas 会一直失败,然后自旋
2.如果一个值原来是A,变成了B,然后又变成了A,那么在CAS检查的 时候会发现没有改变,但是实质上它已经发生了改变,这就是所谓的ABA问题。 对于ABA问题其解决方案是加上版本号,即在每个变量都加上一个版本号,每次 改变时加1,即A —> B —> A,变成1A —> 2B —> 3A。
AQS同步机制(AbstractQueuedSynchronizer)
AQS的核心思想是:通过一个volatile修饰的int属性state代表同步状态,例如0是无锁状态,1是上锁状态。多线程竞争资源时,通过CAS的方式来修改state,例如从0修改为1,修改成功的线程即为资源竞争成功的线程,将其设为exclusiveOwnerThread,也称【工作线程】,资源竞争失败的线程会被放入一个FIFO的队列中并挂起休眠,当exclusiveOwnerThread线程释放资源后,会从队列中唤醒线程继续工作,循环往复。
Synchronized 锁
在Java中,synchronized是一种关键字,用于实现线程同步。它可以用于方法或代码块,用于保证同一时间只有一个线程可以执行被synchronized修饰的代码。
synchronized的锁机制有两种使用方式:
- 同步方法:可以在方法声明中使用synchronized关键字。当一个线程调用同步方法时,会自动获取该方法所属对象的锁,其他线程将被阻塞,直到该线程释放锁为止。
- 同步代码块:使用synchronized关键字可以修饰一段代码块。当一个线程进入synchronized代码块时,它会尝试获取锁,如果锁已经被其他线程获取,那么该线程将被阻塞,直到锁被释放。
synchronized锁是基于对象的,每个对象都有一个关联的锁。当多个线程同时访问某个对象的同步方法或同步代码块时,它们会竞争该对象的锁。
1.如果synchronized锁加在实例方法上,则默认使用的是this锁
2.如果synchronized锁加载静态方法上,则默认使用的是 类名.class 锁(Java反射技术中说到一个class文件只会在jvm中存在一份)
另外,要注意避免过多地使用synchronized,因为过多的同步操作可能会导致性能下降。在某些情况下,可以考虑使用更灵活的并发工具,如Lock和Condition接口
对象头:
自旋:线程会一直循环检查该锁是否被释放,直到获取到该锁为止。这个循环等待的过程被称为自旋
1)偏向锁
只有一个线程争抢锁资源的时候.将线程拥有者标识为当前线程。引入了偏向锁目的是来尽可能减少无竞争情况下的同步操作开销。当一个线程访问同步块并获取对象的锁时,会将锁的标记记录在线程的栈帧中,并将对象头中的Thread ID设置为当前线程的ID。此后,当这个线程再次请求相同对象的锁时,虚拟机会使用已经记录的锁标记,而不需要再次进入同步块。
偏向锁(Biased Locking)就是为了在无竞争的情况下减少同步操作的开销。它通过记录线程ID来避免对锁的加锁和解锁操作,提高了单线程访问同步代码块时的性能。
2)轻量级锁(自旋锁)
一个或多个线程通过CAS去争抢锁,如果抢不到则一直自旋。虚拟机会将对象的Mark Word复制到线程的栈帧中作为锁记录,并尝试使用CAS(Compare and Set)操作尝试获取锁。如果CAS成功,则表示线程获取了轻量级锁,并继续执行同步块。如果CAS失败,说明有竞争,虚拟机会通过自旋(spinning)等待其他线程释放锁
轻量级锁是为了减少线程切换的开销。它使用CAS(Compare and Set)操作来尝试获取锁,如果成功则可以继续执行同步块,无需线程切换;如果失败,则会进行自旋操作等待锁的释放。自旋操作避免了线程挂起和切换的开销,提高了多线程竞争时的性能。
使用对象头中的一部分位来存储线程ID和锁标记,不需要额外的内存存储锁的状态。相对于传统的重量级锁,它能够节省内存消耗。
3)重量级锁
如果自旋等待不成功,虚拟机会将轻量级锁升级为重量级锁。在这种状态下,虚拟机会将线程阻塞,并使用操作系统的互斥量来实现锁的释放和获取。
需要注意的是,锁的升级是逐级升级的过程,而不会存在降级。换句话说,一旦锁升级到更高级别,就不会回到低级别。
升级过程:
- 1)当只有一个线程去争抢锁的时候,会先使用偏向锁,就是给一个标识,说明现在这个锁被线程a占有.
- 2)后来又来了线程b,线程c,说凭什么你占有锁,需要公平的竞争,于是将标识去掉,也就是撤销偏向锁,升级为轻量级锁,三个线程通过CAS自旋进行锁的争抢(其实这个抢锁过程还是偏向于原来的持有偏向锁的线程).
- 3)现在线程a占有了锁,线程b,线程c一直在循环尝试获取锁,后来又来了十个线程,一直在自旋,那这样等着也是干耗费CPU资源,所以就将锁升级为重量级锁,向内核申请资源,直接将等待的线程进行阻塞.
优化方法
## 1、适应性自旋
解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
## 2、锁粗化(Lock Coarsening)
锁粗化的概念应该比较好理解,就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁。
stringBuffer.append("a");
stringBuffer.append("b");
stringBuffer.append("c");
这里每次调用stringBuffer.append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。
## 3、锁消除
锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。看下面这段程序:
public static void main(String[] args) {
SynchronizedTest02 test02 = new SynchronizedTest02();
//启动预热
for (int i = 0; i < 10000; i++) {
i++;
}
long start = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
test02.append("abc", "def");
}
虽然StringBuffer的append是一个同步方法,但是这段程序中的StringBuffer属于一个局部变量,并且不会从该方法中逃逸出去,所以其实这过程是线程安全的,可以将锁消除。
对比
锁 | 优点 | 缺点 | 应用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 | 适用于只有一个线程访问同步块场景。 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度。 | 如果始终得不到锁竞争的线程使用自旋会消耗CPU。 | 追求响应时间。 同步块执行速度非常快。 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU。 | 线程阻塞,响应时间缓慢。 | 追求吞吐量。 同步块执行速度较长。 |
底层实现原理
synchronized 是 JVM 的内置锁,基于 Monitor 机制实现。每一个对象都有一个与之关联的监视器 (Monitor),这个监视器充当了一种互斥锁的角色。当一个线程想要访问某个对象的 synchronized 代码块,首先需要获取该对象的 Monitor。如果该 Monitor 已经被其他线程持有,则当前线程将会被阻塞,直至 Monitor 变为可用状态。当线程完成 synchronized 块的代码执行后,它会释放 Monitor,并把 Monitor 返还给对象池,这样其他线程才能获取 Monitor 并进入 synchronized 代码块。
每个Java对象都有一个与之关联的Monitor。这个Monitor的实现是在JVM的内部完成的,它采用了一些底层的同步原语,用以实现线程间的等待和唤醒机制,这也是为什么等待(wait)和通知(notify)方法是属于Object类的原因。这两个方法实际上是通过操纵与对象关联的Monitor,以完成线程的等待和唤醒操作,从而实现线程之间的同步。
Synchrpnized和lock的区别
(1)synchronized是关键字,lock是一个类,synchronize是在JVM层面实现的,发生异常后jvm会释放锁。lock是JDK代码实现的,需要手动释放,在finally块中释放,容易死锁。
(2) 用法不一样:synchronize可以用在代码块上,方法上。lock只能写在代码里,不能直接修改方法。synchronized在发生异常时会自动释放锁,lock需要手动释放锁
(3)synchronized是非公平锁、可重入锁(每个线程获取锁的顺序不是按照线程访问锁的先后顺序获取)不可中断锁,lock是可公平锁、可中断锁、可重入锁。
(4)synchronized适用于少量同步,lock适用于大量同步。
(5)锁状态是否可以判断:synchronized 不可以,lock可以。
## Synchronized
优点:实现简单,语义清晰,便于JVM堆栈跟踪,加锁解锁过程由JVM自动控制,提供了多种优化方案,使用更广泛
缺点:悲观的排他锁,不能进行高级功能
## Lock
优点:可定时的、可轮询的与可中断的锁获取操作,提供了读写锁、公平锁和非公平锁
缺点:需手动释放锁unlock,不适合JVM进行堆栈跟踪
## 可重入锁
可重入锁和不可重入锁的最大区别在于,可重入锁允许同一个线程在获得锁之后再次获得该锁,而不可重入锁不允许。
如果一个线程已经获得锁,那么在该线程释放该锁之前,它可以再次获得该锁而不会被阻塞。
实现原理:
每次获得锁时,计数器加1,每次释放锁时,计数器减1。只有当计数器为0时,其他线程才有机会获得该锁。
## ReentrantLock (用于替代synchronized)
ReentrantLock提供了可轮询的锁请求。它会尝试着去获取锁,如果成功则继续,否则可以等到下次运行时处理,而synchronized则一旦进入锁请求要么成功要么阻塞,所以相比synchronized而言,ReentrantLock会不容易产生死锁些。
## 公平锁
按先来后到的顺序获取锁
## 读写锁 ReentrantReadWriteLock (乐观锁)
读写锁维护着一对锁,一个读锁和一个写锁。通过分离读锁和写锁,使得并发性比一般的互斥锁有了较大的提升:在同一时间可以允许多个读线程同时访问,但是在写线程访问时,所有读线程和写线程都会被阻塞。
保证多线程的安全性?
-
使用synchronized关键字:synchronized关键字可以将某些代码块或方法设为同步代码,确保同一时刻只有一个线程可以访问。这种方式需要注意锁的粒度,使得锁住的代码块尽可能的短,以避免影响程序性能。
-
使用Volatile关键字:Volatile关键字可以用于修饰变量,确保多线程之间的可见性,即当一个线程修改了共享变量的值,其他线程会立即查询最新的值。
-
使用Lock对象:Lock是JDK提供的同步机制,Lock提供的Lock()和Unlock()方法可以在同一个时刻,只允许一个线程进入执行Lock()和Unlock()方法之间的代码块,其他线程必须等待。
-
使用原子类:Java提供了很多原子类,包括AtomicInteger、AtomicLong和AtomicBoolean等等,这些类可以保证特定操作的原子性,避免多线程同时访问一个共享资源所造成的数据安全问题。(atomic是通过CAS实现的)
-
使用ThreadLocal类:ThreadLocal类可以在多线程中为每个线程创建一个独立的实例,避免多线程对同一资源的争夺,从而保证了数据安全性。
sleep()和wait()的区别
- wait()是Object的方法,sleep()是Thread类的方法
- wait()会释放锁,sleep()不会释放锁
- wait()要在同步方法或者同步代码块中执行,sleep()没有限制
- wait()要调用notify()或notifyall()唤醒,sleep()自动唤醒
yield()和join()区别
- 暂停当前正在执行的线程,并执行其他线程。(可能没有效果),如果成功则调用后线程进入就绪状态,告诉当前线程把机会交给其他高优先级的线程
- join将两个交替执行的线程合并为顺序执行的线程,A线程中调用B线程的join() ,则B执行完前A进入阻塞状态,使并行的程序串行化执行
Thread、Runable的区别
Thread和Runnable的实质是继承关系,没有可比性。无论使用Runnable还是Thread,都会new Thread,然后执行run方法。用法上,如果有复杂的线程操作需求,那就选择继承Thread,如果只是简单的执行一个任务,那就实现runnable。
JAVA内存模型
所有变量都存在主存中,主存是线程共享区域;每个线程都有自己独有的工作内存,线程想要操作变量必须从主从中copy变量到自己的工作区,每个线程的工作内存是相互隔离的
## 栈帧的压入
局部变量表 主要存放了编译期可知的各种数据类型、对象引用。
操作数栈 主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。
动态链接 主要服务一个方法需要调用其他方法的场景。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用,这个过程也被称为 动态连接
## 栈帧的弹出
Java 方法有两种返回方式,一种是 return 语句正常返回,一种是抛出异常。不管哪种返回方式,都会导致栈帧被弹出。也就是说, 栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。