java锁的基础

synchronized

Synchronized 关键字表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。
作用:(1)确保线程互斥的访问同步代码。(2)保证共享变量修改能够及时可见。(3)有效解决重排序问题。
用法:(1)修饰普通方法。(2)修饰静态方法。(3)指定对象,代码块。
特点:
• 阻塞未获取到锁、竞争同一个对象锁的线程
• 获取锁无法设置超时
• 无法实现公平锁
• 控制等待和唤醒需要结合加锁对象的 wait() 和 notify()、notifyAll()
• 锁的功能是 JVM 层面实现的
• 在加锁代码块执行完或者出现异常,自动释放锁
原理:
• 同步代码块是通过 monitorenter 和 monitorexit 指令获取线程的执行权
• 同步方法通过加 ACC_SYNCHRONIZED 标识实现线程的执行权的控制

Volatile

Volatile 关键字是一个类型的修饰符。JDK1.5之后,对其语义进行了增强。
(1) 保证了不同线程对共享变量进行操作时的可见性,即一个线程修改了共享变量的值,共享变量修改后的值对其他线程立即可见。
(2) 通过禁止编译器,cpu指令重排序和部分happens-before规则,解决有序性问题。
Volatile可见性的实现
• 在生成汇编代码指令时会在 volatile 修饰的共享变量进行写操作的时候会多出 Lock 前缀的指令
• Lock 前缀的指令会引起 CPU 缓存写回内存
• 一个 CPU 的缓存回写到内存会导致其他 CPU 缓存了该内存地址的数据无效
• volatile 变量通过缓存一致性协议保证每个线程获得最新值
• 缓存一致性协议保证每个 CPU 通过嗅探在总线上传播的数据来检查自己缓存的值是不是修改
• 当 CPU 发现自己缓存行对应的内存地址被修改,会将当前 CPU 的缓存行设置成无效状态,重新从内存中把数据读到 CPU 缓存
Volatile 有序性的实现
• 3 个 happens-before 规则实现:

  1. 对一个 volatile 变量的写 happens-before 任意后续对这个 volatile 变量的读
  2. 在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作
  3. happens-before 传递性,A happens-before B,B happens-before C,则 A happens-before C
    • 内存屏障(Memory Barrier 又称内存栅栏,是一个 CPU 指令)禁止重排序
  4. 在程序运行时,为了提高执行性能,在不改变正确语义的前提下,编译器和 CPU 会对指令序列进行重排序。
  5. Java 编译器会在生成指令时,为了保证在不同的编译器和 CPU 上有相同的结果,通过插入特定类型的内存屏障来禁止特定类型的指令重排序
  6. 编译器根据具体的底层体系架构,将这些内存屏障替换成具体的 CPU 指令
  7. 内存屏障会告诉编译器和 CPU:不管什么指令都不能和这条 Memory Barrier 指令重排序
    内存屏障
    • 为了实现 volatile 内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的 CPU 重排序。
    • 对于编译器,内存屏障将限制它所能做的重排序优化;对于 CPU,内存屏障将会导致缓存的刷新操作
    • volatile 变量的写操作,在变量的前面和后面分别插入内存屏障;volatile 变量的读操作是在后面插入两个内存屏障
  8. 在每个 volatile 写操作的前面插入一个 StoreStore 屏障
  9. 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障
  10. 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障
  11. 在每个 volatile 读操作的后面插入一个 LoadStore 屏障
    • 屏障说明
  12. StoreStore:禁止之前的普通写和之后的 volatile 写重排序;
  13. StoreLoad:禁止之前的 volatile 写与之后的 volatile 读/写重排序
  14. LoadLoad:禁止之后所有的普通读操作和之前的 volatile 读重排序
  15. LoadStore:禁止之后所有的普通写操作和之前的 volatile 读重排序

我觉得,有序性最经典的例子就是 JDK 并发包中的显式锁 java.util.concurrent.locks.Lock 的实现类对有序性的保障。

Java中的锁是什么

