一个对象被多个并发线程反复调用和修改,会一直产生正确的期望结果。
下面的就是一个线程不安全的操作
public static ArrayList<Integer> numberList =new ArrayList<Integer>(); public static class AddToList implements Runnable{ int startnum=0; public AddToList(int startnumber){ startnum=startnumber; } @Override public void run() { 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()); }
Exception in thread "Thread-0" java.lang. ArrayIndexOutOfBoundsException: 64
at java.util.ArrayList.add(ArrayList.java:444)
at com.felink.data.adstat.Main$AddToList.run(Main.java:38)
at java.lang.Thread.run(Thread.java:745)
1000035
从上面可以看到,不止结果不对,还抛出了一个异常,集合在扩容的时候集合是不可用,而某个线程这时又对集合进行了操作。
JVM创建每个对象时候会添加一个32位的信息比较,成为Mark Word,用来标识对象的Hash,锁信息,垃圾回收标记
和垃圾回收标记,年龄 (指向锁记录的指针,指向monitor的指针,GC标记,偏向锁线程ID )
1 偏向锁
2 轻量级锁
3 自旋锁
4 普通锁
所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程,JVM默认开启-XX:+UseBiasedLocking 是启用的,将对象头Mark的标记设置为偏向,并将线程ID写入对象头Mark,只要没有竞争,获得偏向锁的线程,在将来进入同步块,不需要做同步,所以当大部分情况是没有竞争的,可以通过偏向来提高性能,但是竞争激烈的场合,偏向锁会频繁的被丢弃,获取会增加系统的负担。
下面我们看一个偏向锁提高性能的例子
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); }
VM:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
参数的结果是:2957
VM:-XX:+UseBiasedLocking
参数的结果是:4625
注:JVM默认开启偏向锁,但是JVM认为刚启动的时候所有任务都要启动,处于繁忙阶段会延时几秒在启用偏向锁
XX:BiasedLockingStartupDelay=0标识一启动就使用偏向锁的功能
BasicObjectLock,这是JVM中的一个锁,是嵌入在线程栈中的对象。该对象包含俩部分:1 BasicLock(主要成员markOop_displaced_header) 2 ptr to obj hold the lock(指向对象的指针)。那么 这一个对象如何获取轻量级锁:将对象头的Mark指针保存到锁对象中,将对象头设置为指向锁的指针(在线程栈空间中)
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中,去掉此参数,改为内置实现
如果同步块很长,自旋失败,会降低系统性能, 如果同步块很短,自旋成功,节省线程挂起切换时间,提升系统性能
JVM内置获取锁的优化方法和获取锁的步骤
偏向锁可用会先尝试偏向锁
轻量级锁可用会先尝试轻量级锁
以上都失败,尝试自旋锁
再失败,尝试普通锁,使用OS互斥量在操作系统层挂起
public synchronized void syncMethod(){ othercode1(); mutextMethod(); othercode2(); }
改造如下,将所从外围函数转移到函数内部成员:
public void syncMethod2(){ othercode1(); synchronized(this){ mutextMethod(); } othercode2(); }
将大对象,拆成小对象,大大增加并行度,降低锁竞争, 偏向锁,轻量级锁成功率提高,性能也会提高。
下面我们看俩个例子,前面是一个大粒度的实现,后者就是减少锁粒度的典型
HashMap的同步实现
Collections.synchronizedMap(Map<K,V> m)
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 :Node<K,V>[] nodes,每个 Segment中维护HashEntry<K,V>
put操作时, 先定位到Segment,锁定一个Segment,执行put, 在减小锁粒度后, ConcurrentHashMap允许若干个线程同时进入
当然想比于前者,后者提升了性能的同时,也会产生大量的内存垃圾,这在数据库是经常用到的,我们知道数据库是由基于块存储,为了保证并发,提高吞吐,不建议将每个存储块设置较大,因为这样会产生热块效应。当然也不只有调整块大小的操作,反向索引也是一种方式(mysql没有)
根据功能进行锁分离 , ReadWriteLock就是JVM提供的一个读写锁, 读多写少的情况,可以提高性能。
|
读锁 |
写锁 |
读锁 |
可访问 |
不可访问 |
写锁 |
不可访问 |
不可访问 |
public static ArrayList<Integer> numberList =new ArrayList<Integer>(); public static class AddToList implements Runnable{ int startnum=0; public AddToList(int startnumber){ startnum=startnumber; } @Override public void run() { int count=0; lock.readLock().lock(); System.out.println(numberList.size()); lock.readLock().unlock(); lock.writeLock().lock(); while(count<1000000){ numberList.add(startnum); startnum+=2; count++; } lock.writeLock().unlock(); } }
main函数就是1.1节
输出:
0
0
2000000
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短, 即在使用完公共资源后,应该立即释放锁。只有这样,等待在这个锁上的其他线程才能尽早的获得资源执行任务。 但是,如果对同一个锁不停的进行请求、同步和释放, 其本身也会消耗系统宝贵的资源,反而不利于性能的优化。
例如下面的例子
public void demoMethod(){ synchronized(lock){ //do sth. } //做其他不需要的同步的工作,但能很快执行完毕 synchronized(lock){ //do sth. } }
只要俩个锁之间也就是非同步块的操作能够很快完成那么就应该扩大锁的范围
public void demoMethod(){ //整合成一次锁请求 synchronized(lock){ //do sth. //做其他不需要的同步的工作,但能很快执行完毕 } }
还有下面的例子
while(count<1000000){ lock.writeLock().lock(); numberList.add(startnum); startnum+=2; count++; lock.writeLock().unlock(); }
lock.writeLock().lock(); while(count<1000000){ numberList.add(startnum); startnum+=2; count++; } lock.writeLock().unlock();
MySql的解释器会对sql 的子查询,或者外连接等比较耗费的性能的地方做一些优化,既子查询消除,外连接消除等,通用JVM也有类似的优化。
StringBuffer是线程安全的,但是如果在一个明确对象不可能被共享时候,就会对这些对象进行锁消除的操作,当然这些锁不是RD引入的,JDK自带的一些库
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(); }
1 -server -XX:+EliminateLocks
craeteStringBuffer: 107 ms
2 -server -XX:- EliminateLocks
craeteStringBuffer: 230 ms
锁是避免冲突的手段,预期只要是能产生操作冲突的地方,都要求加上锁,是一种悲观的操作。无锁是以相反的角度看待问题,不管是否会造成冲突,先执行,如果遇到冲突了在考虑其他的方式。
java的 java.util.concurrent.atomic.*就是采用无锁的方式,冲突的地方采用CAS(compare and swap)方式。
什么是CAS? CAS算法的过程是这样:它包含3个参数CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即时没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
public final int getAndSetInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var4)); return var5; }
java.util.concurrent.atomic包使用无锁实现,性能高于一般的有锁操作
只有了解了为什么会有锁,JVM自带的锁,才能为后面的多线程的开发,性能调优打下一个好的基础