锁的优化
提高锁的性能
减少锁的持有时间
减少锁的持有时间,有助于降低所冲突的可能性,进而提升系统的并发能力。例如:
public synchronized void syncMethod(){
othercode1();
mutexMethod();
othercode2();
}
改为只在必要的时候进行同步:
public void syncMethod2(){
othercode1();
synchronized (this){
mutexMethod();
}
othercode2();
}
再如Pattern类中的matcher() 方法:
public Matcher matcher(CharSequence input) {
if (!compiled) {
synchronized(this) {
if (!compiled)
compile();
}
}
Matcher m = new Matcher(this, input);
return m;
}
matcher() 方法有条件地申请行锁,只在表达式未编译时局部加锁。
减小锁的粒度
减小锁的粒度,就是缩小锁定对象的范围,从而降低锁冲突的可能性。
例如ConcurrentHashMap类,内部进一步划分为若干个小的HashMap,称之为段(Segment)。如果在ConcurrentHashMap中添加一个新的表项,并不是对整个HashMap加锁,而是根据hashcode得到的该表项应该存在哪个段中,对该段进行加锁,然后完成put() 操作。
读写锁替换独占锁
对系统功能点进行分割,在读多写少的情况下有效提升系统的并发能力。
锁分离
将读写锁的思想进一步延伸。依据应用程序的功能特点,对独占锁进行分离。
例如在java.util.concurrent.LinkedBlockingQueue的实现中,take() 方法和put() 方法分别是往队列中取数据和增加数据。因此两个操作分别是在队头和队尾,理论上来说,两者不会发生冲突。如果使用独占锁,则要求两种操作进行时需要获得当前队列的独占锁,那么take() 方法和put() 方法就不能实现真正的并发。
锁粗化
锁粗化的思想与减少锁的持有时间是相反的,在不同的场合下根据实际情况进行权衡。
例如对同一个锁不停地进行请求、同步和释放,其本身也会消耗系统资源,反而不利于性能的优化。此时,可以将所有对锁的操作整合为一次请求,减少对锁的同步请求次数。
public void demoMethod(){
synchronized (lock){
// 同步操作
}
// 无需同步的操作,但很快可以执行完毕
synchronized (lock){
// 同步操作
}
}
整合为:
public void demoMethod(){
synchronized (lock){
// 同步操作
// 无需同步的操作,但很快可以执行完毕
// 同步操作
}
}
再如:
for (int i = 0; i < circle; i++) {
synchronized (lock){
// 同步操作
}
}
整合为:
synchronized (lock){
for (int i = 0; i < circle; i++) {
// 同步操作
}
}
JVM中对锁的优化策略
锁偏向
针对加锁操作的优化手段。核心思想是:如果有一个线程获得了锁,那么锁进入偏向模式,当这个线程再次请求锁时,无需再做任何同步操作。
对于几乎没有锁竞争的场合,偏向锁有比较好的优化效果。而对于锁竞争比较激烈的场合,最有可能的情况是每次都是不同的线程来请求相同的锁,使偏向模式失效,效果不佳。
轻量级锁
如果偏向锁失败,虚拟机不会立刻挂起线程,而会使用一种称为轻量级锁的优化手段。轻量级锁的操作为,将对象头部作为指针指向持有锁的线程堆栈的内部,来判断一个线程是否持有对象锁。
如果线程获取轻量级锁成功,则进入临界区。若失败,则膨胀为重量级锁。
自旋锁
在锁膨胀后,为了避免线程真实地在操作系统层面挂起,虚拟机会做最后的努力——自旋锁。虚拟机会让当前线程做几个空循环(自旋的含义),经过若干个循环后如果能够得到锁,就能顺利进入临界区。若不能,才会在操作系统层面挂起。
锁消除
锁消除是一种更为彻底的锁优化方法。Java虚拟机在JIT编译时,通过对运行上下文的扫描,出去不可能存在共享资源竞争的锁,。通过锁消除,可以节省毫无意义的请求锁的时间。
例如在使用一些JDK的内置API时,比如StringBuilder、Vector时,很可能会在不存在并发竞争的场合使用。Vector内部使用了synchronized请求锁。在如下代码中:
public String[] createString() {
Vector<String> v = new Vector<String>();
for (int i = 0;i < 100;i++){
v.add(Integer.toString(i));
}
return v.toArray(new String[]{});
}
由于变量v只在createString() 函数中使用,因此只是一个局部变量。局部变量是在线程栈上分配的,不会有其他线程访问。这种情况下加锁是没有必要的。
锁消除涉及的一项关键技术为逃逸分析,就是观察某一个变量是否会逃出某一个作用域。
ThreadLocal
除了控制资源的访问之外,还可以通过增加资源来保证所有对象的线程安全。如果说锁的使用是第一种思路,那么ThreadLocal的使用就是第二种思路。
ThreadLocal的使用
ThreadLocal是一个线程的局部变量,只有当前线程可以访问,因此是线程安全的。
public class DateParseBadDemo {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static class ParseDate implements Runnable {
private int i = 0;
public ParseDate(int i) {
this.i = i;
}
@Override
public void run() {
try {
Date t =sdf.parse("2015-03-29 19:29:"+i%60);
System.out.println(i+":"+t);
}catch (ParseException e){
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(10);
for (int i = 0;i<1000;i++){
es.execute(new ParseDate(i));
}
}
}
上述代码在运行中会出现大量的java.lang.NumberFormatException,原因在于SimpleDateFormatparse() 方法是线程不安全的,一种可行的办法是在sdf.parse() 方法的前后加锁。但这里可以采取另一种思路:使用ThreadLocal为每一个线程创建一个SimpleDateFormat对象实例:
static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<>();
public static class ParseDate implements Runnable {
private int i = 0;
public ParseDate(int i) {
this.i = i;
}
@Override
public void run() {
try {
if (tl.get() == null) {
tl.set(new SimpleDateFormat("yyyy-MM-dd "));
}
Date t = tl.get().parse("2015-03-29 19:29:" + i % 60);
System.out.println(i + ":" + t);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
其中,为每一个线程分配一个对象的工作不是由ThreadLocal来完成的,而是需要在应用层面完成,ThreadLocal只起到了容器的作用。
ThreadLocal的实现原理
ThreadLocal的实现原理如下图所示:
由于ThreadLocalMap是定义在Thread类内部的,因此只要线程不退出,对象的引用将一直存在。例如在使用线程池时,线程在结束任务之后不一定会退出。如果将一些比较大的对象设置在ThreadLocal中,可能会使系统出现内存泄漏的现象。
- 最好的方法是使用ThreadLocal.remove() 方法将变量移除。
- 此外由于Entry是弱引用,可以特意写出 tl = null ,使tl所指向的对象更容易被垃圾回收器发现,从而加速回收。
性能影响
如果共享对象由于竞争的处理容易引起性能损失,此时可以考虑ThreadLocal为每一个线程分配单独的对象。例如下面这个例子:多线程下陈胜随机数。
public class ThreadLocalRandom {
public static final int GEN_COUNT = 10000000;
public static final int THREAD_COUNT = 4;
static ExecutorService exe = Executors.newFixedThreadPool(THREAD_COUNT);
public static Random rnd = new Random(123);
public static ThreadLocal<Random> tRnd = new ThreadLocal<Random>() {
@Override
protected Random initialValue() {
return new Random(123);
}
};
public static class RndTask implements Callable<Long> {
private int mode = 0;
public RndTask(int mode) {
this.mode = mode;
}
public Random getRandom() {
if (mode == 0) {
return rnd;
} else if (mode == 1) {
return tRnd.get();
} else {
return null;
}
}
@Override
public Long call() throws Exception {
long b = System.currentTimeMillis();
for (long i = 0; i < GEN_COUNT; i++) {
getRandom().nextInt();
}
long e = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + " spend" + (e - b) + "ms");
return e - b;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
Future<Long>[] futs = new Future[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
futs[i] = exe.submit(new RndTask(0));
}
long totaltime = 0;
for (int i = 0; i < THREAD_COUNT; i++) {
totaltime += futs[i].get();
}
System.out.println("多线程访问同一个Random实例:" + totaltime + "ms");
// ThreadLocal的情况
for (int i = 0;i<THREAD_COUNT;i++){
futs[i] = exe.submit(new RndTask(1));
}
totaltime = 0;
for (int i = 0; i<THREAD_COUNT;i++){
totaltime += futs[i].get();
}
System.out.println("使用ThreadLocal包装Random实例:"+ totaltime+"ms");
exe.shutdown();
}
}
OUTPUT
pool-1-thread-4 spend2653ms
pool-1-thread-2 spend2789ms
pool-1-thread-1 spend2812ms
pool-1-thread-3 spend2814ms
多线程访问同一个Random实例:11068ms
pool-1-thread-3 spend92ms
pool-1-thread-1 spend93ms
pool-1-thread-2 spend93ms
pool-1-thread-4 spend93ms
使用ThreadLocal包装Random实例:371ms
无锁
对于并发控制而言,锁是一种悲观的策略,总是假设每一次临界区操作都会产生冲突。而无锁是一种乐观的策略,它假设对资源是没有冲突的,没有冲突自然不需要等待。无锁策略使用一种叫做比较交换(CAS,Compare and Swap)的技术来鉴别线程的冲突,一旦检测到冲突发生,就重试当前操作直到没有冲突为止。
CAS算法
CAS算法包含三个参数CAS(V,E,N),其中
- V表示内存中的实际值
- E表示预期值(也就是旧的值)
- N表示新值(要将V改为N)
当V值等于E值时(说明中间没有发生修改),才会将V值修改为N值。CAS最终返回当前V的真实值。当多个线程使用CAS操作一个变量时,只有一个线程会成功,并成功更新变量值,其他线程均会失败。失败线程会重新尝试或将线程挂起(阻塞)。
在硬件层面,大多数的现代操作系统都已经支持原子化的CAS指令。在JDK1.5之后,虚拟机就可以使用这种指令来实现并发操作和并发数据结构。
无锁的线程安全整数:AtomicInteger
在JDK并发包中有一个atomic包,其中包含了一些可以直接使用CAS操作的线程安全的类型。其中最常用的是AtomicInteger。与Integer不同的是,AtomicInteger是可变的,并且是线程安全的,对其进行的任何操作都是用CAS指令进行的。
AtomicInteger中的核心字段value
private volatile int value;
AutomicInteger使用的例子:
public class AtomicIntegerDemo {
static AtomicInteger i = new AtomicInteger();
public static class AddThread implements Runnable {
@Override
public void run() {
for (int k = 0; k < 10000; k++) {
i.incrementAndGet();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] ts = new Thread[10];
for (int k = 0; k < 10; k++) {
ts[k] = new Thread(new AddThread());
}
for (int k = 0; k < 10; k++) {
ts[k].start();
}
for (int k = 0; k < 10; k++) {
ts[k].join();
}
System.out.println(i);
}
}
OUTPUT
100000
和AutomicInteger类似的类还有:AutomicLong、AtomicBoolean、AutomicReference等。
Java中的指针:Unsafe类
Java和C++的一个重要区别就是在Java中无法直接操作一块内存区域,如申请内存和释放内存。但Java中的Unsafe类提供了类似C++手动管理内存的能力。从名字可以看出JDK开发人员并不希望大家使用这个类,仅作为JDK内部使用的一个专属类。
Unsafe类是“final”的,不允许继承。构造方法为私有,只能通过工厂方法获得实例,且当且仅当调用getUnsafe()方法的类为引导类加载器所加载时才合法,否则抛出SecurityException异常。
其他Unsafe类的相关内容可以参考:
Java中的Unsafe:https://www.jianshu.com/p/db8dce09232d
Unsafe实现CAS操作的例子
在使用AtomicInteger进行累加的例子中,i.incrementAndGet()的具体过程如下:
compareAndSwapInt()方法是一个本地方法,var1位给定的对象,var2为对象内的偏移量,通过这两个参数可以快速定位字段。var4为期望值,var5为要设置的新值。当内存中的实际值与var4相同时,才会将内存中的值改为var5。在getAndAddInt方法中不断循环compareAndSwapInt()方法,直到设置成功。
无锁的对象引用:AtomicReference
AtomicReference与AtomicInteger类似,不同在于AtomicInteger是对整数的封装,AtomicReference是对普通对象的引用,可以保证在修改对象引用时的线程安全性。
ABA问题
ABA问题指的是:在CAS操作中,已经获得了对象的值,准备进行修改前,对象的值被多次修改但最终恢复为原值。此时无法判断该对象是否被修改过。
一般来说,发生ABA的情况很小,且在对于状态不敏感的场景中,即使发生了ABA现象也无关紧要。但某些场景,是否能修改对象的值不仅取决于当前值,还和对象的变化过程有关,此时AtomicReference就失效了。
假设这样一个场景:有一家蛋糕店为了挽留客户,决定为贵宾卡里余额小于20元的客户一次性赠送20元,来刺激客户充值和消费,但是条件是每一个顾客只能被赠送一次。
public class AtomicReferenceDemo {
static AtomicReference<Integer> money = new AtomicReference<>();
public static void main(String[] args) {
// 设置账户初始值小于20,显然是一个需要被充值的账户
money.set(19);
// 模拟多个线程同时更新后台数据库,为用户充值
for (int i = 0; i < 3; i++) {
new Thread() {
@Override
public void run() {
while (true) {
while (true) {
Integer m = money.get();
if (m<20){
if (money.compareAndSet(m,m+20 )){
System.out.println("余额小于20元,充值成功,余额:"+money.get()+"元");
break;
}
}else {
// System.out.println("余额大于20元,无需充值");
break;
}
}
}
}
}.start();
}
// 用户消费线程,模拟消费行为
new Thread(){
@Override
public void run(){
for (int i = 0 ;i<100;i++){
while (true){
Integer m = money.get();
if (m>10){
System.out.println("大于10元");
if (money.compareAndSet(m,m-10)){
System.out.println("成功消费10元,余额:"+money.get()+"元");
break;
}
}else {
System.out.println("没有足够的资金");
break;
}
}
}
}
}.start();
}
}
上述代码会进行多次充值,为解决这一问题,JDK提供了AtomicStampedReference。
带有时间戳的对象引用:AtomicStampedReference
AtomicStampedReference内部不仅维护了对象值,还维护了一个时间戳(可以用任意整数表示状态值)。当AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还需更新时间戳。当AtomicStampedReference设置对象的值时,对象的时间戳必须满足期望值,才会写入成功。
AtomicStampedReference的几个API在AtomicRenference的基础上新增了关于时间戳的信息。
// 比较设置,参数依次为:期望值、写入新值、期望时间戳、新时间戳
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp)
// 获得当前对象引用
public V getReference()
// 获得当前时间戳
public int getStamp()
// 设置当前对象引用和时间戳
public void set(V newReference, int newStamp)
通过AtomicStampedReference修改AtomicReference的例子:
public class AtomicStampedReferenceDemo {
static AtomicStampedReference<Integer> money = new AtomicStampedReference<>(19, 0);
public static void main(String[] args) {
// 模拟多个线程同时更新后台数据库,为用户充值
for (int i = 0; i < 3; i++) {
final int timestamp = money.getStamp();
new Thread() {
@Override
public void run() {
while (true) {
while (true) {
Integer m = money.getReference();
if (m<20){
if (money.compareAndSet(m,m+20,timestamp,timestamp+1 )){
System.out.println("余额小于20元,充值成功,余额:"+money.getReference()+"元");
break;
}
}else {
// System.out.println("余额大于20元,无需充值");
break;
}
}
}
}
}.start();
}
// 用户消费线程,模拟消费行为
new Thread(){
@Override
public void run(){
for (int i = 0 ;i<100;i++){
while (true){
int timestamp = money.getStamp();
Integer m = money.getReference();
if (m>10){
System.out.println("大于10元");
if (money.compareAndSet(m,m-10,timestamp,timestamp+1)){
System.out.println("成功消费10元,余额:"+money.getReference()+"元");
break;
}
}else {
System.out.println("没有足够的资金");
break;
}
}
}
}
}.start();
}
}
output:
余额小于20元,充值成功,余额:39元
大于10元
成功消费10元,余额:29元
大于10元
成功消费10元,余额:19元
大于10元
成功消费10元,余额:9元
没有足够的资金
没有足够的资金
...
无锁数组:AtomicIntegerArray
JDK提供了可用的原子数组有:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray,分别表示整数数组、long型数组和普通对象的数组。
以AtomicIntegerArray为例,本质上是对int[]类型的封装,使用Unsafe类通过CAS的方式控制int[]在多线程下的安全性。
例子:
public class AtomicIntegerArrayDemo {
static AtomicIntegerArray array = new AtomicIntegerArray(10);
public static class AddThread implements Runnable{
@Override
public void run() {
for (int k=0;k<10000;k++){
array.getAndIncrement(k%array.length());
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] ts = new Thread[10];
for (int k = 0;k<10;k++){
ts[k]=new Thread(new AddThread());
ts[k].start();
}
for (int k=0;k<10;k++){
ts[k].join();
}
System.out.println(array);
}
}
output:
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]
让普通变量也使用原子操作:AtomicIntegerFiledUpdater
由于初期考虑不周,或者后期需求发生变化,一些普通变量可能也会有线程安全的需求。如果改动不大,可以简单地修改程序中每一个使用到该变量的地方。但该种方式不符合软件设计中的开闭原则。因此可以通过Updater,以少量的代码修改使普通变量也具有线程安全性。
Updater根据数据类型不同,分为3类:AtomicIntegerFiledUpdater、AtomicLongFiledUpdater和AtomicReferenceFiledUpdater。
举例:假设进行一次选举,选民投一票记为1,否则记为0。最终选票是简单求和。
public class AtomicIntegerFieldUpdaterDemo {
public static class Candidate {
int id;
volatile int score;
}
public final static AtomicIntegerFieldUpdater<Candidate> scoreUpdater =
AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");
// 用于检查Updater是否正确
public static AtomicInteger allScore = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
final Candidate stu = new Candidate();
Thread[] t = new Thread[10000];
for (int i = 0; i < 10000; i++) {
t[i] = new Thread() {
@Override
public void run() {
if (Math.random() > 0.4) {
scoreUpdater.incrementAndGet(stu);
allScore.incrementAndGet();
}
}
};
t[i].start();
}
for (int i = 0; i < 10000; i++) {
t[i].join();
}
System.out.println("score=" + stu.score);
System.out.println("allScore=" + allScore);
}
}
output:
score=5922
allScore=5922
注意事项
- Updater只能修改它可见范围内的变量,因为Updater使用反射得到这个变量。比如score声明为private,就是不可行的。
- 为了确保变量被正确读取,它必须是volatile的。如果源代码中未声明这个类型,简单添加即可。
- 由于CAS操作会通过对象实例中的偏移量直接进行赋值,因此它不支持static字段(Unsafe.objectFieldOffset() 方法不支持静态变量)。
无锁的Vector
SynchronousQueue的实现
死锁
死锁:两个或多个线程相互占用对方需要的资源,而都不进行释放,导致彼此之间相互等待对方释放资源,产生无限制等待的现象。