java中的锁

关于锁锁

锁解决了什么问题?

  众所周知高并发编程中线程安全是重要的关注点,那造成线程安全的主要问题是什么?最主要就是多个线程存在操作和使用临界资源(共享资源),为了解决这个问题我们需要一个机制,这个机制就是保证同一时刻有且只有一个线程在操作临界资源,这种方式有个高尚的名称叫互斥锁,也就是说当一个共享数据被当前正在访问的线程加上互斥锁后,在同一个时刻,其他线程只能处于等待的状态,直到当前线程处理完毕释放该锁。在java中可以使用synchronized关键字(保证可见性(主要是共享数据的变化被其他线程所看到),完全可以替代Volatile功能),和Lock类两种锁其中主要区别如下

类别 synchronized Lock
存在层次 Java的关键字,在jvm层面上 是一个类
锁的释放 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 在finally中必须释放锁,不然容易造成线程死锁
锁的获取 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待
锁状态 无法判断 可以判断
锁类型 可重入 不可中断 非公平 可重入 可判断 可公平(两者皆可)
性能 1.6版本优化之后性能同lock差不多,官方推荐 大量同步
锁使用 1:修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
2:修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁(对于静态方法锁,锁对象是当前class对象)
3:修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
在加锁和解锁处需要通过lock()和unlock()显示指出就可以,nulocak一般写在finally块中。

java中的自旋锁与自适应自旋

java中线程的挂起和恢复操作都需要转入内核态中完成,这些操作给系统的并发性能带来很大的压力,因为在大多数情况下共享数据的锁 定状态持续很短时间,为了这段很短时间去挂起和恢复线程操作是划不来的,比如说有两个线程同时并发执行,第一个线程拿到锁开始执行,第二锁就可以执行一个忙循环(自旋),这种方式就是所谓的自旋锁。
   自旋锁在jdk1.4.2中引入,默认是关闭的可以使用-XX:+UseSpinning参数开启自旋锁,1.6中默认开始,自旋锁不能代替堵塞,自旋锁虽然避免了线程切换的开销,但是本书需要占用处理器时间,如果锁被占用时间较长,那么自旋锁只会白白浪费处理器资源,所以jvm提供自旋锁等待时间和获取锁次数限定,如果超过限定值就采用传统方式将线程挂起,自旋次数默认是10,可以通过-XX:PreBlockSpin参数修改默认值。

锁消除

锁消除是指虚拟机即时编译时候,对一些代码上要求同步,但是被检查到不可能存在资源竞争的锁进行消除。

1
2
3
4
5
6
7
8
9
10
11
12
public String concatString(String s1,String s2,String s3){
return s1+s2+s3;
}

//下面代码javac 编译后的,1.5之前是StringBuffer 之后是StringBuilder
public String concatString(String s1,String s2,String s3){
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}

我们知道String 是个不可变类,对字符串的链接总是通过产生新的String对象来进行的,因此Javac编译器会对String 链接做自动优化,在1.5之前会转换为StringBuffer 对象的连续append()操作,在1.5之后会转换为StringBuilder对象的连续append()操作,StringBuffer.append()方法中都有一个同步块,锁的对象就是sb,我们可以看出sb对象动态作用域是在concatString中,说明sb用于无法逃逸到concatString之外去,其他线程无法访问,因此这里即便是有锁也可以安全的消除掉锁,在JIT编译之后,这段代码会忽略掉所有的同步直接指向。

关于lock原理参考这篇博文(非常棒)

目前设计锁的分类

主流为乐观锁,悲观锁,自旋锁,重入锁
关于锁类型介绍参考这篇博文

关于使用锁的思考

我们在使用锁的时候应该要清楚为什么使用锁使用什么锁,它能为我们带来什么缺点是什么,不要盲目使用锁!