在并发编程中,经常会遇到多个线程访问一个共同变量,当同时对共享变量进行读写操作时,就会产生数据不一致的情况。
为了解决这个问题
• JDK 1.5 之前,使用 synchronized 关键字,拿到 Java 对象的锁,保护锁定的代码块。JVM 保证同一时刻只有一个线程可以拿到这个 Java 对象的锁,执行对应的代码块。
• JDK 1.5 开始,引入了并发工具包 java.util.concurrent.locks.Lock,让锁的功能更加丰富。

Java中的常见的锁

(1)Synchronized关键字锁定代码库
(2)可重入锁 java.util.concurrent.lock.Reentrantlock
(3)可重复读写锁java.util.concurrent.lock.ReentrantReadWriteLock
Java中不同维度的锁分类
可重入锁
• 指在同一个线程在外层方法获取锁的时候,进入内层方法会自动获取锁。JDK 中基本都是可重入锁,避免死锁的发生。上面提到的常见的锁都是可重入锁。

公平锁 / 非公平锁
• 公平锁,指多个线程按照申请锁的顺序来获取锁。如 java.util.concurrent.lock.ReentrantLock.FairSync
• 非公平锁,指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程先获得锁。如 synchronized、java.util.concurrent.lock.ReentrantLock.NonfairSync

独享锁 / 共享锁
• 独享锁,指锁一次只能被一个线程所持有。synchronized、java.util.concurrent.locks.ReentrantLock 都是独享锁
• 共享锁,指锁可被多个线程所持有。ReadWriteLock 返回的 ReadLock 就是共享锁

悲观锁 / 乐观锁
• 悲观锁,一律会对代码块进行加锁,如 synchronized、java.util.concurrent.locks.ReentrantLock
• 乐观锁,默认不会进行并发修改,通常采用 CAS 算法不断尝试更新
• 悲观锁适合写操作较多的场景,乐观锁适合读操作较多的场景

粗粒度锁 / 细粒度锁
• 粗粒度锁,就是把执行的代码块都锁定
• 细粒度锁,就是锁住尽可能小的代码块,java.util.concurrent.ConcurrentHashMap 中的分段锁就是一种细粒度锁
• 粗粒度锁和细粒度锁是相对的,没有什么标准

偏向锁 / 轻量级锁 / 重量级锁
• JDK 1.5 之后新增锁的升级机制,提升性能。
• 通过 synchronized 加锁后,一段同步代码一直被同一个线程所访问,那么该线程获取的就是偏向锁
• 偏向锁被一个其他线程访问时,Java 对象的偏向锁就会升级为轻量级锁
• 再有其他线程会以自旋的形式尝试获取锁,不会阻塞,自旋一定次数仍然未获取到锁,就会膨胀为重量级锁

自旋锁
• 自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环占有、浪费 CPU 资源
Java中的锁如何使用?有什么注意事项
Synchronized的三种用法
(1)修饰普通方法,执行方法代码,需要获取对象本身的this锁
(2)修饰静态方法,执行方法代码,需要获取class对象的锁
(3)锁定java对象,修饰代码块,显示指定需要获取的java对象锁
可重入锁 java.util.concurrent.lock.ReentrantLock 的使用示例
可重复读写锁 java.util.concurrent.lock.ReentrantReadWriteLock 的使用示例

锁使用的注意事项

