Java高并发程序设计学习笔记(九):锁的优化和注意事项

Java高并发程序设计学习笔记(九):锁的优化和注意事项

转自:https://blog.csdn.net/dataiyangu/article/details/87612028

锁优化的思路和方法
减少锁持有时间
减小锁粒度
锁分离
锁粗化
举个栗子
举个栗子
锁消除
虚拟机内部的锁优化(当使用synchronize关键字的时候里面会做那些事情)
对象头Mark
偏向锁
举个栗子
轻量级锁
自旋锁
举个栗子
偏向锁,轻量级锁,自旋锁总结
一个错误使用锁的案例
ThreadLocal及其源码分析
举个栗子
为每一个线程分配一个实例
如果使用共享实例,起不到效果
源码分析

注意:只要是持有锁的,性能就会比无锁要差,不论如何优化。
锁优化的思路和方法
注意tryLock是无锁的,只是去尝试下,拿不到就会接着做其他的事情,Lock是有锁的操作。

减少锁持有时间
public synchronized void syncMethod(){
othercode1();
mutextMethod();
othercode2();
}
1
2
3
4
5
转化为:

public void syncMethod2(){
othercode1();
synchronized(this){

mutextMethod();
}
othercode2();
}
1
2
3
4
5
6
7
值同步相关的代码,无关的代码就不要同步。

减小锁粒度
将大对象,拆成小对象,大大增加并行度,降低锁竞争 ,原来可能是对一个很大的对象加锁。
偏向锁,轻量级锁成功率提高
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);}
}
1
2
3
4
5
6
ConcurrentHashMap
– 若干个Segment :Segment<K,V>[] segments
– Segment中维护HashEntry<K,V>
– put操作时
• 先定位到Segment,锁定一个Segment,执行put
在减小锁粒度后, ConcurrentHashMap允许若干个线程同时进入
就是讲hashmap拆分成多个hashMap,即拆分成多个对象。

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

读锁 写锁
读锁 可访问 不可访问
写锁 不可访问 不可访问
读操作和读操作是不需要阻塞的,将阻塞的并发变成了无等待的并发。

读写分离思想可以延伸,只要操作互不影响,锁就可以分离
LinkedBlockingQueue
– 队列
– 链表
就像队列put和take操作,一个在头部一个在尾部互不影响

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

举个栗子
public void demoMethod(){
synchronized(lock){

//do sth.
}
//做其他不需要的同步的工作,但能很快执行完毕
synchronized(lock){

//do sth.
}
}
1
2
3
4
5
6
7
8
9
应该转化成

public void demoMethod(){ //整合成一次锁请求
synchronized(lock){
//do sth.
//做其他不需要的同步的工作,但能很快执行完毕
}
}
1
2
3
4
5
6
不断的加锁释放锁也是会消耗很大的性能的,这个时候就应该锁粗化,但是前提上面的代码中的其他不需要同步的工作是能够很快的执行完毕的,否则不应该所粗化。

举个栗子
for(int i=0;i<CIRCLE;i++){
synchronized(lock){

}
}
1
2
3
4
synchronized(lock){
for(int i=0;i<CIRCLE;i++){

}
}
1
2
3
4
锁消除
在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作(jdk自身的优化)

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();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
观察上面的代码,在jdk中有许多类似StringBuffer这样的类,本身就是在锁的基础上封装的,自己是线程安全的,我用了它的某些方法(append等)后,发现sb这个变量是局部变量,不会被其他的线程访问到,并不需要为了线程安全,而进行锁的操作。

解决办法:
CIRCLE= 2000000

-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
craeteStringBuffer: 187 ms
-server -XX:+DoEscapeAnalysis -XX:-EliminateLocks
craeteStringBuffer: 254 ms
1
2
3
4
自私server模式下能有更多的操作,-XX:+DoEscapeAnalysis :逃逸分析,分析sb这个变量会不会被其他的线程访问到,如果会怎样,如果会不怎样,+EliminateLocks:经过逃逸分析,看是否打开锁消除。
通过上面的时间显示,进行了锁消除,确实性能有很大的提升。

