同步锁java
- 锁是用来控制多个线程访问共享资源的方式
- 通常来讲,一个锁可以防止多个线程同时访问共享资源,
- 在Lock接口出现以前,Java应用程序只能依靠synchronized关键字来实现同步锁的功能,
- 在java5之后,增长了JUC(java.util.concurrent)的并发包且提供了Lock接口用来实现锁的功能,
- 它提供了与synchroinzed关键字相似的同步功能,
- 只是它比synchronized更灵活,可以显式的获取和释放锁。
Lock的初步使用面试
- Lock是一个接口,核心的两个方法lock和unlock,
- 它有不少的实现,好比ReentrantLock、ReentrantReadWriteLock;
ReentrantLock缓存
- 重入锁,表示支持从新进入的锁,
- 也就是说,若是当前线程t1经过调用lock方法获取了锁以后,
- 再次调用lock,是不会再阻塞去获取锁的,
- 直接增长重试次数就好了。
public class AtomicDemo {
private static int count = 0;
static Lock lock = new ReentrantLock();
public static void inc() {
lock.lock();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
lock.unlock();
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
AtomicDemo.inc();
}).start();
}
Thread.sleep(3000);
System.out.println("result:" + count);
}
}
ReentrantReadWriteLock安全
- 咱们之前理解的锁,基本都是排他锁,
- 也就是这些锁在同一时刻只容许一个线程进行访问,
- 而读写锁在同一时刻能够容许多个线程访问,
- 可是在写线程访问时,全部的读线程和其余写线程都会被阻塞。
- 读写锁维护了一对锁,一个读锁、一个写锁;
- 通常状况下,读写锁的性能都会比排它锁好,
- 在读多于写的状况下,读写锁可以提供比排它锁更好的并发性和吞吐量.
public class LockDemo {
static Map<String, Object> cacheMap = new HashMap<>();
static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
static Lock read = rwl.readLock();
static Lock write = rwl.writeLock();
public static final Object get(String key) {
System.out.println("开始读取数据");
read.lock(); //读锁
try {
return cacheMap.get(key);
} finally {
read.unlock();
}
}
public static final Object put(String key, Object value) {
write.lock();
System.out.println("开始写数据");
try {
return cacheMap.put(key, value);
} finally {
write.unlock();
}
}
}
- 在这个案例中,经过hashmap来模拟了一个内存缓存,
- 当执行读操做的时候,须要获取读锁,在并发访问的时候,读锁不会被阻塞,
- 在执行写操做是,线程必需要获取写锁,当已经有线程持有写锁的状况下,
- 当前线程会被阻塞,只有当写锁释放之后,其余读写操做才能继续执行。
- 使用读写锁提高读操做的并发性,也保证每次写操做对全部的读写操做的可见性
- l 读锁与读锁能够共享
- l 读锁与写锁不能够共享(排他)
- l 写锁与写锁不能够共享(排他)
Lock和synchronized的简单对比数据结构
- 经过咱们对Lock的使用以及对synchronized的了解,基本上能够对比出这两种锁的区别了。
- 由于这个也是在面试过程当中比较常见的问题::
- Ø 从层次上,一个是关键字、一个是类, 这是最直观的差别
- Ø 从使用上,lock具有更大的灵活性,能够控制锁的释放和获取;
- 而synchronized的锁的释放是被动的,当出现异常或者同步代码块执行完之后,才会释放锁
- Ø lock能够判断锁的状态、而synchronized没法作到
- Ø lock能够实现公平锁、非公平锁;
AQS(AbstractQueuedSynchronizer)多线程
- Lock之因此能实现线程安全的锁,
- 主要的核心是AQS(AbstractQueuedSynchronizer),
- AbstractQueuedSynchronizer提供了一个FIFO队列,
- 能够看作是一个用来实现锁以及其余须要同步功能的框架。
- AQS的使用依靠继承来完成,
- 子类经过继承自AQS并实现所需的方法来管理同步状态。
- 例如常见的ReentrantLock,CountDownLatch等AQS的两种功能
- 从使用上来讲,AQS的功能能够分为两种:独占和共享。
- 独占锁模式下,每次只能有一个线程持有锁,
- 好比前面给你们演示的ReentrantLock就是以独占方式实现的互斥锁
- 共享锁模式下,容许多个线程同时获取锁,并发访问共享资源,
- 好比ReentrantReadWriteLock。
- 很显然,独占锁是一种悲观保守的加锁策略,它限制了读/读冲突,
- 若是某个只读线程获取锁,则其余读线程都只能等待,
- 这种状况下就限制了没必要要的并发性,
- 由于读操做并不会影响数据的一致性。
- 共享锁则是一种乐观锁,它放宽了加锁策略,
AQS的内部实现并发
- 同步器(AQS)依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,
- 当前线程获取同步状态失败时,
- 同步器会将当前线程以及 等待状态 等信息
- 构形成为一个节点(Node)并将其加入同步队列,
- 同时会阻塞当前线程,
- 当同步状态释放时,会把首节点中的线程唤醒,
Node的主要属性以下app
static final class Node {
int waitStatus; //表示节点的状态,包含cancelled(取消);condition 表示节点在等待condition 也就是在condition队列中
Node prev; //前继节点
Node next; //后继节点
Node nextWaiter; //存储在condition队列中的后继节点
Thread thread; //当前线程
}
- AQS类底层的数据结构是使用双向链表,是队列的一种实现。
- 包括一个head节点和一个tail节点,
- 分别表示头结点和尾节点,
- 其中头结点不存储Thread,仅保存next结点的引用。

