为了更好地支持并发程序,“锁”是较为经常使用的同步方法之一。在高并发环境下,激励的锁竞争会致使程序的性能降低。
因此咱们将在这里讨论一些有关于锁使用和问题以及一些注意事项。前端
重入锁能够彻底替代Synchronized
关键字,但其必须显式的调用unlock。建议视为Synchronized的高级版,比起Synchronized关键字,其可定时、可轮询并含有可中断的锁获取操做,公平队列以及非块结构的锁。java
/** * 重入锁演示 * */ public class ReeterLock implements Runnable{ public static ReentrantLock lock = new ReentrantLock(); public static int i = 0; @Override public void run() { for (int j=0;j<10000;j++){ //手动上锁,能够上N把,这里是为了演示 lock.lock(); lock.lock(); lock.lock(); try { i ++; } finally { //不管如何须须释放锁,上几把 释放几把 lock.unlock(); lock.unlock(); lock.unlock(); } } } public static void main(String[] a) throws InterruptedException { ReeterLock rl = new ReeterLock(); Thread t1 = new Thread(rl); Thread t2 = new Thread(rl); t1.start(); t2.start(); t1.join(); t2.join(); System.out.print(i); } }
那么咱们能够明显的看到重入锁保护着临界区资源i,确保多线程对i操做的安全。在demo中咱们也是加了3次锁并释放了3次锁。程序员
须要注意的是,若是同一线程屡次得到锁,那么在释放锁的时候,也必须释放相同次数。若是释放的次数多了,会获得一个java.lang.IllegalMonitorStateException
异常;反之则会致使当前线程一直持有该锁,致使其余线程没法进入临界区。算法
对于synchronized来讲,若是一个线程在等待锁,那么结果只有两种状况,要么它得到这把锁继续执行,要么它就保持等待。而使用重入锁,则提供另一种可能,那就是线程能够被中断。也就是在等待锁,程序能够根据须要取消对锁的请求。有些时候,这么作是很是有必要的。数组
lockInterruptibly()方法是一个能够对中断进行响应的锁申请动做,即在等待锁的过程当中,中断响应。缓存
public class IntLock implements Runnable{ public static ReentrantLock lock1 = new ReentrantLock(); public static ReentrantLock lock2 = new ReentrantLock(); int lock; /** * 控制加锁顺序,制造死锁 * @param lock */ public IntLock(int lock) { this.lock = lock; } @Override public void run() { try { /** * 1号线程,先占用 1号锁,再申请 2号锁 * 2号线程,先占用 2号锁,再申请 1号锁 * 这样就很容易形成两个线程相互等待. */ if (lock == 1){ //加入优先响应中断的锁 lock1.lockInterruptibly(); System.out.println(Thread.currentThread().getName() + " 进入..."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } /** * 这时候,1号线程 想要持有 2号锁 ,可是2号线程已经先占用了2号锁,因此1 号线程等待. * 2号线程也同样,占用着2号锁 不释放,还想申请1号锁,而1号锁 被1号线程占用且不释放. */ lock2.lockInterruptibly(); System.out.println(Thread.currentThread().getName() + " 完成..."); }else { lock2.lockInterruptibly(); System.out.println(Thread.currentThread().getName() + " 进入..."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } lock1.lockInterruptibly(); System.out.println(Thread.currentThread().getName() + " 完成..."); } } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName() + " 被中断,报异常..."); e.printStackTrace(); } finally { if (lock1.isHeldByCurrentThread()) { System.out.println(Thread.currentThread().getName() + " 释放..."); lock1.unlock(); } if (lock2.isHeldByCurrentThread()) { System.out.println(Thread.currentThread().getName() + " 释放..."); lock2.unlock(); } System.out.println(Thread.currentThread().getName() + " 线程退出..."); } } public static void main(String[] a) throws InterruptedException { IntLock re1 = new IntLock(1); IntLock re2 = new IntLock(2); Thread t1 = new Thread(re1," 1 号线程 "); Thread t2 = new Thread(re2," 2 号线程 "); t1.start(); t2.start(); //主线程sleep 2秒,让两个线程相互竞争资源.形成死锁 Thread.sleep(2000); //中断2号线程 t2.interrupt(); /* 执行结果: 1 号线程 进入... 2 号线程 进入... 2 号线程 被中断,报异常... // 执行 t2.interrupt(); java.lang.InterruptedException 2 号线程 释放... at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898) 2 号线程 线程退出... at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222) 1 号线程 完成... // 只有1号线程能执行完成 at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335) 1 号线程 释放... at com.iboray.javacore.Thread.T3.IntLock.run(IntLock.java:55) 1 号线程 释放... at java.lang.Thread.run(Thread.java:745) 1 号线程 线程退出... */ } }
除了等待外部通以外,避免死锁还有另一种方法,就是限时等待,给定一个等待时间让线程自动放弃。安全
public class TimeLock implements Runnable{ public static ReentrantLock lock = new ReentrantLock(); @Override public void run() { System.out.println(Thread.currentThread().getName() + " 申请资源..."); try { //申请3秒,若是获取不到,返回false,退出. if (lock.tryLock(5, TimeUnit.SECONDS)) { System.out.println(Thread.currentThread().getName() + " 得到资源,开始执行..."); //持有锁6秒 Thread.sleep(6000); System.out.println(Thread.currentThread().getName() + " 执行完成..."); }else { System.out.println(Thread.currentThread().getName() + " 申请锁失败..."); } } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName() + " 中断..."); e.printStackTrace(); }finally { if (lock.isHeldByCurrentThread()) { System.out.println(Thread.currentThread().getName() + " 释放锁..."); lock.unlock(); } } } public static void main(String[] a) throws InterruptedException { TimeLock re = new TimeLock(); Thread t1 = new Thread(re," 1 号线程 "); Thread t2 = new Thread(re," 2 号线程 "); t1.start(); t2.start(); /* 执行结果: 1 号线程 申请资源... 2 号线程 申请资源... 1 号线程 得到资源,开始执行... 2 号线程 释放锁... //等待了5秒后,依然申请不到锁,就返回false 1 号线程 执行完成... 1 号线程 释放锁... */ } }
因为占用锁的线程会持有锁长达6秒,故另外一个线程没法在5秒的等待时间内获取锁,所以,请求锁会失败。性能优化
在大多数状况下,锁的申请都是非公平的。也就是说,线程1首先请求了锁A,接着线程2也请求了锁A。那么当锁A可用时,是线程1仍是线程2能够得到锁呢?显然这是不必定的。系统只会从这个锁的等待队列中随机挑选一个,所以不能保证其公平性。数据结构
公平锁会按照实际的前后顺序,保证先到先得,它不会产生饥饿,只要排队,最终均可以等到资源。在建立重入锁时,经过有参构造函数,传入boolean类型的参数,true表示是公平锁。实现公平所必然要维护一个有序队列,因此公平锁的实现成本高,性能相对也很是低,默认状况下,锁是非公平的。多线程
public class ReentrantLockExample3 implements Runnable{ //建立公平锁 public static ReentrantLock lock = new ReentrantLock(true); static int i = 0; @Override public void run() { for (int j = 0;j<5;j++){ lock.lock(); try { i++; System.out.println(Thread.currentThread().getName() + " 得到锁 " + i); } finally { lock.unlock(); } } } public static void main(String[] a) throws InterruptedException { ReentrantLockExample3 re = new ReentrantLockExample3(); Thread t1 = new Thread(re," 1 号线程 "); Thread t2 = new Thread(re," 2 号线程 "); Thread t3 = new Thread(re," 3 号线程 "); Thread t4 = new Thread(re," 4 号线程 "); t1.start(); t2.start(); t3.start(); t4.start(); /* 执行结果: 1 号线程 得到锁 1 2 号线程 得到锁 2 3 号线程 得到锁 3 4 号线程 得到锁 4 1 号线程 得到锁 5 2 号线程 得到锁 6 3 号线程 得到锁 7 4 号线程 得到锁 8 ..... 4 号线程 得到锁 16 1 号线程 得到锁 17 2 号线程 得到锁 18 3 号线程 得到锁 19 4 号线程 得到锁 20 */ } }
就重入锁实现来看,它主要集中在Java 层面。在重入锁实现中,主要包含三个要素:
若是你们理解了Object.wait()
和Object.notify()
方法的话,就能很容易地理解Condition对象了。它和wait()和notify()方法的做用是大体相同的。可是wait()方法和notify()方法是和synchronized关键字合做使用的,而Condtion是与重入锁相关联的。经过Lock接口(重入锁就实现了这个接口)的Condtion newCondition()方法能够生成一个与当前重入锁绑定的Condition实例。利用Condition对象,咱们就可让线程在合适的时间等待,或者在某一个特定的时刻获得通知,继续执行。
Condition接口提供的基本方法以下:
void await() throws InterruptedException; void awaitUninterruptibly(); long awaitNanos(long nanosTimeout) throws InterruptedException; boolean await(long time, TimeUnit unit) throws InterruptedException; boolean awaitUntil(Date deadline) throws InterruptedException; void signal(); void signalAll();
以上方法含义以下
await()
方法会使当前线程等待,同时释放当前锁,当其余线程中使用signal()
或者signalAll()
方法时候,线程会从新得到锁并继续执行。或者当线程被中断时,也能跳出等待。这和Object.wait()
方法很类似。awaitUninterruptibly()
和await()
方法相似,但它不会再等待过程当中响应中断。singal()
用于唤醒一个等待队列中的线程。singalAll()
是唤醒全部等待线程。public class ConditionExample implements Runnable{ public static ReentrantLock lock = new ReentrantLock(); public static Condition condition = lock.newCondition(); @Override public void run() { try { lock.lock(); System.out.println(Thread.currentThread().getName() + " 获取到锁..."); //等待 condition.await(); System.out.println(Thread.currentThread().getName() + " 执行完成"); } catch (InterruptedException e) { e.printStackTrace(); }finally { //释放锁 lock.unlock(); System.out.println(Thread.currentThread().getName() + " 释放锁"); } } public static void main(String[] a) throws InterruptedException { ConditionExample re = new ConditionExample(); Thread t1 = new Thread(re,"1 号线程 "); t1.start(); //主线程sleep,1号线程会一直等待.直到获取到1号线程的锁资源,并将其唤醒. Thread.sleep(2000); //得到锁 lock.lock(); //唤醒前必须得到当前资源对象的锁 condition.signal(); //释放锁 lock.unlock(); } }
ReadWriteLock是JDK5中提供的读写分离锁。读写分离锁能够有效地帮助减小锁竞争,以提高系统性能。用锁分离的机制来提高性能很是容易理解,好比线程A一、A二、A3进行写操做,B一、B二、B3进行读操做,若是使用重入锁或者内部锁,则理论上说全部读之间、读与写之间、写和写之间都是串行操做。当B1进行读取时,B二、B3则须要等待锁。因为读操做并不对数据的完整性形成破坏,这种等待显然是不合理的。所以,读写锁就有了发挥功能的余地。
在这种状况下,读写锁运行多个线程同时读。可是考虑到数据完整性,写写操做和读写操做间依然是须要互相等待和持有锁的。总的来讲,读写锁的访问约束以下表。
读 | 写 | |
---|---|---|
读 | 非阻塞 | 阻塞 |
写 | 阻塞 | 阻塞 |
若是在系统中,读的次数远远大于写的操做,读写锁就能够发挥最大的功效,提高系统的性能。
栗子:
public class ReadWriteLockExample { //建立普通重入锁 private static Lock lock = new ReentrantLock(); //建立读写分离锁 private static ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock(); //建立读锁 private static Lock readLock = rwlock.readLock(); //建立写锁 private static Lock writeLock = rwlock.writeLock(); private int value; public Object HandleRead(Lock lock) throws InterruptedException { try { //上锁 lock.lock(); //模拟处理业务 Thread.sleep(1000); System.out.println(Thread.currentThread().getName() + " Read..."); return value; } finally { //释放锁 lock.unlock(); } } public void HandleWrite(Lock lock,int index) throws InterruptedException { try { lock.lock(); Thread.sleep(1000); value = index; System.out.println(Thread.currentThread().getName() + " Write..."); }finally { lock.unlock(); } } public static void main(String[] a ) throws InterruptedException { final ReadWriteLockExample rwle = new ReadWriteLockExample(); //建立读方法 Runnable readR = new Runnable() { @Override public void run() { try { //rwle.HandleRead(lock); //普通锁 rwle.HandleRead(readLock); } catch (InterruptedException e) { e.printStackTrace(); } } }; //建立写方法 Runnable writeR = new Runnable() { @Override public void run() { try { //rwle.HandleWrite(lock,new Random().nextInt()); //普通锁 rwle.HandleWrite(writeLock,new Random().nextInt()); } catch (InterruptedException e) { e.printStackTrace(); } } }; //18次读 for (int i=0;i<18;i++){ Thread s = new Thread(readR); s.start(); } //2次写 for (int i=18;i<20;i++){ Thread s = new Thread(writeR); s.start(); } /** * 结论: * * 用普通锁运行,大约执行20秒左右 * * 用读写分离锁,大约执行3秒左右 * */ } }
在读锁和写锁之间的交互能够采用多种实现方式。ReadWriteLock中的一些可选实现包括:
闭锁是一种同步工具类,能够延迟线程的进度直到其到达终止状态。闭锁的做用至关于一扇门:在闭锁到达结束状态以前,这扇门一直是关闭的,而且没有任何线程能经过,当到达结束状态时,这扇门会打开并容许全部的线程经过。当闭锁到达结束状态后,将不会再改变状态,所以这扇门将永远保持打开状态。闭锁能够用来确保某些活动直到其余活动都完成才继续执行,例如:
CountDownLatch 就是一种灵活的闭锁实现,能够在上述的各类状况中使用,它可使一个或多个线程等待一组时间发生CountDown在英文中意为倒计时,Latch为门闩。闭锁的状态包括一个计数器,该计数器被初始化为一个正数,表示须要等待的事情数量。countDown方法递减计数器,表示有一个事件已经发生了,而await方法等待计数器达到零,这表示全部须要等待的事情都已经发生。若是计数器的值是非零,那么await会一直阻塞直到计数器为零,或者等待中的线程中断,或者等待超时。所以,这个工具一般用来控制线程等待,它可让某一个线程等待直到倒计时结束,再开始执行。
CountDownLatch的构造函数接收一个整数做为参数,即当前这个计数器的技术个数。
public CountDownLatch(int count)
下面这个简单的示例,演示了CountDownLatch的使用。
public class CountDownLatchExample implements Runnable{ static final CountDownLatch cdl = new CountDownLatch(10); static final CountDownLatchExample cdle = new CountDownLatchExample(); @Override public void run() { try { Thread.sleep(new Random().nextInt(10) * 1000); System.out.println(Thread.currentThread().getName() + " 部件检查完毕..."); //一个线程完成工做,倒计时器减1 cdl.countDown(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] a) throws InterruptedException { ExecutorService exec = Executors.newFixedThreadPool(10); for (int i=0;i<10;i++){ exec.submit(cdle); } //等待全部线程完成,主线程才继续执行 cdl.await(); System.out.println(Thread.currentThread().getName() + " 全部检查完成,上跑道起飞..."); //关闭线程池 exec.shutdown(); } }
FutureTask也能够用作闭锁。FutureTask表示的计算是经过Callable来实现的,至关于一种可生成结果的Runnable,而且能够处于如下3种状态:
Future.get的行为取决于任务的状态。若是任务已经完成,那么get会当即返回结果,不然get将阻塞直到任务进入完成状态,而后返回结果或者抛出异常。FutureTask将计算结果从执行的计算的线程传递到获取这个结果的线程,而FutureTask的规划确保了这种传递过程可以实现结果的安全发布。
FutureTask在ExeCutor中表示异步任务,此外还能够用来表示一些时间较长的计算,这些计算能够在使用计算结果以前启动。
技术信号量(Counting Semaphore)用来控制同时访问某个特定资源的操做数量,或者同时执行某个指定操做的数量。计数信号量还能够用来实现某种资源池,或者对容器施加边界。
信号量为多线程协做提供了更为强大的控制方法。广义上说,信号量是对锁的扩展。不管是内部锁synchronized仍是重入锁ReentrantLock,一次都只容许一个线程访问一个资源,而信号量却能够指定多个线程,同时访问某一个资源。信号量主要提供了如下构造函数:
public Semaphore(int permits) public Semaphore(int permits,boolean fair) //第二个参数能够指定是否公平
在构造信号量对象时,必需要指定信号量的准入数,即同时能申请多少个许可。当每一个线程每次只申请一个许可时,这就至关于指定了同时有多少个线程能够访问某一个资源。信号量的主要逻辑方法有:
public void acquire() //尝试得到一个准入的许可。若没法得到,则线程会等待,直到有线程释放一个许可或者当前线程被中断 public void acquireUninterruptibly()//和acquire()相似,可是不响应中断 public boolean tryAcquire()//尝试得到一个许可,成功true失败fasle,不会等待,马上返回 public boolean tryAcquire(long timeout,TimeUnit unit) public void release()//线程访问资源结束后,释放一个许可,以使其余等待许可的线程进行资源访问
栗子:
public class SemaphoreExample implements Runnable { //指定信号量,同时能够有5个线程访问资源 public static final Semaphore s = new Semaphore(5); @Override public void run() { try { //申请信号量,也能够直接使用 s.acquire(); if (s.tryAcquire(1500, TimeUnit.SECONDS)) { //模拟耗时操做 Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + " 完成了任务.."); //离开时必须释放信号量,否则会致使信号量泄露——申请了但没有释放 s.release(); } } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] a) throws InterruptedException { //申请20个线程 ExecutorService exec = Executors.newFixedThreadPool(20); final SemaphoreExample re = new SemaphoreExample(); for (int i=0;i<20;i++){ exec.submit(re); } exec.shutdown(); } }
Semaphore中管理者一组虚拟的许可(permit),许可的初始数量可经过构造函数来指定。在执行操做时能够先得到许可(只要还有剩余的许可),并在使用之后释放许可。若是没有许可,那么acquire将阻塞直到有许可(或者被中断或者操做超时)。release方法将返回一个许可给信号量。计算信号量的一种简化形式是二值信号量,即初始值为1的Semaphore。二值信号量能够用作互斥体(mutex),并具有不可重入的加锁语义:谁拥有这个惟一的许可,谁就拥有了互斥锁。
一样,咱们也可使用Semaphore将任何一种容器变成有界阻塞容器。
和以前的CountDownLatch相似,它(循环栅栏)也能够实现线程间的技术等待,但它的功能比CountDownLatch更加复杂强大。它能阻塞一组线程直到某个事件发生。所以,栅栏能够用于实现一些协议,例如“开会必定要在xx地方集合,等其余人到了再讨论下一步要作的事情”。
CyclicBarrier的使用场景也很丰富。好比,司令下达命令,要求10个士兵一块儿去完成一项任务。这时,就会要求10个士兵先集合报道,接着,一块儿雄赳赳气昂昂地执行任务。当10个士兵把本身手头的任务都执行完成了,那么司令才能对外宣布,任务完成!
下面的栗子使用CyclicBarrier演示了上述司机命令士兵完成任务的场景。
public class CyclicBarrierExample { public static class Soldier implements Runnable{ private String name; private CyclicBarrier cyclicBarrier; public Soldier(String name, CyclicBarrier cyclicBarrier) { this.name = name; this.cyclicBarrier = cyclicBarrier; } @Override public void run() { try { System.out.println(name + " 来报道.."); //等待全部士兵到齐 cyclicBarrier.await(); doWork(); //等待全部士兵完成任务 cyclicBarrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } } void doWork(){ try { Thread.sleep(new Random().nextInt(10) * 1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(name + " 任务已完成.."); } } public static class doOrder implements Runnable{ boolean flag; int n; public doOrder(boolean flag, int n) { this.flag = flag; this.n = n; } @Override public void run() { if (flag){ System.out.println("司令 : 士兵 " + n +"个 任务完成"); }else { System.out.println("司令 : 士兵 " + n +"个 集合完毕"); //执行完后 改变完成标记.当下一次调用doOrder时,能够进入if flag = true; } } } public static void main(String[] a){ final int n = 10; //是否完成了任务 boolean flag = false; //建立10个士兵线程 Thread[] allSoldier = new Thread[n]; //建立CyclicBarrier实例 //这里的意思是,等待10个线程都执行完,就执行doOrder()方法 CyclicBarrier c = new CyclicBarrier(n, new doOrder(flag,n)); for (int i=0;i<n;i++){ //System.out.println("士兵" + i + " 报道"); //装配士兵线程 allSoldier[i] = new Thread(new Soldier("士兵" + i,c)); /** * 开启士兵线程,可是执行到第一个cyclicBarrier.await()栅栏时, * 要等待,等到10个士兵线程都到这里等着,等到执行完doOrder()方法后,完成第一次计数. * * 这样才能继续执行下一个方法doWork(),而doWork()完成后,又须要第二次等待, * 等待所有士兵线程都到等待队列后,再次调用doOrder()方法.完成第二次计数. * 而这个方法中,每一个线程的flag都已经改变,利用flag,完成任务. * */ allSoldier[i].start(); /* 执行结果: 士兵0 来报道.. 士兵1 来报道.. ...... 士兵8 来报道.. 士兵9 来报道.. 司令 : 士兵 10个 集合完毕 士兵2 任务已完成.. 士兵8 任务已完成.. ...... 士兵9 任务已完成.. 士兵4 任务已完成.. 司令 : 士兵 10个 任务完成 */ } } }
通常咱们对于锁的优化有如下几个大体方向:
在某些状况下,能够将锁分解技术进一步扩展为对一组独立对象上的锁进行分解,这种状况被称为锁分段。例如,在ConcurrentHashMap的实现中使用了一个包含16个锁的数组,每一个锁保护全部散列桶的1/16,其中第N个散列桶由第(N mod 16)个锁来保护。假设散列函数具备合理的分布性,而且关键字可以均匀分布,那么这大约能把对于锁的请求减小到原来的1/16,正是这项技术使得ConcurrentHashMap可以支持多达16个并发的写入器。(要使得拥有大量处理器的系统在高访问量的状况下实现更高的并发性,还能够进一步增长锁的数量,但仅当你能证实并发写入线程的竞争足够激烈并须要突破这个限制时,才能将锁分段的数量超过默认的16个。)
另外一个典型的案例就是LinkedBlockingQueue的实现。
take()和put()方法虽然都对队列进行了修改操做,但因为是链表,所以,两个操做分别做用于队列的前端和末尾,理论上二者并不冲突。使用独占锁,则要求在进行take和put操做时获取当前队列的独占锁,那么take和put就不可能真正的并发,他们会彼此等待对方释放锁。在JDK的实现中,取而代之的是两把不一样的锁,分离了take和put操做.削弱了竞争的可能性.实现类取数据和写数据的分离,实现了真正意义上成为并发操做。
锁分段的一个劣势在于:与采用单个锁来实现独占访问相比,要获取多个锁来实现独占访问将更加困难而且开销更高。一般,在执行一个操做时最多只需获取一个锁,但在某些状况下须要加锁整个容器,例如当ConcurrentHashMap须要扩展映射范围,以及从新计算键值的散列值要分布到更大的桶集合中时,就须要获取分段锁集合中的全部锁。
锁分解和锁分段技术都能提升可伸缩性,由于它们都能使不一样的线程在不一样的数据(或者同一个数据的不一样部分)上操做,而不会相互干扰。若是程序采用锁分段或分解技术,那么必定要表现出在锁上的竞争频率高于在锁保护的数据上发生竞争的频率。若是一个锁保护两个独立变量X和Y,而且线程A想要访问X,而线程B想要访问Y(这相似于在ServerStatus中,一个线程调用addUser,而另外一个线程调用addQuery),那么这两个线程不会在任何数据上发生竞争,即便它们会在同一个锁上发生竞争。
当每一个操做都请求多个变量时,锁的粒度将很难下降。这是在性能与可伸缩性之间相互制衡的另外一个方面,一些常见的优化措施,例如将一些反复计算的结果缓存起来,都会引入一些”热点域“,而这些热点域每每会限制可伸缩性。
当实现HashMap时,你须要考虑如何在size方法中计算Map中的元素数量。最简单的方法就是,在每次调用时都统计一次元素的数量。一种常见的优化措施是,在插入和移除元素时更新一个计数器,虽然这在put和remove等方法中略微增长了一些开销,以确保计数器是最新的值,但这把size方法的开销从O(n)下降到O(1)。
在单线程或者采用彻底同步的实现中,使用一个独立的计算器能很好地提升相似size和isEmpty这些方法的执行速度,但却致使更难以提高实现的可伸缩性,由于每一个修改map的操做都须要更新这个共享的计数器。即便使用锁分段技术来实现散列链,那么在对计数器的访问进行同步时,也会从新致使在使用独占锁时存在的可伸缩性问题。一个看似性能优化的措施——缓存size操做的结果,已经变成了一个可伸缩性问题。在这种状况下,计数器也被称为热点域,由于每一个致使元素数量发生变化的操做都须要访问它。
为了不这个问题,ConcurrentHashMap中的size将对每一个分段进行枚举并将每一个分段中的元素数量相加,而不是维护一个全局计数。为了不枚举每一个元素,ConcurrentHashMap为每一个分段都维护一个独立的计数,并经过每一个分段的锁来维护这个值。
第三种下降竞争锁的影响的技术就是放弃使用独占锁,从而有助于使用一种友好并发的方式来管理共享状态。例如,使用并发容器、读-写锁、不可变对象以及原子变量。
ReadWriteLock实现了一种在多个读取操做以及单个写入操做状况下的加锁规则:若是多个读取操做都不会修改共享资源,那么这些读取操做能够同时访问该共享资源,但在执行写入操做时必须以独占方式来获取锁。对于读取操做占多数的数据结构,ReadWriteLock可以提供比独占锁更高的并发性。而对于只读的数据结构,其中包含的不变性能够彻底不须要加锁操做。
原子变量提供了一种方式来下降更新“热点域”时的开销,例如竞态计数器、序列发生器、或者对链表数据结构中头节点的引用。原子变量类提供了在整数或者对象引用上的细粒度原子操做(所以可伸缩性更高),并使用了现代处理器中提供的底层并发原语(例如比较并交换)。若是在类中只包含少许的热点域,而且这些域不会与其余变量参与到不变性条件中,那么用原子变量来替代他们能提升可伸缩性。(经过减小算法中的热点域,能够提升可伸缩性——虽然原子变量能下降热点域的更新开销,但并不能彻底消除。)
若是对一个锁不停地进行请求,同步和释放,其自己也会消耗系统宝贵的资源,反而不利于性能优化.
虚拟机在遇到须要一连串对同一把锁不断进行请求和释放操做的状况时,便会把全部的锁操做整合成对锁的一次请求,从而减小对锁的请求同步次数,这就是锁的粗化。
偏向锁是一种针对加锁操做的优化手段,他的核心思想是:若是一个线程得到了锁,那么锁就进行偏向模式.当这个线程再次请求锁时,无需再作任何同步操做.这样就节省了大量操做锁的动做,从而提升程序性能.
所以,对于几乎没有锁竞争的场合,偏向锁有比较好的优化效果.由于极有可能连续屡次是同一个线程请求相同的锁.而对于锁竞争激烈的程序,其效果不佳.
使用Java虚拟机参数:-XX:+UseBiasedLocking 能够开启偏向锁.
若是偏向锁失败,虚拟机并不会当即挂起线程.它还会使用一种称为轻量级的锁的优化手段.轻量级锁只是简单的将对象头部做为指针,指向持有锁的线程堆栈内部,来判断一个线程是否持有对象锁.若是线程得到轻量锁成功,则能够顺利进入临界区.若是失败,则表示其余线程争抢到了锁,那么当前线程的锁请求就会膨胀为重量级锁.
锁膨胀后,虚拟机为了不线程真实的在操做系统层面挂起,虚拟机还作了最后的努力就是自旋锁.若是一个线程暂时没法得到索,有可能在几个CPU时钟周期后就能够获得锁,
那么简单粗暴的挂起线程多是得不偿失的操做.虚拟机会假设在很短期内线程是能够得到锁的,因此会让线程本身空循环(这即是自旋的含义),若是尝试若干次后,能够获得锁,那么久能够顺利进入临界区,
若是还得不到,才会真实地讲线程在操做系统层面挂起.
锁消除是一种更完全的锁优化,Java虚拟机在JIT编译时,经过对运用上下文的扫描,去除不可能存在的共享资源竞争锁,节省毫无心义的资源开销.
咱们可能会问:若是不可能存在竞争,为何程序员还要加上锁呢?
在Java软件开发过程当中,咱们必然会用上一些JDK的内置API,好比StringBuffer、Vector等。你在使用这些类的时候,也许根本不会考虑这些对象到底内部是如何实现的。好比,你颇有可能在一个不可能存在并发竞争的场合使用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[]{}); }
上述代码中的Vector,因为变量v只在createString()函数中使用,所以,它只是一个单纯的局部变量。局部变量是在线程栈上分配的,属于线程私有的数据,所以不可能被其余线程访问。因此,在这种状况下,Vector内部全部加锁同步都是没有必要的。若是虚拟机检测到这种状况,就会将这些无用的锁操做去除。
锁消除设计的一项关键技术是逃逸分析,就是观察某个变量是否会跳出某个做用域(好比对Vector的一些操做).在本例中,变量v显然没有逃出createString()函数以外。以次为基础,虚拟机才能够大胆将v内部逃逸出当前函数,也就是说v有可能被其余线程访问。若是是这样,虚拟机就不能消除v中的锁操做。
逃逸分析必须在-server模式下进行,可使用-XX:+DoEscapeAnalysis参数打开逃逸分析。使用-XX:+EliminateLocks参数能够打开锁消除。