• synchronized 修饰代码块时,最好不要锁定基本类型的包装类,如 jvm 会缓存 -128 ~ 127 Integer 对象,每次向如下方式定义 Integer 对象,会获得同一个 Integer,如果不同地方锁定,可能会导致诡异的性能问题或者死锁
• synchronized 修饰代码块时,要线程互斥地执行代码块,需要确保锁定的是同一个对象,这点往往在实际编程中会被忽视
• synchronized 不支持尝试获取锁、锁超时和公平锁
• ReentrantLock 一定要记得在 finally{} 语句块中调用 unlock() 方法释放锁,不然可能导致死锁
• ReentrantLock 在并发量很高的情况,由于自旋很消耗 CPU 资源
• ReentrantReadWriteLock 适合对共享资源写操作很少,读操作频繁的场景;可以从写锁降级到读锁,无法从读锁升级到写锁
可重入锁与不可重入锁之间的区别与性能差异?
可重入锁
指在同一个线程在外层方法获取锁的时候,进入内层方法会自动获取锁。为了避免死锁的发生,JDK 中基本都是可重入锁。
测试不可重入锁
两种方式:通过snychronized wait notify实现;通过CAS+自旋方式实现
Java中锁之间的区别是什么?
Synchronized和java.util. concurrent.lock.Lock之间的区别
• 实现层面不一样。synchronized 是 Java 关键字,JVM层面 实现加锁和释放锁;Lock 是一个接口,在代码层面实现加锁和释放锁
• 是否自动释放锁。synchronized 在线程代码执行完或出现异常时自动释放锁;Lock 不会自动释放锁,需要在 finally {} 代码块显式地中释放锁
• 是否一直等待。synchronized 会导致线程拿不到锁一直等待;Lock 可以设置尝试获取锁或者获取锁失败一定时间超时
• 获取锁成功是否可知。synchronized 无法得知是否获取锁成功;Lock 可以通过 tryLock 获得加锁是否成功
• 功能复杂性。synchronized 加锁可重入、不可中断、非公平;Lock 可重入、可判断、可公平和不公平、细分读写锁提高效率
java.util. concurrent.lock.Lock与java.util. concurrent.lock.ReadWriteLock之间的区别
• ReadWriteLock 定义了获取读锁和写锁的接口,读锁之间不互斥,非常适合读多、写少的场景

适用场景:

• JDK 1.6 开始,对 synchronized 方式枷锁进行了优化,加入了偏向锁、轻量级锁和锁升级机制,性能得到了很大的提升。性能与 ReentrantLock 差不多
• 读多写少的情况下,考虑使用 ReadWriteLock

Synchronized锁升级的原理是什么?

锁的级别从低到高
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
锁分级别的原因:没有优化以前,synchronized 是重量级锁(悲观锁),使用 wait 和 notify、notifyAll 来切换线程状态非常消耗系统资源;线程的挂起和唤醒间隔很短暂,这样很浪费资源,影响性能。所以 JVM 对 synchronized 关键字进行了优化,把锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。
锁升级的目的是为了减低锁带来的性能消耗,在 Java 6 之后优化 synchronized 为此方式。
无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功。
偏向锁:对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,降低获取锁带来的性能开销。偏向锁,指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。
偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;
如果线程处于活动状态,升级为轻量级锁的状态。
轻量级锁:轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B 会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。
当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁;当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁。
重量级锁:指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。

Synchronized锁升级的过程:

• 在锁对象的对象头里面有一个 threadid 字段,未访问时 threadid 为空
• 第一次访问 jvm 让其持有偏向锁,并将 threadid 设置为其线程 id
• 再次访问时会先判断 threadid 是否与其线程 id 一致。如果一致则可以直接使用此对象;如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁
• 执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁
什么是死锁?
线程死锁是指由于两个或者多个线程互相持有所需要的资源,导致这些线程一直处于等待其他线程释放资源的状态,无法继续执行,如果线程都不主动释放所占有的资源,将产生死锁。
当线程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
• 持有系统不可剥夺资源,去竞争其他已被占用的系统不可剥夺资源,形成程序僵死的竞争关系。
• 持有资源的锁,去竞争锁已被占用的其他资源,形成程序僵死的争关系。
• 信号量使用不当。

如何避免死锁?

并发程序一旦死锁,往往我们只能重启应用。解决死锁问题的最好方法就是避免死锁。
死锁发生的条件:
• 互斥,共享资源只能被一个线程占用
• 占有且等待,线程 t1 已经取得共享资源 s1,尝试获取共享资源 s2 的时候,不释放共享资源 s1
• 不可抢占,其他线程不能强行抢占线程 t1 占有的资源 s1
• 循环等待,线程 t1 等待线程 t2 占有的资源,线程 t2 等待线程 t1 占有的资源

