并发编程中两个关键问题:线程之间如何通信(隐式进行,对程序员完全透明)以及如何同步
线程之间的通信由JMM(java内存模型)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见,抽象来说共享变量存储在主内存,每个线程有一个私有的本地内存,里面存放了该线程读/写共享变量的副本
就是JMM通过控制主内存与每个线程的本地内存进行交互,为程序员提供可见性的保证
Java锁:
synchronized和reentrantlock的底层实现及重入的底层实现原理
reentrantLock通过AQS类提供了一种基于FIFO等待队列的同步机制,通过CAS控制锁的获取和释放;
会通过CAS控制AQS的状态值,值为1表示线程已经获得锁,线程释放锁时会通过AQS的release方法来释放资源
并支持公平锁和非公平锁两种模式,具备可重入特性;同步非阻塞,采用乐观并发策略
公平锁模式下按照锁申请的顺序依次获取锁,非公平模式下通过抢占模式获取锁
synchronized底层是同步阻塞机制,采用的是悲观并发策略,基于进入和退出Monitor对象来实现方法同步和代码块同步
对象头主要由Mark Word和Class Metadata Address构成,前者存储对象的hashCode,锁信息或GC标志等;后者通过指针确定该对象是哪个类的实例
描述锁的四种状态和升级过程
锁对象的对象头中由一个threadid字段,第一次访问threadid为空,JVM让其持有偏向锁,将threadid设置为id,再次进入时会先判断threadid是否与线程id一致,一致则可以直接使用,不一致则升级为轻量级锁,会自旋一定次数获取锁,若还未正常获取要使用到要获取到对象,此时就会把锁从轻量级升级为重量级锁
自旋锁一定比重量级锁效率高吗
自旋锁:一个线程获取锁时,锁已经被其他线程获取,线程将循环等待,不断判断直到获取锁才能退出循环
线程尝试进入同步块时,发现锁已经被其他线程占用会进入阻塞状态,等待的线程会通过竞争获取锁
一定,效率高
synchronized实际上是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换打断,具有可重入性,若当前线程已经获得了某个对象的锁,那它可以获得该对象的其他synchronized代码块,不会被自己持有的锁阻塞,使用sysnchronized关键字时内存屏障会保证本地线程中被修改的变量被刷新回主内存,从而保证了多个线程之间对变量修改的可见性。可有效解决重排序问题,确保线程互斥的访问同步代码。
一个对象里面有多个synchronized方法,某一时间,只要有一个线程调用其中的一个synchronized方法,其他的线程都只能等待。锁的是当前对象,被锁定后,不能进入到当前对象的其他synchronized方法
静态同步方法锁的是当前类的Class对象,同步方法块,锁定的是synchronized括号内的对象
所有的静态同步方法用的是同一把锁---类对象本身,普通同步方法锁的是实例对象本身,如果一个实例对象的普通同步方法拿到锁后,该实例对象的其他普通同步方法必须等待该普通同步方法释放锁才能获取锁;静态同步方法和普通同步方法之间不会有竞争关系
synchronzied实现原理是什么?
代码块同步使用monitorenter和monitorexit指令实现,方法同步也可以使用这两个指令实现
是基于 Java对象头的监视器实现,同步块的进入和退出都需要获取和释放对象的监视器,当线程尝试进入一个被锁住的同步块时,会先尝试获取对象的监视器锁,若锁已经被占用,线程就会进入阻塞状态,直到被释放。
虽然 monitorenter
和 monitorexit
是字节码层面的指令,它们在 JVM 层面实现了线程同步的具体操作。
synchronized优化原理:
如果一个线程虽然有多线程访问,但是多线程的访问是错开的,那么可以使用轻量级锁来优化。但每次发生锁重入的时候,仍然需要执行CAS操作。
在尝试加轻量级锁的过程中,CAS操作无法完成,有其它线程为此对象加上了轻量级锁,这时需要进行锁膨胀,将轻量级锁变为重量级锁。
重量级锁竞争时,还可以使用自旋锁来进行优化,若当前线程自旋成功,那么线程可以避免阻塞。
synchronized用的锁存在于java对象头中的Mark word中,锁升级依赖MarkWord中的锁标志位和释放偏向锁标志位;
偏向锁:MarkWord存储偏向的线程ID;多线程时,存在锁由同一个线程多次获得,当有另外线程来竞争锁时,需要升级为轻量级锁
轻量锁:存储指向线程栈中Lock Record的指针
重量锁:存储指向堆中的monitor对象的指针
自适应自旋锁:线程如果自旋成功,下次自旋的次数将会增加;如果很少自旋成功,下次会减少自旋次数甚至不自旋
轻量锁每次退出同步块都需要释放锁,偏向锁只有在发生竞争时才会释放锁
锁的优缺点:
锁消除:java虚拟机在JIT时,通过扫描运行的上下文,去除掉不可能存在共享资源竞争的锁,通过锁消除,可以节省没有意义的锁请求时间;
锁粗化:多数情况下,我们期望将多次锁的请求合并为一个请求,降低短时间内大量锁请求,同步,释放带来的性能损耗。
Synchronized和Lock的区别:
Synchronized为内置的java关键字,Lock是一个java类
Synchronized无法判断锁的状态,Lock可以判断是否获取了锁
Synchronized会自动释放锁,Lock不会释放(不释放可能造成死锁)
Synchronized可重入锁,不可以中断,非公平;Lock,可重入锁,可以判断锁,非公平(可以自己设置)
Synchronized适合锁少量代码,Lock适合锁大量代码
CAS:
CAS是乐观锁的一种典型实现机制,JDK提供的一种非阻塞性原子操作,解决了原子性问题,存在ABA问题,只能保证一个共享变量的原子操作,体现的是无锁并发和无阻塞并发
包含三个操作数-----内存位置,预期原值及更新值
执行CAS操作,将内存位置的值与预期原值比较,若匹配则处理器自动将该位置的 值改变为新值,不匹配则处理器不做任何操作,多个线程执行CAS只有一个能成功
通过硬件保证了比较-更新 的原子性,java中CAS操作的执行依赖于Unsafe类的方法,Unsafe类的内部操作方法可以直接操作内存,直接调用操作系统底层资源执行相应的任务
CAS原理:
读取内存中的值,读取的值与预期的值比较,比较的结果符合预期,写入新值
缺点:循环时间长开销大
CAS的问题是什么?
ABA问题,一个线程A读取某个变量值为A,此时有另一个B修改变量值变为B,又改回A,此时线程A未发现变量值发生改变;采用版本号解决;
是一种乐观锁机制,若大量线程同时竞争同一个资源,会造成锁自旋,会造成大量CPU浪费;高并发环境下,性能影响很大;长时间自旋的话可以采用互斥锁或者设置自旋次数上限;
只能保证单变量的原子操作,需要多个变量同时更新时,会导致变量的不一致,采用锁或者封装成一个对象;
ReentrantLock:可中断;可以设置超时时间;可以设置为公平锁,支持多个条件变量 ,支持可重入
可重入指的是同一个线程如果首次获得了这把锁,此线程就是这把锁的拥有者,有权利再次获得这把锁,如果是不可重入锁,第二次获得锁时拥有这把锁的线程也会被挡住。
ReentrantLock的条件变量支持多个。
其基本实现:先通过CAS尝试获取锁,如果此时已经有线程占据了锁,那就加入AQS队列并挂起,当锁被释放之后,排在队列队首的线程被唤醒,然后CAS再次尝试获取锁。
AQS:volatile+CAS+一个虚拟的FIFO双向队列(CLH)仅仅定义同步状态获取和释放的方法
内部还有一个关键变量,记录当前加锁的是那个线程
抽象的同步队列,解决数据安全问题,有一个共享资源,一个线程操作共享资源没有问题;如果有多个线程去操作共享资源就会产生线程安全问题
重量级基础框架及JUC的基石,用于解决锁分配给“谁”的问题 ,整体是抽象的FIFO队列去完成资源获取线程的排队工作,通过一个int类型的变量state表示持有锁状态
如果共享资源被占用,需要一定的阻塞等待唤醒机制保证锁分配,采用的是CLH变体实现,将暂时获取不到锁的线程加入到这个队列中。将每个要去抢占资源的线程封装成一个Node节点实现锁的分配,通过CAS完成对State值的修改 。
AQS唤醒节点为何从后往前找?
可以更快地找到一个合适的线程进行唤醒,优化线程唤醒的性能,减少不必要的遍历操作
ReentrantReadWriteLock可重入读写锁
解决读多写少的问题,存在写锁饥饿问题,锁降级
只允许读读共存,读写和写写依然是互斥的,只允许一个资源被多个读操作访问,一个写操作访问,读没有完成的时候,其他线程写锁无法获得
锁降级:将写入锁降级为读锁,如果一个线程持有写锁,在没有释放写锁时,还可以获得读锁
先获取写锁,之后获取读锁,最后释放写锁保证数据可见性,避免如果直接释放写锁的话,如果此时有其他的线程获取到写锁并修改,当前线程无法得知数据发生了修改。
目的:为了让当前线程感受到 数据变化,保证数据的可见性(写操作)
潜在问题:读锁结束,写锁有望;写锁独占,读写全堵
写锁饥饿问题:读操作较多时,获取写锁会比较困难,当前可能一直存在读锁,设置为公平性锁可缓解
邮戳锁(StampedLock):对ReentrantReadWriteLock的升级优化,是一种乐观读锁,其他线程尝试获取写锁时不会 被阻塞,在读的时候允许其他线程写
获取锁的时候,都会返回一个邮戳,0即代表获取失败
不可重入,一个线程持有一把写锁,再去获取写锁会导致死锁
ThreadLocal用于线程间的数据隔离,为每一个线程都提供了变量的副本,使得每一个线程在同一时刻访问的不是同一个对象,隔离了多个线程对数据的数据共享。避免了线程安全问题
ThreadLocalMap是一个保存ThreadLocal对象的map,是Thread的一个成员变量,ThreadLocal本身并不存储对象,只是作为key让线程从ThreadLocalMap中获取value值,ThreadLocal里面的一个内部类ThreadLocalMap,调用里面的set()方法就是往ThreadLocalMap设置值,key为ThreadLocal,value为传递进来的对象;get()就是获取值,是经过了两层包装的ThreadLocal对象,第一层是将ThreadLocal对象包装成弱引用对象,第二层是定义一个Entry类扩展
使用弱引用是使ThreadLocal对象在方法执行完毕后顺利被回收,减少内存泄露问题
ThreadLocal造成内存泄漏的原因是什么?
ThreadLocal没有被外部强引用时,发生GC会被回收,那ThreadLocalMap中的 key会变为null,但是Entry被threadLocalMap对象引用,threadLocalMap对象被Thread对象引用,当Thread对象不终结会导致value一直存在于内存,导致内存泄漏;
解决方案:
使用完threadLocal后,调用remove方法;ThreadLocalMap中使用弱引用
ThreadLocal的理解:
1.代替参数的显示传递
2.全局存储用户信息
3.解决线程安全问题
标签:CAS,同步,Java,获取,对象,编程,并发,线程,内存 From: https://blog.csdn.net/weixin_47559057/article/details/144583639