锁和线程池

在这里插入图片描述

要不要锁住线程呢?乐观 / 悲观

  • 乐观锁:总认为使用数据时别的线程不会修改数据,故不加锁,只在更新时判断其他线程在此之前有没有更改数据,一般用版本号机制/CAS实现
    版本号:在数据库中加一个数据版本号version字段,表示被修改次数,当数据被修改,version+1。线程A更新数据时,先读数据和数据版本号,提交更新时,吧刚读到的version和数据库中的version对比,一致才更新。
    CAS算法:(比较和交换)是一种无锁算法,不使用锁的情况下实现多线程间变量同步。
    三个操作数:V:内存中地址存放的实际值。A:进行比较的值(最开始的值)。B:要写入的新值。
    当且仅当V==A时,CAS使用原子操作用B更新V的值,不相等则只返回B的值。多个线程同时进行CAS操作时,只有一个线程会成功,并且更新V的值,其余的线程会失败。失败后可以选择不断的进行CAS操作,也可以直接挂起进行等待。
    ABA问题:内存值原来是A,然后变成B,后来又变成A,这时VAS进行检查时会发现值未改变,但实际上改变了
    解决: 在变量前加版本号,每更新一次就版本号加一,从而有 1-A,2-B,3-A
    JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在
    compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值
  • 悲观锁:认为使用数据时一定会有别的线程来修改数据,故获取数据前会先加锁,别的线程来了都会阻塞挂起。synchronized和Lock实现类。

同步资源获取失败线程要阻塞吗?自旋锁/非自旋锁

由于无法获得同步资源,如果在这时切换CPU状态为阻塞,则会浪费处理器时间。如果代码简单同步资源锁定时间短,那么切换时间可能比等待到同步资源的时间还长。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。
自旋锁:线程获取锁的时候,如果锁被其他线程持有,则当前线程将循环等待,直到获取到锁。原理是CAS。
bad:
1、虽然自旋锁避免线程切换的开 销,不过也会占用处理器时间。如果锁的占用时间很长,自旋的线程只会白白浪费处理器资源。(自旋时间有限度,自旋超过一定次数(默认10)仍未获得锁,则挂起线程。)
2、自旋是一种不公平的模式:处于阻塞状态的线程无法立刻竞争被释放的锁;而处于自旋状态的线程很有可能先获取到锁。不能让等待时间最长的线程优先获得锁,存在“线程饥饿”问题。

自适应的自旋锁:自适应即可以由同一锁对象的前一次自旋时间和锁的拥有者的状态决定。若上一次自旋等待刚成功获得锁,则虚拟机认为下次自旋也有可能成功,就允许等相对更长的时间,反之很少成功自旋的则直接挂起后面的线程。

synchronized

一、使用
synchronized可以加在方法上,
可以先Object o=new Object() synchronized(o){代码块}
可以用synchronized(this){代码块}(注意方法和属性若为静态,就没有this对象可以锁定,要用对象名.class获得对象)
但注意synchronized锁定的是当前的对象
一个加了synchronized的代码块相当于一个原子操作,线程执行该代码块就不会被打断。

  • synchronized是可重入锁
  • 线程抛出异常后锁就被释放,若不想释放要加try catch
    synchronized通过Monitor来实现线程同步,Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步。

互斥锁在Java对象头里 ,Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。
每一个Java对象中有一把锁,称为内部锁或者Monitor锁。
Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用,此时不能被其它线程再占用。

volatile

当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。
对volatile 变量进行读写的时候,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步

1.保证此变量对所有的线程的可见性。当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新
2、保证线程间有序性。禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)

volatile关键字,使一个变量在多个线程间可见,只保证可见性没有原子性,即读过来的一定是内存堆中的新值,但是不能处理多个线程共同修改变量时修改值重复覆盖所带来的不一致问题

AtomicInteger实现

AtomicInteger有原子性(但多个使用AtomicInteger的方法无原子性)
value使用volatile修饰,保证线程间可见性。
在这里插入图片描述
当线程1和线程2通过getIntVolatile拿到value的值都为1,线程1被挂起,线程2继续执行.
线程2在compareAndSwapInt操作中由于预期值和内存值都为1,因此成功将内存值更新为2。
线程1在compareAndSwapInt操作中,预期值是1,而当前的内存值为2,CAS操作失败,什么都不做,返回false。线程1通过getIntVolatile拿到线程2更新的内存值2,此时再进行compareAndSwapInt操作就成功,内存值更新为3