虚拟机内部的锁优化(当使用synchronize关键字的时候里面会做那些事情)
对象头Mark
每个对象都有一个对象头
Mark Word,对象头的标记,32位(32位操作系统中)
描述对象的hash、锁信息,垃圾回收标记,年龄
– 指向锁记录的指针
– 指向monitor的指针
– GC标记
– 偏向锁线程ID

偏向锁
大部分情况是没有竞争的,所以可以通过偏向来提高性能
所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程
将对象头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);
}
1
2
3
4
5
6
7
8
9
10
//本例中,使用偏 向锁,可以获得 5%以上的性能 提升
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
-XX:-UseBiasedLocking
1
2
3
BiasedLockingStartupDelay是系统启动的几秒之内金童偏向锁,因为在系统刚刚启动的时候确实会有许多的锁竞争,虚拟机会提供一个默认的时间,这里设置为0,是因为需要写的代码很少,能够在很短的时间内执行完毕,所以将BiasedLockingStartupDelay设置为0。

轻量级锁
BasicObjectLock
– 嵌入在线程栈中的对象
如果偏向锁失败,就会去执行轻量级锁的操作。
普通的锁处理性能不够理想,轻量级锁是一种快速的锁定方法。
操作系统层面的锁性能是比较差的,jvm相当于操作系统上的一个应用,jvm级别的锁能够提高性能。
如果对象没有被锁定
– 将对象头的Mark指针保存到锁对象中
– 将对象头设置为指向锁的指针(在线程栈空间中)

lock->set_displaced_header(mark);
if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark))
{

TEVENT (slow_enter: release stacklock) ;
return ;
}
1
2
3
4
5
6
lock位于线程栈中
注意上面说的,对象头mark保存在锁中,锁位于线程栈中,对象头设置为锁的指针,所以如果对象头指向了线程栈中,则表示持有这把锁,上面的指令同样是cas操作。

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

自旋锁
举个栗子
concurrentHashMao中的put就是线程的锁被其他人拿走之后,不急着挂起自己,而是执行几次trylocak操作,因为一旦挂起会消耗八万个时光周期。不断的循环trylock,当多次循环之后还是没有拿到,就挂起。

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

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

一个错误使用锁的案例
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);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
注意这句话:synchronized(i)
其中的i是Integer类型的,i++操作一般是在int类型上,Integer类型会有自动的拆箱装箱的操作,并不是将i++的值直接赋值给了i,而是new了一个新的对象,然后将i指向它,所以这里synchronized(i)中的i不能确定是代码中的i还是new的一个新的Integer对象,从而不能保证拿到的是同一个对象,所以这段代码并不是线程安全的。

ThreadLocal及其源码分析
把锁去掉,为每一个线程都提供一个对象实例,不同的线程,都去访问自己的对象,而不去访问别人的对象,这个时候锁就完全没有必要等待。

举个栗子
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) {

ExecutorService es=Executors.newFixedThreadPool(10);
for(int i=0;i<1000;i++){

es.execute(new ParseDate(i));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
SimpleDateFormat不是线程安全的,被多线程访问是容易抛出一些异常,不能正常的工作。

为每一个线程分配一个实例
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));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
如果使用共享实例,起不到效果
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));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
注意上述代码中tl.set(sdf ); 正确的应该是tl.set(new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”));
每次都new一个新的实例,只有这样才能起到作用,否则锁一个的threadlocal里面存的还是一个对象,还是会出现线程不安全的现象。

源码分析
public void set(T value) {

Thread t = Thread.currentThread();
//拿到ThreadlocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {

//可以看到Threadlocal本身就隶属于这个线程
return t.threadLocals;
}
//在当前的线程中,也就是每个线程中都有自己的map
ThreadLocal.ThreadLocalMap threadLocals = null;
//key是ThreadLocal本身,value就是设进去的值
private void set(ThreadLocal<?> key, Object value) {

// We don’t use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.

Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);

for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {

ThreadLocal<?> k = e.get();

if (k == key) {

e.value = value;
return;
}

if (k == null) {

replaceStaleEntry(key, value, i);
return;
}
}

tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
//nextIndex其实是i++的操作,因为get出来,发现如果有了会有hash冲突,这个时候
//下标加一,再赋值。
private static int nextIndex(int i, int len) {

return ((i + 1 < len) ? i + 1 : 0);
}
//执行清理的工作,可以看到并不是全部清理,只是n >>>= 1右移操作,清理部分。
//前面知道Entry是弱引用的,只有当entry!=null并且e.get()–》ThreadLocal是null的时候
//通过expungeStaleEntry方法将tab[staleSlot].value = null;
//tab[staleSlot] = null;这两个置为null
private boolean cleanSomeSlots(int i, int n) {

boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {

i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {

n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
//可以看到 ThreadLocalMap.Entry e = map.getEntry(this);
//也是传的ThreadLocal本身
public T get() {

Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {

ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {

@SuppressWarnings(“unchecked”)
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
//这里注意Entry是继承于弱引用的,也就是没有数据引用到这个对象,这个对象就会被系统释放掉。
static class Entry extends WeakReference<ThreadLocal<?>> {

/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {

super(k);
value = v;
}
———————
作者:Leesin Dong
来源:CSDN
原文:https://blog.csdn.net/dataiyangu/article/details/87612028
版权声明:本文为博主原创文章,转载请附上博文链接!

转载于:https://www.cnblogs.com/sharpest/p/10850990.html

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/100930.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • RAID10磁盘阵列损坏修复操作

    RAID10磁盘阵列损坏修复操作-f模拟硬盘损坏mdadm/dev/md0-f/dev/sdb1、查看损坏磁盘阵列的情况2、将损坏的硬盘设备移除3、插上新的硬盘(在真机上操作,虚拟机之间将损坏的硬盘删除,然后在添加新的硬盘即可)4、卸载挂载操作5、将新的硬盘添加到RAID10磁盘阵列中6、查看修复成功后的磁盘阵列信息(因为新添加的需要等待一段时间等待系统重新创建)7、重新挂载1、查看损坏后的磁盘阵列信息2、将损坏的硬盘从磁盘阵列中移除mdadm/dev/md0-r损坏的硬盘设备名mdadm-D/

    2022年6月3日
    38
  • 分治策略结合递归思想求最大子序列和

    分治策略结合递归思想求最大子序列和

    2021年12月6日
    36
  • 23岁的一无所有,其实是理所应当的「建议收藏」

    23岁的一无所有,其实是理所应当的「建议收藏」23岁那年你正处在哪个状态?现在呢? 我,23岁,应届毕业生。生活,工作,爱情都处于人生的低谷,一穷二白,一无所有,一事无成。分享一下成长的建议吧。匿名用户23岁那年…就是去年…… 在22岁的时候我毕业,同时第二年准备考研,结果因为压力太大,期望太高,又失利了,但是我依然满怀信心和憧憬 在我23岁那年四月,当我深爱的女孩(在这之前我追了她四年)说她要去北京时,我在毫无准备的情况下,…

    2022年7月25日
    10
  • vs2013注册_vs2008是什么软件

    vs2013注册_vs2008是什么软件
    VS2008注册方法:
      VS2008注册方法非常简单,在开始>设置>控制面版>添加或删除程序>卸载vs.net2008(名字不太记得了)>出现卸载界面>点击Next>输入上面CD-key->出现成功画面即可完美将试用版升级成为正式版。
    VS2008正式版序列号CDKEY:PYHYP-WXB3B-B2CCM-V9DX9-VDY8T

    2022年8月22日
    7
  • 黑马程序员—只要路对,不怕路远——寂寞中蔓延着成功路途[通俗易懂]

    黑马程序员—只要路对,不怕路远——寂寞中蔓延着成功路途[通俗易懂]文章来源:黑马程序员,黑马论坛

    2022年9月27日
    0
  • Awvs详细使用教程「建议收藏」

    Awvs详细使用教程「建议收藏」Awvs的是一款非常好用的web漏洞扫描工具,他的扫描速度比较快,可以自己选择扫描速度,比较灵活。Awvs分为老的版本和新版本,下面我介绍的是新版本的功能和用法。功能介绍如下:仪表盘(监视器)功能,添加目标功能,漏洞排序功能,扫描功能,发现功能,用户功能,扫描配置功能,网络扫描功能,追踪器功能,防火墙设置,邮件设置,引擎,时间排除功能,代理功能,常规设置主要使用的功能是前面的6个,后面的根据个人的需要进行配置详细介绍如下:Dashboard功能:翻译意思仪表盘(监视器),可以对扫描对扫描

    2025年8月24日
    2

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号