锁是用来控制多个线程访问共享资源的。一般来说,锁能够防止多个线程同时访问共享资源。但是有也有些锁可以允许多个线程并发的访问共享资源,比如读写锁。
以下简单说一下锁的内存语意:当线程获取锁时,会把该线程对应的本地内存置为无效,从而使得被该线程必须从主内存中读取共享变量;当线程释放锁时,会把该线程对应的本地内存中的共享变量刷新到主内存中。
A线程释放锁后,共享数据的状态示意图如图所示:
下图是锁获取的状态示意图:
对比锁释放-获取的内存语义与volatile写-读的内存语义可以看出:
锁释放与volatile写有相同的内存语义锁获取与volatile读有相同的内存语义
下面对锁释放和锁获取的内存语义做个总结:
线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。 1 synchronized VS ReentrantLock 1.1 synchronized
synchronized [ˈsɪŋkrənaɪzd] 同步的;同步化的
synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。**利用synchronized实现同步的基础是Java中的每一个对象都可以作为锁。**具体表现为以下3种形式:
对于普通同步方法,锁是当前实例对象对于静态同步方法,锁是当前类的Class对象对于同步方法块,锁是synchonized括号里配置的对象
在下面例子中,使用了同步块和同步方法:
public class SynchronizedTest {
public static void main(String[] args) {
// 对Synchronized Class对象进行加锁
synchronized (SynchronizedTest.class) {
}
m();
}
// 静态同步方法,对Synchronized Class对象进行加锁
public static synchronized void m() {
}
}
synchronized机制是给共享资源上锁,只有拿到锁的线程才可以访问共享资源。也就是说,当一个线程试图访问同步代码块时,必须获取锁,退出或者抛出异常时必须释放锁。
synchronized的作用主要有三个:
确保线程互斥的访问同步代码(排他性);保证共享变量的修改能够及时可见(可见性);有效解决重排序问题;
synchronized也被称为重量级锁,需要调用操作系统相关接口,效率较低,有时候给线程加锁的时间甚至会大于操作消耗的时间。
JDK 1.6对synchronized进行了很多优化之后,如偏向锁,轻量锁,自旋锁,自适应锁等等,效率有了本质上的提高。在之后推出的JDK 1.7与JDK 1.8中,均对该关键字的实现了机理优化。引入了偏向锁和轻量级锁,都是在对象头中有标记位,不需要经过操作系统加锁。
Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在JDK 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。 这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
1.2 ReentrantLock在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的,而JDK 1.6之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。
虽然Lock缺少了(通过synchronized块或者方法所提供的)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。
以下是Lock和ReentrantLock的源码:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
void unlock();
Condition newCondition();
}
public class ReentrantLock implements Lock, java.io.Serializable {
...
}
synchronized依赖于JVM实现,执行完成后,会被JVM自动解锁;而Reentrantlock依赖于API,通过方法lock()与unlock()来进行加锁与解锁操作。
因为ReentrantLock加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用ReentrantLock必须在finally控制块中进行解锁操作。 如下所示:
Lock lock = new ReentrantLock();
lock.lock();
try {
} finally {
lock.unlock(); // 释放锁,目的是保证在获取到锁之后,最终能够被释放
}
在ReentrantLock中,调用lock()方法获取锁;调用unlock()方法释放锁。不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常, 异常抛出的同时,也会导致锁无故释放。
Lock是一个接口,它定义了锁获取和释放的基本操作,如下表所示:
ReentrantLock的实现依赖于Java同步器框架AbstractQueuedSynchronizer(AQS):
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
abstract void lock();
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
final ConditionObject newCondition() {
return new ConditionObject();
}
final boolean isLocked() {
return getState() != 0;
}
}
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
public void lock() {
sync.lock();
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public void unlock() {
sync.release(1);
}
public Condition newCondition() {
return sync.newCondition();
}
}
队列同步器AbstractQueuedSynchronizer(简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int类型的volatile变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。
synchronizer [ˈsɪŋkrənaɪzə(r)] 同步装置;同步闪光装置
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
implements java.io.Serializable {
private volatile int state;
protected final int getState() {
return state;
}
protected final void setState(int newState) {
state = newState;
}
protected final boolean compareAndSetState(int expect, int update) {
return U.compareAndSwapInt(this, STATE, expect, update);
}
}
同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。
可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者, 它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。 锁和同步器很好地隔离了使用者和实现者所需关注的领域。
1.3 synchronized和ReentrantLock对比synchronized是JVM隐式实现的,无需手动释放锁;ReentrantLock是Java语言提供的API,需要手动加锁和释放锁,如果忘记释放锁,则会造成资源被永久占用;ReentrantLock只能修饰代码块,而synchronized可以用于修饰方法、代码块等;ReentrantLock可以知道是否成功获得了锁,而synchronized却不行;ReentrantLock可设置为公平锁,而synchronized却不行; 2 锁的类型 2.1 公平锁 VS 非公平锁
ReentrantLoc引入两个概念:公平锁与非公平锁。
公平锁是指多个线程按照申请锁的顺序来获取锁;非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,可能造成优先级反转或者饥饿现象。
对于ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
对于synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
2.2 悲观锁 VS 乐观锁乐观锁:认为某个线程在处理共享资源的时候,其他的线程不会对此资源进行修改,只有在处理完毕写入内存的时候,检测资源是否在之前未被修改。类似于读写锁的读锁就是乐观锁。
Java中的乐观锁大部分都是通过CAS(Compare And Swap,比较并交换)操作实现的,CAS是一个多线程同步的原子指令,包含三个重要的信息,即内存位置、预期原值和新值。如果内存位置的值和预期的原值相等的话,那么就可以把该位置的值更新为新值,否则不做任何修改。
swap [swɑːp] 交换,交易
CAS可能会造成ABA的问题,ABA问题指的是,线程拿到了最初的预期原值A,然而在将要进行CAS的时候,被其他线程抢占了执行权,把此值从A变成了B,然后其他线程又把此值从B变成了A,然而此时的A值已经并非原来的A值了,但最初的线程并不知道这个情况,在它进行CAS的时候,只对比了预期原值为A就进行了修改,这就造成了ABA的问题。
ABA的常见处理方式是添加版本号或者时间戳,每次修改之后更新版本号,每次修改之前校验版本号是否被修改过,修改过则需要重新获取值再修改,这样就解决了ABA的问题。
JDK 1.5 时提供了AtomicStampedReference类也可以解决ABA的问题,此类维护了一个“版本号“——Stamp,每次在比较时不止比较当前值还比较版本号,这样就解决了ABA的问题。
悲观锁:认为某个线程在处理共享资源的时候, 一定会有线程来对此资源进行修改,所以拿到这个资源就直接加锁,不让其它线程来操作,加锁在逻辑处理之前。synchronized就是悲观锁。
从上面的描述可以得出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
悲观锁在Java中的使用,·就是利用各种锁。乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
2.3 可重入锁/递归锁可重入锁又名递归锁,是指同一个线程在某个方法获取锁后,此方法又调用了另一个加锁的方法,如果需要获取的锁对象是一样的,可再次获得已经获取并且未释放的锁。ReentrantLock和synchronized都是可重入锁。
可重入锁的实现原理,是在锁内部存储了一个线程标识,用于判断当前的锁属于哪个线程,并且锁的内部维护了一个计数器,当锁空闲时此计数器的值为0,当被线程占用和重入时分别加1,当锁被释放时计数器减1,直到减到0时表示此锁为空闲状态。
2.4 独享锁/共享锁独享锁是指该锁一次只能被一个线程所持有;共享锁是指该锁可被多个线程所持有。 独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
synchronized和·ReentrantLock是独享锁。但是对于Lock的另一个实现类ReadWriteLock(读写锁),其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
独占锁可以理解为悲观锁,当每次访问资源时都要加上互斥锁,而共享锁可以理解为乐观锁,它放宽了加锁的条件,允许多线程同时访问该资源。
2.5 自旋锁在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
2.6 偏向锁/轻量级锁/重量级锁这三种锁是指锁的状态,并且是针对synchronized。在JDK 1.6通过引入锁升级的机制来实现高效synchronized。 这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价;轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能;重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁升级为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低; 参考
https://blog.csdn.net/u014553029/article/details/113887744
https://www.cnblogs.com/shengluBlog/p/12874355.html