竞争同步资源的流程不同?无锁/偏向/轻量级/重量级

对象实例由对象头、实例数据组成。
对象头包括Mark Word(标记字段)、Klass Pointer(类型指针)
Mark Word:用于存储对象的HashCode,GC分代年龄和锁标识位信息。
(运行期间Mark Word里存储的数据会随着锁标志位的变化而变化)
Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

锁升级

为了提高获得锁与释放锁的效率,JDK1.6之后对内建锁做了优化(新增偏向,轻量级锁),
锁可以升级(偏向->轻量级->重量级)但不能降级,目的是提高获得和释放锁的效率

无锁

没有锁定资源,所有线程都可以访问并修改同一个资源,但只有一个线程可以成功修改
修改操作在循环内进行。线程会不断尝试修改共享资源,修改成功就会退出。CAS的原理及应用就是无锁的。

偏向锁

若同步代码从始至终只被一个线程访问,那么该线程会自动获取锁降低获取锁的代价

**步骤:**线程访问同步代码块并获得锁时,会在对象头的栈帧中的锁记录中记录存储偏向锁的线程ID。再次进入时,先检测MarkWord中是否存储当前线程ID,若是则直接进入同步代码块无需CAS加锁和解锁;若未存储ID则检测当前偏向锁字段是否是0,若是0则将偏向锁字段设为1,并将自己的线程ID更新至MarkWord中;若为1则表示此偏向锁已经被别的线程获取。则不断尝试使用CAS操作获取偏向锁/撤销偏向锁升级为轻量级锁(偏向锁的撤销开销较大,需要等待线程进入全局安全点(当前线程在CPU上没有任何有用的字节码在执行),而后会暂停所有拥有偏向锁的线程,判断锁对象是否处于被锁定状态。撤销锁后恢复至无锁(标志位01)或轻量级锁(00))

轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可

适用:从始至终只被一个线程访问的代码块。
good:加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距
bad:若线程有锁竞争,会带来额外的锁撤销的消耗。

轻量级锁

偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁。其他线程会通过自旋的形式尝试获取锁,不会阻塞。从而提高性能。
1、线程请求访问代码块时,若标志位为01(无锁),虚拟机会在当前线程的栈帧中建立一个叫锁记录的空间,将代码块的对象头中的MarkWord复制到锁记录中。
2、虚拟机使用CAS将对象的MarkWord中的指针指向改线程的锁记录。并且将锁记录的owner指针指向对象的MarkWord。
3、若更新成功,线程拥有对象的锁,锁标志位设置为00(轻量级锁)。
若更新失败,虚拟机先检查对象的MarkWord是否指向当前线程的栈帧
4、若是则表明线程已经拥有锁,直接访问代码块;
若不指向当前线程的栈帧,说明存在竞争
5、若目前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。

适合:响应时间短,同步代码块执行快,多线程在不同时间申请同一把锁的情况
good:竞争的线程不会阻塞,提高了程序的响应速度。
bad:自旋消耗cpu。

重量级锁

适合:追求吞吐量,同步块执行速度较长。
good:线程竞争不使用自旋,不会消耗CPU。
bad:线程阻塞,响应时间缓慢。

升级为重量级锁后,标识位是10。此时除了拥有锁的线程其他所有等待锁的线程都进入阻塞状态。

多线程竞争锁要排队吗?公平/非公平

公平锁

多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。
good:等待锁的线程不会饿死。
bad:整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大

非公平锁

多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待
若此时锁刚好可用,就不用阻塞直接获取锁。
good:减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。
bad:处于等待队列中的线程可能会饿死,或者等很久才会获得锁

区别

唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()
在这里插入图片描述
判断当前线程是否位于同步队列中的第一个。如果是则返回true,否则返回false

ReentrantLock里面有一个内部类Sync,Sync继承AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在Sync中实现的。它有公平锁FairSync和非公平锁NonfairSync两个子类。ReentrantLock默认使用非公平锁

一个线程的多个流程能不能获得同一把锁?可重入/非可重入

