目录
一、线程、进程、程序
二、线程状态
三、线程的七大参数
四、lock与synchronized锁机制
一)、lock与synchronized锁区别
二)、synchronized锁原理
三)、Lock锁原理
五、synchronized锁升级原理
一)、锁升级基础知识
二)、锁升级过程有什么用?
三)、synchronized锁升级具体过程
六、Volatile底层原理(可见性和禁止指令重排序)
一)线程安全三要素
二)volatile关键字是如何保证可见性和有序性呢?
三)volatile关键字是线程安全的吗?
一、线程、进程、程序
1.进程: 我们把运行中的程序叫做进程,每个进程都会占用内存与CPU资源,进程与进程之间互相独立,例如360杀毒软件中运行中。
2.线程: 线程就是进程中的一个执行单元,负责当前进程中程序的执行。一个进程可以包含多个线程。多线程可以提高程序的并行运行效率。360杀毒软件中的垃圾清理功能
3.程序:是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码,例如360杀毒软件。
二、线程状态
1.新建(New):创建线程对象时
2.就绪(Runnable):线程调用start方法,有执行资格没有执行权
3.运行:当就绪状态时抢到cpu的执行权之后,进入运行状态
4.阻塞(Blocked):当获取锁失败后,进入阻塞状态
5.等待(Waiting):等待被notify()方法唤醒
6.休眠(sleep):休眠一段时间,时间到了之后,进入就绪状态。
7.终止(Terminated):线程死亡
注意1:调用start方法方可启动线程,而run方法只是thread类中的一个普通方法调用,还是在主线程里执行。
注意2:wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态。
sleep是Thread的静态方法,而 wait()都是 Object 的成员方法,每个对象都有
sleep方法是暂停当前线程,相当于休眠一段时间,之后会自动唤醒,而wait()必须被notify 或者notifyall方法唤醒,不然会一直阻塞。
Wait会释放锁,sleep不会:wait方法的调用必须先获取wait对象的锁,而sleep则无此限制,wait方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用);但是如果在 synchronized代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了),sleep方法执行后不会释放对象锁。
三、线程的七大参数
1.核心线程数(corePoolSize):表示保持活动状态的最小线程数。
2.最大线程数(maximumPoolSize):表示允许创建的最大线程数。
3.空闲时间(keepAliveTime):表示当线程数量超过核心线程数时,多余的空闲线程能够保持存活的时间。
4.阻塞队列(workQueue):表示用于存储等待执行的任务的阻塞队列。
5.单位(unit):表示空闲时间的单位,例如毫秒、秒等。
6.拒绝策略(rejectedExecutionHandler):表示当任务无法提交给线程池执行时采取的策略。
四、lock与synchronized锁机制
一)、lock与synchronized锁区别
1.语法层面:
1.1.synchronized是关键字,源码在jvm中,用c++ 语言实现
1.2.Lock 是接口,源码由jdk 提供,用java语言实现
2.释放锁机制:
使用 synchronized 时,退出同步代码块锁会由jvm自动释放锁,比较被动而使用Lock时,需要手动调用unlock方法释放锁,否则可能会导致死锁等问题。
3.都支持可重入锁
都可以用来控制多个线程访问共享资源的互斥性。都支持可重入锁机制,即同一个线程在已经获得锁的情况下能够再次获取该锁。
4.性能:
synchronized适用于简单线程同步场景,Lock适用于高并发场景下,拥有更好的性能和灵活性。
5.synchronized和lock如何保证线程安全?
synchronized一般情况下通过修饰方法或代码块保证线程安全,而lock一般搭配unlock实现线程安全,lock和unlock中间放需要保证线程安全的代码。但为了避免lock死锁问题,一般将unlock放置于finally代码块中。
//控制方法
public synchronized void sync(){
}Object lock = new Object();
//控制代码块
public void sync(){
synchronized(lock){}Lock lock =new ReentrantLock();public void sync(){
lock.lock();//上锁
//TODO线程安全的代码
lock.unlock//释放锁,避免死锁}
二)、synchronized锁原理
其主要原理是基于Java对象的内部锁,即监视器锁(Monitor Lock),确保在同一时刻只有一个线程可以访问被保护的代码块或方法。当一个线程尝试获取被synchronized关键字保护的资源时,如果该资源已被其他线程占用,该线程就会进入等待状态。占用资源的线程释放该资源时,等待队列中的线程会竞争获取该资源,并且只有一个线程会成功获取到该资源,其他线程继续等待。
synchronized关键字保证了可见性和原子性,可见性是通过JVM底层的内存屏障来实现的,原子性则是通过监视器锁的互斥性来实现的。在synchronized块内,线程获得了锁,它将会清空工作内存,从而使得该线程使用的变量能够从主内存中重新读取,同时也会把工作内存中的变量写回到主内存中。这样,其他线程就可以读取到最新的值,从而保证了可见性。
三)、Lock锁原理
Lock的实现是基于Java的AbstractQueuedSynchronizer(AQS)框架的。Lock接口定义了多个获取和释放锁的方法,其中比较重要的是lock()和unlock()方法。当一个线程调用lock()方法获取锁时,如果锁未被占用,则该线程会占用锁并继续执行;否则,该线程会进入阻塞状态,直到锁被释放。当一个线程调用unlock()方法释放锁时,会通知等待队列中的其他线程继续尝试获取锁。
五、synchronized锁升级原理
一)、锁升级基础知识
1)偏向锁
只有一个线程争抢锁资源的时候.将线程拥有者标识为当前线程。引入了偏向锁目的是来尽可能减少无竞争情况下的同步操作开销。当一个线程访问同步块并获取对象的锁时,会将锁的标记记录在线程的栈帧中,并将对象头中的Thread ID设置为当前线程的ID。此后,当这个线程再次请求相同对象的锁时,虚拟机会使用已经记录的锁标记,而不需要再次进入同步块。
2)轻量级锁(自旋锁)
一个或多个线程通过CAS去争抢锁,如果抢不到则一直自旋。虚拟机会将对象的Mark Word复制到线程的栈帧中作为锁记录,并尝试使用CAS自旋操作尝试获取锁。如果CAS成功,则表示线程获取了轻量级锁,并继续执行同步块。如果CAS失败,说明有竞争,虚拟机会通过自旋等待其他线程释放锁。
3)重量级锁
如果自旋等待不成功,虚拟机会将轻量级锁升级为重量级锁。在这种状态下,虚拟机会将线程阻塞,并使用操作系统的互斥量来实现锁的释放和获取。
需要注意的是,锁的升级是逐级升级的过程,而不会存在降级。换句话说,一旦锁升级到更高级别,就不会再回到低级别。
二)、锁升级过程有什么用?
1)减少无竞争情况下的同步操作开销
在多线程环境下,如果没有竞争,每个线程都可以安全地访问共享资源,无需进行同步操作。锁的升级过程中的第一阶段偏向锁(Biased Locking)就是为了在无竞争的情况下减少同步操作的开销。它通过记录线程ID来避免对锁的加锁和解锁操作,提高了单线程访问同步代码块时的性能。
2)尽量避免线程切换的开销
锁的升级过程中的第二阶段轻量级锁(Lightweight Locking)是为了减少线程切换的开销。它使用CAS操作来尝试获取锁,如果成功则可以继续执行同步块,无需线程切换;如果失败,则会进行自旋操作等待锁的释放。自旋操作避免了线程挂起和切换的开销,提高了多线程竞争时的性能。
3)降低内存消耗
锁的升级过程中的第二阶段轻量级锁使用对象头中的一部分位来存储线程ID和锁标记,不需要额外的内存存储锁的状态。相对于传统的重量级锁,它能够节省内存消耗。
4)提高系统吞吐量
锁的升级过程可以使多个线程在无竞争情况下快速获取锁,避免了线程阻塞和等待的开销。这样,系统的吞吐量会更高,因为更多的线程可以并发地执行任务。
总而言之,锁的升级过程是为了提高多线程环境下的性能和吞吐量,减少同步操作的开销,并尽量避免线程切换的开销。Java虚拟机根据线程竞争的情况和锁的使用情况自动进行锁的升级和降级,以优化多线程程序的性能。
三)、synchronized锁升级具体过程
1)当只有一个线程去争抢锁的时候,会先使用偏向锁,就是给一个标识,说明现在这个锁被线程a占有。
2)后来又来了线程b,线程c,说凭什么你占有锁,需要公平的竞争,于是将标识去掉,也就是撤销偏向锁,升级为轻量级锁,三个线程通过CAS自旋进行锁的争抢(其实这个抢锁过程还是偏向于原来的持有偏向锁的线程).
3)现在线程a占有了锁,线程b,线程c一直在循环尝试获取锁,后来又来了十个线程,一直在自旋,那这样等着也是干耗费CPU资源,所以就将锁升级为重量级锁,向内核申请资源,直接将等待的线程进行阻塞。
六、Volatile底层原理(可见性和禁止指令重排序)
volatile 变量是一种比 sychronized 关键字更轻量级的同步机制。
一)线程安全三要素
原子性:一个操作或者多个操作,要么全部执行成功,要么全部执行失败。满足原子性的操作,中途不可被
中断。
可见性:多个线程共同访问共享变量时,某个线程修改了此变量,其他线程能立即看到修改后的值。
有序性:程序执行的顺序按照代码的先后顺序执行。(由于JVM模型中允许编译器和处理器为了效率,进行指令重排序的优化。指令重排序在单线程内表现为串行语义,在多线程中会表现为无序。那么多线程并发编程中,就要考虑如何在多线程环境下可以允许部分指令重排,又要保证有序性)
二)volatile关键字是如何保证可见性和有序性呢?
保证可见性:当一个变量被volatile修饰后,JVM会把工作内存中的最新变量值强制刷新到主内存中,会导致其他线程中的本地缓存失效。这样,其他线程使用缓存时,发现本地工作内存中此变量失效,便会从主内存中获取,这样获取到的值就是最新的值,实现了线程之间可见性。
保证有序性:通过编译器在生成字节码时,在指令序列中添加“内存屏障”来禁止指令重排序的,从而保证了有序性。(屏蔽在多线程环境下CPU的指令重排)
三)volatile关键字是线程安全的吗?
因为volatile保证不了原子性,满足不了线程安全三要素,所以volatile不是线程安全的。
值得说明的是,对 volatile 变量的单次读/写操作可以保证原子性的。如 long 和 double 类型变量,
但是并不能保证 i++这种操作的原子性,因为本质上 i++是读、写两次操作。在某些场景下可以代替 Synchronized。但是volatile 的不能完全取代 Synchronized 的位置,只有在一些特殊的场景下,才能适用 volatile。
总的来说,必须同时满足下面两个条件才能保证在并发环境的线程安全:
1、对变量的写操作不依赖于当前值(比如 i++),或者说是单纯的变量赋值(boolean
flag = true)。
2、该变量没有包含在具有其他变量的不变式中,也就是说,不同的 volatile 变量之间,不
能互相依赖。只有在状态真正独立于程序内其他内容时才能使用 volatile。