常见的锁的概念
常见的锁的概念
- 可重入锁
- 公平锁/非公平锁
- 独享锁/共享锁
- 互斥锁/读写锁
- 乐观锁/悲观锁
- 分段锁
- 偏向锁/轻量级锁/重量级锁
- 自旋锁
修改 使用锁 或者同步机制
仅仅给变量添加volatile 是不行的 还会出现多卖少买状况
synchronized 简介 :非常经典的处理手段,具体使用有多种形式,它的核心思想就是修饰一个方法或者一段代码,这段代码不能同时两个以上的线程同时运行。
代码块 中的this 是调用该方法的对象 一般都是使用代码块
理解一下synchronized 直译过来 是同步的意思 但是我们京城称呼其为一种锁,为什么叫锁,原则上每个对象都可以持有一把锁,当某一个线程操作这个对象时, 这个线程就获得该对象的锁,在此期间其他线程就无法操作该对象了
Lock
简介:Lock是自JDK1.5 之后推出的一个接口,当热也是一系列子实现类,接口非常重要,定义了Java所认定的锁的概念,跟synchronized有显著区别,
synchronized是一个关键字,使用它可以实现类似锁的效果,但是严格的来说它并不是锁,Lock是一个接口,详细描述了锁的概念,其中重点包含若干方法,这些方法就是核心内容了。
方法:
lock():这个方法用来加锁,或者可以理解为获取对象的锁,如果获取成功,就执行下面的代码,若获取失败则一直等待,重复尝试获取
lockInterruptibly(); 也是尝试获取锁,但是对应带了Interrupter这个部分,所以对于获取锁的结果会进行进一步工作;
tryLock(); 尝试获取锁,返回值boolean类型,会返回获取锁的结果,跟lock()不一样,如果获取失败,返回false ,并且不会继续尝试获取;
unlock():解锁,运行之后即可释放锁,就能让其他线程获取了;
ReentrantLock:
是最常用的子实现类,加强对它的熟悉字面翻译它叫重入锁,确实它也是符合重入锁的要求,但是它仅仅可以表示重入锁。
可重入锁:
是一个概念,可重用锁在大部分时候并不直接表示RenntrantLock,可重用锁强调的是一种类型,这个类型关键特点是可重入,什么是可重入?
就是一个已经获得锁的线程还能继续调用加锁的代码。
这里我们强调可重入是一种典型的锁的类型,我们自己可以编码实现一些可重入锁,在实际项目中它的运用可以体现灵活性等等诸多优势点,就不一一展开。
ThreadDemo类
/**
* @Author: Jiangjun
* @Date: 2019/10/7 11:53
* @Description: 描述购票的逻辑
*/
public class ThreadDemo implements Runnable{
Lock lock = new ReentrantLock();
volatile int num = 20;
@Override
public void run() {
lock.lock();
while (num>0){
addlock();
System.out.println(Thread.currentThread().getName() + "认为此时还有票可选");
num = num - 1;
System.out.println(Thread.currentThread().getName() + "买到了一张票,还剩" + num);
}
lock.unlock();
}
public void addlock(){
lock.lock();
}
}
主函数:
public class TestMain {
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
Thread t1 = new Thread(td,"掌上编程");
Thread t2 = new Thread(td,"公众号");
t1.start();
t2.start();
}
}
公平锁:
公平=先来后到,先来的获取锁,就来的后获取锁。
同样公平锁也是一种概念,也是一种锁的类型,如果设计锁的机制符合公平要求,那么这个锁就是公平的锁。
若适用购票场景,如果强调先来后到,(从性能角度考虑)就必须采取特备的方案来实现这种先后顺序,这样必然会带来性能损失,所以公平锁的性能会劣于非公平锁。
如果我们使用非公平锁也会带带来一定风险,容易造成真实意义上的不公平,就有一些线程老早就在等待了,但是运气不好一直拿不到锁,这个可以成为锁饥饿现象。
因此我们强调根据情况来选择锁是否公平,一般如果要避免掉锁饥饿现象,就要考虑使用公平锁,否则可以使用非公平锁。
Synchronized 只能是非公平锁
ReentrantLock 是后来加入的类,所以在设计上更加完善,可以直接通过构造函数来制定锁是否公平。
细节注意:
养成比较规范的编码习惯,一旦使了Lock,要充分考虑到可能出现的报错情况,所以一般编码会这样写try...catch...fianlly
public class LockFairThread implements Runnable{
//创建公平锁
private static ReentrantLock lock = new ReentrantLock(true);
@Override
public void run() {
lock.lock();
try{
System.out.println(Thread.currentThread().getName() + "获得锁");
}catch (Exception e){
System.out.println("异常相关提示");
}finally {
lock.unlock();
}
}
}
独享锁/共享锁:
独享锁只能提供一个线程所享用,共享锁表示锁可以提供多个线程享用。
这里会觉得比较奇怪,感觉锁好像就是提供一个线程使用,这个其实不绝对,比如一些操作是可以共享的,比如我们在修改数据时,必须要独享,不允许多个地方同时对数据进行修改,容易导致一些错误,但是在读取数据时,其实可以多线程一起读。共享锁主要在一些场合使用,提升一下灵活性。
互斥锁/读写锁
乐观锁/悲观锁:
乐观锁强调认为并发不一定会导致数据出现不一致等问题,所以原则来说就是允许并发的发生,比如允许多个线程同时读写同一个变量,但是单纯的乐观相信也是不行的,还要想办法控制一下。
所以乐观锁强调一种思路和理念通过方式来允许并发出现又能够规避掉出错的情况,典型的方案就是使用版本号控制,下面就详细描述一下:
场景是修改商品的库存数据,比如原本库存100个,现在多个线程在操作商品表,他们在操作的时候,需要先读取数据,再减数据,比如先要读一下商品库存还有多少个,如果足够再做减法,减去商品的库存。这个商品中如果使用悲观锁,就是非常简单的直接把从读商品到减商品做成同步的,同时同一时间只允许一个线程来做先读后减。这就是悲观锁的典型思路,就是觉得一旦有并发出现,多个线程同时读写数据,一旦导致数据出现问题所以不允许出现并发情况的。显然悲观锁使用得非常少。使用乐观锁,就是允许多个线程同时去读写商品库存数据,但是想要个法子让他们同时读写之后还要不出问题。所以我们要加入一个检查机制,如果发现出现问题的话,就取这次操作,重新来过.具体做法就是给商品表额外添加一个字段,叫做version,然后每次读取商品之前先检查询version,再每一次修改商品之后让version+1,d当然操作和处理商品之前要检查一下version是否匹配。
下面我们通过一个现时场景进行描述:
- 线程A查看库存,此时读取的版本号,发现是0.
- 线程B查看库存,此时读取的版本号,发现是0.
- 线程A此时想要修改库存,所以检查下库存了,这时可以把检查版本号和扣减库存一次性完成,比如执行如下sql:UPDATE 表明 SET 库存=库存-20,version=version+1 WHERE 商品id=...AND version=0
- 执行第三步之后,赶紧检查一下自己修改是否成功,如果修改失败,意味着什么?意味着此时在查询数据时,根据版本号=0没有查询出来,表示在第一步和第三步之间,商品已经被修改了,所以就要撤销重做。如果修改成功,表示第一步和第三步之间没有出现问题,商品库存没有被其他线程修改过,就可以顺利的成功修改。
- 让B修改商品,此时就会失败,因为线程A在过程中已经修改过商品库存了,之前N读取的库存已经失效,它需要重新读取。
面试10个问题 1-2多线程
-
多线程基础(线程状态 、run/start 、 Thread/Runnable)
-
常用方法和经典问题(wait notify join 生产消费者)
-
同步基础 (volatile synchronized)
-
核心类库(Callable 集合)
-
JUC原子类(LongAdder AtomicReference)
-
JUC锁 (Lock AQS RenntrantLock ReadWriteLock)
-
锁概念(重入锁、公平锁、读写锁、乐观锁等等)