导图社区 锁分类限制多线程入门教程思维导图
锁分类限制多线程入门教程思维导图,包括锁的基本问题,锁的分类,分布式锁,数据库锁。感兴趣的小伙伴可以下载收藏哦。
编辑于2023-02-21 22:52:11 广东锁分类限制多线程入门教程思维导图
锁的基本问题
1、锁是什么?干什么用?
锁主要用来实现资源共享的同步。只有获取到了锁才能访问该同步代码,否则等待其他线程使用结束释放锁。 一句话:限制多线程资源竞争
2、知道下面这些锁吗?
自旋锁
阻塞锁
可重入锁
读写锁
互斥锁
悲观锁
乐观锁
公平锁
偏向锁
对象锁
线程锁
锁粗化
锁消除
轻量级锁
重量级锁
信号量
独享锁
共享锁
分段锁
闭锁
锁分类
其实我们真正用到的锁也就那么两三种,只不过依据设计方案和性质对其进行了大量的划分。
常见(kao)的锁
Synchronized
Synchronized,它就是一个:非公平,悲观,独享,互斥,可重入的重量级 Synchronized a (){ b(); } Synchronized b (){ }
Lock
以下两个锁都在JUC包下,是API层面上的实现 ReentrantLock,它是一个:默认非公平但可实现公平的,悲观,独享,互斥,可重入,重量级锁。 ReentrantReadWriteLocK,它是一个,默认非公平但可实现公平的,悲观,写独享,读共享,读写,可重入,重量级锁。 ReentrantLock lock = new ReentrantLock(); lock .lock(); {} lock .unlock(); 公平锁:排队机制 AQS:百度
ReentrantLock与synchronized 的区别
ReentrantLock(可重入锁)
中断等待
ReentrantLock 拥有Synchronized相同的并发性和内存语义,此外还多了 锁投票,定时锁等候和中断锁等候。 线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定 如果使用 synchronized ,如果A不释放,B将一直等下去,不能被中断 如果 使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时间以后,中断等待,而干别的事情
ReentrantLock获取锁定有三种方式
lock(),
如果获取了锁立即返回,如果别的线程持有锁, 当前线程则一直处于休眠状态,直到获取锁
tryLock(),
如果获取了锁立即返回true, 如果别的线程正持有锁,立即返回false
tryLock(long timeout,TimeUnit unit)
如果获取了锁定立即返回true, 如果别的线程正持有锁, 会等待参数给定的时间, 在等待的过程中,如果获取了锁定,就返回true, 如果等待超时,返回false;
lockInterruptibly:
如果获取了锁定立即返回, 如果没有获取锁定,当前线程处于休眠状态, 直到获取锁定,或者当前线程被别的线程中断
可实现公平锁
对于ReentrantLock而言, 通过构造函数指定该锁是否是公平锁, 默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
锁绑定多个条件
锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无须这样做,只需要多次调用newCondition()方法即可。
synchronized(也可重入)
优势
synchronized是在JVM层面上实现的, 不但可以通过一些监控工具监控synchronized的锁定, 而且在代码执行时出现异常,JVM会自动释放锁定, 但是使用Lock则不行,lock是通过代码实现的, 要保证锁定一定会被释放,就必须将unLock()放到finally{}中
场景
在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock, 但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态; 实际上,我推荐大家以压力测试为准
按照性质分类
公平锁/非公平锁
公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序, 有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁
非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁。
ReentrantLock是通过AQS的来实现线程调度,实现公平锁(AbstractQueuedSynchronizer)
乐观锁/悲观锁
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度
悲观锁
悲观锁认为对于同一个数据的并发操作, 一定是会发生修改的,哪怕没有修改,也会认为修改。 因此对于同一个数据的并发操作,悲观锁采取加锁的形式
悲观锁适合写操作非常多的场景
悲观锁在Java中的使用,就是利用各种锁。
乐观锁
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。 在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。 乐观的认为,不加锁的并发操作是没有事情的
乐观锁适合读操作非常多的场景
不加锁会带来大量的性能提升
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法, 典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。 CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。
独享锁/共享锁
独享锁
独享锁是指该锁一次只能被一个线程所持有。
ReentrantLock是独享锁。
Synchronized是独享锁
共享锁
共享锁是指该锁可被多个线程所持有
ReentrantReadWriteLock,其读锁是共享锁,其写锁是独享锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。独享锁与共享锁也是通过AQS来实现的, 通过实现不同的方法,来实现独享或者共享。
互斥锁/读写锁
独享锁/共享锁是一种广义的说法,互斥锁/读写锁就是具体的实现。
互斥锁
ReentrantLock
读写锁
读写锁在Java中的具体实现就是ReentrantReadWriteLock
可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层加锁方法会自动获取锁。
a(){ if(lock.lock()){ }else{ // a(); 递归 //while true;自旋 } }
ReentrantLock、Synchronized都是可重入锁,可重入锁的一个好处是可一定程度避免死锁。
public sychrnozied void test() { xxxxxx; test2(); } public sychronized void test2() { yyyyy; } 在上面代码段中,执行 test 方法需要获得当前对象作为监视器的对象锁,但方法中又调用了 test2 的同步方法。 如果锁是具有可重入性的话,那么该线程在调用 test2 时并不需要再次获得当前对象的锁,可以直接进入 test2 方法进行操作。 如果锁是不具有可重入性的话,那么该线程在调用 test2 前会等待当前对象锁的释放,实际上该对象锁已被当前线程所持有,不可能再次获得。
如果锁是不具有可重入性特点的话,那么线程在调用同步方法、含有锁的方法时就会产生死锁。
所以所有的锁都应该被设计成可重入的
按照设计分类
自旋锁/自适应自旋锁
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞, 而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。 非阻塞方式获取锁
自适应自旋
如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费。
在JDK1.6中引入了自适应的自旋锁。
自旋的时间不固定,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长时间,比如100个循环。 如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源
什么是阻塞方式获取锁
阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)
sleep():睡眠
Thread.sleep (long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。 顺序进入同步块的,不释放锁,持有monitor对象锁,其他线程是不能进入的。//忍让一段时间
阻塞而不释放锁
wait():等待
wait() 与 notify/notifyAll()都是放在同步代码块中才能够执行的。 Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait() 一样。 wait() 和 notify() 方法:两个方法配套使用,wait() 使得线程进入阻塞状态,它有两种形式,一种允许 指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify() 被调用. 当调用wait方法后,当前线程会释放持有的monitor对象锁,因此,其他线程还可以进入到同步方法,线程被唤醒后(如果加时间参数的话,则会在时间被消耗后唤醒,否则需要通过notify或notifyall唤醒),需要竞争锁,获取到锁之后再继续执行。//无条件忍让一段时间
阻塞并释放锁
public class Service { public void testMethod(Object lock) { try { synchronized (lock) { System.out.println("begin wait() ThreadName=" + Thread.currentThread().getName()); lock.wait(); System.out.println(" end wait() ThreadName=" + Thread.currentThread().getName()); } } catch (InterruptedException e) { e.printStackTrace(); } } public void synNotifyMethod(Object lock) { try { synchronized (lock) { System.out.println("begin notify() ThreadName=" + Thread.currentThread().getName() + " time=" + System.currentTimeMillis()); lock.notify(); Thread.sleep(5000); System.out.println(" end notify() ThreadName=" + Thread.currentThread().getName() + " time=" + System.currentTimeMillis()); } } catch (InterruptedException e) { e.printStackTrace(); } } } ================== public class ThreadA extends Thread { private Object lock; public ThreadA(Object lock) { super(); this.lock = lock; } @Override public void run() { Service service = new Service(); service.testMethod(lock); } } public class SynNotifyMethodThread extends Thread { private Object lock; public SynNotifyMethodThread(Object lock) { super(); this.lock = lock; } @Override public void run() { Service service = new Service(); service.synNotifyMethod(lock); } } ================== public class Test { public static void main(String[] args) throws InterruptedException { Object lock = new Object(); ThreadA a = new ThreadA(lock); a.start(); //NotifyThread notifyThread = new NotifyThread(lock); // notifyThread.start(); SynNotifyMethodThread c = new SynNotifyMethodThread(lock); c.start(); } }
yield():礼让
Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。yield() 使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程. 放弃当前时间片,将Running状态转变为Runnable状态,不能指定多长时间。//假装忍让,完成具有不确定性不受监督的切换
暂停当前线程,主动让出自己的CPU时间
join():插队
类似sleep,停止当前线程,让join线程先执行完毕,或执行指定的时间。//插队拼接 ======main线程要等到t1线程运行结束后,才会输出“main end”。如果不加t1.join(),main线程和t1线程是并行的。而加上t1.join(),程序就变成是顺序执行了。=========== public static void main(String[] args) throws InterruptedException { System.out.println("main start"); Thread t1 = new Thread(new Worker("thread-1")); t1.start(); t1.join(); System.out.println("main end"); } ============================ ==============线程是顺序执行的。============================= public static void main(String[] args) throws InterruptedException { System.out.println("main start"); Thread t1 = new Thread(new Worker("thread-1")); Thread t2 = new Thread(new Worker("thread-2")); t1.start(); //等待t1结束,这时候t2线程并未启动 t1.join(); //t1结束后,启动t2线程 t2.start(); //等待t2结束 t2.join(); System.out.println("main end"); }
当前线程等待join进来的执行完,再继续
suspend()和resume():暂停/恢复
两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume() 被调用,才能使得线程重新进入可执行状态。典型地,suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。Thread中suspend()和resume()两个方法在JDK1.5中已经废除,不再介绍。因为有死锁倾向。
有死锁倾向
线程类常用方法
sleep(): 强迫一个线程睡眠N毫秒。 isAlive(): 判断一个线程是否存活。 join(): 等待线程终止。 activeCount(): 程序中活跃的线程数。 enumerate(): 枚举程序中的线程。 currentThread(): 得到当前线程。 isDaemon(): 一个线程是否为守护线程。 setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束) setName(): 为线程设置一个名称。 wait(): 强迫一个线程等待。 notify(): 通知一个线程继续运行。 setPriority(): 设置一个线程的优先级。
锁粗化/锁消除
锁粗化和消除其实设计原理都差不多,都是为了减少没必要的加锁
锁消除
锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。 锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。
锁粗化
如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部
偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,并且是针对Synchronized
偏向锁
偏向锁是指一段同步代码一直被一个线程所访问, 那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁
是指当锁是偏向锁的时候,被另一个线程所访问, 偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁
当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去, 当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。 重量级锁会让其他申请的线程进入阻塞,性能降低。
分段锁
分段锁是一种锁的设计,并不是具体的一种锁
ConcurrentHashMap并发的实现就是通过分段锁的形式来实现高效的并发操作
ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。 当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。 但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
分布式锁
Redisson
数据库锁
http://hedengcheng.com/?p=771#_Toc374698307
表锁
行锁
间隙锁
悲观锁
乐观锁
共享锁(读锁)
排他锁(写锁)