synchronized
synchronized的三种应用方式
- 1、修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁。
- 2、修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁。
- 3、修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象。
Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。Synchronized的作用主要有三个:
- 1、确保线程互斥的访问同步代码
- 2、保证共享变量的修改能够及时可见
- 3、有效解决重排序问题
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
synchronize底层原理:
Java 虚拟机中的同步(Synchronization)基于进入和退出Monitor对象实现, 无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。在 Java 语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。同步方法 并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法表结构的 ACC_SYNCHRONIZED 标志来隐式实现的,关于这点,稍后详细分析。
同步代码块:monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁;
在JVM中,对象在内存中的布局分为三块区域:对象头、实例变量和填充数据。如下:
实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。
对象头:Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。其中Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。
Mark Word:用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit),但是如果对象是数组类型,则需要三个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
Monior:我们可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。Monitor 是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联(对象头的MarkWord中的LockWord指向monitor的起始地址),同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。其结构如下:
Owner:初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;
EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程。
RcThis:表示blocked或waiting在该monitor record上的所有线程的个数。
Nest:用来实现重入锁的计数。
HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。
Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁。
Java虚拟机对synchronize的优化:
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级,关于重量级锁,前面我们已详细分析过,下面我们将介绍偏向锁和轻量级锁以及JVM的其他优化手段。
偏向锁
偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
轻量级锁
倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。
自旋锁
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。
锁消除
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。
synchronized的可重入性:
从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功,在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。
Lock
- 锁是一种工具,用于控制对共享资源的访问
- Lock和synchronized,这两个是最常见的锁,它们都可以达到线程安全的目的,但是在使用上和功能上又有较大的不同
- Lock并不是用来替代synchronized的,而是当使用synchronized不合适或不足以满足要求的时候,来提供高级功能
- Lock接口中最常见的实现类是ReentrantLock
- 通常情况下,Lock只允许一个线程来访问这个共享资源,不过有的时候,一些特殊的实现也可允许并发访问,比如ReadWriteLock里面的ReadLock
synchronize和Lock的区别:
两者都是锁,用来控制并发冲突,区别在于Lock是个接口,提供的功能更加丰富,除了这个外,他们还有如下区别:
- 1、synchronize自动释放锁,而Lock必须手动释放,并且代码中出现异常会导致unlock代码不执行,所以Lock一般在Finally中释放,而synchronize释放锁是由JVM自动执行的。
- 2、Lock有共享锁的概念,所以可以设置读写锁提高效率,synchronize不能。(两者都可重入)
- 3、Lock可以让线程在获取锁的过程中响应中断,而synchronize不会,线程会一直等待下去。lock.lockInterruptibly()方法会优先响应中断,而不是像lock一样优先去获取锁。
- 4、Lock锁的是代码块,synchronize还能锁方法和类。
- 5、Lock可以知道线程有没有拿到锁,而synchronize不能
为什么需要Lock
为什么synchronized不够用
- 效率低:锁的释放情况少,试图获得锁时不能设定超时,不能中断一个正在试图获得锁的线程
- 不够灵活(读写锁更灵活):加锁和释放锁的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的
- 无法知道是否成功获取到锁
Lock的主要方法介绍
- Lock中声明了4个方法来获取锁
- lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()
lock()
- lock()就是最普通的获取锁,如果锁已被其他线程获取,则进行等待
- lock()不会像synchronized一样在异常的时候自动释放锁
- 因此最佳实践是,在finally中释放锁,以保证发生异常时锁一定被释放
- lock()方法不能被中断,这会带来很大隐患:一旦陷入死锁,lock()就会陷入永久等待
tryLock()
- tryLock()用来尝试获取锁,如果当前锁没有被其他线程占用,则获取成功返回true;否则返回false,代表获取锁失败
- 相比于lock(),这样的方法显然功能更强大了,我们可以根据是否能获取到锁来决定后续程序的行为
- 该方法会立刻返回,即便在拿不到锁时也不会一直在那等待
tryLock(long time, TimeUnit unit)
- tryLock(long time, TimeUnit unit):超时就放弃
lockInterruptibly()
- lockInterruptibly():相当于tryLock(long time, TimeUnit unit)把超时时间设置为无限,在等待锁的过程中,线程可以被中断
unlock()
- unlock():解锁
可见性的保证
可见性
什么是可见性?如果一个线程对于另外一个线程是可见的,那么这个线程的修改就能够被另一个线程立即感知到。
Java锁保证可见性的具体实现
Happens-before规则
从JDK 5开始,JSR-133定义了新的内存模型,内存模型描述了多线程代码中的哪些行为是合法的,以及线程间如何通过内存进行交互。
新的内存模型语义在内存操作(读取字段,写入字段,加锁,解锁)和其他线程操作上创建了一些偏序规则,这些规则又叫作Happens-before规则。它的含义是当一个动作happens before另一个动作,这意味着第一个动作被保证在第二个动作之前被执行并且结果对其可见。我们利用Happens-before规则来解释Java锁到底如何保证了可见性。
Java内存模型一共定义了八条Happens-before规则,和Java锁相关的有以下两条:
- 内置锁的释放锁操作发生在该锁随后的加锁操作之前
- 一个volatile变量的写操作发生在这个volatile变量随后的读操作之前
Lock的加解锁和synchronized有同样的内存语义,也就是说,下一个线程加锁后可以看到所有前一个线程解锁前发生的操作
锁的分类
- 这些分类是从各种不同角度出发去看的
- 这些分类并不是互斥的,也就是说多个类型可以并存,有可能一种锁同时属于2种类型
-
比如ReentrantLock既是互斥锁,又是可重入锁
为什么会诞生非互斥同步锁(乐观锁)
- 互斥同步锁(悲观锁)的劣势
- 阻塞和唤醒带来的性能劣势
- 永久阻塞:如果持有锁的线程被永久阻塞,比如遇到了无限循环、死锁等活跃性问题,那么等待该线程释放锁的那几个悲催线程,将永远也得不到执行
- 优先级反转
悲观锁
- 如果不锁住这个资源,别人就会来争抢,就会造成数据结果的错误,所以每次悲观锁为了确保结果的正确性,会在每次获取并修改数据时,把数据锁住,让别人无法访问该数据,这样就可以确保数据万无一失了
- Java中悲观锁的实现就是synchronized和Lock相关的类
乐观锁
- 认为自己在处理操作的时候不会有其他线程阿里干扰,所以并不会锁住被操作对象
- 在更新的时候,去对比在我修改的期间数据有没有被其他人改变过,如果没有被改变过,就说明真的是只有我自己在操作,那我就正常去修改数据;如果数据和我一开始拿到的不一样了,说明其他人在这段时间内修改过数据,那么我就不能继续刚才的更新数据过程了,我会选择放弃、报错、重试等
- 乐观锁的实现一般都是利用CAS算法来实现的
典型例子
- 悲观锁:synchronized和Lock相关的类
- 乐观锁的典型例子就是原子类、并发容器等
- Git:Git就是乐观锁的典型例子,当我们往远程仓库push的时候,git会检查远程仓库的版本是不是领先于我们现在的版本,如果远程仓库的版本号和本地的不一样,就表示有其他人修改了远端代码,那么我们这次提交就失败,如果远端和本地的版本号一致,我们就可以顺利提交版本到远端仓库
- 数据库:select for update就是悲观锁;使用version控制数据库的数据,那么就是乐观锁
悲观锁和乐观锁开销对比
- 悲观锁的开始开销要高于乐观锁,但是特点是一劳永逸,临界区持锁时间就算越来越差,也不会对互斥锁的开销造成影响
- 相反,虽然乐观锁一开始的开销比悲观锁小,但是如果自旋时间很长或者不停重试,那么消耗的资源也会越来越多
悲观锁和乐观锁的使用场景
- 悲观锁:适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗,典型情况:
- 临界区有I/O操作
- 临界区代码复杂或循环量大
- 临界区竞争非常激烈
- 乐观锁:适合并发写入少,大部分是读取的场景,不加锁能让读取性能大幅提高
可重入锁和非可重入锁
源码对比:可重入锁ReentrantLock以及非可重入锁ThreadPoolExecutor的Worker类
ReentrantLock的其他方法介绍
- isHeldByCurrentThread():锁是否被当前线程持有
- getQueueLength():返回当前正在等待这把锁的队列有多长,一般这两个方法是开发和调试的时候使用,上线后用到的不多
公平锁和非公平锁
- 公平指的是按照线程请求的顺序,来分配锁,非公平指的是,不完全按照线程的请求顺序,在一定情况下可以插队
- 注意:非公平页同样不提倡“插队”行为,这里的非公平,指的是“在合适的时机”插队,而不是盲目插队
- 如果在创建ReentrantLock对象时,参数填写为true,那么这就是个公平锁
- 针对tryLock()方法,它是很猛的,它不遵守设定的公平的规则
-
例如,当有线程执行tryLock()的时候,一旦有线程释放了锁,那么这个正在tryLock()的线程就能获取到锁,即使在它之前已经有其他线程正在等待队列里等待了
共享锁和排它锁
- 排它锁,又称为独占锁、独享锁
- 共享锁,又称为读锁,获得共享锁之后,可以查看但无法修改和删除数据,其他线程此时也可以获取到共享锁,也可以查看,但无法修改和删除数据
- 共享锁和排它锁的典型是读写锁ReentrantReadWriteLock,其中读锁是共享锁,写锁是排它锁
读写锁的作用
- 在没有读写锁之前,我们设定使用ReentrantLock,那么虽然我们保证了线程安全,但是也浪费了资源:多个读操作同时进行,并没有线程安全问题
- 在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,提高了程序的执行效率
读写锁的规则
- 多个线程只申请读锁,都可以申请到
- 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁
- 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁
- 一句话总结:要么是一个或多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现
- 换一种思路更容易理解:读写锁只是一把锁,可以通过两种方式锁定:读锁定和写锁定。读写锁可以同时被一个或多个线程读锁定,也可以被单一线程写锁定。但是永远不能同时对这把锁进行读锁定和写锁定
ReentrantReadWriteLock使用
public class CinemaReadWrite {
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
public static void read(){
readLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"得到了读锁,并正在读取中");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
System.out.println(Thread.currentThread().getName()+"释放了读锁");
}
}
public static void write(){
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"得到了写锁,并正在写入中");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
System.out.println(Thread.currentThread().getName()+"释放了写锁");
}
}
public static void main(String[] args) {
new Thread(() -> read(),"Thread-1").start();
new Thread(() -> read(),"Thread-2").start();
new Thread(() -> write(),"Thread-3").start();
new Thread(() -> write(),"Thread-4").start();
}
}
读锁插队策略
- 公平锁:不允许插队
- 非公平锁
- 写锁可以随时插队
- 读锁仅在等待队列头节点不是想获取写锁的线程的时候才可以插队
代码演示:
public class NonfairBargeDemo {
//公平锁,读锁无法插队,非公平锁,读锁可以插队
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(true);
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
public static void read(){
System.out.println(Thread.currentThread().getName()+"开始尝试获取读锁");
readLock.lock();
try{
System.out.println(Thread.currentThread().getName()+"得到读锁正在读取");
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
System.out.println(Thread.currentThread().getName()+"释放读锁");
}
}
public static void write(){
System.out.println(Thread.currentThread().getName()+"开始尝试获取写锁");
writeLock.lock();
try{
System.out.println(Thread.currentThread().getName()+"得到写锁正在写入");
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
System.out.println(Thread.currentThread().getName()+"释放写锁");
}
}
public static void main(String[] args) {
new Thread(() -> write(),"Thread-1").start();
new Thread(() -> read(),"Thread-2").start();
new Thread(() -> read(),"Thread-3").start();
new Thread(() -> write(),"Thread-4").start();
new Thread(() -> read(),"Thread-5").start();
new Thread(new Runnable() {
@Override
public void run() {
Thread[] threads = new Thread[1000];
for (int i = 0; i < 1000; i++) {
threads[i] = new Thread(() -> read(),"子线程创建的Thread" + i);
}
for (int i = 0; i < 1000; i++) {
threads[i].start();
}
}
}).start();
}
}
锁的升降级
- 支持锁的降级,不支持锁的升级
public class Upgrading {
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
public static void readUpgrading(){
readLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"得到了读锁,并正在读取中");
Thread.sleep(1000);
System.out.println("升级会带来阻塞");
writeLock.lock();
System.out.println(Thread.currentThread().getName()+"获取到了写锁,升级成功");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
System.out.println(Thread.currentThread().getName()+"释放了读锁");
}
}
public static void writeDowngrading(){
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"得到了写锁,并正在写入中");
Thread.sleep(1000);
readLock.lock();
System.out.println("在不释放写锁的情况下,直接获取读锁");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
writeLock.unlock();
System.out.println(Thread.currentThread().getName()+"释放了写锁");
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("先演示降级是可以的");
Thread thread1 = new Thread(() -> writeDowngrading(), "Thread-1");
thread1.start();
System.out.println("----------------------------------");
thread1.join();
System.out.println("先演示升级是不可以的");
new Thread(() -> readUpgrading(),"Thread-2").start();
}
}
共享锁和排它锁总结
- 1、ReentrantReadWriteLock实现了ReadWriteLock,最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁
- 2、所得申请和释放的策略
- 多个线程只申请读锁,都可以申请到
- 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁
- 如果有一个线程已经占用的写锁,则此时其他线程如果申请写锁或读锁,则申请的线程会一直等待释放写锁
- 3、插队策略:为了防止饥饿,读锁不能插队
- 4、升降级策略:只能降级,不能升级
- 5、适用场合:相比于ReentrantLock适用于一般场合,ReentrantReadWriteLock适用于读多写少的情况,合理使用可以进一步提高并发策略
自旋锁和阻塞锁
自旋锁
自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其他线程改变时 才能进入临界区
由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。如果线程竞争不激烈,并且保持锁的时间段,适合使用自旋锁。
在Java1.5版本及以上的并发框架java.util.concurrent的atmoic包下的类基本都是自旋锁的实现
AtomicInteger的实现:自旋锁的实现原理是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改过程中遇到其他线程竞争导致没有修改成功,就在while里死循环,直至修改成功
自旋锁适用场景
- 自旋锁一般用于多核服务器,在并发度不是特别高的情况下,比阻塞锁的效率高
- 自旋锁适用于临界区比较短小的情况,否则如果临界区很大(线程一旦拿到锁,很久以后才会释放),那也是不合适的
阻塞锁
阻塞锁,与自旋锁不同,改变了线程的运行状态。
在JAVA环境中,线程Thread有如下几个状态:
- 1、新建状态
- 2、就绪状态
- 3、运行状态
- 4、阻塞状态
- 5、死亡状态
阻塞锁,可以说是让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间) 时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态。
JAVA中,能够进入\退出、阻塞状态或包含阻塞锁的方法有 ,synchronized 关键字(其中的重量锁),ReentrantLock,Object.wait()\notify(),LockSupport.park()/unpart()(j.u.c经常使用)
阻塞锁的优势在于,阻塞的线程不会占用cpu时间, 不会导致 CPu占用率过高,但进入时间以及恢复时间都要比自旋锁略慢。
在竞争激烈的情况下 阻塞锁的性能要明显高于 自旋锁。
理想的情况则是; 在线程竞争不激烈的情况下,使用自旋锁,竞争激烈的情况下使用,阻塞锁。
可中断锁
- 在Java中,synchronized就不是可中断锁,而Lock是可中断锁,因为tryLock(time)和lockInterruptibly都能响应中断
- 如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等了,想先处理其他事情,那么可以中断它,这就是可中断锁
在写代码时如何优化锁和提高并发性能
- 缩小同步代码块
- 尽量不要锁住方法
- 减少锁的次数
- 避免人为制造“热点”
- 锁中尽量不要包含锁
- 选择合适的锁类型或合适的工具类
参考:
https://www.cnblogs.com/mingyao123/p/7424911.html
https://www.cnblogs.com/sunny-miss/p/11794156.html
https://blog.csdn.net/huxuhang/article/details/92838075
网友评论