关于并发编程,锁是不可缺少的一部分,今天来聊聊锁,简单的介绍一些锁的概念
synchronized的底层实现
synchronized修饰方法和代码块的实现不一样
Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现。
修饰方法
它实现在方法调用和返回操作之中。 虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。
修饰代码块
代码块是通过字节码指令monitorenter和monitorexit实现的,monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处
synchronized能锁什么
锁定当前对象
同步方法,锁this
synchronized T methodName(){}
T methodName(){ synchronized(this){} }
锁定临界资源对象
T methodName(){ synchronized(object){} }
锁类
当使用synchronized修饰类静态方法时,那么当前加锁的级别就是类
synchronized锁的状态
自旋锁,偏向锁,轻量级锁,重量级锁都是synchronized的状态,synchronized也是一个可重入锁,下面说一下这几种状态
自旋锁
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗
自旋锁用于线程竞争不激烈,占有锁的时间短,性能能大幅度提升,要注意根据任务设定一个合适的自旋时间
偏向锁
大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。
如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
轻量级锁
轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;
重量级锁
获取不到锁的线程进入阻塞,获取到锁的线程,在释放锁时会有唤醒操作。
volatile
volatile可以看做是一种synchronized的轻量级锁,他能够保证并发时,被它修饰的共享变量的可见性,每个线程拥有自己的工作内存,实际上线程所修改的共享变量是从主内存中拷贝的副本,当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
使用场景
1.访问变量不需要加锁(加锁的话使用volatile就没必要了)
2、对变量的写操作不依赖于当前值(因为他不能保证原子性,i=i+1就是依赖当前值)
3.该变量没有包含在具有其他变量的不变式中。
一般用volatile修饰布尔类型变量做标识变量
Lock
ReentrantLock可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是Reentrant Lock重新进入锁。对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。
ReentrantLock有几种获取锁的方式,有什么区别?
lock.lock(): 此方式会始终处于等待中,即使B调用interrupt()也不能中断,除非线程A调用LOCK.unlock()释放锁。
lock.lockInterruptibly(): 此方式会等待,如果B调用interrupt()会被中断等待,并抛出InterruptedException异常,否则会与lock()一样始终处于等待中,直到线程A释放锁。
lock.tryLock(): 不会等待,获取不到锁并直接返回false,去执行下面的代码。
lock.tryLock(10, TimeUnit.SECONDS):该处会在10秒时间内处于等待中,B调用interrupt()会被中断等待,并抛出InterruptedException。10秒时间内如果线程A释放锁,会获取到锁并返回true,否则10秒过后会获取不到锁并返回false,去执行下面的代码。
公平锁/非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的吞吐量比公平锁大。对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
默认是非公平锁
可以传一个参数,如果是true就是公平锁
非公平锁的实现,如果A线程刚好释放锁,B线程刚好来获取锁,队列中的线程还继续等待,B实现插队操作
公平锁的实现,公平锁多了一些判断,让新来的线程没有插队的机会
乐观锁/悲观锁
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。(类似于版本号,看是否有更新数据)乐观的认为,不加锁的并发操作是没有事情的。悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
独享锁/共享锁/互斥锁/读写锁
独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReentrantReadWriteLock,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。Synchronized而言,当然是独享锁。
互斥锁/读写锁就是具体的实现。互斥锁在Java中的具体实现就是ReentrantLock,读写锁在Java中的具体实现就是ReentrantReadWriteLock
可中断锁
可中断锁就是可以中断的锁synchronized就不是可中断锁,lock就是可中断锁,如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
总结:
为什么有synchronized还要用Lock
Lock比synchronized功能更加强大,他是一个可中断锁,线程执行过程中可以中断,Lock可以获取锁的状态,Lock更加灵活,在线程竞争激烈的情况下Lock的效率就会远远高于synchronized
在用锁的时候控制锁的粒度,根据任务,设定合适的参数
文章只是作为自己的学习笔记,借鉴了网上的一些案例,如果觉得有点帮助,希望多交流,如有错误,希望指正