本文共 14642 字,大约阅读时间需要 48 分钟。
声明: 该文由笔者东拼西凑查阅了很多资料再自己整理总结提取出一些较为简练精悍的语言, 如有侵权私聊我 谢谢
另外如有不足, 欢迎各位大佬指正, 谢谢!
文章目录
1. 锁的类型
1.1 乐观锁
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。1.1.1 CAS
- 全称
compare and swap
,一个CPU原子指令,在硬件层面实现的机制,体现了乐观锁的思想。- JVM用C语言封装了汇编调用。Java的基础库中有很多类就是基于 JNI 调用C接口实现了多线程同步更新的功能。
1.1.2 原理
CAS有三个操作数:
-
当前主内存变量的值V
-
线程本地变量预期值A
-
线程本地待更新值B。
当需要更新变量值的时候,会先获取到内存变量值V然后跟预期值A进行比较,如果相同则更新为B,如果不同,则将最新的变量值更新到预期值中再重新尝试上面的步骤,直到成功为止。
如果普通线程执行加减操作, 反编译可以看到其是由三个指令构成的:
所以多线程切换
可能会造成数据更新的不同步
就是对被操作的数据加锁,可以是悲观锁,可以是乐观锁,这里使用的就是基于乐观锁实现的AtomicInteger类
1.1.3 CAS的缺点
- ABA问题:我内存对象从A变成B在变成A,CAS会当成没有变化,进而去更新值,实际是有变化的。
- 循环时间开销大:一直和预期值不对的情况下,会一直循环。
- 只能保证一个共享变量的原子操作。
1.2 悲观锁
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以 每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。 java中的悲观锁就是Synchronized
1.3 公平锁/非公平锁
- 公平锁是FIFO机制,谁先来谁就在队列的前面,就能优先获得锁
- 非公平锁支持抢占模式,先来的不一定能得到锁。
package thread;import java.util.ArrayList;import java.util.concurrent.locks.ReentrantLock;public class Test6 { public static class Service { public ReentrantLock fairLock = new ReentrantLock(true); public void testMethod() { try { fairLock.lock(); System.out.println(Thread.currentThread().getName() + "拿到了锁"); } finally { fairLock.unlock(); } } } public static void main(String[] args) { Service service = new Service(); ArrayListlist = new ArrayList<>(); Runnable runnable = new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "开始运行"); service.testMethod(); } }; for (int i = 0; i < 10; i++) { list.add(new Thread(runnable)); } System.out.println(list); for (Thread thread : list) { thread.start(); } }}----output----[Thread[Thread-0,5,main], Thread[Thread-1,5,main], Thread[Thread-2,5,main], Thread[Thread-3,5,main], Thread[Thread-4,5,main], Thread[Thread-5,5,main], Thread[Thread-6,5,main], Thread[Thread-7,5,main], Thread[Thread-8,5,main], Thread[Thread-9,5,main]]Thread-0开始运行Thread-1开始运行Thread-0拿到了锁Thread-2开始运行Thread-2拿到了锁Thread-3开始运行Thread-3拿到了锁Thread-4开始运行Thread-5开始运行Thread-6开始运行Thread-4拿到了锁Thread-1拿到了锁Thread-6拿到了锁Thread-7开始运行Thread-8开始运行Thread-5拿到了锁Thread-9开始运行Thread-7拿到了锁Thread-8拿到了锁Thread-9拿到了锁运行的顺序:0,1,2,3,4,5,6,7,8,9.获得锁的顺序:0,2,3,4,1,6,5,7,8,9.把true去掉改成非公平锁----output----[Thread[Thread-0,5,main], Thread[Thread-1,5,main], Thread[Thread-2,5,main], Thread[Thread-3,5,main], Thread[Thread-4,5,main], Thread[Thread-5,5,main], Thread[Thread-6,5,main], Thread[Thread-7,5,main], Thread[Thread-8,5,main], Thread[Thread-9,5,main]]Thread-0开始运行Thread-1开始运行Thread-2开始运行Thread-3开始运行Thread-4开始运行Thread-1拿到了锁Thread-6开始运行Thread-6拿到了锁Thread-5开始运行Thread-2拿到了锁Thread-7开始运行Thread-8开始运行Thread-7拿到了锁Thread-9开始运行Thread-3拿到了锁Thread-0拿到了锁Thread-4拿到了锁Thread-5拿到了锁Thread-8拿到了锁Thread-9拿到了锁运行的顺序:0,1,2,3,4,5,6,7,8,9拿到锁的顺序:1,6,2,7,3,6,4,5,8,9
上面的例子可以看出,公平锁的是有顺序的,非公平锁是无序的。同样除了重入锁,重入读写锁一样可以设置非公平和公平。
由重量级细化分为四种:自旋锁,轻量级锁,偏向锁,重量级锁
1.4 可重入锁
可重入锁有 :
- sychronized
- ReentrantLock ReentrantLock 和 synchronized 不一样,需要手动释放锁,所以使用 ReentrantLock的时候一定要手动释放锁,并且加锁次数和释放次数要一样
可重入锁代码:
package com.test.reen;import java.util.Random;import java.util.concurrent.locks.ReentrantLock;// 演示可重入锁是什么意思public class WhatReentrant2 { public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(); new Thread(new Runnable() { @Override public void run() { try { lock.lock(); System.out.println("第1次获取锁,这个锁是:" + lock); int index = 1; while (true) { try { lock.lock(); System.out.println("第" + (++index) + "次获取锁,这个锁是:" + lock); try { Thread.sleep(new Random().nextInt(200)); } catch (InterruptedException e) { e.printStackTrace(); } if (index == 10) { break; } } finally { lock.unlock(); } } } finally { lock.unlock(); } } }).start(); }}
1.5 读写锁
所有读写锁的实现必须确保写操作对读操作的内存影响。换句话说,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。
读写锁比互斥锁允许对于共享数据更大程度的并发。每次只能有一个写线程,但是同时可以有多个线程并发地读数据。ReadWriteLock
适用于读多写少的并发情况。
Java并发包中 ReadWriteLock 是一个接口,主要有两个方法,如下:
public interface ReadWriteLock { /** * 返回读锁 */ Lock readLock(); /** * 返回写锁 */ Lock writeLock();}
Java并发库中 ReetrantReadWriteLock
实现了 ReadWriteLock
接口并添加了可重入的特性。
1.5.1 ReentrantReadWriteLock
特性:
- 获取顺序
- 非公平模式 (默认) 当以非公平初始化时,读锁和写锁的获取的顺序是不确定的。非公平锁主张竞争获取,可能会延缓一个或多个读或写线程,但是会比公平锁有更高的吞吐量。
- 公平模式 当以公平模式初始化时,线程将会以队列的顺序获取锁。当当前线程释放锁后,等待时间最长的写锁线程就会被分配写锁;或者有一组读线程组等待时间比写线程长,那么这组读线程组将会被分配读锁。 当有写线程持有写锁或者有等待的写线程时,一个尝试获取公平的读锁(非重入)的线程就会阻塞。这个线程直到等待时间最长的写锁获得锁后并释放掉锁后才能获取到读锁。
- 可重入 允许读锁可写锁可重入。写锁可以获得读锁,读锁不能获得写锁。
- 锁降级 允许写锁降低为读锁
- 中断锁的获取 在读锁和写锁的获取过程中支持中断
- 支持Condition 写锁提供Condition实现
- 监控 提供确定锁是否被持有等辅助方法
1.5.1 读锁
读锁是非独占锁, 只读的话可以多线程共同占有
1.5.2 写锁
写锁是独占锁,
扩展 : 操作系统
用户态核心态
两个操作系统的概念,用户态,核心态
- 当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态)。
- 当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。
线程阻塞的代价
java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间;
如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的。2. sychronized和lock的区别
2.1 相同
Synchronized
和Lock
都可以
- 用来做代码块的同步控制
- 都是可重入锁
2.2 区别
它俩的底层实现不一样,性能也不一样,需要根据不同的应用场景来选择合适的同步器
- 来源不同
- Synchronized是Java提供的关键字,属于Java语法层面的互斥锁,也称“隐式锁”。竞争锁、释放锁的过程开发者无需关心也不能干预,由JVM来完成。
- Lock是指
java.util.concurrent
包下的Lock接口,描述的是一把同步锁,由Java代码来控制多线程同步,也称“显式锁”。可以自己实现一把锁,也可以直接使用由并发大神Doug Lea编写的ReentrantLock
。
- 锁的释放不同
- Synchronized锁的释放由JVM来完成,开发者无法干预。同步代码块运行结束,或者出现异常JVM均会释放锁。
- Lock加的锁必须开发者手动释放,如果同步代码块抛了异常,锁没释放则会发生死锁,一般释放锁代码建议写在
finally
块中,确保锁一定释放。
- 性能不同
- Synchronized在JDK6之前,采用OS级别的互斥锁,竞争锁失败的线程会被挂起,性能非常低,JDK6做了大量优化,会自动进行锁膨胀,降低了锁开销,性能提升很大,但是竞争激烈时性能还是会下降。
- Lock不管锁竞争激烈与否,性能基本保持在一个数量级,适合锁竞争比较激烈的应用场景。
- 竞争锁失败的线程状态不同
- Synchronized竞争锁失败的线程状态是:BLOCKED。
- Lock竞争锁失败的线程状态是:WAITING。
- JVM堆栈跟踪
- Synchronized阻塞的线程更加便于JVM跟踪,使用jstack可以清楚的看到。
- Lock通过LockSupport.park()来阻塞线程,不利于JVM跟踪。
- 响应中断
- Synchronized不支持响应中断,竞争不到锁会一直阻塞。
- Lock支持响应中断。
- 锁超时
- Synchronized不支持锁超时,竞争不到锁会一直死等,容易造成死锁。
- Lock支持锁超时,在给定时间内获取不到锁可以进行其他处理。
- 公平/非公平锁
- Synchronized采用非公平锁,且不允许修改,可能会造成“线程饿死”。
- Lock支持公平锁与非公平锁,开发者可以自己选择。
- 尝试获取锁判断
- Synchronized不支持获取锁成功与否的判断。
- Lock支持。
- 读写锁
- Synchronized不支持读写锁,对于读多写少的场景无法优化性能。
- Lock支持读写锁,读读不互斥,对于读多写少的场景可以进一步优化性能。
对于OS来说,线程只有三种状态:Ready、Running、Blocked。
JVM中的线程不管是BLOCKED还是WAITING,在OS看来都是Blocked阻塞态。
都是阻塞,JVM为什么还要区分不同状态呢?
- 为了便于JVM管理线程,虽然在OS看来都是阻塞
- 但是在JVM看来,BLOCKED和WAITING是两种不同的含义:一个是线程被BLOCKED、一个是线程主动WAITING。
例如:当线程释放锁后,JVM只需要去BLOCKED队列唤醒一个线程,而线程调用notify()则去WAITING队列唤醒一个线程。
3. Synchronized实现原理
它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
- Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
- Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中;
- Wait Set:哪些调用wait方法被阻塞的线程被放置在这里;
- OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck;
- Owner:当前已经获取到所资源的线程被称为Owner;
- !Owner:当前释放锁的线程。
锁的是对象,而非代码
- 修饰实例方法时,锁的是实例对象。
- 修饰静态方法时,锁的是类的class对象。
- 修饰代码块时,锁的是给定对象。
在Java中,对象除了自身的实例数据外,还有开发者看不到的一些数据:对象头、对齐字节。如下图:
当线程成功竞争到锁时,会修改对象头中的Mark Word
数据:偏向线程ID和锁标记。
补充点: 在Java中,任何对象都有对象头信息,这意味着任何对象都可以当锁。
基本数据类型不是对象,没有对象头信息,这也就解释了:为什么基本数据类型不能作为锁对象?
3.1 syn中的monitorenter和monitorexit指令
Synchronized的实现依赖于JVM指令
monitorenter
和monitorexit
。
public class MonitorDemo { synchronized void syncMethod(){ } void method(){ synchronized (this){ } }}
使用javac编译成class文件,再使用javap -verbose
生成JVM汇编指令,如下图:
同步方法中Synchronized流程:
JVM 会给其加上
ACC_SYNCHRONIZED
标识,当线程执行一个方法前,会先检查方法是否存在ACC_SYNCHRONIZED
标识,如果存在则要去竞争对应的monitor锁,竞争锁成功再执行方法,否则线程阻塞。
同步代码块中Synchronized流程:
JVM则会在代码块开始前后插入
monitorenter
和monitorexit
指令,分别为竞争锁和释放锁。
3.1.1 monitorenter
在Java中,每个对象都会与一个monitor相关联,当某个monitor被拥有之后就会被锁住,当线程执行到monitorenter指令时,就会去尝试获得对应的monitor。
因为Synchronized锁是可以重入的,所以每个monitor都维护了一个计数器。
- 线程每次执行
monitorenter
前都会进行判断,如果当前线程拥有monitor,指令计数器就会加1; - 如果monitor说明没有获得锁,线程阻塞。
3.1.1 monitorexit
- 线程每执行完一次
monitorexit
,计数器就减 1,当计数器减至 0 时,monitor将会被释放,其他线程可以来竞争。
4. JDK6
的锁优化
在JDK6之前,Synchronized是非常笨重的,以至于开发者不太愿意使用而慢慢摒弃它
但是在JDK6中,对Synchronized做了大量的优化,性能和
ReentrantLock
已经不相上下,官方也更加推荐使用Synchronized。
4.1 锁消除
设计一个类时,为了考虑并发安全,往往会对代码块上锁。
但是有时候压根就不会产生并发问题
例如:在线程私有的栈内存中使用线程安全的类实例,且实例不存在逃逸。
如果不存在并发安全,那还有什么理由上锁呢?
在 JIT 编译时,会对运行上下文进行扫描,去除不可能产生并发问题的锁。public String method(){ StringBuffer sb = new StringBuffer(); sb.append("1"); sb.append("2"); return sb.toString();}
如上代码,StringBuffer的append()方法被synchronized
修饰,但是在该方法中不存在并发问题,方法栈内存为线程私有,sb实例不可能被其他线程访问到,对于这种情况就会进行锁消除。
4.2 锁粗化
由于锁的竞争和释放开销比较大,如果代码中对锁进行了频繁的竞争和释放,那么JVM会进行优化,将锁的范围适当扩大。
如下代码,在循环内使用synchronized,JVM锁粗化后,会将锁范围扩大到循环外面。
public String method(){ for (int i= 0; i < 100; i++) { synchronized (this){ ... } }}
4.3 自旋锁
线程阻塞不必直接转化为内核态, 尝试自旋可以节省下来切换成内核态(因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。)
所需要的时间
Question : 当有多个线程在竞争同一把锁时,竞争失败的线程如何处理?
面对这种情况有两种选择:
- 将线程挂起,锁释放后再将其唤醒。
- 线程不挂起,自旋操作,不断的监测锁状态并竞争。 如果锁竞争非常激烈,且短时间得不到释放,那么将线程挂起效率会更高,因为竞争失败的线程不断自旋会造成CPU空转,浪费性能。
优势:
如果锁竞争并不激烈,且锁会很快得到释放,那么自旋效率会更高。因为将线程挂起和唤醒是一个开销很大的操作。
自旋锁的优化是针对“锁竞争不激烈,且会很快释放”的场景,避免了OS频繁挂起和唤醒线程。
缺点:
但是线程自旋是需要消耗CPU的,说白了就是让CPU在做无用功,如果一直获取不到锁,那线程也不能一直占用CPU自旋做无用功,所以需要设定一个自旋等待的最大时间.4.4 自适应自旋锁
当线程竞争锁失败时,自旋和挂起哪一种更高效?
自适应自旋锁 解决的就是这个问题。
策略:
- 当线程竞争锁失败时,会自旋N次,如果仍然竞争不到锁,说明锁竞争比较激烈,继续自旋会浪费性能,JVM就会将线程挂起。
- JDK6之前: 自旋的次数通过JVM参数
-XX:PreBlockSpin
设置,但是开发者往往不知道该设置多少比较合适- 于是在JDK6中: 对其进行了优化,加入了“自适应自旋锁”。
自适应自旋锁的大致原理 :
- 线程如果自旋成功了,那么下次自旋的最大次数会增加,因为JVM认为既然上次成功了,那么这一次也很大概率会成功。
- 反之,如果很少会自旋成功,那么下次会减少自旋的次数甚至不自旋,避免CPU空转。
4.5 锁膨胀
在JDK6之前,Synchronized用的都是重量级锁,依赖于OS的Mutex Lock来实现,OS将线程从用户态切换到核心态,成本非常高,性能很低。
在JDK6中,针对锁进行优化,不直接使用重量级锁,而是逐步进行锁的膨胀。
锁状态的级别由低到高为:无锁、偏向锁、轻量级锁、重量级锁。 偏向锁、轻量级锁都属于乐观锁,重量级锁属于悲观锁。默认为无锁状态,随着锁竞争的激烈程度会不断膨胀,最终才会使用开销最大的重量级锁。
4.5.1 无锁
在对象的头信息Mark Word中记录了对象的锁状态,如下图:
如果没有任何线程竞争锁,那么对象默认为无锁状态。
4.5.2 偏向锁
针对单线程锁竞争做的优化,最乐观的锁。
HotSpot作者经过研究发现,开发者为了保证线程安全问题给代码块上了锁,但是大多数情况下,锁并不存在多线程竞争,而是单线程反复获得。
单一线程,为什么还要去频繁的获取和释放锁呢?所以就有了“偏向锁”的概念。
偏向锁是针对单线程反复获得锁而做的优化,是最乐观的锁:只有单个线程来竞争锁。
在JDK5中偏向锁是关闭的,JDK6中默认开启,可以通过JVM参数
-XX:-UseBiasedLocking
来关闭偏向锁。
偏向锁大致流程如下:
- 线程A第一次获得锁后,CAS操作修改对象头信息中的Mark Word:无锁->偏向锁、偏向线程ID->线程A。
- 线程A需要再次获得锁时,首先判断偏向线程ID是否是自己,如果是则直接获得锁,速度非常快。 偏向锁并不会主动释放,需要等待其他线程来竞争。 线程B来竞争锁,发现锁偏向线程A,此时CAS操作失败,则进一步判断:线程A是否还在占用锁?
- 线程A未占用:将锁重新偏向线程B,线程B获得锁。
- 线程A仍占用:说明锁存在多线程竞争,升级为:轻量级锁。
4.5.3 轻量级锁
针对锁竞争不激烈做的优化,使用自旋锁避免线程频繁挂起和唤醒。
只有单一线程竞争锁时用的是偏向锁,最乐观的锁也是性能最高的锁。
一旦涉及到多线程竞争锁,就会升级为轻量级锁。- 轻量级锁认为:存在多线程竞争锁,但是竞争不激烈。
- 轻量级锁的实现原理:让竞争锁失败的线程自旋而不是挂起。
如果将竞争锁失败的线程直接挂起,然后锁释放后再将其唤醒,这是一个开销很大的操作。
而大多数情况下,锁的占用时间往往非常短,会很快被释放,那么轻量级锁认为:不要挂起线程,而是让其进行自旋,执行一些无用的指令,只要锁被释放,线程马上就能获得锁,而不用等待OS将其唤醒。
线程A获得锁未释放,此时线程B来竞争锁,发现锁被线程A占用,线程B认为线程A可能很快就会释放锁,于是进行自旋操作:
- 自旋成功:说明锁的占用时间并不长,下次会自适应增加最大自旋次数(自适应自旋)。
- 自旋失败:锁的占用时间较长,继续自旋会浪费CPU资源,线程被挂起,升级为:重量级锁。
4.5.4 重量级锁
开销最大,性能最低的悲观锁,锁竞争激烈时采用。
- 锁竞争不激烈时,竞争锁失败的线程进行自旋而非挂起可以提升性能,因为
自旋的开销<线程挂起、唤醒的开销
。 - 但是锁竞争激烈时,自旋会造成更大的资源开销。 例如:100个线程竞争同一把锁,99个线程在自旋,意味着99%的CPU资源被浪费,此时
自旋的开销>线程挂起、唤醒的开销
。
当竞争比较激烈时,就会膨胀为重量级锁,因为轻量级锁的效率此时更低。
重量级锁通过监视器锁(Monitor)实现,Monitor又依赖于底层OS的Mutex Lock
实现。
升级为重量级锁后,所有竞争锁失败的线程都会被阻塞挂起,锁被释放后再将线程唤醒。
线程频繁的挂起和唤醒,OS需要将线程从用户态切换为核心态,这个操作成本是非常高的,需要花费较长的时间
,这就导致重量级锁效率很低。
4.5.5 性能比较测试
在Synchronized和Lock的区别中已经说过,在不同场景下两者的性能表现不同。
尽管JDK6为Synchronized做了大量优化,但是在竞争比较激烈时,Synchronized的性能依然会有所下降。
而Lock不管锁竞争激烈与否,性能基本保持在一个数量级,适合锁竞争比较激烈的应用场景。分别对Synchronized和Lock进行性能测试,1、10、100线程下分别进行1亿次自增运算,采样5次。
测试代码:
/** * @Author: pch * @Date: 2020/1/28 20:29 * @Description: 性能测试模板类 */public abstract class PerformanceTemplate { protected int threadCount = 0;//线程数 protected int index = 0; protected final int count; protected long startTime = System.currentTimeMillis(); private final CyclicBarrier cb; public PerformanceTemplate(int count, int threadCount) { this.count = count; this.threadCount = threadCount; this.cb = new CyclicBarrier(threadCount); } public void test() { int c = count / threadCount; for (int i = 0; i < threadCount; i++) { new Thread(() -> { try { cb.await(); } catch (Exception e) { e.printStackTrace(); } while (true) { func(); } }).start(); } } protected abstract void func(); protected void print(){ System.out.println("耗时:" + (System.currentTimeMillis() - startTime)+"ms"); startTime = System.currentTimeMillis(); }}/** * @Author: pch * @Date: 2020/1/28 15:05 * @Description: synchronized性能测试 */public class Sync extends PerformanceTemplate { public Sync(int count, int threadCount) { super(count, threadCount); } @Override protected synchronized void func() { if (++index % count == 0) { print(); } } @Override protected void print() { System.out.print("Synchronized:1亿次运算,"+threadCount+"线程耗时:"); super.print(); }}/** * @Author: pch * @Date: 2020/1/28 21:11 * @Description: Lock性能测试 */public class Lock extends PerformanceTemplate { private ReentrantLock lock = new ReentrantLock(); public Lock(int count, int threadCount) { super(count, threadCount); } @Override protected void func() { lock.lock(); if (++index % count == 0) { print(); } lock.unlock(); } @Override protected void print() { System.out.print("Lock:1亿次运算,"+threadCount+"线程耗时:"); super.print(); }}
测试结果:
Synchronized:1亿次运算,1线程耗时:耗时:3174msSynchronized:1亿次运算,1线程耗时:耗时:1878msSynchronized:1亿次运算,1线程耗时:耗时:2404msSynchronized:1亿次运算,1线程耗时:耗时:2392msSynchronized:1亿次运算,1线程耗时:耗时:2409ms---Synchronized:1亿次运算,10线程耗时:耗时:4835msSynchronized:1亿次运算,10线程耗时:耗时:5407msSynchronized:1亿次运算,10线程耗时:耗时:5391msSynchronized:1亿次运算,10线程耗时:耗时:5406msSynchronized:1亿次运算,10线程耗时:耗时:5462ms---Synchronized:1亿次运算,100线程耗时:耗时:4538msSynchronized:1亿次运算,100线程耗时:耗时:4921msSynchronized:1亿次运算,100线程耗时:耗时:4957msSynchronized:1亿次运算,100线程耗时:耗时:4999msSynchronized:1亿次运算,100线程耗时:耗时:4980ms
Lock:1亿次运算,1线程耗时:耗时:1985msLock:1亿次运算,1线程耗时:耗时:1961msLock:1亿次运算,1线程耗时:耗时:1857msLock:1亿次运算,1线程耗时:耗时:2138msLock:1亿次运算,1线程耗时:耗时:1912ms---Lock:1亿次运算,10线程耗时:耗时:2986msLock:1亿次运算,10线程耗时:耗时:2861msLock:1亿次运算,10线程耗时:耗时:2792msLock:1亿次运算,10线程耗时:耗时:2792msLock:1亿次运算,10线程耗时:耗时:2773ms---Lock:1亿次运算,100线程耗时:耗时:3023msLock:1亿次运算,100线程耗时:耗时:2743msLock:1亿次运算,100线程耗时:耗时:2706msLock:1亿次运算,100线程耗时:耗时:2714msLock:1亿次运算,100线程耗时:耗时:2765ms
可以看到,Synchronized经过优化之后,性能并不差,和Lock差不多,Lock性能稍微高一丢丢。
4.5.6 Sychornized和lock两者如何选择?
-
Synchronized是Java内置的同步器,使用简单,语法清晰易读,性能也不差,而且便于JVM堆栈跟踪,官方也表示Synchronized性能后期还有优化的余地,所以如果没有特殊要求,建议尽量使用Synchronized。
-
虽然建议尽量使用Synchronized,但是它毕竟自身存在一些功能上的缺陷,例如:无法响应中断,不支持锁超时,不能采用公平锁等等,如果确实需要这些高级特性,那么还是应该使用ReentrantLock(重进入锁)。
两者如何选择,还是应该根据实际的业务需求来,另外并发大神Doug Lea也给出了答案:
在一些内置锁无法满足需求的情况下,ReentrantLock可以作为一种高级工具。当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的,可轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁。否则,还是应该优先使用Synchronized。
转载地址:https://blog.csdn.net/weixin_40597409/article/details/114671510 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!