本博客为炼数成金JVM教程第九课
目录
- 线程安全
- 对象头Mark
- 偏向锁
- 轻量级锁
- 自旋锁
- 较少锁持有时间
- 较小锁粒度
- 锁分离
- 锁粗化
- 锁消除
- 无锁
线程安全
多线程网站统计访问人数
使用锁,维护计数器的串行访问和安全性
我们举一个不加锁访问ArrayList的例子
import java.util.ArrayList;
import java.util.List;
public class AddToList implements Runnable {
private static List<Integer> numberList = new ArrayList<Integer>();
int startnum = 0;
public AddToList(int startnumber) {
this.startnum = startnumber;
}
@Override
public void run() {
// TODO Auto-generated method stub
int count = 0;
while(count < 1000000) {
numberList.add(startnum);
startnum+=2;
count++;
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new AddToList(0));
Thread t2 = new Thread(new AddToList(1));
t1.start();
t2.start();
while(t1.isAlive() || t2.isAlive()) {
Thread.sleep(1);
}
System.out.println(numberList.size());
}
}
按道理应该不会出现异常,且输出的值为2000000.
但结果如下:
抛出了一个ArrayIndexOutOfBoundException的异常,且打出的总数为1000269
因为在多线程中,Arraylist不是线程安全的,由于它的空间不够,在做扩展的时候,ArrayList处于一个不可用的状态,而另一个线程在往里放数据,因此就抛出异常
对象头Mark
Mark Word, 对象头的标记,32位
描述对象的hash,锁信息,垃圾回收标记,年龄
指向锁记录的指针
指向monitor的指针
GC标记
偏向锁线程ID
偏向锁
大部分情况是没有竞争的,所以可以通过偏向来提高性能
所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程(即当前这个线程已经拿到锁,那么这个线程试图再次拿到锁,会以最快的方式拿到锁,而不需要通过monitor)
将对象头Mark的标记设置为偏向,并将线程ID写入对象头Mark
只要没有竞争,获取偏向锁的线程,在将来进入同步块,不需要做同步
当其他线程请求相同的锁时,偏向模式结束
-XX:+UseBiasedLocking (默认开启)
在竞争激烈的场合,偏向锁会增加系统负担
import java.util.List;
import java.util.Vector;
public class AddToVector {
public static List<Integer> numberList = new Vector<Integer>();// Vector 线程安全
public static void main(String[] args) {
long begin = System.currentTimeMillis();
int count = 0;
int startnum = 0;
while(count < 1000000) {
numberList.add(startnum);
startnum += 2;
count++;
}
long end = System.currentTimeMillis();
System.out.println(end - begin);
}
}
运行参数:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
结果:
22
运行参数
-XX:-UseBiasedLocking
结果
32
在本例中,使用偏向锁,可以获得5%以上的性能提升
轻量级锁
先介绍一个概念:BasicObjectLock(JVM的一个锁)
这是一个嵌入在线程栈中的对象
BasicObjectLock 由两部分组成,一个是BasicLock,里面存的是displaced_header 可以理解为这是一个对象头。另外一部分是指向持有这个锁的对象的指针
普通的锁处理性能不够理想,轻量级锁是一种快速的锁定方式
如果对象没有被锁定,锁定时,将对象头的Mark指针保存到锁对象中(BasicObjectLock),并将对象头设置为指向锁的指针(在线程栈空间中)
伪代码如下:
lock->set_displaced_header(mark);
if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
TEVENT (slow_enter: release stacklock) ;
return ;
}
由于Lock位于线程栈中,因此只需要判断对象头的指针所指向的方向是不是在线程栈中。如果是说明线程持有锁。
如果轻量级锁失败,表示存在竞争,升级为重量级锁(常规锁)
在没有竞争的前提下,轻量级锁减少了传统锁使用OS互斥量产生的性能损耗
在竞争激烈时,轻量级锁会做很多额外操作,导致性能下降
自旋锁
当竞争存在时,如果线程可以很快获得锁,那么可以不在OS层挂起线程,让线程做几个空操作(自旋)
JDK1.6中, -XX:+UseSpinning 开启
JDK1.7中,去掉此参数,改为内置实现
如果同步块很长,自旋失败,会降低系统性能(一个线程的同步块很长,在这个同步块待得时间特别长,那么后面的线程的自旋的成功率大大降低,就会降低性能。本来自旋的目的是不需要挂起线程就可以获取锁,用空转指令代替线程挂起和恢复的开销,只要空转指令的成本小于挂起和恢复的开销,那么就是合适的。如果自旋之后还拿不到锁,还是要线程挂起,那么系统性能肯定是会降低的)
如果同步快很短,自旋成功,节省线程挂起切换时间,提升系统性能
偏向锁,轻量级锁,自旋锁的总结
不是Java语言层面的锁优化方式
内置于JVM中获取锁的优化方法和获取锁的步骤
第一步:偏向锁可用会先尝试偏向锁
第二步:轻量级锁可用会优先尝试轻量级锁
第三步:以上都失败,会尝试自旋锁
第四步: 再失败,尝试普通锁,使用OS互斥在操作系统层挂起
减少锁持有时间
假设有一个同步方法:
public synchronized void syncMethod(){
othercode1();
mutextMethod(); // 这个方法需要同步
othercode2();
}
对于这个方法而言,锁加在对象上面,一旦进入这个方法就需要获取锁,直到方法执行完成释放锁。
如果有些方式没有必要做同步,那么就没有必要放到同步块里面去,我们可以做如下的修改:
public void syncMethod2(){
othercode1();
synchronized(this){
mutextMethod();
}
othercode2();
}
可以减少锁的持有时间。在高并发的情况下,锁等待的时间就会减少,自旋的成功率就会增大。
减少锁粒度
将大对象,拆成小对象,大大增加并行度,降低锁竞争
锁竞争降低后,偏向锁,轻量级锁成功率提高,则性能会提高
根据结构进行性能优化
HashMap的同步实现:
Collections.synchroniezdMap(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);}
}
对于同步的hash表,get操作和put操作都需要锁住对象。当多个线程做操作是,都需要获取锁。所以一个线程操作时,其余的线程都无法操作。因此竞争相对激烈。
ConcurrentHashMap
存在若干个Segment: Segment<K,V>[] segments
Segment 中维护HashEntry<K,V> (可以理解为一个segment就是一个小的hashmap)
put操作时,先定位到Segement,锁定该Segement,执行put
假设两个线程操作ConcurrentHashMap,分别操作两个Segment,那么两个线程可以同时操作segment,不需要等待锁,竞争就小了很多
在减小锁粒度后,ConcurrentHashMap允许若干个线程同时进入
锁分离
根据功能进行锁分离
例子: ReadWriteLock
在对对象进行读写的时候加锁,在读的时候加读锁,在写的时候加写锁。而不是一棍子就加所有的锁。
如果一个线程拿了读锁,另一个线程仍可以拿到读锁,读写锁允许多个线程同时获得读锁,真正做到多个线程并发读取,不需要做等待,这个时候系统性能提高。
写的时候,对象需要做修改,对象在写的时候状态不可用,所以在写的时候,其余线程不可读,同时其余线程也不可写。
读写锁在读多写少的情况,可以提高性能
读写分离思想可以延伸,只要操作互不影响,锁就可以分离
例子: LinkedBlockingQueue(链表+队列)
对于链表来说有两个操作,一个是从链表中拿出一个元素,如图中拿出A,另一个操作是往列表中增加一个元素,如图中往D后面添加一个元素。这两个操作,一个往链表尾巴上添加元素,一个往列表头上删除元素。这两个操作互不影响。对take只要把A拿出来,把head设置为A的next,对put来讲,在D的next设置为新的元素就可以。在多线程的环境中,既要take又要put,我们可以在take的时候拿take锁,在put的时候拿put锁。
锁粗化
通常情况下,为了保持多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。只有这样,等待在这个锁上的其他线程才能尽早的获得资源执行任务。但是,凡事都有一个度,如果对同一个锁不停的进行请求,其本身也会消耗系统宝贵的资源,反而不利于性能的优化。
public void demoMethod(){
synchronized(lock){
//do sth.
}
//做其他不需要的同步的工作,但能很快执行完毕
synchronized(lock){
//do sth.
}
}
前后两次拿了相同的锁,并且频率很高,因为其他的操作很快就完成了,根据锁粗化的思想,我们做如下修改:
public void demoMethod(){
//整合成一次锁请求
synchronized(lock){
//do sth.
//做其他不需要的同步的工作,但能很快执行完毕
}
}
这个前提是:不需要同步的操作,是很快就可以完成的
再有一种情况:
for(int i=0;i<CIRCLE;i++){
synchronized(lock){
}
}
每一次循环都要获取锁,这是很浪费资源的,我们可以做如下修改
synchronized(lock){
for(int i=0;i<CIRCLE;i++){
}
}
锁消除
锁消除是JVM当中的一种优化手段,但我们可以通过JVM的参数进行控制
在即时编译时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作
public static void main(String args[]) throws InterruptedException {
long start = System.currentTimeMillis();
for (int i = 0; i < 2000000; 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();
}
StringBuffer 是线程安全的,所以无意中使用了锁(JVM默认),但sb很明显是不会被其余线程使用的,因此这个锁操作的多余的,可以被消除掉。
运行参数如下:
-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
开启逃逸分析:锁消除基于逃逸分析,锁消除的基本前提是这个变量没有逃逸出当前代码的局部代码块
开启锁消除
结果
createStringBuffer: 187 ms
运行参数如下:
-server -XX:+DoEscapeAnalysis -XX:-EliminateLocks
关闭锁消除
结果
createStringBuffer: 254 ms
无锁
锁是一种悲观的操作,我们预计存在竞争所以加锁
无锁是乐观的操作,认为这次进去不会遇到竞争,如果发生了竞争,再考虑怎么解决问题
无锁的一种实现方式:CAS(Compare And Swap), 一种非阻塞的同步
CAS(V,E,N) V: 要更新的变量,E一种期望值,希望V当前是多少,N新的值,
CAS()要做的是把N赋给V,前提是当且仅当V=E,才把N赋给V,最后把V的真实值返回
在应用层面判断多线程的干扰,如果有干扰,则通知线程重试
java.util.concurrent.atomic包使用无锁实现,性能高于一般的有锁操作
例子:
java.util.concurrent.atomic.AtomicInteger
public final int getAndSet(int newValue) { // 设置新值
for (;;) {
int current = get();
if (compareAndSet(current, newValue)) // 更新成功返回true
return current;
}
}