在程序中可以通过synchronized实现锁功能,对于它可以称为内置锁,是由Java语言层面直接为我们提供使用的。可以在程序中隐式的获取锁。但是对于它的使用方式是固化的,只能先获取再释放。而且在使用的过程中,当一个线程获取到某个资源的锁后,其他线程再要获取该资源则必须要进行等待。synchronized并没有提供中断或超时获取的操作。
为了解决这些问题,所以才出现了显示锁。在显示锁中其提供了三个很常见方法:lock()、unLock()、tryLock()。
基础介绍
Lock的标准用法
lock.lock(); try { i++; }finally { lock.unLock(); }
不要将获取锁的过程写在try中,因为如果在获取锁时发生异常,异常抛出的同时会导致锁的释放。
在 finally 块中释放锁,目的是保证在获取到锁之后,最终能够被释放。
何时选择用synchronized还是Lock 如果在锁的使用过程中,不需要考虑尝试取锁或锁中断的这些特性的话。尽量使用synchronized。因为synchronized在现在的JDK中对于synchronized的优化是很多的。如锁优化升级。
同时synchronized要比显示锁的内存消耗要少。为什么呢? 因为synchronized是一个语言层面的内容,而lock是一个接口,在使用Lock时需要获取其对象实例后才能进行操作。特别在锁很多的情况下,如没特殊需求,建议使用synchronized。
ReentrantLock 标准使用方式 根据源码可知Lock本身是一个接口,那么对于其实现类来说,最常用的就是ReentrantLock。
那么ReentrantLock应该如何来使用呢? 其实很简单,只要遵循使用规范即可。
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(); } }
执行结果
根据执行结果可以看到,当同一个线程调用多个同步方法时,当其第一次获取锁成功时,接着调用其他同步方法时,仍然可以继续向下调用,不会发生阻塞。实现了锁的可重入。
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,减少了线程唤醒的上下文切换的时间。
公平锁
非公平锁
代码实现 在ReentrantLock和synchronized中,默认都为非公平锁。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它是JDK 1.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" ); 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++){ 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 StampedLock类是在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中,写锁可以降级为读锁,而读锁不能升级为写锁。
使用示例 此处引用Oracle官方案例。https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/StampedLock.html
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); } } }