文章目录
在 java 中 wait 和 sleep 方法的不同?
- 最大的不同在于等待时,wait 会释放锁,而sleep一直持有锁
- wait 通常用于线程间的交互,sleep通常被用于暂停执行
死锁产生的条件,如何避免死锁
产⽣死锁必须具备以下四个条件:
-
互斥条件:该资源任意⼀个时刻只由⼀个线程占⽤。
-
请求与保持条件:⼀个进程因请求资源⽽阻塞时,对已获得的资源保持不放。
-
不剥夺条件:线程已获得的资源在末使⽤完之前不能被其他线程强⾏剥夺,只有⾃⼰使⽤完毕后才释放资源。
-
循环等待条件:若⼲进程之间形成⼀种头尾相接的循环等待资源关系。
为了避免死锁,我们只要破坏产⽣死锁的四个条件中的其中⼀个就可以了。
-
破坏互斥条件 :这个条件我们没有办法破坏,因为我们⽤锁本来就是想让他们互斥的(临界资源需要互斥访问)。
-
破坏请求与保持条件 :⼀次性申请所有的资源。
-
破坏不剥夺条件 :占⽤部分资源的线程进⼀步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
-
破坏循环等待条件 :靠按序申请资源来预防。按某⼀顺序申请资源,释放资源则反序释放。
ThreadLocal 了解么?-- 等有时间再研究,先不看(4大引用)
-
ThreadLocal 类主要解决的就是让每个线程绑定⾃⼰的值,可以将 ThreadLocal 类形象的⽐喻成存放数据的盒⼦,盒⼦中可以存储每个线程的私有数据。
-
如果你创建了⼀个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是 ThreadLocal 变量名的由来。他们可以使⽤ get() 和 set()⽅法来获取默认值或将其值更改为当前线程所存的副本的值,从⽽避免了线程安全问题。
线程池原理
- 线程池做的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务
- 如果线程数量超过了最大数量,超出数量的线程排队等候,等待其他执行完毕后再从队列中取出任务来执行
- 主要特点是:线程复用,控制最大并发数,管理线程
线程复用
- 因为每一个Thread的类都有一个start 方法,当调用start()启动线程时Java虚拟机会调用该类的run方法
- 那么该类的run()方法中就是调用了Runnable对象的run()方法。因为Thread类实现Runnable接口
- 我们可以继承重写Thread类,在其start()方法中添加不断循环调用传递过来的Runnable对象。这就是线程池的实现原理
- 循环方法中不断获取Runnable是用队列来实现的,在获取下一个Runnable之前可以是阻塞的
线程池的组成
- 线程池管理器:用于创建并管理线程池
- 工作线程:线程池中的线程
- 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
- 任务队列:用于存放待处理的任务,提供一种缓冲机制
拒绝策略
- 线程池中的线程已经用完了,无法继续为新任务提供服务。同时,等待队列也已经满了,再也塞不下新的任务了。这个时候我们就需要拒绝策略机制合理处理这个问题
- JDK内置的拒绝策略
- AbortPolicy:直接抛出异常,阻止系统正常运行
- CallerRunsPolicy:该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
- DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案
线程池的工作流程
-
线程池刚刚创建时,里面没有一个线程。任务队列 是作为参数传递进来的。不过,要注意的是,就算此时队列中有任务,线程池也不会马上去执行他们。
-
当调用 execute() 方法添加一个任务时,线程池会做如下判断
-
如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务
-
如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列
-
如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是 要创建非核心线程立刻运行这个任务
-
如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程
池会抛出异常
-
-
当一个线程完成任务时,它会从队列中取下一个任务来执行
-
当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前
运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,
它最终会收缩到 corePoolSize 的大小
线程执行的顺序
-
当线程数小于核心线程数时,会一直创建线程直到线程数等于核心线程数;
-
当线程数等于核心线程数时,新加入的任务会被放到任务队列等待执行;
-
当任务队列已满,又有新的任务时,会创建线程直到线程数量等于最大线程数;
-
当线程数等于最大线程数,且任务队列已满时,新加入任务会被拒绝。
线程池的核心参数有哪些
corePoolSize = 1
queueCapacity = Integer.MAX_VALUE
maxPoolSize = Integer.MAX_VALUE
keepAliveTime = 60 秒
allowCoreThreadTimeout = false
rejectedExecutionHandler = AbortPolicy()
-
corePoolSize : 核心线程数
-
maxPoolSize:最大线程数
-
queueCapacity:任务队列容量
-
keepAliveTime:线程空闲时间
- allowCoreThreadTimeout:允许核心线程超时。默认为false,当设置为true时,线程会超时关闭
-
rejectedExecutionHandler:任务拒绝处理器
为什么我们调⽤ start() ⽅法时会执⾏ run() ⽅法,为什么我们不能直接调⽤ run() ⽅法?
调⽤ start() ⽅法⽅可启动线程并使线程进⼊就绪状态,直接执⾏ run() ⽅法的话不会以多线程的⽅式执⾏
说⼀说⾃⼰对于 synchronized 关键字的了解
- synchronized 关键字解决的是多个线程之间访问资源的同步性
- synchronized 关键字可以保证被它修饰的⽅法或者代码块在任意时刻只能有⼀个线程执⾏
说说⾃⼰是怎么使⽤ synchronized 关键字
- 修饰实例⽅法: 作⽤于当前对象实例加锁,进⼊同步代码前要获得 当前对象实例的锁
- 修饰静态⽅法: 也就是给当前类加锁,会作⽤于类的所有对象实例 ,进⼊同步代码前要获得当前 class 的锁。
-
synchronized
关键字加到 static 静态⽅法和synchronized(class)
代码块上都是是给 Class类上锁。 -
synchronized
关键字加到实例⽅法上是给对象实例上锁
请你说下volatile这个关键字
volatile只能保证变量的可见性、有序性,但是不能保证原子性
volatile的作用
- 保证内存可见性:
- 一个线程对一个volatile变量的修改,对于其它线程来说是可见的。
- volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
- 禁止指令重排序
volatile和synchronized区别
-
一方面是因为synchronized是一种锁机制,存在阻塞问题和性能问题,而volatile并不是锁,所以不存在阻塞和性能问题。
-
另外一方面,因为volatile借助了内存屏障来帮助其解决可见性和有序性问题,而内存屏障的使用还为其带来了一个禁止指令重排的附件功能,所以在有些场景中是可以避免发生指令重排的问题的。
-
所以,在日后需要做并发控制的时候,如果不涉及到原子性的问题,可以优先考虑使用volatile关键字