interrupt()
方法的基本概念
-
中断线程:
interrupt()
是Thread
类的一个实例方法,用于中断当前线程。当一个线程通过interrupt()
方法被中断时,它会收到一个中断信号,但不会立即停止执行。具体的中断响应需要在线程的代码中进行处理。 -
如果是运行中的线程被interrupt,那么会记录打断状态。(通过这个特性可以实现两阶段终止,也就是优雅地关闭线程)。
-
如果不是运行中的线程,被interrupt不会记录打断状态。(也就是说对于wait中的线程,interrupt是叫醒,对于运行中的线程就是去提醒“嘿哥们,你收拾东西准备走吧”)。
synchronized(对象),
自己理解:给对象加锁底层就是给对象关联一个Monitor对象,当某一个线程获得对象的锁,也就是让Monitor的owner指针指向了当前的线程。
线程锁住的是对象,锁住后线程持有对象的锁(线程把对象锁住了,锁住后才能为所欲为,才能执行操作),一个线程想要获取锁要等其他线程把锁释放掉。
一段代码如果对共享资源存在进行多线程读写操作,那么这段代码叫做临界区。
synchronized锁的底层是使用操作系统提供的Monitor来实现的,每次加锁释放锁开销太大,为了减小开销,(在一些场景下)就使用了轻量级锁和偏向锁来进行优化。
场景:
1.在单线程或者多线程中同步代码块的的竞争程度不高),使用偏向锁,预防可能的并发问题,这样可以减少额外操作,提升性能。偏向锁有一个批量重偏向的功能,和一个批量撤销。
2.在多线程在临界区对共享资源没有交错(没有争夺,也就是一个白天用一个晚上用这样子),使用轻量级锁,可以不用申请Monitor对象,减少资源消耗。
3.有交错的(在一个线程使用资源期间,会有另外的线程也来执行,有交错),这时候使用重量级锁,重量级锁有一个锁自旋的概念(也就是尝试获取锁没获取到,会再尝试几次,万一获取到就不用被阻塞了)。
如果不满足各自的使用场景,就会发生锁膨胀的情况,比如由偏向锁升级为轻量级锁,或者偏向锁升级为重量级锁。
锁消除:JIT即时编译器会对代码进行逃逸分析,如果加锁对象不是共享对象(也就是在锁住的对象在方法内定义,且没有return等方式传到外边,就是没有逃逸),就会进行锁消除,把锁相关的代码给去除掉。
锁的粒度:可以给一个对象设置几个小的成员变量(去引用对象),这样每个方法可以去锁小的对象,锁的粒度变小提高并发能力。(但是要保证使用小锁对象的方法之间没有关联)锁粒度变小可能会发生死锁。
//wait和notify:涉及到Monitor锁对象中的waitSet等待队列。
场景:当获取锁的对象发现有一些资源没有获得,就可以调用wait,先到waitset里面去等待,当其他线程使用notify唤醒的时候,再加入到entrylist,就可以再去获取锁了。
注意事项:1.wait和notify都必须是在已经获取锁的对象的线程里执行。2.这两个方法必须与synchronized一起使用(其实注意事项1的原理相同)
区分:sleep是Thread类的静态方法,调用sleep,当前线程如果有锁的话不会释放锁,不必和synchroized连用。
wait是Object的方法(每个对象都有这个方法),锁对象调用wait,线程会释放当前锁对象,当前线程到waitset进行等待唤醒。
正确使用姿势:
synchronized(对象){
while(判断条件不成立){
对象.wait();
}
//判断条件成立
//执行后续的方法...
}
//唤醒的线程
synchronized(对象){
对象.notifyAll();
}
同步模式--保护性暂停:
用来实现一个线程等待另一个线程的执行结果。使用一个GuardObject来实现(底层是,wait、notify的正确使用姿势)
特点:产生结果的线程和消费结果的线程是一一对应的。
保护性暂停模式的解耦。
(自己的理解):当我们使用正常的保护性暂停模式,其实是把要传递的信息放在了锁对象中保存,这个时候我们两个线程想要通信(获取执行结果),就必须同时都能够获取到这个对象(GuardObject对象),也就是对象需要进行传递(比如做饭和吃饭,可能是两个类来完成的,mama类做饭,child类吃饭,饭就是暂停等待的对象,不进行解耦的话,这两个类想要通信就需要把饭这个对象来回传递)。这样耦合度就太高了。于是,就加了一层(没有什么事情是加一层不能解决的,如果不行就再加一层)。我们将需要使用的GuardObject类对象封装起来,具体就是放到一个辅助类中,这个辅助类中用一个静态Map的成员变量来存储。(这里必须使用静态,跟单例模式异曲同工,这样就只用获取到这个类,就能获取所有的GuardObject对象,对象也就不用来回传递了),举个例子就是,之前是妈妈直接给孩子饭,现在妈妈把饭放到桌子上,孩子去找桌子就能找到饭(两个人都去找桌子就行了,桌子也只有一个static,这样就完成了解耦)。
join--一个线程等待另一个线程的结束。--底层使用的就是保护性暂停模式的增强。
异步模式--生产者消费者模式。
为什么异步呢? 因为消息不会立马被消费
生产者消费者模式,主要是让生产者和消费者解耦,专注做自己的事情。相比于上面的保护性暂停,一对一的关系。生产者消费者模式可以实现一对多,节省资源。
哲学家就餐问题:
可以使用按顺序获取锁,也就是只能先获取编号较小的筷子,这样就一定会有一个哲学家拿到两个筷子,从而打破死锁。但是会产生饥饿问题。可重入锁能解决死锁和饥饿两个问题。
JMM:
在单线程的情况下,为了提高效率,jvm会对代码进行一些优化,比如指令重排,从线程的本地工作缓存中读取数据而不是每次去主存读取数据。这样在单线程情况下可以优化效率但是如果是在多线程的情况下,会引发可见性和有序性的问题。为了解决这些问题,使用volatile关键字通过读写屏障来实现可见性可有序性的问题。之前学习的synchronized关键字也可以解决这些问题,因为保证了原子性也就解决了多线程的问题这样也就解决了多线程引起的这些问题。
乐观锁:
底层是通过cas和volatile配合实现的,
CAS(Compare-And-Swap)是一种原子操作,用于实现无锁的并发控制。它比较内存中的值与预期值是否相同,如果相同,就将其更新为新值。CAS 操作通常用于实现高效的并发数据结构,如无锁队列、栈等。
CAS 的工作原理如下:
- 读取内存中的当前值。
- 将当前值与预期值进行比较。
- 如果相同,则将当前值替换为新值;否则,不做任何操作。
CAS 的核心优势是它可以在不加锁的情况下保证数据的一致性,广泛应用于高并发编程中。
volatile保证可见性。
CAS的效率跟cpu的核心数有关,只有在核心数高的情况的下才能提高效率,因为如果只有单核cpu,还是会因为cpu给线程分配的时间片用完导致上下文切换。
JUC工具包里使用CAS和voilatile配合实现了很多线程安全的类,比如原子整数,原子引用(里面还有版本号,和是否修改的版本),原子数组(用来实现数组里面数据的原子性),原子更改器(线程安全的更改对象里面的属性),原子累加器,LongAdder(对累加操作进行优化,具体方法是,底层通过累加单元数组,不同的线程在更改的时候去更改累加数组的元素,而不是去争夺同一个元素,将最后累加单元数组的元素进行求和就是最后的结果。里面还有一个缓存行伪共享(不同cpu核心对缓存行的进行更改,那么其他cpu核心使用对应的缓存行就会失效,需要去内存获取数据,这样就会影响效率,通过一个注解解决了这个问题)。
unsafe类我们可以通过反射去进行操作,他可以帮助我们自己去实现类似原子整数的类。
不可变类:
我们经常使用的String 、Integer等包装类,他们都是不可变类(使用final修饰,不对外提供set方法,更改值的时候其实是新创建了一个对象。也就是内部的值不可变),这些不可变类是线程安全的(指的是多线程在操作不可变类的对象的时候,实际上每个线程都是新创建了一个对象,本来的对象是不会变的,也就是说不存在一个线程能改另一个线程对象的值),但同样也仅仅是内部的值不会被改变,引用是可以改变的,当多个线程对这种线程安全类进行修改操作仍然可能出现问题,所以这个时候就需要使用CAS实现的原子类型出手了。
也就是说,不可变类的线程安全是相对的。
final关键字原理
AQS,ReentryLock原理: