synchronized
重量级锁,监视器锁(monitor),依赖于底层的操作系统的 Mutex Lock,需要操作系统帮忙完成,需要从用户态转换到内核态
sychronized 关键字修饰的方法
锁住整个对象
加到 static 非静态方法和 synchronized(object)代码块
sychronized 关键字修饰的代码块
更好,不会锁住整个对象,同步的范围越小越好
修饰静态方法
占用的锁是当前类的锁
加到 static 静态方法和 synchronized(class)代码块
双重检验锁方式实现单例
uniqueInstance 采用 volatile 关键字修饰也是很有必要的
为 uniqueInstance 分配内存空间
初始化 uniqueInstance
将 uniqueInstance 指向分配的内存地址
由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
把instance声明为volatile之后,对它的写操作就会有一个内存屏障。在它的赋值完成之前,就不用会调用读操作
保证了在一个写操作([1-2-3])完成之前,不会调用读操作(if (instance == null))
底层原理
monitorenter和monitorexit
为什么会有两个monitorexit
防止在同步代码块中线程因异常退出,而锁没有得到释放,这必然会造成死锁(等待的线程永远获取不到锁)
synchronized可重入
一个线程获取到该锁之后,该线程可以继续获得该锁
维护一个计数器,当线程获取该锁时,计数器加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁。
自旋
synchronized 里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized 的边界做忙循环
做了多次循环发现还没有获得锁,再阻塞
锁升级
对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id
再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁
执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁
锁可以升级但不能降级
无状态锁,偏向锁,轻量级锁和重量级锁
synchronized、volatile、CAS
synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。
volatile 共享变量可见性和禁止指令重排序
CAS 是基于冲突检测的乐观锁(非阻塞)
volatile
变量修饰符
保证可见性和禁止指令重排
和 CAS 结合,保证了原子性
java.util.concurrent.atomic 包下的类,比如 AtomicInteger
volatile关键字为域变量的访问提供了一种免锁机制
将当前处理器缓存行的数据写回系统内存;
这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效
数组
只是一个指向数组的引用,而不是整个数组
改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了
乐观锁和悲观锁
悲观锁
synchronized 、数据库:行锁,表锁,读锁,写锁
乐观锁
版本号
多读的应用类型,提高吞吐量
java.util.concurrent.atomic 包 CAS
CAS
Compare and Swap
CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置 V 的值与预期原值 A 相匹配,那么处理器会自动将该位置值更新为新值 B。否则处理器不做任何操作。
问题
ABA 问题
原有位置A被改成B,又被改回A,误以为没有改变
AtomicStampedReference
循环时间长开销大
资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大
只能保证一个共享变量的原子操作
对多个共享变量操作时,循环 CAS 就无法保证操作的原子性
死锁
必要条件
1、互斥条件:所谓互斥就是进程在某一时间内独占资源。
2、请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3、不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
4、循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
防止死锁
尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
尽量使用 Java. util. concurrent 并发类代替自己手写锁。
尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
尽量减少同步的代码块。
活锁
没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。
饥饿
无法获得所需要的资源,导致一直无法执行的状态
AQS AbstractQueuedSynchronizer
CLH队列锁
将暂时获取不到锁的线程加入到队列中
虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)
将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配
使用一个int成员变量来表示同步状态,使用CAS对该同步状态进行原子操作实现对其值的修改
AQS定义两种资源共享方式
Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
可重入锁(ReentrantLock)
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。
CountDownLatch
任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
ReadWriteLock
读写锁是用来提升并发程序性能的锁分离技术,ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。
(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
(2)重进入:读锁和写锁都支持线程重进入。
(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。
ConcurrentHashMap
jdk6
Segment 分段锁
segment维护了哈希散列表的若干个桶,每个桶由HashEntry构成的链表
segment继承了ReentrantLock充当锁的角色,为每一个segment提供了线程安全的保障
原子操作类
CAS 操作——Compare & Set
自旋锁
CAS (compare and swap) + volatile 和 native
ABA 问题
AtomicMarkableReference(通过引入一个 boolean来反映中间有没有变过)
AtomicStampedReference(通过引入一个 int 来累加来反映中间有没有变过)