Synchronized
-
JAVA关键字,独占式的悲观锁,可重入锁。
-
主要解决多个线程之间的访问资源的同步性,可以保证被他修饰的方法或者代码块在任意时刻只能有一个线程执行
-
早期是重量级锁,JAVA6后引入大量优化,自旋锁,适应性自旋锁,偏向锁,轻量级锁,锁消除,锁粗化减少锁的开销
使用方式
-
修饰实例方法
-
修饰静态方法
-
修饰代码块
修饰实例方法 (锁当前对象实例)
给当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁 。
synchronized void method() { //业务代码 }
2、修饰静态方法 (锁当前类)
给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。
这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。
synchronized static void method() { //业务代码 }
静态 synchronized
方法和非静态 synchronized
方法之间的调用互斥么?不互斥!如果一个线程 A 调用一个实例对象的非静态 synchronized
方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized
方法,是允许的,不会发生互斥现象,因为访问静态 synchronized
方法占用的锁是当前类的锁,而访问非静态 synchronized
方法占用的锁是当前实例对象锁。
3、修饰代码块 (锁指定对象/类)
对括号里指定的对象/类加锁:
-
synchronized(object)
表示进入同步代码库前要获得 给定对象的锁。 -
synchronized(类.class)
表示进入同步代码前要获得 给定 Class 的锁
synchronized(this) { //业务代码 }
总结:
-
synchronized
关键字加到static
静态方法和synchronized(class)
代码块上都是是给 Class 类上锁; -
synchronized
关键字加到实例方法上是给对象实例上锁; -
尽量不要使用
synchronized(String a)
因为 JVM 中,字符串常量池具有缓存功能
构造方法可以用 synchronized 修饰么?
先说结论:构造方法不能使用 synchronized 关键字修饰。
构造方法本身就属于线程安全的,不存在同步的构造方法一说。
Synchronized原理
synchronized 关键字底层原理属于 JVM 层面的东西
synchronized
同步语句块的实现使用的是 monitorenter
和 monitorexit
指令,其中 monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置
执行monitorenter
时,会尝试获取对象的锁(获取对象的监视器锁(monitor lock),也称为互斥锁。),如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。
在执行 monitorexit
指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。
synchronized
修饰的方法并没有 monitorenter
指令和 monitorexit
指令,取得代之的确实是 ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法,会尝试获取所属对象的监视器锁(monitor lock)。
如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM 会尝试获取当前 class 的锁。
两者的本质都是对对象监视器 monitor 的获取。
ReentrantLock
实现了Lock接口,独占式,可重入锁,和Synchronized关键字类似,不过ReentrantLock更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。
ReentrantLock是如何实现锁公平和非公平的
首先解释下锁公平和非公平。
-
锁公平,竞争锁资源的一个线程,严格按照请求的顺序来分配锁。
-
锁非公平,竞争锁资源的线程,允许插队抢占锁资源。
在ReentrantLock内部主要采取AQS来实现锁资源竞争,在线程需要抢占锁资源时,会加入到AQS同步队列里面,这个队列是一个FIFO的一个双向链表,在这个背景下,
-
公平锁的实现是在每次线程需要抢占系统资源的时候,会先判断AQS中有没有等待线程,若有,就会增加到队列尾部,按顺序等待锁资源。
-
而非公平锁的实现方式就是不管队列中是否存在等待线程,都会去尝试抢占锁资源。若抢占不到,再加入到AQS同步队列中等待。
不管是ReentrantLock还是Synchronized都默认采用非公平锁的策略,我认为主要是考虑性能的原因,公平锁按顺序调用线程时,AQS再把等待队列里面的线程唤醒,就会涉及内核级的切换,对性能的影响比较大。而非公平锁,当前线程可能正好在上一个线程释放的临界点去抢占到锁,那么意味着这个线程不需要内核级的切换,虽然对原本该唤醒的线程不公平,但是提高锁竞争效率。
ReentrantLock是如何实现可重入
ReentrantLock的实现是基于队列同步器AQS,可重入功能主要基于AQS的同步状态,state。
大致原理为:当线程获取锁后,state+1,并记录当前持有锁线程,再有线程来获取锁资源时,先判断是否为同一线程,是,则state+1,不是,就阻塞线程。
当线程释放锁资源时,state-1,当降为0时,表示彻底释放锁,并将记录当前锁线程的字段设为null,同时唤醒其他线程,重新竞争锁。
这里的state用了volatile,保证了该字段对所有线程的可见性。确保对state变量读写原子性和一致性(有序性,禁止指令重排)。
Synchronized和ReentrantLock
-
都是重入锁
-
用法不同,Synchronized可以作用于普通方法、静态方法和代码块;而ReentrantLock只能用在代码块。
-
获取和释放锁方式不同,Synchronized会自动加锁和释放锁,而ReentrantLock需要手动加锁和释放锁
-
Synchronized是JVM层面通过监视器实现,而ReentrantLock依赖于API,基于AQS实现
-
ReentrantLock比Synchronized增加了一些高级功能,主要有三点
-
等待(响应)可中断,解决死锁问题
-
可实现公平锁
-
可实现选择性通知(锁可以绑定多个条件),通过Condition接口实现一个ReentrantLock对象可以绑定多个对象。(Synchronized相当于整个对象就一个Condition实例,所有线程都注册在上面)
-
CAS
概念
CAS(Compare And Swap/Set)是一种乐观锁的实现方式,全称为“比较并交换”,是一种无锁的原子操作。
三个核心参数
V(var,要更新的变量)、E(expected,预期值,旧值)、N(new,新值)
过程
判断V是否等于E,若等于,V=N。若不等,放弃更新什么都不做
问题
ABA:初始A,修改时,发现是A,就修改。但看到虽然A,中间可能发生A变B,B变A情况。此时A非彼A,数据即使成功修改,也可能有问题。
-
解决:加版本号,每次修改变量都对版本号加1,。
循环性能开销:自旋CAS,若一直循环执行,一直不成功,会给CPU带来非常大的执行开销。
-
解决:设置自旋次数,超过就停止自旋
只能保证一个变量的原子性:CAS操作本质上是针对单个内存位置或变量的原子操作。在A和B交换中,可能A修改后变成B,B没变成A,没有保证多个变量的原子性。
-
解决:可以考虑改用锁来保证操作的原子性
也可以考虑合并多个变量,将多个变量封装成一个对象,通过AtomicReference(Java类)来保证原子性。
java中
在java中,存在于Unsafe类里面,比如compareAndSwapInt()方法,有四个参数,分别是当前对象实例、要修改的变量在内存中的偏移量,预期值,期望更改后的值。CAS机制会比较,变量的内存地址偏移量对应的值和预期值是否一致,是的话就修改。否则返回false,整个过程是原子的,不存在线程安全问题。
CompareAndSwap 是一个 native 方法,实际上它最终还是会面临同样的问题,就是先从内存地址中读取 state 的值,然后去比较,最后再修改。这个过程不管是在什么层面上实现,都会存在原子性问题。
所以呢,CompareAndSwap 的底层实现中,在多核 CPU 环境下,会增加一个 Lock指令对缓存或者总线加锁,从而保证比较并替换这两个指令的原子性。
CAS 主要用在并发场景中,比较典型的使用场景有两个。
-
第一个是 J.U.C 里面 Atomic 的原子实现,比如 AtomicInteger,AtomicLong。
-
第二个是实现多线程对共享资源竞争的互斥性质,比如在 AQS、ConcurrentHashMap、ConcurrentLinkedQueue 等都有用到。
AQS
AbstractQueuedSynchronizer,抽象队列同步器,是一个多线程同步器。(构建锁和同步器)是JUC包中多个组件的底层实现,如Lock、CountDownLatch、Semaphore都用到了AQS。
从本质上来说,AQS提供了两种锁机制,分别是排它锁和共享锁。
排它锁就是存在多个线程同时竞争同一共享资源时,同一时刻只允许一个线程访问该共享资源,也就是多个线程只有一个线程能够获得锁资源,比如Lock中的ReentrantLock重入锁实现,就用到了AQS的排它锁。
共享锁也称为读锁,就是在同一时刻允许多个线程获得锁资源,比如CountDownLatch和Semaphore,都用到了AQS中的共享锁。
AQS本身并没有太多掌握,主要就是一个使用int型,由volatile修饰并且基于CAS修改的核心属性state,来表示同步状态。初始0,表示未锁定。获得锁会加1。还有一个由Node组成的一个双向链表,还有实现线程等待以及唤醒的Condition也是一个链表结构。能否获得锁资源,主要是看能否利用CAS将State从0改成1.如果没有获取到就进入由Node组成的双向链表去做排队,如果持有锁的线程执行了await方法,就需要进入Condition链表中等待被唤醒。
核心思想:若请求的共享资源空闲,则设置当前请求资源的线程为有效工作线程,并设置该共享资源为锁定状态。若请求的共享资源被占用,则线程堵塞,等待被唤醒时锁分配,具体机制基于CLH锁
CLH锁
自旋锁的改进,虚拟双向队列,也就是仅结点间有关联关系。请求的线程封装成CLH队列锁的一个节点,保存线程的引用,节点状态,前驱结点,后继结点。
标签:中锁,Synchronized,synchronized,ReentrantLock,实例,线程,AQS From: https://blog.csdn.net/qq_62097431/article/details/142834156