可重入锁一定程度避免了死锁

同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁,不会因为之前已经获取过还没释放而阻塞
ReentrantLock和synchronized都是可重入锁
good:可一定程度避免死锁,因为外层方法的使用中可能需要调用内层方法,而此时外层的锁还未释放,内层方法无法获取锁,外层方法因此也无法执行完毕从而释放锁,产生死锁。

分析源码:
ReentrantLock(可重入)和NonReentrantLock(非可重入)都继承父类AQS,其父类AQS中维护了一个同步状态status来计数重入次数,status初始值为0。

线程尝试获取锁时,可重入锁先尝试获取并更新status值,如果status == 0表示没有其他线程在执行同步代码,则把status置为1。若status!=0判断当前线程是不是获取到这个锁的线程,若是则status+1,且当前线程可再次获取锁而不是像非重入锁那样,status!=0则获取锁失败,阻塞当前线程。
线程释放锁时,可重入锁先获取status值,在当前线程是持有锁的线程的前提下,若status-1==0,则表示当前线程所有重复获得锁的操作都指行完了,这是该线程才释放锁。非可重入锁则在确定线程是持有锁的线程后直接将status=0,释放锁。
在这里插入图片描述

多个线程可否持有一个锁?共享/独享锁

独享(互斥)锁

该锁一次只能被一个线程所持有。
如果线程T对数据A加上互斥锁后,则其他线程不能再对A加任何类型的锁
获得互斥锁的线程即能读数据又能修改数据
JDK中的synchronized和JUC中Lock的实现类就是互斥锁

共享锁

该锁可被多个线程所持有。
如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加互斥锁
获得共享锁的线程只能读数据,不能修改数据

读写锁解析

在这里插入图片描述
ReentrantReadWriteLock有两把锁:ReadLock(读)和WriteLock(写),而ReadLock和WriteLock是靠内部类Sync实现的锁,这个Sync是AQS的一个子类,这种结构在CountDownLatch、ReentrantLock、Semaphore里面也都存在。
读锁和写锁的加锁方式不一样,读锁是共享锁,可以保证并发度,写锁是独享锁。读写、写读、写写的过程互斥。因为读写锁是分离的。故ReentrantReadWriteLock并发性比一般的互斥锁有了很大提升

读锁和写锁具体加锁方式上,看AQS
在这里插入图片描述
AQS是一个抽象类,定义了同步器中获取锁和释放锁,目的来让自定义同步器组件来使用或重写。其子类大多用Sync的静态内部类继承AQS类,重写AQS的一些方法来实现自定义同步器。

AQS的state关键词(int 32位)用于描述有多少线程获得锁。
ReentrantReadWriteLock中有读、写两把锁,故分为高16位描述读锁个数,低16位描述写锁个数。

写锁加锁源码

在这里插入图片描述

  • 先获得当前锁的个数c,通过c获取写锁个数w(高16位和0与运算后是0,剩下的就是低位运算的值)
  • 判断是否已经有线程持有了锁。
  • 如果已经有线程持有了锁(c!=0),查看当前写锁线程的数目,若写线程数为0(有读锁),或持有锁的线程非当前线程就返回失败
  • 若写锁数量大于最大数(2^16-1)抛出error
  • 若无线程持有锁,此时如果
    写线程数为0,且当前线程需要阻塞就返回失败;或通过CAS增加写线程失败也返回失
    败。
  • 如果c=0,w=0或者c>0,w>0(重入),则设置当前线程或锁的拥有者,返回成功

除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:必须确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。

只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0时表示写锁已被释放,然后等待的读写线程才能够继续访问读写锁,同时前次写线程的修改对后续的读写线程可见

读锁代码

在这里插入图片描述
如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。
读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是“1<<16”。

所以读写锁才能实现读读的过程共享,而读写、写读、写写的过程互斥。

ReentrantLock

在这里插入图片描述
ReentrantLock虽然有公平锁和非公平锁两种,但是它们添加的都是独享锁。根据源码所示,当某一个线程调用lock方法获取锁时,如果同步资源没有被其他线程锁住,那么当前线程在使用CAS更新state成功后就会成功抢占该资源。而如果公共资源被占用且不是被当前线程占用,那么就会加锁失败。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值