基础介绍
在程序中可以通过synchronized实现锁功能,对于它可称之为内置锁,是由Java语言层面直接为我们提供使用的,可以在程序中隐式的获取锁。但是对于它的使用方式是固化的,只能先获取在释放。而且在使用的过程中,当一个线程获取到某个资源的锁后,其他线程再要获取锁资源则必须进行等待,synchronized并没有提供中断或超时获取的操作。
为了解决这些问题,所以才出现了显示锁。在显示锁中提供了三个很常见的方法:lock()、unLock()、try Lock().
lock的标准用法
//加锁
lock.lock();
//业务逻辑
try{
i++;
}finally{
//解锁
lock.unLock();
}
不要将获取的过程写在try中,因为如果在获取锁时发生异常,异常抛出的同时会导致锁的释放。
在finally块释放锁,目的是保证获取到锁之后,最终能够将锁释放
何时使用Synchronized还是Lock
如果在使用锁的过程中,不需要考虑尝试获取锁或锁中断的这些特性的话。尽量使用synchronized。因为synchronized在现在的JDK中优化还是很多的,如锁优化升级。
同时synchronized要不显示锁消耗的内存要少,为什么呢?因为synchronized是一个语言层面上的内容,而lock是一个接口,在使用lock时需要获取其对象实例化后才能进行操作,特别在锁很多的情况下,如果没有特殊要求,建议使用synchronized。
ReentrantLock
标准使用方式
根据源码可知Lock本身是一个接口,那么对于其实现类来说,最常用的就是ReentrantLock。
那么ReentarntLock应该如何使用呢?其实很简单,只要遵循其规范即可:
//通过两个线程对value进行count次自增
public class LockTest extends Thread{
private static int count = 100000;
private static int value = 0;
private static Lock lock = new ReentrantLock();
@Override
public void run() {
for (int i = 0; i < count; i++) {
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+" : "+value);
value++;
}finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
LockTest l1 = new LockTest();
LockTest l2 = new LockTest();
l1.start();
l2.start();
TimeUnit.SECONDS.sleep(5);
System.out.println(value);
}
}
在加锁时务必注意,对于解锁需要在finally进行,因为在执行业务逻辑时,有可能会发生异常导致锁无法被释放。
而synchronized的使用要么作用在方法上,要么作用在语句块。当出现异常后,代表脱离了执行的代码块,锁自然就会释放。而显示锁本身就是一个对象的实例,如果加锁之后,没有进行释放操作的话,那么锁就会一直存在。
可重入
ReentrantLock一般会把它称之为可重入锁,其是一种递归无阻塞的同步机制。它可以等同于synchronized的使用,但是ReentrantLock提供了比synchronized更强大、灵活的锁机制,可以减少死锁发生的概率。
简单来说就是:同一个线程对于已经获取的锁,可以多次继续申请到该锁的使用权。而synchronized关键字隐式支持重进入,比如一个synchronized修饰的递归方法,在方法执行的时,执行线程在获取了锁之后仍能连续多次地获得该锁。ReentrantLock在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。
其内部的实现流程为:
- 每个锁关联一个线程持有者和计数器,当计数器为0时,表示该锁没有被任何线程持有,那么线程都会可能获得该锁而调用对应的方法。
- 当某个线程请求成功后,JVM会记录锁的持有线程,并将计数器置为1,此时其他线程请求获取锁,则必须等待。
- 当持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器递增。
- 当持有锁的线程退出同步代码块时,计数器递减,当计数器为0时,则释放锁
synchronized可重入
public class SynDemo {
public static synchronized void lock1(){
System.out.println("lock1");
lock2();
}
public static synchronized void lock2(){
System.out.println("lock2");
}
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
lock1();
}
}.start();
}
}
执行结果
lock1
lock2
根据结果可以看到,当同一个线程调用多个同步方法时,当其第一次获取锁成功时,接着带哦用其它同步方法时,仍然可以继续向下调用,不会发生阻塞。实现了锁的可重入。
ReentrantLock可重入
public class ReentrantTest {
private static Lock lock = new ReentrantLock();
private static int count = 0;
public static int getCount() {
return count;
}
public void test1(){
lock.lock();
try {
count++;
test2();
}finally {
lock.unlock();
}
}
public void test2(){
lock.lock();
try {
count++;
}finally {
lock.unlock();
}
}
static class MyThread implements Runnable{
private ReentrantTest reentrantTest;
public MyThread(ReentrantTest reentrantTest) {
this.reentrantTest = reentrantTest;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
reentrantTest.test1();
}
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantTest reentrantTest = new ReentrantTest();
new Thread(new MyThread(reentrantTest)).start();
TimeUnit.SECONDS.sleep(2);
System.out.println(count);
}
}
运行可以发现,虽然进行了多次加锁,但是并没有阻塞,代表其也是支持可重入的。
公平锁与非公平锁
原理
在多线程执行并发中,当有多个线程同时来获取同一把锁,如果是按照谁等待时间最长,谁先获取锁,则代表是一把公平锁。反之如果是随机获取的,CPU时间片轮询到哪个线程,哪个线程就获取锁,则代表是一把非公平锁。
那么公平锁和非公平锁哪个性能好呢?答案是非公平锁性能好,因为充分利用了CPU,减少线程唤醒的上下文切换的时间。
公平锁
非公平锁
代码实现
在ReentarntLock和synchroized中,默认都是非公平锁,ReentrantLock可以通过参数将其开启使用公平锁。
1)ReentrantLock公平锁
public class FairLockTest {
//开启公平锁
private static Lock lock = new ReentrantLock(true);
public static void test(){
for (int i = 0; i < 2; i++) {
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"获取到锁");
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
new Thread("线程A"){
@Override
public void run() {
test();
}
}.start();
new Thread("线程B"){
@Override
public void run() {
test();
}
}.start();
}
}
根据结果可以看到,其获取锁的过程是按照公平策略来进行。
2)ReentrantLock非公平锁
只需要在实例化ReentrantLock时,不传入参数即为非公平锁。 根据执行结果可以看到,是按照非公平策略来进行锁的获取。
ReentrantLock与synchronized的比较
相似点:
都是以阻塞性方式进行加锁同步的,也就是说如果一个线程获取了对象锁,执行同步代码块,则其它线程要访问都要阻塞等待,直到获取锁的线程释放了锁才能进行访问。
不同点:
- 对于synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm的实现,而ReentrantLock它是JDK1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来实现完成。
- synchronized的使用比较方便简洁,并且由编译器保证锁的释放和加锁,而ReentrantLock需要手工声明来加锁和释放锁,为了避免手工释放锁造成死锁,所以最好在finally中声明释放锁。
- ReentrantLock的锁粒度和灵活度要优于synchronized。
ReentrantReadWriteLock
对于之前学习的ReentrantLock或者synchronized都可以称之为独占锁、排它锁,可以理解为是悲观锁,这些锁在同一时刻只允许一个线程进行访问。但是对于互联网项目来说,绝大多数场景都是读多写少,比例大概在10:1。按照数据库的场景来说,对于读多写少的处理,就会进行读写分离。
在读多写少的场景下,对业务代码的处理,此时也可以考虑进行读写分别加锁的操作,此时就可以使用ReentrantReadWriteLock。其对ReadWriteLock接口进行实现,内部会维护一对锁,分别为读锁和写锁。
读写锁特性
读操作不互斥、写操作互斥、读写互斥
公平性:支持公平性和非公平性
重入性:支持锁重入
锁降级:写锁能降级为读锁,遵循获取写锁、获取读锁在写锁的次序。读锁不能升级为写锁。
读写锁实现原理
ReentrantReadWriteLock实现接口ReadWriteLock,该接口维护了一对相关的锁,一个用于读操作,一个用于写操作。
ReadWriteLock定义了两个方法。readLock()返回用于读操作的锁,writeLock()返回用于写操作的锁,ReentrantReadWriteLock定义如下:
其内部的writeLock()用于获取写锁,readLock()用于读锁
读写锁演示
读写锁的特点在于写互斥、读不互斥、读写互斥。下面就通过例子来演示具体效果:
public class ReentrantReadWriteLockDemo {
private static int count = 0;
private static class WriteDemo implements Runnable{
ReentrantReadWriteLock lock ;
public WriteDemo(ReentrantReadWriteLock lock) {
this.lock = lock;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.writeLock().lock();
count++;
System.out.println("写锁: "+count);
lock.writeLock().unlock();
}
}
}
private static class ReadDemo implements Runnable{
ReentrantReadWriteLock lock ;
public ReadDemo(ReentrantReadWriteLock lock) {
this.lock = lock;
}
@Override
public void run() {
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.readLock().lock();
count++;
System.out.println("读锁: "+count);
lock.readLock().unlock();
}
}
public static void main(String[] args) {
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
WriteDemo writeDemo = new WriteDemo(lock);
ReadDemo readDemo = new ReadDemo(lock);
//运行多个写线程,不会重复,证明写互斥
//运行多个读线程,可能重复,证明读不互斥
//同时运行,读锁和写锁后面不会出现重复的数字,证明读写互斥
for (int i = 0; i < 3; i++) {
new Thread(writeDemo).start();
}
for (int i = 0; i < 3; i++) {
new Thread(readDemo).start();
}
}
}
锁降级
读写锁是支持锁降级的,但不支持锁升级。写锁可以被降级为读锁,但读锁不能被升级写锁。什么意思呢?简单来说就是获取到了写锁的线程能够再次获取到同一把锁的读锁,因为支持提到过ReentrantReadWriteLock这把锁内部是维护了两个锁的。 而获取到了读锁的线程不能再次获取同一把锁的写锁。
1)写锁降级读锁
public class LockDegradeDemo1 {
private static class Demo{
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void fun1(){
//获取写锁
lock.writeLock().lock();
System.out.println("fun1");
fun2();
lock.writeLock().unlock();
}
public void fun2(){
//获取读锁
lock.readLock().lock();
System.out.println("fun2");
lock.readLock().unlock();
}
}
public static void main(String[] args) {
new Demo().fun1();
}
}
根据执行结果可知,当一个线程获取到了写锁后,其可以继续向下来获取同一把锁的读锁。
2)读锁升级写锁
public class LockDegradeDemo2 {
private static class Demo{
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void fun1(){
//获取写锁
lock.writeLock().lock();
System.out.println("fun1");
//fun2();
lock.writeLock().unlock();
}
public void fun2(){
//获取读锁
lock.readLock().lock();
System.out.println("fun2");
fun1();
lock.readLock().unlock();
}
}
public static void main(String[] args) {
new Demo().fun2();
}
}
根据执行结果可知。当线程获取到读锁,不能继续获取写锁。
性能优化演示
在读多写少的情况下,通过读写锁可以优化原有的synchronized对于程序执行的性能。
public class Sku {
private String name;
private double totalMoney;//总销售额
private int storeNumber;//库存数
public Sku(String name, double totalMoney, int storeNumber) {
this.name = name;
this.totalMoney = totalMoney;
this.storeNumber = storeNumber;
}
public double getTotalMoney() {
return totalMoney;
}
public int getStoreNumber() {
return storeNumber;
}
public void changeNumber(int sellNumber){
this.totalMoney += sellNumber*25;
this.storeNumber -= sellNumber;
}
}
public interface SkuService {
//获得商品的信息
Sku getSkuInfo();
//设置商品的数量
void setNum(int number);
}
以synchronized形式运行
public class SkuServiceImplSync implements SkuService{
private Sku sku;
public SkuServiceImplSync(Sku sku) {
this.sku = sku;
}
@Override
public synchronized Sku getSkuInfo() {
try {
TimeUnit.MILLISECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
return this.sku;
}
@Override
public synchronized void setNum(int number) {
try {
TimeUnit.MILLISECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
sku.changeNumber(number);
}
}
public class SkuExec {
//读写线程的比例
static final int readWriteRatio = 10;
//最少线程数
static final int minthreadCount = 3;
//读操作
private static class ReadThread implements Runnable{
private SkuService skuService;
public ReadThread(SkuService skuService) {
this.skuService = skuService;
}
@Override
public void run() {
long start = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
skuService.getSkuInfo();
}
System.out.println(Thread.currentThread().getName()+"读取商品数据耗时:"
+(System.currentTimeMillis()-start)+"ms");
}
}
//写操作
private static class WriteThread implements Runnable{
private SkuService skuService;
public WriteThread(SkuService skuService) {
this.skuService = skuService;
}
@Override
public void run() {
long start = System.currentTimeMillis();
Random r = new Random();
for(int i=0;i<10;i++){//操作10次
skuService.setNum(r.nextInt(10));
}
System.out.println(Thread.currentThread().getName()
+"写商品数据耗时:"+(System.currentTimeMillis()-start)+"ms---------");
}
}
public static void main(String[] args) throws InterruptedException {
Sku sku = new Sku("computer",10000,10000);
SkuService skuService = new SkuServiceImplSync(sku);
for(int i = 0;i<minthreadCount;i++){
Thread setT = new Thread(new WriteThread(skuService));
for(int j=0;j<readWriteRatio;j++) {
Thread getT = new Thread(new ReadThread(skuService));
getT.start();
}
TimeUnit.MILLISECONDS.sleep(100);
setT.start();
}
}
}
执行结果
Thread-0写商品数据耗时:58ms---------
Thread-11写商品数据耗时:58ms---------
Thread-22写商品数据耗时:58ms---------
Thread-4读取商品数据耗时:2577ms
Thread-5读取商品数据耗时:3029ms
Thread-6读取商品数据耗时:3040ms
Thread-9读取商品数据耗时:4016ms
Thread-29读取商品数据耗时:4980ms
Thread-15读取商品数据耗时:8971ms
Thread-13读取商品数据耗时:9742ms
Thread-18读取商品数据耗时:10257ms
Thread-19读取商品数据耗时:10417ms
Thread-25读取商品数据耗时:10805ms
Thread-26读取商品数据耗时:11250ms
Thread-27读取商品数据耗时:11645ms
Thread-31读取商品数据耗时:12137ms
Thread-3读取商品数据耗时:13257ms
Thread-7读取商品数据耗时:13714ms
Thread-10读取商品数据耗时:13911ms
Thread-32读取商品数据耗时:13730ms
Thread-28读取商品数据耗时:14101ms
Thread-23读取商品数据耗时:14409ms
Thread-1读取商品数据耗时:14808ms
Thread-20读取商品数据耗时:14986ms
Thread-17读取商品数据耗时:15150ms
Thread-14读取商品数据耗时:15691ms
Thread-16读取商品数据耗时:16312ms
Thread-21读取商品数据耗时:16494ms
Thread-24读取商品数据耗时:16514ms
Thread-30读取商品数据耗时:16637ms
Thread-8读取商品数据耗时:16867ms
Thread-2读取商品数据耗时:16982ms
Thread-12读取商品数据耗时:16986ms
以ReentrantReadWriteLock形式执行
public class SkuServiceImplReen implements SkuService{
private Sku sku;
public SkuServiceImplReen(Sku sku) {
this.sku = sku;
}
private ReadWriteLock lock = new ReentrantReadWriteLock();
private Lock readLock = lock.readLock();
private Lock writeLock = lock.writeLock();
@Override
public Sku getSkuInfo() {
readLock.lock();
try {
TimeUnit.MILLISECONDS.sleep(5);
return this.sku;
} catch (InterruptedException e) {
e.printStackTrace();
return null;
} finally {
readLock.unlock();
}
}
@Override
public void setNum(int number) {
writeLock.lock();
try {
TimeUnit.MILLISECONDS.sleep(5);
sku.changeNumber(number);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
}
修改启动类
SkuService skuService = new SkuServiceImplReen(sku);
执行结果
Thread-0写商品数据耗时:66ms---------
Thread-11写商品数据耗时:68ms---------
Thread-22写商品数据耗时:62ms---------
Thread-2读取商品数据耗时:765ms
Thread-8读取商品数据耗时:764ms
Thread-10读取商品数据耗时:764ms
Thread-5读取商品数据耗时:765ms
Thread-1读取商品数据耗时:765ms
Thread-4读取商品数据耗时:765ms
Thread-7读取商品数据耗时:764ms
Thread-6读取商品数据耗时:764ms
Thread-3读取商品数据耗时:765ms
Thread-9读取商品数据耗时:770ms
Thread-15读取商品数据耗时:760ms
Thread-17读取商品数据耗时:760ms
Thread-12读取商品数据耗时:760ms
Thread-13读取商品数据耗时:760ms
Thread-14读取商品数据耗时:760ms
Thread-18读取商品数据耗时:759ms
Thread-16读取商品数据耗时:760ms
Thread-20读取商品数据耗时:765ms
Thread-19读取商品数据耗时:765ms
Thread-21读取商品数据耗时:765ms
Thread-31读取商品数据耗时:704ms
Thread-28读取商品数据耗时:705ms
Thread-25读取商品数据耗时:705ms
Thread-30读取商品数据耗时:704ms
Thread-29读取商品数据耗时:705ms
Thread-27读取商品数据耗时:705ms
Thread-26读取商品数据耗时:705ms
Thread-23读取商品数据耗时:705ms
Thread-24读取商品数据耗时:705ms
Thread-32读取商品数据耗时:704ms
根据最终结果可以看出,其性能的提升是非常巨大的。
StamptedLock
stamptedLock类是在JDK8引入的一把新锁,其是对原有的ReentrantReadWriteLock读写锁的增强,增加了一个乐观读模式,内部提供了相关API不仅优化了读锁、写锁的访问,也可以让读锁和写锁间相互转换,从而更细粒度的控制并发。
ReentrantReadWriteLock存在的问题
在使用读写锁时,还容易出现写线程饥饿的问题。主要是因为读锁和写锁互斥。比方说:当线程 A 持有读锁读取数据时,线程 B 要获取写锁修改数据就只能到队列里排队。此时又来了线程 C 读取数据,那么线程 C 就可以获取到读锁,而要执行写操作线程 B 就要等线程 C 释放读锁。由于该场景下读操作远远大于写的操作,此时可能会有很多线程来读取数据而获取到读锁,那么要获取写锁的线程 B 就只能一直等待下去,最终导致饥饿。
对于写线程饥饿问题,可以通过公平锁进行一定程度的解决,但是它是以牺牲系统吞吐量为代价的。
StampedLock特点
1)获取锁的方法,会返回一个票据(stamp),当该值为0代表获取锁失败,其他值都代表成功。
2)释放锁的方法,都需要传递获取锁时返回的票据,从而控制是同一把锁。
3)StampedLock是不可重入的,如果一个线程已经持有了写锁,再去获取写锁就会造成死锁。
4)StampedLock提供了三种模式控制读写操作:写锁、悲观读锁、乐观读锁
写锁: 使用类似于ReentrantReadWriteLock,是一把独占锁,当一个线程获取该锁后,其他请求线程会阻塞等待。 对于一条数据没有线程持有写锁或悲观读锁时,才可以获取到写锁,获取成功后会返回一个票据,当释放写锁时,需要传递获取锁时得到的票据。
悲观读锁: 使用类似于ReentrantReadWriteLock,是一把共享锁,多个线程可以同时持有该锁。当一个数据没有线程获取写锁的情况下,多个线程可以同时获取到悲观读锁,当获取到后会返回一个票据,并且阻塞线程获取写锁。当释放锁时,需要传递获取锁时得到的票据。
乐观读锁: 这把锁是StampedLock新增加的。可以把它理解为是一个悲观锁的弱化版。当没有线程持有写锁时,可以获取乐观读锁,并且返回一个票据。值得注意的是,它认为在获取到乐观读锁后,数据不会发生修改,获取到乐观读锁后,其并不会阻塞写入的操作。 那这样的话,它是如何保证数据一致性的呢? 乐观读锁在获取票据时,会将需要的数据拷贝一份,在真正读取数据时,会调用StampedLock中的API,验证票据是否有效。如果在获取到票据到使用数据这期间,有线程获取到了写锁并修改数据的话,则票据就会失效。 如果验证票据有效性时,当返回true,代表票据仍有效,数据没有被修改过,则直接读取原有数据。当返回flase,代表票据失效,数据被修改过,则重新拷贝最新数据使用。 乐观读锁使用与一些很短的只读代码,它可以降低线程之间的锁竞争,从而提高系统吞吐量。但对于读锁获取数据结果必须要进行校验。
5)在StampedLock中读锁和写锁可以相互转换,而在ReentrantReadWriteLock中,写锁可以降级为读锁,而读锁不能升级为写锁。
使用示例:
class Point {
//定义共享数据
private double x, y;
//实例化锁
private final StampedLock sl = new StampedLock();
//写锁案例
void move(double deltaX, double deltaY) {
//获取写锁
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
//释放写锁
sl.unlockWrite(stamp);
}
}
//使用乐观读锁案例
double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead(); //获得一个乐观读锁
double currentX = x, currentY = y; //将两个字段读入本地局部变量
if (!sl.validate(stamp)) { //检查发出乐观读锁后同时是否有其他写锁发生?
stamp = sl.readLock(); //如果有,我们再次获得一个读悲观锁
try {
currentX = x; // 将两个字段读入本地局部变量
currentY = y; // 将两个字段读入本地局部变量
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
//使用悲观读锁并锁升级案例
void moveIfAtOrigin(double newX, double newY) {
// 获取悲观读锁
long stamp = sl.readLock();
try {
while (x == 0.0 && y == 0.0) { //循环,检查当前状态是否符合
//锁升级,将读锁转为写锁
long ws = sl.tryConvertToWriteLock(stamp);
//确认转为写锁是否成功
if (ws != 0L) {
stamp = ws; //如果成功 替换票据
x = newX; //进行状态改变
y = newY; //进行状态改变
break;
}
else { //如果不成功
sl.unlockRead(stamp); //显式释放读锁
stamp = sl.writeLock(); //显式直接进行写锁 然后再通过循环再试
}
}
} finally {
//释放读锁或写锁
sl.unlock(stamp);
}
}
}