文章目录
Synchronized
Synchronized
实现同步的基础:Java中的每一个对象都可以作为锁。
在 Java 中,每个对象都会有一个 monitor 对象。
通过monitor enter和monitor exit两个指令实现原子性。
// 普通同步方法,锁是当前实例对象。
public synchronized void add(){
}
// 静态同步方法,锁是当前类的Class对象。
public static synchronized void add(){
}
// 同步方法块
public int add(){
// 锁是obj对象。
synchronized(obj){
}
// 锁是当前实例对象。
synchronized(this){
}
}
使用Synchronized有哪些要注意点
- 锁对象不能为空,因为锁的信息都保存在对象头里
- 作用域不宜过大,影响程序执行的速度,控制范围过大,编写代码也容易出错
- 避免死锁
- 在能选择的情况下,既不要用Lock也不要用synchronized关键字,用
java.util.concurrent
包中的各种各样的类,如果不用该包下的类,在满足业务的情况下,可以使用synchronized关键,因为代码量少,避免出错
Synchronized关键字的底层原理是什么
synchronized是用来做线程同步的,可以对类和对象进行加锁,是跟jvm指令和monitor有关系,如果用synchronized关键字修饰一段代码块后,编译后,会有monitorenter和monitorexit两个指令,执行被修饰的代码块的时候是会先执行monitorenter指令,退出方法后会执行monitorexit指令。
执行过程就是每个对象都会关联一个monitor,它里面有一个计数器,初始是0,如果有线程要获取monitor的锁,会先看计数器是不是0,如果是0说明没有线程在获取锁,那这个线程就可以获取lock锁,然后对计数器加1。如果不为0,判断线程是不是自己,是的话就重入锁,计数器+1,如果不是的话线程会陷入block阻塞状态。线程执行完代码退出后,底层会执行monitorexit指令,这个时候对应的monitor计数器会减1,如果是多次重入锁,那么就会多次减1,直到为0,然后block状态的线程可以再次获取锁。
ReentrantLock
Java中独占锁的实现除了使用关键字synchronized外,还可以使用ReentrantLock。虽然在性能上ReentrantLock和synchronized没有什么区别,但ReentrantLock相比synchronized而言功能更加丰富,支持公平锁实现,支持中断响应、限时等待等等,可以配合一个或多个Condition条件方便的实现等待通知机制,使用起来更为灵活,也更适合复杂的并发场景。
ReentrantLock结合Condition实现等待通知机制
使用synchronized结合Object上的wait和notify方法可以实现线程间的等待通知机制。
ReentrantLock结合Condition接口同样可以实现这个功能,而且相比前者使用起来更清晰也更简单。
Condition使用简介
Condition由ReentrantLock对象创建,并且可以同时创建多个。
static Condition notEmpty = lock.newCondition();
static Condition notFull = lock.newCondition();
Condition接口在使用前必须先调用ReentrantLock的lock()方法获得锁。之后调用Condition接口的await()方法将释放锁,并且在该Condition上等待,直到有其他线程调用Condition的signal()方法唤醒线程。使用方式和wait/notify类似。
一个使用condition的简单例子
public class ReentrantLocSimpleConditionDemo {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "\t await");
// 释放锁并等待其他线程的通知
condition.await();
System.out.println(Thread.currentThread().getName() + "\t execute");
} catch (Exception e) {
} finally {
lock.unlock();
}
}, "AA").start();
new Thread(() -> {
try {
// 保证线程 AA 先执行
TimeUnit.SECONDS.sleep((long) new Random().nextInt(5) + 1);
lock.lock();
System.out.println(Thread.currentThread().getName() + "\t signal");
condition.signal();
} catch (Exception e) {
} finally {
lock.unlock();
}
}, "BB").start();
}
}
打印:
AA await
BB signal
AA execute
使用Condition实现简单的阻塞队列
// ReentrantLock 使用 Condition 案例,简单模拟了 Queue
class MyQueue {
// 容器初始值
private Integer data = 1;
// 容器最大值
private Integer max = 3;
private Lock lock = new ReentrantLock();
// 容器满时 等待
private Condition fullCondition = lock.newCondition();
// 容器空时 等待
private Condition emptyCondition = lock.newCondition();
public void put() {
lock.lock();
try {
while (data == max) {
// 容器已满,等待
System.out.println(Thread.currentThread().getName() + "\t put await");
fullCondition.await();
}
data++;
System.out.println(Thread.currentThread().getName() + "\t put:" + data);
// 容器不是空的,唤醒一个取数据线程
emptyCondition.signal();
} catch (Exception e) {
} finally {
lock.unlock();
}
}
public void take() {
lock.lock();
try {
while (data == 0) {
// 容器空的,等待
System.out.println(Thread.currentThread().getName() + "\t take await");
emptyCondition.await();
}
data--;
System.out.println(Thread.currentThread().getName() + "\t take:" + data);
// 容器不是满的,唤醒一个入数据线程
fullCondition.signal();
} catch (Exception e) {
} finally {
lock.unlock();
}
}
}
public class ReentrantLockConditionDemo {
public static void main(String[] args) {
MyQueue queue = new MyQueue();
for (int i = 1; i <= 15; i++) {
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep((long) new Random().nextInt(5) + 1);
} catch (InterruptedException e) {
}
queue.put();
}, "AA").start();
}
for (int i = 1; i <= 15; i++) {
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep((long) new Random().nextInt(5) + 1);
} catch (InterruptedException e) {
}
queue.take();
}, "BB").start();
}
}
}
使用 Condition 精确唤醒案例
/**
* 多线程之间按顺序调用,实现 A - B - C 三个线程启动。
* AA 打印5次,BB打印10次,CC打印15次
* 接着。。。
* AA 打印5次,BB打印10次,CC打印15次
* 来10轮
*/
class ShareResource {
private int number = 1;
private Lock lock = new ReentrantLock();
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
public void print5() {
lock.lock();
try {
// 判断
while (number != 1) {
c1.await();
}
// 干活
number = 2;
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
// 唤醒
c2.signal();
} catch (Exception e) {
} finally {
lock.unlock();
}
}
public void print10() {
lock.lock();
try {
// 判断
while (number != 2) {
c2.await();
}
// 干活
number = 3;
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
// 唤醒
c3.signal();
} catch (Exception e) {
} finally {
lock.unlock();
}
}
public void print15() {
lock.lock();
try {
// 判断
while (number != 3) {
c3.await();
}
// 干活
number = 1;
for (int i = 1; i <= 15; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
// 唤醒
c1.signal();
} catch (Exception e) {
} finally {
lock.unlock();
}
}
}
public class ReentrantLockConditionDemo {
public static void main(String[] args) {
ShareResource shareResource = new ShareResource();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
shareResource.print5();
}
}, "AA").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
shareResource.print10();
}
}, "BB").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
shareResource.print15();
}
}, "CC").start();
}
}
深入理解ArrayBlockingQueue:
ArrayBlockingQueue 是一个先进先出的数据有界队列。
java.util.concurrent.ArrayBlockingQueue<E>
:定义在Java并发包中的,所以是支持多线程安全的,内部使用ReentrantLock实现线程安全,使用Condition线程间通信。
源码解析:
final ReentrantLock lock = new ReentrantLock(true); // 所有的入队出队都是用同一锁,使用公平锁
private final Condition notEmpty = lock.newCondition();//由lock创键的Condition notEmpty非空即当取出元素时使用
private final Condition notFull = lock.newCondition(); // 同样由lock创建,添加时使用
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 此时采用优先响应中断的加锁方式
try {
while (count == items.length)
notFull.await(); // 如果队列已满,添加线程等待
insert(e); // 否则调用insert方法
} finally {
lock.unlock();
}
}
private void insert(E x) {
items[putIndex] = x;
putIndex = inc(putIndex); // 添加后 坐标加1
++count;
notEmpty.signal(); // 取出线程被唤醒
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await(); // 当队列为空时,阻塞等待
return extract();
} finally {
lock.unlock();
}
}
private E extract() {
final Object[] items = this.items;
E x = this.<E>cast(items[takeIndex]);
items[takeIndex] = null; //取出时,将该位置置为null
takeIndex = inc(takeIndex); //取出坐标加一
--count;
notFull.signal(); //唤醒添加线程
return x;
}
ReentrantLock和synchronized区别
-
锁的实现:
synchronized
是关键字依赖JVM
实现的,底层是通过monitor
对象来完成。(monitorenter、monitorexit
)ReenTrantLock
是具体类java.util.concurrent.locks.Lock
是JDK
实现的,类似于操作系统控制实现和用户自己写代码实现。 -
可重入性:
ReenTrantLock
和synchronized
使用的锁都是可重入的,两者都是同一个线程每进入一次,锁的计数器都自增1,所以等到锁的计数器下降为0时才能释放锁。 -
性能的区别:
synchronized
在JDK5
优化后,两者性能差不多了。如果两种方法都可以使用的情况下,官方建议使用synchronized
,因为使用便利。 -
使用方法:
synchronized
由编译器加锁和释放,默认是非公平锁,ReenTrantLock
手动加锁和释放锁。 -
ReenTrantLock独有的能力:
ReenTrantLock
可以指定是公平锁还是非公平锁,synchronized
只能是非公平锁,所谓的公平锁就是先等待的线程先获取锁。
ReenTrantLock
提供了中断锁和等待锁的功能,通过lock.lockInterruptibly()
实现中断锁,通过lock.tryLock()
实现等待锁。
ReenTrantLock
提供了一个Condition
类,实现了线程之间的精确唤醒;而不是像synchronized
只能通过 wait/notify 唤醒,要么随机唤醒一个线程,要么唤醒全部线程。 -
什么情况下使用
ReenTrantLock
:
如果应用中需要有排队功能,比如客服分配,必须先到先得服务,不能出现饿死现象,可以使用ReenTrantLock
的公平锁,new ReenTrantLock(true)
表示公平锁。
如果需要精确唤醒某个线程执行任务,比如:ArrayBlockingQueue
阻塞队列,底层是通过ReenTrantLock+Condition
进行通信的。
JVM中锁的优化
在JVM中monitorenter
和monitorexit
字节码依赖于底层的操作系统的Mutex Lock
(Mutex 采取独占方式控制对资源的并发访问)来实现的,但是由于使用Mutex Lock
需要将当前线程挂起并从用户态
切换到内核态
来执行,这种切换的代价是非常昂贵的;
在JDK1.6中对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁
等技术来减少锁操作的开销。已经和ReenTrantLock
性能相差无几了。
线程并发量不大的情况下,Synchronized因为自旋锁、偏向锁、轻量级锁的原因,不用将等待线程挂起,偏向锁甚至不用自旋。
只要线程可以在30到50次自旋里拿到锁,那么Synchronized就不会升级为重量级锁,而等待的线程也就不用被挂起,我们也就少了挂起和唤醒这个上下文切换的过程开销。
在 JDK1.6 之前synchronized被称为重量级锁,这个重指的是它会阻塞和唤醒线程。而1.6之后,对synchonized进行了优化。加入了偏向锁以及轻量级锁。在使用synchronized的过程中,随着竞争的加剧,锁会经历由:无锁->偏向锁->轻量级锁->重量级锁
的升级过程。
为什么synchronized要设计一个升级过程呢?
答:阻塞和唤醒线程带来的上下文切换的开销,往往比线程执行的开销要大得多。为了避免这种开销,尽量减少线程阻塞和唤醒的次数。
无锁、偏向锁、轻量级锁、重量级锁
无锁(自旋锁)
自旋锁其实就是无锁。
无锁是指线程通过循环来执行更新操作,如果执行成功就退出循环,如果执行失败(有其他线程更新了值),则继续执行,直到成功为止。
CAS操作就属于无锁。如果从性能的角度来看,无锁状态的性能是非常高的。
自旋锁的前提假设是锁被其它线程占用的时间很短。如果其它线程占用锁的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而带来性能上的浪费。
自旋次数的默认值是10次,用户可以通过使用参数-XX:PreBlockSpin来更改。
偏向锁
加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。
轻量级锁
竞争的线程不会阻塞,提高了程序的响应速度。
重量级锁
线程阻塞,响应时间缓慢。