1、线程、进程、程序
线程是⼀个比进程更小的执行单位,它是执行处理机调度的基本单位;一个进程可以有多个线程,这些线程共享同一地址空间。
进程是资源(CPU、内存等)分配的基本单位,指运行中的应用程序,每个进程都有自己独立的地址空间(内存空间)。
程序是为完成特定任务、用某种语言编写的一组指令的集合,是一段静态的代码。
2、并发和并行
并发: 同⼀时间段,多个任务都在执行 (单位时间内不⼀定同时执行);
并行: 单位时间内,多个任务同时执行。
3、线程基本状态
- 新建:NEW(尚未启动的线程的线程状态)
- 运行:RUNNABLE(可运行线程的线程状态。包括Running,Ready和其他状态)
- 阻塞:BLOCKED(等待监视器锁而阻塞的线程的线程状态)
- 等待:WAITING(等待线程的线程状态,一个处于等待状态的线程正在等待另一个线程执行一个特定的操作)
- 超时等待: TIMED_WAITING(具有指定等待时间的等待线程的线程状态,)
- 终结:TERMINATED(已终止线程的线程状态。线程已完成执行)
4、内存泄漏
4.1、什么是内存泄露
对于应用程序来说,当对象已经不再被使用,但是Java的垃圾回收器不能回收它们的时候,就产生了内存泄露。
4.2、为什么会发生内存泄露
比如对象A引用对象B,A的生命周期比B的生命周期长, 当B在程序中不再被使用的时候,A仍然引用着B,在这种情况下, 垃圾回收器是不会回收B对象的,这就可能造成了内存不足问题,特别是基于这种场景,A除了引用B之外还引用了其他生命周期比A短的对象,对象B也这样,这些对象都无法被回收,那么就会造成内存资源浪费。
4.3、怎样阻止内存泄露
- 使用List、Map等集合时,在使用完成后赋值为null
- 使用大对象时,在用完后赋值为null
- 目前已知的jdk1.6的substring()方法会导致内存泄露(1.7后已优化)
- 避免一些死循环等重复创建或对集合添加元素,撑爆内存
- 简洁数据结构、少用静态集合等
- 及时关闭打开的文件,socket句柄等
- 多关注事件监听(listeners)和回调(callbacks),比如注册了一个listener,当它不再被使用的时候,忘 了注销该listener,可能就会产生内存泄露
5、线程池
5.1、为什么要创建线程池?(线程池优点)
- 使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用。(提高资源利用率)
- 可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃
- 提高线程的可管理性
5.2、缺点
- 使用不当可能会导致内存资源不足
- 死锁(父任务依赖子任务,且它们共用一个线程池,当这个线程池的线程全在执行父任务时,由于子任务未执行而父任务一直在等待子任务结果导致死锁)
- 线程泄漏:当从池中除去一个线程以执行一项任务,而在任务完成后该线程却没有返回池时,会发生这种情况。
- 发生线程泄漏的一种情形出现在任务抛出一个 RuntimeException 或一个 Error 时。如果池类没有捕捉到它们,那么线程只会退出而线程池的大小将会永久减少一个。当这种情况发生的次数足够多时,线程池最终就为空,而且系统将停止,因为没有可用的线程来处理任务。
- 发生线程泄漏的一种情形出现在任务抛出一个 RuntimeException 或一个 Error 时。如果池类没有捕捉到它们,那么线程只会退出而线程池的大小将会永久减少一个。当这种情况发生的次数足够多时,线程池最终就为空,而且系统将停止,因为没有可用的线程来处理任务。
5.3、创建自定义线程池有哪几个核心参数?
public ThreadPoolExecutor(int corePoolSize, // 核心线程数量大小
int maximumPoolSize, // 线程池最大容纳线程数
long keepAliveTime, // 线程空闲后的存活时长
TimeUnit unit,
//缓存异步任务的队列 //用来构造线程池里的worker线程
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
//线程池任务满载后采取的任务拒绝策略
RejectedExecutionHandler handler)
线程池执行流程
- 当线程池中线程数量小于 corePoolSize 则创建线程,并处理请求。
- 当线程池中线程数量大于等于 corePoolSize 时,则把请求放入 workQueue 中,随着线程池中的核
心线程们不断执行任务,只要线程池中有空闲的核心线程,线程池就从 workQueue 中取任务并处
理。 - 当 workQueue 已存满,放不下新任务时则新建非核心线程入池,并处理请求直到线程数目达到
maximumPoolSize(最大线程数量设置值)。 - 如果线程池中线程数大于 maximumPoolSize 则使用 RejectedExecutionHandler 来进行任务拒绝
处理。
任务队列 BlockingQueue
任务队列 workQueue 是用于存放不能被及时处理掉的任务的一个队列,它是 一个 BlockingQueue 类型
关于 BlockingQueue,虽然它是 Queue 的子接口,但是它的主要作用并不是容器,而是作为线程同步的工具,他有一个特征,当生产者试图向 BlockingQueue 放入(put)元素,如果队列已满,则该线程被阻塞;当消费者试图从 BlockingQueue 取出(take)元素,如果队列已空,则该线程被阻塞。
可选择的组测队列
- 无界队列
- 队列大小无限制,常用的为无界的LinkedBlockingQueue
- 当任务耗时较长时可能会导致大量新任务在队列中堆积最终导致OOM
- 有界队列
- 常用的有两类,一类是遵循FIFO原则的队列如ArrayBlockingQueue与有界的LinkedBlockingQueue
- 另一类是优先级队列如PriorityBlockingQueue
- 同步移交
- 如果不希望任务在队列中等待而是希望将任务直接移交给工作线程,可使用SynchronousQueue作为等 待队列。
- 要将一个元素放入 SynchronousQueue中,必须有另一个线程正在等待接收这个元素
- 只有在使用无界线程池或者有饱和策略(拒绝策略)时才建议使用该队列
任务拒绝类型
- ThreadPoolExecutor.AbortPolicy
- 当线程池中的数量等于最大线程数时抛 java.util.concurrent.RejectedExecutionException 异常, 涉及到该异常的任务也不会被执行,线程池默认的拒绝策略就是该策略
- ThreadPoolExecutor.DiscardPolicy()
- 当线程池中的数量等于最大线程数时,默默丢弃不能执行的新加任务,不报任何异常
- ThreadPoolExecutor.CallerRunsPolicy()
- 当线程池中的数量等于最大线程数时,重试添加当前的任务;它会自动重复调用execute()方法
- ThreadPoolExecutor.DiscardOldestPolicy()
- 当线程池中的数量等于最大线程数时,抛弃线程池中工作队列头部的任务(即等待时间最久的任务),并执行新传入的任务
- 当线程池中的数量等于最大线程数时,抛弃线程池中工作队列头部的任务(即等待时间最久的任务),并执行新传入的任务
如何合理的设置线程池大小
- CPU密集型:尽量使用较小的线程池,一般Cpu核心数+1,因为CPU密集型任务CPU的使用率很高,若开过多的线程,只能增加线程上下文的切换次数,带来额外的开销
- IO密集型
- 方法一:可以使用较大的线程池,一般CPU核心数 * 2;IO密集型CPU使用率不高,可以让CPU等待IO的时候处理别的任务,充分利用cpu时间
- 方法二: 最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目;线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程
- 混合型:可以将任务分为CPU密集型和IO密集型,然后分别使用不同的线程池去处理,按情况而定
5.4、如何创建线程
线程创建方法
- 继承Thread类创建线程类
public class Thread01 {
public static void main(String[] args) {
MyThread01 t1=new MyThread01();
MyThread01 t2=new MyThread01();
t1.start();
t2.start();
}
}
class MyThread01 extends Thread{
@Override
public void run() {
System.out.println("线程名:"+currentThread().getName());
}
}
- 实现Runnable接口
public class Thread02 {
public static void main(String[] args) {
MyThread02 target=new MyThread02();
Thread t1=new Thread(target);
Thread t2=new Thread(target);
t1.start();
t2.start();
}
}
class MyThread02 implements Runnable{
@Override
public void run() {
System.out.println("线程名:"+Thread.currentThread().getName());
}
}
- 通过Callable和Future创建线程
public class Thread03 {
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
Callable callable=new MyThread03();
FutureTask task1=new FutureTask(callable);
FutureTask task2=new FutureTask(callable);
new Thread(task1).start();
new Thread(task2).start();
Thread.sleep(10);//等待线程执行结束
//task.get() 获取call()的返回值。若调用时call()方法未返回,则阻塞线程等待返回值
System.out.println(task1.get());
System.out.println(task2.get());
//get的传入参数为等待时间,超时抛出超时异常;传入参数为空时,则不设超时,一直等待
System.out.println(task1.get(10L, TimeUnit.MILLISECONDS));
System.out.println(task2.get(10L, TimeUnit.MILLISECONDS));
}
}
class MyThread03 implements Callable{
@Override
public Object call() throws Exception {
System.out.println("线程名:"+Thread.currentThread().getName());
return "实现callable:"+Thread.currentThread().getName();
}
}
Runnable和Callable的区别
- Callable规定的方法是call(),Runnable规定的方法是run().
- Callable的任务执行后可返回值,而Runnable的任务是不能返回值得
- call方法可以抛出异常,run方法不可以,因为run方法本身没有抛出异常,所以自定义的线程类在重写run的时候也无法抛出异常
- 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
start()和run()的区别
- start()方法用来,开启线程,但是线程开启后并没有立即执行,他需要获取cpu的执行权才可以执行
- run()方法是由jvm创建完本地操作系统级线程后回调的方法,手动调用视为普通方法
多线程创建方法(阿里推荐使用5.3自定义线程池)
- newCachedThreadPool 创建一个可缓存的线程池,如果线程池长度超过处理需求,可灵活回收空闲线程,若无可回收,则新建线程
public class ThreadPoolDemo01 {
public static void main(String[] args) {
ExecutorService cachedThreadPool= Executors.newCachedThreadPool();
for(int i=0;i<20;i++){
final int index=i;
try {
Thread.sleep(index*100);
} catch (InterruptedException e) {
e.printStackTrace();
}
cachedThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println("此线程名:"+Thread.currentThread().getName());
}
});
}
}
}
- newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
public class ThreadPoolDemo02 {
public static void main(String[] args) {
ExecutorService fixedThreadPool= Executors.newFixedThreadPool(3);
for(int i=0;i<10;i++){
final int index=i;
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程名:"+Thread.currentThread().getName());
}
});
}
}
}
- newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行
public class ThreadPoolDemo03 {
public static void main(String[] args) {
ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(5);
scheduledThreadPool.schedule(new Runnable() {
@Override
public void run() {
System.out.println("线程名:"+Thread.currentThread().getName());
}
},3, TimeUnit.SECONDS);
}
}
- newSingleThreadExecutor 创建一个单线程化的线程池,它只会唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行
public class ThreadPoolDemo04 {
public static void main(String[] args) {
ExecutorService singleThreadExecutor= Executors.newSingleThreadExecutor();
for(int i=0;i<10;i++){
singleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程名:"+Thread.currentThread().getName());
}
});
}
}
}
CompletableFuture
Future中get()阻塞的方式和异步编程的设计理念相违背,而isDone()轮询的方式会消耗无畏的CPU资源,JDK8设计出CompletableFuture
核心的四个静态方法(分为两组)
CompletableFuture.runAsync 无返回值
// 默认线程
public static CompletableFuture<Void> runAsync(Runnable runnable)
// 使用指定线程池
public static CompletableFuture<Void> runAsync(Runnable runnable,
Executor executor)
CompletableFuture.supplyAsync 有返回值
// 默认线程
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
// 指定线程池
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,
Executor executor)
使用CompletableFuture时,需要注意主线程业务代码少的情况下立刻结束了导致CompletableFuture默认使用的线程池会立刻关闭—》可以在主线程执行休眠
6、多线程安全机制
6.1、线程安全?
- 线程安全:如果线程执行过程中不会产生共享资源的冲突,则线程安全
- 线程不安全:如果有多个线程同时在操作主内存中的变量,则线程不安全
6.2、安全机制
- 互斥同步锁: Synchorized / ReentrantLock 可重入锁
- 非阻塞同步锁: 原子类(CAS)不可重入锁
- 无同步方案
- 可重入代码: 在执行的任何时刻都可以中断-重入执行而不会产生冲突。特点就是不会依赖堆上的共享资源
- ThreadLocal/Volaitile: 线程本地的变量,每个线程获取一份共享变量的拷贝,单独进行处理
- 线程本地存储:如果一个共享资源一定要被多线程共享,可以尽量让一个线程完成所有的处理操作,比如生产者消费者 模式中,一般会让一个消费者完成对队列上资源的消费
6.3、保证并发线程安全的三大性质
- 原子性
- 一个操作是不可中断的,要么全部执行成功要么全部执行失败
- 有序性
- java中编译器和处理器会进行指令重排,如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另一个线程, 所有的操作都是无序的
- 可见性
- 当一个线程修改了共享变量后,其他线程能够立即得知这个修改
- 当一个线程修改了共享变量后,其他线程能够立即得知这个修改
6.4、volatile
volatile是Java虚拟机提供的轻量级同步机制,能够保证数据的可见性,但是不能保证其原子性,且禁止指令重排。
如何保证数据的可见性
读操作时JMM会把工作内存中对应的值设为无效,要求线程从主内存中读取数据;写操作时JMM会把工作内存中对应的数据刷新到主内存中,这种情况下,其它线程就可以读取变量 的最新值
使用volatile关键字的场景
- 状态标志,如:初始化或请求停机
volatile boolean shutdownRequested;
public void shutdown() {
shutdownRequested = true;
}
public void doWork() {
while (!shutdownRequested) {
// do stuff
}
}
线程1执行doWork()的过程中,可能有另外的线程2调用了shutdown,所以boolean变量必须是 volatile。
- 一次性安全发布,如:单例模式
//注意volatile!!!!!!!!!!!!!!!!!
private volatile static Singleton instace;
public static Singleton getInstance(){
//第一次null检查
if(instance == null){
synchronized(Singleton.class) { //1
//第二次null检查
if(instance == null){ //2
instance = new Singleton();//3
}
}
}
return instance;
}
在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在。
如果不用volatile,则因为内存模型允许所谓的“无序写入”,可能导致失败。某个线程可能会获得一个未完全初始化的实例
- 线程 1 进入 getInstance() 方法。
- 由于 instance 为 null,线程 1 在 //1 处进入synchronized 块。
- 线程 1 前进到 //3 处,但在构造函数执行之前,使实例成为非null。
- 线程 1 被线程 2 预占。
- 线程 2 检查实例是否为 null。因为实例不为 null,线程 2 将instance 引用返回,返回一个构造完
整但部分初始化了的Singleton 对象。 - 线程 2 被线程 1 预占。
- 线程 1 通过运行 Singleton 对象的构造函数并将引用返回给它,来完成对该对象的初始化。
- 独立观察,如:定期更新某个值
- “volatile bean” 模式
- 开销较低的“读-写锁”策略,如:计数器
如果读操作远远超过写操作,您可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销
@ThreadSafe
public class CheesyCounter {
// Employs the cheap read-write lock trick
// All mutative operations MUST be done with the 'this' lock held
@GuardedBy("this") private volatile int value;
//读操作,没有synchronized,提高性能
public int getValue() {
return value;
}
//写操作,必须synchronized。因为x++不是原子操作
public synchronized int increment() {
return value++;
}
}
6.5、ThreadLocal
ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量。
实现原理
Thread类中有两个变量threadLocals和inheritableThreadLocals,二者都是ThreadLocal内部类ThreadLocalMap类型的变量,我们通过查看内部内ThreadLocalMap可以发现实际上它类似于一个HashMap。在默认情况下,每个线程中的这两个变量都为null,只有当线程第一次调用ThreadLocal的set或者get方法的时候才会创建他们。
一个Thread中只有一个ThreadLocalMap,一个ThreadLocalMap中可以有多个 ThreadLocal对象,其中一个ThreadLocal对象对应一个ThreadLocalMap中的一个Entry(key为ThreadLocal对象,value为要操作的值)
ThreadLocal可能引起的OOM内存溢出问题简要分析
如果使用了线程池并且设置了固定的线程,处理一次业务的时候存放到 ThreadLocalMap中一个大对象,处理另一个业务的时候,又一个线程存放到ThreadLocalMap中一个大 对象,但是这个线程由于是线程池创建的他会一直存在,不会被销毁,这样的话,以前执行业务的时候 存放到ThreadLocalMap中的对象可能不会被再次使用,但是由于线程不会被关闭,因此无法释放Thread 中的ThreadLocalMap对象,造成内存溢出
6.6、synchronized
synchronized 关键字解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有⼀个线程执行
实现原理
- synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位置。
- synchronized 修饰的方法ACC_SYNCHRONIZED 标识,这个标识指明了这个方法是⼀个同步方法
三种用法
- 修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
synchronized void method() { //业务代码 }
- 修饰静态方法:给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得当前 class 的锁
synchronized void staic method() { //业务代码 }
- 修饰代码块:指定加锁对象,对给定对象/类加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁。synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁
synchronized(this) { //业务代码 }
synchronized、volatile区别
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
- volatile仅能实现变量的修改可见性,并不能保证原子性;synchronized则可以保证变量的修改可见性和原子性
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
6.7、ReentrantLock
ReentantLock 继承接口 Lock 并实现了接口中定义的方法,他是一种可重入锁,除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线 程死锁的方法。
ReentrantLock 与synchronized
- ReentrantLock 通过方法 lock()与 unlock()来进行加锁与解锁操作,与 synchronized 会被 JVM 自动解锁机制不同,ReentrantLock 加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用 ReentrantLock 必须在 finally 控制块中进行解锁操作。
- synchronized 关键字无法设置锁的超时时间,不是可中断锁,而 ReentrantLock 提供 tryLock 方法,允许设置线程获取锁的超时时间,如果超时,则跳过,不进行任何操作,避免死锁的发生;
- synchronized 关键字是一种非公平锁,先抢到锁的线程先执行。而 ReentrantLock 的构造方法中允许设置 true/false 来实现公平、非公平锁,设置为 true ,则线程获取锁要遵循"先来后到"的规则;
- 他们都是可重入锁,都保证了可见性和互斥性
tryLock和lock和lockInterruptibly 的区别
- tryLock 能获得锁就返回 true,不能就立即返回 false,tryLock(long timeout,TimeUnit unit),可
以增加时间限制,如果超过该时间段还没获得锁,返回 false - lock 能获得锁就返回 true,不能的话一直等待获得锁
- lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程, lock 不
会抛出异常,而 lockInterruptibly 会抛出异常。
6.8、Java锁升级
参考https://blog.csdn.net/weixin_40482816/article/details/126378882
markword
markword是java对象数据结构中的一部分,markword数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,它的最后2bit是锁状态标志位,用来标记当前对象的状态,对象的所处的状态,决定了markword存储的内容,如下表所示:
状态 | 偏向锁位 1bit(是否偏向锁) | 锁标志位 2bit | 存储内容 |
---|---|---|---|
未锁定 | 0 | 01 | 对象哈希码、对象分代年龄 |
可偏向 | 1 | 01 | 偏向线程ID、偏向时间戳、对象分代年龄 |
轻量级锁定 | - | 00 | 指向锁记录的指针 |
膨胀(重量级锁定) | - | 10 | 执行重量级锁定的指针 |
GC标记 | - | 11 | 空(不需要记录信息) |
偏向锁
它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁/解锁的一些CAS操作,这种情况下,就会给线程加一个偏向锁。 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁
偏向锁获取过程
- 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。
- 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3。
- 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4。
- 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the word)
- 执行同步代码。
偏向锁的释放
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态
轻量级锁
轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;
轻量级锁的加锁过程
- 在进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝
- 拷贝对象头中的Mark Word复制到锁记录中;
- 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5。
- 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图所示。
- 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。
轻量级锁的释放
由轻量锁切换到重量锁,是发生在轻量锁释放锁的期间,之前在获取锁的时候它拷贝了锁对象头的markword,在释放锁的时候如果它发现在它持有锁的期间有其他线程来尝试获取锁了,并且该线程对markword做了修改,两者比对发现不一致,则切换到重量锁。
自旋锁
指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环
优点
自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
缺点
- 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
- 上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。
自旋锁的开启:
JDK1.6中-XX:+UseSpinning开启;
JDK1.7后,去掉此参数,由jvm控制;
重量级锁
重量级锁会让其他申请的线程之间进入阻塞,性能降低。重量级锁也就叫做同步锁,这个锁对象 Mark Word 再次发生变化,会指向一个监视器(Monitor)对象,该监视器对象用集合的形式,来登记和管理排队的线程
锁优化
- 减少锁的时间
不需要同步执行的代码,能不放在同步块里面执行就不要放在同步快内,可以让锁尽快释放;
- 减少锁的粒度
它的思想是将物理上的一个锁,拆成逻辑上的多个锁,增加并行度,从而降低锁竞争。(空间换时间)
拆锁的粒度不能无限拆,最多可以将一个锁拆为当前cup数量个锁即可
- 锁粗化—》增加锁的粒度
假如有一个循环,循环内的操作需要加锁,我们应该把锁放到循环外面,否则每次进出循环,都进出一 次临界区,效率是非常差的
- 使用读写锁
ReentrantReadWriteLock 是一个读写锁,读操作加读锁,可以并发读,写操作使用写锁,只能单线程写;
- 使用cas
如果需要同步的操作执行速度非常快,并且线程竞争并不激烈,这时候使用cas效率会更高,因为加锁会 导致线程的上下文切换,如果上下文切换的耗时比同步操作本身更耗时,且线程对资源的竞争不激烈, 使用volatiled+cas操作会是非常高效的选择;
6.9、CAS
CAS(Compare And Swap/Set)比较并交换
它包含 3 个参数 CAS(V,E,N)。 V 表示要更新的变量(内存值),E 表示预期值(旧的),N 表示新值。当且仅当 V 值等于 E 值时,才会将 V 的值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后, CAS 返回当前 V 的真实值。
ABA 问题
比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存 中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但是不代表这个过程就是没有问题的。
部分乐观锁通过版本号的方式解决ABA问题
6.10、AQS
AbstractQueuedSynchronizer抽象的队列式的同步器, AQS 定义了一套多线程访问共享资 源的同步器框架,许多同步类实现都依赖于它,如常用的 ReentrantLock/Semaphore/CountDownLatch。
模型
AQS state
state 代表共享资源和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。
state的访问方式有三种:
- getState()
- setState()
- compareAndSetState()
AQS 两种资源共享方式:
- Exclusive:独占,只有一个线程能执行,如ReentrantLock
- Share:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier。
AQS只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现,AQS这里只定义了一个接口, 具体资源的获取交由自定义同步器去实现了(通过state的get/set/CAS)之所以没有定义成 abstract,是因为独占模式下只用实现 tryAcquire-tryRelease,而共享模式下只用实现 tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/ 唤醒出队等),AQS 已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
- isHeldExclusively():该线程是否正在独占资源。只有用到 condition 才需要去实现它。
- tryAcquire(int):独占方式。尝试获取资源,成功则返回 true,失败则返回 false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回 true,失败则返回 false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0 表示成功,但没有剩余可用资 源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回 true, 否则返回 false。
7、三种不同interrupt方法区别调用
interrupt不会立即中止当前线程,而是去修改线程中断标识
public void interrupt() | 实例方法,实例方法interruptl)仅仅是设置线程的中断状态为true,发起一个协商而不会立刻停止线程 |
---|---|
public static boolean interrupted() | 静态方法,Thread.interrupted();判断线程是否被中断,并清除当前中断状态 这个方法做了两件事: 1、返回当前线程的中断状态 2、将当前线程的中断状态设为false |
public boolean isInterrupted() | 实例方法,判断当前线程是否被中断 (通过检查中断标志位) |