一、常用锁
1、 Synchronized
a) synchronized锁是什么?
Java关键字,能够将方法或者代码块锁起来
只要在方法或者代码块中加上关键字synchronized就能实现同步功能
1 package demo; 2 3 public class SynchronizedDemo implements Runnable{ 4 String lock = "aa"; 5 public void run() { 6 methodA(); 7 methodC(); 8 methodB(); 9 } 10 11 public synchronized void methodA(){ 12 System.out.println(this); 13 try { 14 Thread.sleep(5000); 15 } catch (InterruptedException e) { 16 e.printStackTrace(); 17 } 18 } 19 20 public void methodB(){ 21 synchronized(this){ 22 System.out.println(this); 23 } 24 } 25 26 public void methodC(){ 27 synchronized(lock){ 28 System.out.println(lock); 29 } 30 } 31 32 public static void main(String[] args) { 33 SynchronizedDemo f1=new SynchronizedDemo(); 34 new Thread(f1).start(); 35 } 36 }
两种方法都能加锁,效率上使用同步代码块的方式要比使用同步方法的效率高一些,因为同步代码块是在方法中进行同步。
b) Synchronized 原理
上面标记的3和12分别是进入和退出指令,synchronized底层是是通过monitor对象,对象有自己的对象头,存储了很多信息,其中一个信息标示是被哪个线程持有。
2、 StampedLock
StampedLock是java8在java.util.concurrent.locks新增的一个API。
ReentrantReadWriteLock 在沒有任何读写锁时,才可以取得写入锁,这可用于实现了悲观读取(Pessimistic Reading),即如果执行中进行读取时,经常可能有另一执行要写入的需求,为了保持同步,ReentrantReadWriteLock 的读取锁定就可派上用场。
如果读取执行情况很多,写入很少的情况下,使用 ReentrantReadWriteLock 可能会使写入线程遭遇饥饿(Starvation)问题,也就是写入线程吃吃无法竞争到锁定而一直处于等待状态。
StampedLock控制锁有三种模式(写,读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问。
在读锁上分为悲观锁和乐观锁。
所谓的乐观读模式,也就是若读的操作很多,写的操作很少的情况下,你可以乐观地认为,写入与读取同时发生几率很少,因此不悲观地使用完全的读取锁定,程序可以查看读取资料之后,是否遭到写入执行的变更,再采取后续的措施(重新读取变更信息,或者抛出异常) ,这一个小小改进,可大幅度提高程序的吞吐量!!
3、 ReentrantLock
1 package demo; 2 3 import java.util.concurrent.locks.ReentrantLock; 4 5 public class ReentrantLockDemo extends Thread{ 6 public static ReentrantLock lock = new ReentrantLock(); 7 public static int i = 0; 8 9 public ReentrantLockDemo(String name) { 10 super.setName(name); 11 } 12 13 @Override 14 public void run() { 15 for (int j = 0; j < 100000; j++) { 16 lock.lock(); 17 try { 18 System.out.println(this.getName() + " " + i); 19 i++; 20 } finally { 21 lock.unlock(); 22 } 23 } 24 } 25 26 public static void main(String[] args) throws InterruptedException { 27 ReentrantLockDemo test1 = new ReentrantLockDemo("thread1"); 28 ReentrantLockDemo test2 = new ReentrantLockDemo("thread2"); 29 30 test1.start(); 31 test2.start(); 32 test1.join(); 33 test2.join(); 34 System.out.println(i); 35 } 36 }
保证线程安全,需要开启和关闭锁,如果忘记关闭锁那么资源就不会释放,下次在调用这个锁的时候就会一直等待,也就是说会造成死锁,程序崩溃
4、 ReentrantReadWriteLock
提供两把锁,一把用于读操作和一把用于写操作。同时可以有多个线程执行读操作,但只有一个线程可以执行写操作。当一个线程正在执行一个写操作,不可能有任何线程执行读操作。
5、 ConcurrentHashMap
使用分段锁技术,允许多个修改操作并发进行。ConcurrentHashMap内部使用段来表示这些不同的部分,每个段其实就是一个小的Hashtable,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。
HashTable是一个线程安全的类,它使用synchronized来锁住整张Hash表来实现线程安全,即每次锁住整张表让线程独占
有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁,在ConcurrentHashMap内部,段数组是final的,并且其成员变量实际上也是final的,但是,仅仅是将数组声明为final的并不保证数组成员也是final的,这需要实现上的保证。这可以确保不会出现死锁,因为获得锁的顺序是固定的。
6、 总结
synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定;
ReentrantLock、ReentrantReadWriteLock,、StampedLock都是对象层面的锁定,要保证锁定一定会被释放,就必须将unLock()放到finally{}中;
StampedLock 对吞吐量有巨大的改进,特别是在读线程越来越多的场景下;
StampedLock有一个复杂的API,对于加锁操作,很容易误用其他方法;
当只有少量竞争者的时候,synchronized是一个很好的通用的锁实现;
当线程增长能够预估,ReentrantLock是一个很好的通用的锁实现;
二、锁特性
1、 升级锁
读取锁是不能直接升级为写入锁的。因为获取一个写入锁需要释放所有读取锁,所以如果有两个读取锁视图获取写入锁而都不释放读取锁时就会发生死锁。
2、 重入锁
ReentrantLock和Synchronized都具有
可以反复得到相同的一把锁,它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。
读写锁允许读线程和写线程按照请求锁的顺序重新获取读取锁或者写入锁。当然了只有写线程释放了锁,读线程才能获取重入锁。
写线程获取写入锁后可以再次获取读取锁,但是读线程获取读取锁后却不能获取写入锁。
另外读写锁最多支持65535个递归写入锁和65535个递归读取锁。
3、 读写锁
读写锁维护了一对相关的锁,一个用于只读操作,一个用于写入操作。只要没有writer,读取锁可以由多个reader线程同时保持。写入锁是独占的。
互斥锁一次只允许一个线程访问共享数据,哪怕进行的是只读操作;读写锁允许对共享数据进行更高级别的并发访问:对于写操作,一次只有一个线程(write线程)可以修改共享数据,对于读操作,允许任意数量的线程同时进行读取。
与互斥锁相比,使用读写锁能否提升性能则取决于读写操作期间读取数据相对于修改数据的频率,以及数据的争用——即在同一时间试图对该数据执行读取或写入操作的线程数。
读写锁适用于读多写少的情况。
4、 公平锁和非公平锁
公平锁是根据等待顺序前一把锁释放排在最前面的任务获取锁。
5、 中断锁
读取锁和写入锁都支持获取锁期间被中断。这个和独占锁一致。
6、 悲观锁和乐观锁
悲观锁(Pessimistic Lock), 每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞(block)直到它拿到锁。
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
乐观锁(Optimistic Lock),每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。
7、 自旋锁
自旋锁(Spinlock)是一种广泛运用的底层同步机制。自旋锁是一个互斥设备,它只有两个值:“锁定”和“解锁”。它通常实现为某个整数值中的某个位。希望获得某个特定锁得代码测试相关的位。
如果锁可用,则“锁定”被设置,而代码继续进入临界区;相反,如果锁被其他人获得,则代码进入忙循环(而不是休眠,这也是自旋锁和一般锁的区别)并重复检查这个锁,直到该锁可用为止,这就是自旋的过程。“测试并设置位”的操作必须是原子的,这样,即使多个线程在给定时间自旋,也只有一个线程可获得该锁。
8、 互斥锁
就是一次只能有一个线程持有锁,也即所谓独占锁的概念