避免死锁的方法:

对于以上 4 个条件,只要破坏其中一个条件,就可以避免死锁的发生。
对于第一个条件 “互斥” 是不能破坏的,因为加锁就是为了保证互斥。
其他三个条件,我们可以尝试
• 一次性申请所有的资源,破坏 “占有且等待” 条件
• 占有部分资源的线程进一步申请其他资源时,如果申请不到,主动释放它占有的资源,破坏 “不可抢占” 条件
• 按序申请资源,破坏 “循环等待” 条件
实践过程中的最佳方法:
• 使用 Lock 的 tryLock(long timeout, TimeUnit unit)的方法,设置超时时间,超时可以退出防止死锁
• 尽量使用并发工具类代替加锁
• 尽量降低锁的使用粒度
• 尽量减少同步的代码块

什么是活锁和饥锁?

活锁:任务没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程。 处于活锁的实体是在不断的改变状态,活锁有可能自行解开。
死锁:是大家都拿不到资源都占用着对方的资源,而活锁是拿到资源却又相互释放不执行。
解决活锁的一个简单办法就是在下一次尝试获取资源之前,随机休眠一小段时间。
饥饿:
一个线程因为cpu的时间全部被其他线程抢占而得不到cpu运行时间,导致线程无法执行。
产生饥饿的原因:
• 优先级线程吞噬所有的低优先级线程的 CPU 时间
• 其他线程总是能在它之前持续地对该同步块进行访问,线程被永久堵塞在一个等待进入同步块
• 其他线程总是抢先被持续地获得唤醒,线程一直在等待被唤醒

Java中有那些无锁技术来解决并发问题?如何使用?

除了使用synchronized、lock加锁之外,java中还有很多不需要加锁就可以解决并发问题的工具类
(1) 原子工具类:
JDK 1.8 中,java.util.concurrent.atomic 包下类都是原子类,原子类都是基于 sun.misc.Unsafe 实现的。
• CPU 为了解决并发问题,提供了 CAS 指令,全称 Compare And Swap,即比较并交互
• CAS 指令需要 3 个参数,变量、比较值、新值。当变量的当前值与比较值相等时,才把变量更新为新值
• CAS 是一条 CPU 指令,由 CPU 硬件级别上保证原子性
• java.util.concurrent.atomic 包中的原子分为:原子性基本数据类型、原子性对象引用类型、原子性数组、原子性对象属性更新器和原子性累加器
原子性基本数据类型:AtomicBoolean、AtomicInteger、AtomicLong
原子性对象引用类型:AtomicReference、AtomicStampedReference、AtomicMarkableReference
原子性数组:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
原子性对象属性更新:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
原子性累加器:DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder
(2) 线程本地存储
java.lang.ThreadLocal 类用于线程本地化存储。
线程本地化存储,就是为每一个线程创建一个变量,只有本线程可以在该变量中查看和修改值。
典型的使用例子就是,spring 在处理数据库事务问题的时候,就用了 ThreadLocal 为每个线程存储了各自的数据库连接 Connection。
使用 ThreadLocal 要注意,在不使用该变量的时候,一定要调用 remove() 方法移除变量,否则可能造成内存泄漏的问题。
Copy-on-write
• 写时复制,体现的是一种延时策略
• Java 中的 copy-on-write 容器包括:CopyOnWriteArrayList、CopyOnWriteArraySet
• 涉及到数组的全量复制,所以也比较耗内存,在写少的情况下使用比较适合。
简单的 CopyOnWriteArrayList 的示例,这里只是说明 CopyOnWriteArrayList 怎么用,并且是线程安全的。这个场景并不适合使用 CopyOnWriteArrayList,因为写多读少。

Synchronized和volatile区别是什么