- 当一个线程成功地获取了同步状态(或者锁),
- 其余线程将没法获取到同步状态,
- 转而被构形成为节点并加入到同步队列中,
- 而这个加入队列的过程必需要保证线程安全,
- 所以同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Nodeupdate),
- 它须要传递参数当前线程“认为”的尾节点和当前节点,只有设置成功后,
- 当前节点才正式与以前的尾节点创建关联。

- 同步队列遵循FIFO,
- 首节点的线程在释放同步状态时,
- 将会唤醒后继节点,
- 然后继节点将会在获取同步状态成功时

- 设置首节点是经过获取同步状态成功的线程来完成的,
- 因为只有一个线程可以成功获取到同步状态,
- 所以设置头节点的方法并不须要使用CAS来保证,
- 它只须要将首节点设置成为原首节点的后继节点
- 并断开原首节点的next引用便可
compareAndSet框架
- AQS中,除了自己的链表结构之外,
- 还有一个很关键的功能,就是CAS,
- 这个是保证在多线程并发的状况下、保证线程安全的前提下
- 去把线程加入到AQS中的方法,能够简单理解为乐观锁
private final boolean compareAndSetHead(Node update) {
return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
- 首先,用到了unsafe类,
- Unsafe类是在sun.misc包下,不属于Java标准。
- 可是不少Java的基础类库,包括一些被普遍使用的高性能开发库都是基于Unsafe类开发的,
- 好比Netty、Hadoop、Kafka等;
- Unsafe可认为是Java中留下的后门,提供了一些低层次操做,
- 而后调用了compareAndSwapObject这个方法
这里传入了一个headOffset,这个headOffset是什么呢?oop
- 在下面的代码中,经过unsafe.objectFieldOffset


unsafe.objectFieldOffset
- headOffset这个是指类中相应字段在该类的偏移量,
- 在这里具体便是指head这个字段
- 在AQS类的内存中相对于该类首地址的偏移量。
- 一个Java对象能够当作是一段内存,
- 每一个字段都得按照必定的顺序放在这段内存里,
- 经过这个方法能够准确地告诉你
- 用于在后面的compareAndSwapObject中,
这个方法在unsafe.cpp文件中,代码以下::
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapObject(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jobject e_h, jobject x_h))
UnsafeWrapper("Unsafe_CompareAndSwapObject");
oop x = JNIHandles::resolve(x_h); // 新值
oop e = JNIHandles::resolve(e_h); // 预期值
oop p = JNIHandles::resolve(obj);
HeapWord* addr = (HeapWord *)index_oop_from_field_offset_long(p, offset);// 在内存中的具体位置
oop res = oopDesc::atomic_compare_exchange_oop(x, addr, e, true);// 调用了另外一个方法,实际上就是经过cas操做来替换内存中的值是否成功
jboolean success = (res == e); // 若是返回的res等于e,则断定知足compare条件(说明res应该为内存中的当前值),但实际上会有ABA的问题
if (success) // success为true时,说明此时已经交换成功(调用的是最底层的cmpxchg指令)
update_barrier_set((void*)addr, x); // 每次Reference类型数据写操做时,都会产生一个Write Barrier暂时中断操做,配合垃圾收集器
return success;
UNSAFE_END
- 因此其实compareAndSet这个方法,
- 最终调用的是unsafe类的compareAndSwap,
- 这个指令会对内存中的共享数据作原子的读写操做。
- 1. 首先, cpu会把内存中将要被更改的数据与指望值作比较
- 2. 而后,当两个值相等时,cpu才会将内存中的对象替换为新的值。
- 3. 最后,返回操做执行结果
- 很显然,这是一种乐观锁的实现思路。