在程序中加入锁是非常消耗性能的,对锁的申请和释放需要消耗大量的资源,所以我们需要对上锁的程序进行优化,在保证程序正确运行的前提下提高性能。
我们能做的
减小锁的占有时间
减小锁的粒度
用分离锁代替独占锁
锁粗化
JVM能做的
锁偏向
轻量级锁
自旋锁
锁消除
ThreadLocal
我们对比一下下面两个例子:
public synchronized void f(){
method1();
syncmethos();//需要同步
method1();
}
public void g(){
method1();
synchronized(this){
syncmethos();//需要同步
}
method1();
}
在方法f中,我们需要开始就拿到锁,当执行完成所有的方法以后才释放锁资源。但是在方法g中,我们使用了synchronized代码块,在只需要同步的地方上锁,其他方法不需要上锁。这样就减少了线程占有锁的时间,大大提高了程序的并发性能。
减小锁的粒度典型的使用例子是高性能的同步容器ConcurrentHashMap,为什么它的性能要比使用Collections.synchronizedMap要好呢?原因就在把锁的粒度减小了。Collections.synchronizedMap中使用了一把锁来解决同步问题,而ConcurrentHashMap里面有很多把锁,不同的资源有不同的锁,当线程访问了同一个资源才需要排队拿锁。通俗的说,ConcurrentHashMap就好比一栋楼有很多个不同的入口,当人们选择不同的入口时,并不会阻塞;而Collections.synchronizedMap就好比只有一个入口,所有人都要排队进入。默认情况下,ConcurrentHashMap里面有16把锁,如果幸运的有16个线程访问不同的资源,它的性能和非同步容器HashMap相差无几。
分离锁就是分开的两个锁。相似的例子就是之前提到过的**读写锁**ReadWriteLock,假设我们在读取的时候加读取锁,写出的时候加写锁,这两把锁是分离。那么当我们分别执行读取和写出的时候是不会阻塞,只有读-读或者写-写才会阻塞。线比较于独占锁(所有的操作都要发生阻塞),性能就提高了很多。而ReadWriteLock,是分离所的一种扩展,去掉了读锁,使得只有写 写才会阻塞,性能比单纯的分离锁更加出色。
虽然说,减少锁的占有时间可以提高性能,但是凡事都有一个度。如下面的例子:
for(int i=0;i<100;i++)
synchronized(this){
//做一些事
}
这里使用了synchronized块,减少了锁的持有时间,但是,对于同一个锁需要进行100次的请求、同步和释放,消耗了大量的系统资源。所以对于上面的例子,更加合理的方法是将锁粗化。就像这样:
synchronized(this){
for(int i=0;i<100;i++)
//做一些事
}
虽然占用时间增加了,但是只需要对锁进行一次请求、同步和释放。
锁偏向是一种针对加锁操作的优化手段。如果一个线程获得了锁,那么JVM会使得这个锁进入偏向模式,当这个锁再次请求时就不需要在做任何的同步操作了,这就减少了申请锁的时间和消耗,提高了系统的性能。我们可以使用JAVA虚拟机参数-XX:+UseBiaseLocking开启偏向锁。
注意的是:对于锁竞争较少的场合,偏向锁有比较好的优化效果,但是对于竞争激烈的场合,这种模式就会失效。
轻量级锁的本质是使用CAS操作代替锁实现同步,传统的使用锁(互斥)则就是重量锁。
实现原理:虚拟机的对象头分为两部分,一部分用于存储对象自身的运行数据,如对象HashCode,对象分代年龄,官方称为”Mark Word”;另一部分用于存储数据类型的指针。Mark Word中存在一个锁标志位,01表示未锁定或可偏向,00表示轻量级锁,10表示膨胀。当代码进入同步块时,如果对象未锁定(01),则在当前线程栈帧中创建一个锁记录空间(record)用来存储Mark Word的拷贝。然后JVM使用CAS操作将对象的MarkWord指向record,如果操作成功,则获得了对象的锁,此时锁标志位为(00),表示对象处于轻量级锁。如果失败了,JVM首先检查Mark Word是否指向当前线程栈帧,如果当前对象已经获得了锁,则直接进入同步块执行,否则说明有其他线程抢占了锁。如果有多个线程竞争一个锁,则膨胀为重量级锁,此时锁标志为(10),因为多了CAS操作,所以在这种锁存在竞争的环境下效率比重量锁低。
线程的挂起会增大系统的开销,所以为了避免线程真的被挂起,JVM还会做最后一次努力——自旋锁。JVM的努力是这样的:当线程无法获得锁的时候,JVM会做一个赌注:在不久的将来,线程会获得这把锁,所以JVM会让线程做多次空的循环,如果获得了锁,线程就会进入临界区,如果失败,就真的被挂起来了。当线程执行空代码所产生的CPU消耗小于线程的挂起来带的系统额外开销时,自旋锁才有真正的意义;自旋锁盲目的执行空循环会白白消耗CPU的资源,因此线程自旋次数默认为10。
JVM在JIT编译时,会扫描上下文,把不存在资源共享的锁去除,节省毫无意义的请求锁的时间。
public void function(){
ConcurrentHashMap map = new ConcurrentHashMap();
//做一些事情
//.......
}
代码中是不需要使用ConcurrentHashMap的,因为map变量是单纯的局部变量,而局部变量是在线程栈上分配的,是属于线程私有的(就类似于ThreadLocal变量,马上会讲),是不可能被其他线程访问的,所以不需要加锁。如果JVM检测到这些没必要的锁,就会自动清除。
除了控制资源意外,我们还可以通过增加资源来保证所有对象的线程安全。比如让100个人填写一张表格,但是只有一支笔,那大家只能挨个排队,但是如果怎加笔的数量,使得人手一支笔,那就会很快的完成任务。
public class ThreadLocalDemo {
public static class Pen {
private int id;
public Pen(int id) {
this.id = id;
}
public String toString() {
return "笔:编号为:" + id;
}
}
public static class Student implements Runnable {
private int sid;
ThreadLocal local;
Student(int sid, ThreadLocal local) {
this.sid = sid;
this.local = local;
}
@Override
public void run() {
//从threadlocal中拿pen
if (local.get() == null) {
//如果这个线程中没有,就创建一个
local.set(new Pen(sid));
}
System.out.println("学生:"+sid+"拿到了"+local.get());
}
}
public static void main(String[] args) {
ThreadLocal local = new ThreadLocal<>();
//创建线程池
ExecutorService service = Executors.newCachedThreadPool();
for(int i=1;i<=100;i++){
service.execute(new Student(i, local));
}
service.shutdown();
}
}
/*
output:
学生:5拿到了笔:编号为:5
。。。。。。
学生:89拿到了笔:编号为:55
学生:88拿到了笔:编号为:88
。。。。。。
学生:75拿到了笔:编号为:75
*/
我们需要关注的是ThreadLocal中的set和get方法。源码中set方法如下:
public void set(T value) {
//先获得当前线程对象
Thread t = Thread.currentThread();
//获得这个线程中的内部对象ThreadLocalMap
//虽然不是,但是可以简单的理解为HashMap
ThreadLocalMap map = getMap(t);
//如果map不为空,就把value装入
if (map != null)
map.set(this, value);
else
//如果map为空,就创建一个map再把value装入
createMap(t, value);
}
get方法如下:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
get方法首先也是先取得当前线程对象t,然后通过自己作为key取得内部的实际数据。
这样,我们知道上面的取笔例子中,其实创建了100个pen的实例。所以当线程执行完以后我们需要清理这些资源,包括清理这个ThreadLocalMap。所以上面的例子是有非常大的问题的,因为我没有对对创建出的100支笔销毁,更加严重的是,我使用了线程池Executors.newCachedThreadPool(),这将会使系统出现内存泄漏。(对于100支笔我们不能清理它们)。所以,在使用ThreadLocal的时候,最好使用ThreadLocal.remove()将变量对象清除。
非常重要的一点!!!:ThreadLocal是为每一个线程创建一个属于线程私有的对象,因此是不存在所谓的线程安全问题的,因为对象都不同。ThreadLocal有100支笔,虽然样子一样,却是不同的对象。所以,ThreadLocal是不能对共享资源实现线程安全的访问的。