目录
volatile
死锁
互斥锁与自旋锁
读写锁
乐观锁与悲观锁
公平锁和非公平锁
你知道哪几种线程锁
多线程锁
在多线程编程中,volatile关键字的作用是保证变量的可见性,即一个线程修改了一个volatile变量的值,其他线程可以立即看到这个变化。
volatile关键字的主要作用是告诉编译器不要对这个变量进行优化,不要从寄存器上读取,每次读取该变量时都是从内存中读取最新的值。
使用volatile关键字修饰变量有以下几个特点:
指令重排序是指在程序执行过程中,为了提高性能,编译器和处理器会对指令进行重新排序。一般情况下,编译器和处理器会对指令进行优化,以提高代码、指令的执行效率。但是,这种优化可能会导致代码执行顺序与预期不一致,即出现指令重排。
需要注意的是,虽然volatile关键字可以保证可见性和禁止指令重排序,但是它并不能替代锁机制来实现线程安全。在使用volatile关键字时,仍然需要考虑线程安全问题,比如使用互斥锁等机制来保证多个线程对同一个volatile变量的访问不会发生冲突。
简单来说,死锁问题的产生是由两个或者以上线程并行执行的时候,争夺资源而互相等待造成的。
死锁必要条件
举个例子,小林拿了小美房间的钥匙,而小林在自己的房间里,小美拿了小林房间的钥匙,而小美也在自己的房间里。如果小林要从自己的房间里出去,必须拿到小美手中的钥匙,但是小美要出去,又必须拿到小林手中的钥匙,这就形成了死锁。
死锁只有同时满足以下四个条件才会发生:
1.互斥条件
互斥条件是指多个线程不能同时使用同一个资源。
比如下图,如果线程 A 已经持有的资源,不能再同时被线程 B 持有,如果线程 B 请求获取线程 A 已经占用的资源,那线程 B 只能等待,直到线程 A 释放了资源。
2.持有并等待条件
持有并等待条件是指,当线程 A 已经持有了资源 1,又想申请资源 2,而资源 2 已经被线程 B 持有了,所以线程 A 就会处于等待状态,但是线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1。
3.不可剥夺条件
不可剥夺条件是指,当线程已经持有了资源 ,在自己使用完之前不能被其他线程获取,线程 B 如果也想使用此资源,则只能在线程 A 使用完并释放后才能获取。
4.环路等待条件
环路等待条件指的是,在死锁发生的时候,两个线程获取 资源的顺序构成了环形链。
比如,线程 A 已经持有资源 2,而想请求资源 1, 线程 B 已经获取了资源 1,而想请求资源 2,这就形成资源请求等待的环形图。
利用工具排查死锁问题
如果你想排查你的 Java 程序是否死锁,则可以使用 jstack 工具,它是 jdk 自带的线程堆栈分析工具。
由于小林的死锁代码例子是 C 写的,在 Linux 下,我们可以使用 pstack + gdb 工具来定位死锁问题。
pstack 命令可以显示每个线程的栈跟踪信息(函数调用过程),它的使用方式也很简单,只需要 pstack
那么,在定位死锁问题时,我们可以多次执行 pstack 命令查看线程的函数调用过程,多次对比结果,确认哪几个线程一直没有变化,且是因为在等待锁,那么大概率是由于死锁问题导致的。
避免死锁问题的发生
那么避免死锁问题就只需要破环其中的一个必要条件就可以,最常见的并且可行的就是使用资源有序分配法,来破环环路等待条件。
那什么是资源有序分配法呢?
线程 A 和 线程 B 获取资源的顺序要一样,
当线程 A 是先尝试获取资源 1,然后尝试获取资源 2 的时候,
线程 B 同样也是先尝试获取资源 1,然后尝试获取资源 2。
也就是说,线程 A 和 线程 B 总是以相同的顺序申请自己想要的资源,就可以打破死锁了。
二者区别
互斥锁
互斥锁是一种「独占锁」,比如当线程 A 加锁成功后,此时互斥锁已经被线程 A 独占了,只要线程 A 没有释放手中的锁,线程 B 加锁就会失败,于是就会释放 CPU 让给其他线程,既然线程 B 释放掉了 CPU,自然线程 B 加锁的代码就会被阻塞。
互斥锁加锁失败时,会从用户态陷入到内核态,存在一定的性能开销成本。
互斥锁加锁失败时,会用「线程切换」来应对,当加锁失败的线程再次加锁成功后的这一过程,会有两次线程上下文切换的成本,性能损耗比较大。
自旋锁
自旋锁是通过 CPU 提供的 CAS 函数,在「用户态」完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些。
一般加锁的过程,包含两个步骤:
CAS 函数就把这两个步骤合并成一条硬件级指令,形成原子指令,这样就保证了这两个步骤是不可分割的,要么一次性执行完两个步骤,要么两个步骤都不执行。
使用自旋锁的时候,当发生多线程竞争锁的情况,加锁失败的线程会「忙等待」,直到它拿到锁。这里的「忙等待」可以用 while 循环等待实现。
自旋锁开销少,在多核系统下一般不会主动产生线程切换。
读写锁是一种并发控制机制,用于管理多个线程对共享资源的访问。与传统的互斥锁不同,读写锁分为读锁(共享锁)和写锁(互斥锁),它们提供了更细粒度的控制,以提高并发性和性能。
如果只读取共享资源用「读锁」加锁,如果要修改共享资源则用「写锁」加锁。
所以,读写锁适用于能明确区分读操作和写操作的场景。读操作不阻塞读操作
读写锁的工作原理是:
所以说,写锁是独占锁,因为任何时刻只能有一个线程持有写锁,类似互斥锁和自旋锁,
而读锁是共享锁,因为读锁可以被多个线程同时持有。
根据偏袒读方还是写方,可以分为读优先锁和写优先锁,读优先锁并发性很强,但是写线程会被饿死,而写优先锁会优先服务写线程,读线程也可能会被饿死,那为了避免饥饿的问题,于是就有了公平读写锁,它是用队列把请求锁的线程排队,并保证先入先出的原则来对线程加锁(就是保证了加锁的顺序),这样便保证了某种线程不会被饿死,通用性也更好点。
前面提到的互斥锁、自旋锁、读写锁,都是属于悲观锁。
悲观锁做事比较悲观,它认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。
那相反的,如果多线程同时修改共享资源的概率比较低,就可以采用乐观锁。
乐观锁做事比较乐观,它假定冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。
可见,乐观锁的心态是,不管三七二十一,先改了资源再说。另外,你会发现乐观锁全程并没有加锁
乐观锁虽然去除了加锁解锁的操作,但是一旦发生冲突,重试的成本非常高,所以只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。
公平锁和非公平锁是针对锁的获取方式而言的。
公平锁是指多个线程按照申请锁的顺序来获取锁,即先到先得的原则。当线程A释放锁后,线程B、C、D依次获取锁,如果此时线程E申请锁,则它需要等待B、C、D依次获取到锁并释放锁后才能获取锁。
非公平锁是指多个线程获取锁的顺序是随机的,不保证公平性。当线程A释放锁后,线程B、C、D等线程都可以通过竞争获取到锁,而此时线程E也可以通过竞争获取到锁。
在实际应用中,公平锁可以避免饥饿现象,但是由于需要维护线程队列,因此效率相对较低。而非公平锁由于不需要维护线程队列,因此效率相对较高,但是可能会导致某些线程长时间无法获取锁。
互斥锁:它用于保护共享资源,一次只允许一个线程访问资源。如果一个线程已经获得了互斥锁,其他线程必须等待锁被释放才能访问资源。
读写锁:读写锁允许多个线程同时读取共享资源,但只允许一个线程写入资源。这提高了读操作的并发性能,因为多个线程可以同时读取资源,但写操作需要排他性访问资源。
自旋锁:自旋锁是一种基于忙等待的锁,它不会将线程切换到休眠状态,而是让线程一直尝试获取锁,直到成功。自旋锁适用于对资源的竞争时间很短的情况,避免了线程切换的开销。
多线程锁是一种用来保护共享资源的机制。在多线程编程中,如果多个线程同时访问同一个共享资源,可能会发生竞态条件,导致程序的行为出现未定义的情况。为了避免这种情况的发生,可以使用多线程锁来保护共享资源。
多线程锁的基本思想是,在访问共享资源之前先获取锁,访问完成之后再释放锁。这样可以保证同一时刻只有一个线程可以访问共享资源,从而避免竞态条件的发生。
常见的多线程锁包括互斥锁、读写锁、条件变量等。其中,互斥锁用于保护共享资源的访问,读写锁用于在读多写少的情况下提高并发性能,条件变量用于线程之间的同步和通信。
条件变量是一种用于多线程编程的同步原语,用于实现线程之间的协调和通信。条件变量通常与互斥锁一起使用,以等待某个条件的发生并在条件满足时唤醒等待的线程。条件变量提供了一种高级的线程同步机制,用于解决特定类型的并发问题。
互斥锁:它用于保护共享资源,一次只允许一个线程访问资源。如果一个线程已经获得了互斥锁,其他线程必须等待锁被释放才能访问资源。
读写锁:读写锁允许多个线程同时读取共享资源,但只允许一个线程写入资源。这提高了读操作的并发性能,因为多个线程可以同时读取资源,但写操作需要排他性访问资源。
自旋锁:自旋锁是一种基于忙等待的锁,它不会将线程切换到休眠状态,而是让线程一直尝试获取锁,直到成功。自旋锁适用于对资源的竞争时间很短的情况,避免了线程切换的开销。