因为重量级锁总是在用户态和内核态之间转换,大大消耗性能。因此,jdk1.6之后对synchronized进行优化。
一、锁优化手段
1.自旋锁与自适应自旋
共享数据的锁定状态只会持续很短的一段时间, 为了这段时间去挂起和恢复线程并不值得。 现在绝大多数的个人电脑和服务器都是多路(核) 处理器系统, 如果物理机器有一个以上的处理器或者处理器核心, 能让两个或以上的线程同时并行执行, 我们就可以让后面请求锁的那个线程“稍等一会”, 但不放弃处理器的执行时间, 看看持有锁的线程是否很快就会释放锁。 为了让线程等待, 我们只须让线程执行一个忙循环(自旋) , 这项技就是所谓的自旋锁。
a线程占有锁,如果b线程来获取锁发现a线程正在用,之前的逻辑是此时b线程应该挂起了(用户态转换到内核态,然后才能操作系统资源挂起线程),但是a线程占用的时间很短,不至于再让b线程挂起。因此,现在的策略是让b线程自旋等待,就是b线程依然占用cpu资源通过自旋的方式等待a线程释放锁。如果a线程还没有释放锁,那就让b线程现在的状态一直升级到重量级锁,这时候才可以进入阻塞状态。
JDK 6中对自旋锁的优化, 引入了自适应的自旋。 自适应意味着自旋的时间不再是固定的了, 而是由
前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。
道格李把【自旋】用到极致。
在看了AQS+ReentrantLock+ConcurrentHashMap+Semaphore的源码的时候,也就是juc下包类的源码的时候,你始终离不开两个核心:1.自旋(for或者while) 2.CAS。
2.锁粗化
stringbuffer的append方法是被synchronized修饰的,调用三次append方法,如果每次都syn太消耗性能,因此锁粗化-相当于把三个append使用一个syn同步。
public void test(){
buffer.append("zhangsan");
buffer.append("lisi");
buffer.append("wangwu");
}
锁粗化底层使用的是“逃逸分析”中的第一条。
2.1逃逸分析
使用逃逸分析,编译器可以对代码做如下优化:
一、同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
二、将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
User:id、name、age、address。
public String void test(){
User user = new User();
user.setId = 1;
user.setName = "zhangsan";
return user.getName();
}
如上案例:
user这个对象不会在堆上分配,因为最终返回的结果只是一个字符串,user对象随着方法执行的结束而被销毁,因此jvm不会把user放到堆中,而是栈上。
三、分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
标量替换:
还是上面的代码案例,即使在栈上,也不是存储完整的user对象中的内容的,栈上只存储user.name的值和user.id的值,不会分配age、address的空间。
是不是所有的对象和数组都会在堆内存分配空间?
不一定
3.锁消除
锁消除是指虚拟机即时编译器在运行时, 对一些代码要求同步, 但是对被检测到不可能存在共享
数据竞争的锁进行消除。
4.锁膨胀升级
4.1偏向锁
4.2轻量级锁
二、结语
经过Orcale公司的诸多努力,现在的synchronzied性能与Lock的性能不看上下了。