作用:
• synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。
• volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。
区别:
• synchronized 可以作用于变量、方法、对象;volatile 只能作用于变量。
• synchronized 可以保证线程间的有序性(个人猜测是无法保证线程内的有序性,即线程内的代码可能被 CPU 指令重排序)、原子性和可见性;volatile 只保证了可见性和有序性,无法保证原子性。
• synchronized 线程阻塞,volatile 线程不阻塞。
• volatile 本质是告诉 jvm 当前变量在寄存器中的值是不安全的需要从内存中读取;sychronized 则是锁定当前变量,只有当前线程可以访问到该变量其他线程被阻塞。
• volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。
Synchronized和lock有什么区别?
• 实现层面不一样。synchronized 是 Java 关键字,JVM层面 实现加锁和释放锁;Lock 是一个接口,在代码层面实现加锁和释放锁
• 是否自动释放锁。synchronized 在线程代码执行完或出现异常时自动释放锁;Lock 不会自动释放锁,需要再 finally {} 代码块显式地中释放锁
• 是否一直等待。synchronized 会导致线程拿不到锁一直等待;Lock 可以设置尝试获取锁或者获取锁失败一定时间超时
• 获取锁成功是否可知。synchronized 无法得知是否获取锁成功;Lock 可以通过 tryLock 获得加锁是否成功
• 功能复杂性。synchronized 加锁可重入、不可中断、非公平;Lock 可重入、可判断、可公平和不公平、细分读写锁提高效率

Synchronized 和reentrantLock区别是什么?

• synchronized 竞争锁时会一直等待;ReentrantLock 可以尝试获取锁,并得到获取结果
• synchronized 获取锁无法设置超时;ReentrantLock 可以设置获取锁的超时时间
• synchronized 无法实现公平锁;ReentrantLock 可以满足公平锁,即先等待先获取到锁
• synchronized 控制等待和唤醒需要结合加锁对象的 wait() 和 notify()、notifyAll();ReentrantLock 控制等待和唤醒需要结合 Condition 的 await() 和 signal()、signalAll() 方法
• synchronized 是 JVM 层面实现的;ReentrantLock 是 JDK 代码层面实现
• synchronized 在加锁代码块执行完或者出现异常,自动释放锁;ReentrantLock 不会自动释放锁,需要在 finally{} 代码块显示释放
补充:都可以做到同一个线程,同一把锁,可重入代码块。
ReadWritrLock如何使用?
ReadWritrLock,读写锁
ReentrantReadWritrLock是ReadWritrLock的一种实现
特点:
• 包含一个 ReadLock 和 一个 WriteLock 对象
• 读锁与读锁不互斥;读锁与写锁,写锁与写锁互斥
• 适合对共享资源有读和写操作,写操作很少,读操作频繁的场景
• 可以从写锁降级到读锁。获取写锁->获取读锁->释放写锁
• 无法从读锁升级到写锁
• 读写锁支持中断
• 写锁支持Condition;读锁不支持Condition

乐观锁与悲观锁

• 悲观锁(Pessimistic Lock):线程每次在处理共享数据时都会上锁,其他线程想处理数据就会阻塞直到获得锁。
• 乐观锁(Optimistic Lock):线程每次在处理共享数据时都不会上锁,在更新时会通过数据的版本号等机制判断其他线程有没有更新数据。乐观锁适合读多写少的应用场景
两种锁各有优缺点:
• 乐观锁适用于读多写少的场景,可以省去频繁加锁、释放锁的开销,提高吞吐量
• 在写比较多的场景下,乐观锁会因为版本不一致,不断重试更新,产生大量自旋,消耗 CPU,影响性能。这种情况下,适合悲观锁

Volatile关键字能否保证线程安全?

单纯使用 volatile 关键字是不能保证线程安全的
• volatile 只提供了一种弱的同步机制,用来确保将变量的更新操作通知到其他线程
• volatile 语义是禁用 CPU 缓存,直接从主内存读、写变量。表现为:更新 volatile 变量时,JMM 会把线程对应的本地内存中的共享变量值刷新到主内存中;读 volatile 变量时,JMM 会把线程对应的本地内存设置为无效,直接从主内存中读取共享变量
• 当把变量声明为 volatile 类型后,JVM 增加内存屏障,禁止 CPU 进行指令重排

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值