锁的优化与注意事项

目录

一、锁的优化思路和方法

1.1 减少锁持有时间

1.2 减小锁粒度

1.3 锁分离

​编辑

1.4 锁粗化

​编辑

​编辑

1.5 锁消除

二、虚拟机内的锁优化

2.1 对象头Mark

2.2 偏向锁

2.3 轻量级锁

2.4 自旋锁

2.5 总结

三、一个错误使用锁的案例

 3.1 示例

四、ThreadLocal及源码分析

4.1 被多个线程访问

 4.2 为每一个线程分配一个实例    

4.3 如果使用共享实例,起不到效果


一、锁的优化思路和方法

1.1 减少锁持有时间

1.2 减小锁粒度

  • 将大对象,拆成小对象,大大增加并行度,降低锁竞争
  • 偏向锁,轻量级锁成功率提高
  • ConcurrentHashMap
  • HashMap的同步实现

Collections.synchronizedMap(Map<K,V> m)

返回SynchronizedMap对象

public V get(Object key) {
    synchronized (mutex) {
        return m.get(key);
    }
}

public V put(K key, V value) {

    synchronized (mutex) {
        return m.put(key, value);
    }
}
  • ConcurrentHashMap

若干个Segment :Segment<K,V>[] segments
Segment中维护HashEntry<K,V>
put操作时 先定位到Segment,锁定一个Segment,执行put

  • 在减小锁粒度后, ConcurrentHashMap允许若干个线程同时进入

1.3 锁分离

  • 根据功能进行锁分离
  • ReadWriteLock
  • 读多写少的情况,可以提高性能

  • 读写分离思想可以延伸,只要操作互不影响,锁就可以分离
  • LinkedBlockingQueue

    队列

    链表

1.4 锁粗化

通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完 公共资源后,应该立即释放锁。只有这样,等待在这个锁上的其他线程才能尽早的获得资源执行任务。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化

1.5 锁消除

在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作

public static void main(String args[]) throws InterruptedException {

long start = System.currentTimeMillis();

for (int i = 0; i < CIRCLE; i++) {

    craeteStringBuffer("JVM", "Diagnosis");

}

long bufferCost = System.currentTimeMillis() - start; 

System.out.println("craeteStringBuffer: " + bufferCost + " ms");

}

public static String craeteStringBuffer(String s1, String s2) {

 StringBuffer sb = new StringBuffer(); sb.append(s1);

 sb.append(s2);

 return sb.toString();

}

CIRCLE= 2000000

-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks

craeteStringBuffer: 187 ms

-server -XX:+DoEscapeAnalysis -XX:-EliminateLocks

craeteStringBuffer: 254 ms

二、虚拟机内的锁优化

  1. 偏向锁
  2. 轻量级锁
  3. 自旋锁

2.1 对象头Mark

  • Mark Word,对象头的标记,32位
  • 描述对象的hash、锁信息,垃圾回收标记,年龄
    1. 指向锁记录的指针
    2. 指向monitor的指针
    3. GC标记
    4. 偏向锁线程ID

2.2 偏向锁

大部分情况是没有竞争的,所以可以通过偏向来提高性能
所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程
将对象头Mark的标记设置为偏向,并将线程ID写入对象头Mark
只要没有竞争,获得偏向锁的线程,在将来进入同步块,不需要做同步
当其他线程请求相同的锁时,偏向模式结束
 -XX:+UseBiasedLocking
–默认启用
在竞争激烈的场合,偏向锁会增加系统负担

public static List<Integer> numberList =new Vector<Integer>();

public static void main(String[] args) throws InterruptedException {

long begin=System.currentTimeMillis(); 
int count=0;

int startnum=0;

while(count<10000000){

numberList.add(startnum); 

startnum+=2;
count++;

}

long end=System.currentTimeMillis();
System.out.println(end-begin);

}

-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

-XX:-UseBiasedLocking

2.3 轻量级锁

  • 普通的锁处理性能不够理想,轻量级锁是一种快速的锁定方法。
  • 如果对象没有被锁定
    1. 将对象头的Mark指针保存到锁对象中
    2. 将对象头设置为指向锁的指针(在线程栈空间中)
lock->set_displaced_header(mark);
if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark))
{
TEVENT (slow_enter: release stacklock) ;
return ;
}

lock位于线程栈中。

  1. 如果轻量级锁失败,表示存在竞争,升级为重量级锁(常规锁)
  2. 在没有锁竞争的前提下,减少传统锁使用OS互斥量产生的性能损耗
  3. 在竞争激烈时,轻量级锁会多做很多额外操作,导致性能下降

2.4 自旋锁

  1. 当竞争存在时,如果线程可以很快获得锁,那么可以不在OS层挂起线程,让线程做几个空操作(    自旋)
  2. JDK1.6中-XX:+UseSpinning开启
  3. JDK1.7中,去掉此参数,改为内置实现
  4. 如果同步块很长,自旋失败,会降低系统性能
  5. 如果同步块很短,自旋成功,节省线程挂起切换时间,提升系统性能

2.5 总结

  1. 不是Java语言层面的锁优化方法
  2. 内置于JVM中的获取锁的优化方法和获取锁的步骤
  • 偏向锁可用会先尝试偏向锁
  • 轻量级锁可用会先尝试轻量级锁
  • 以上都失败,尝试自旋锁
  • 再失败,尝试普通锁,使用OS互斥量在操作系统层挂起

三、一个错误使用锁的案例

 3.1 示例

public class IntegerLock {

static Integer i=0;

public static class AddThread extends Thread { 

public void run(){

for(int k=0;k<100000;k++){

synchronized(i){

i++;

}
}
}
}
public static void main(String[] args) throws InterruptedException { 

AddThread t1=new AddThread();

AddThread t2= new AddThread();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}

四、ThreadLocal及源码分析

4.1 被多个线程访问

private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
 
public static class ParseDate implements Runnable{

int i=0;

public ParseDate(int i){this.i=i;} 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) {

// SimpleDateFormat被多线程访问
ExecutorService es=Executors.newFixedThreadPool(10);

for(int i=0;i<1000;i++) {

es.execute(new ParseDate(i));


}
}

 4.2 为每一个线程分配一个实例    


static ThreadLocal<SimpleDateFormat> tl=new ThreadLocal<SimpleDateFormat>(); 

public static class ParseDate implements Runnable{

int i=0;
public ParseDate(int i){
this.i=i;
}

public void run() {

try {

if(tl.get()==null){

tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

}
Date t=tl.get().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));

}
}

4.3 如果使用共享实例,起不到效果

static ThreadLocal<SimpleDateFormat> tl=new ThreadLocal<SimpleDateFormat>();

private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

public static class ParseDate implements Runnable {
int i=0;

public ParseDate(int i){this.i=i;} public void run() {

try {

if(tl.get()==null){
tl.set(sdf);

}

Date t=tl.get().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));

}
}
  • 17
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

纵然间

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值