synchronized背后的原理
- synchronized的底层是使用操作系统的mutex lock实现的
- 当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
- 运行的线程数量越多,性能下降越快(归还对象锁的时候,就有越多的线程不停的在被唤醒、阻状态切换)
- 同步代码执行时间越短,性能下降也较快
Synchronized作用范围
- 作用于方法时,锁住的是对象的实例(this);
- 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen (jdk1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁, 会锁所有调用该方法的线程;
- 代码块使用 synchronized 修饰的写法,使用代码块,如果传入的参数是 this,那么锁定的也是当前的对象:
对象锁(monitor)机制
-
每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加
上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的
-
关于 monitorenter 和 monitorexit 的作用,我们可以抽象地理解为每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
-
当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
- 如果目标锁对象的计数器为 0,那么说明它没有被其他线程所持有。在这个 情况下,Java 虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加 1。
- 在目标锁对象的计数器不为 0 的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将 其计数器加 1,否则需要等待,直至持有线程释放该锁。
-
当执行 monitorexit 时:Java 虚拟机则需将锁对象的计数器减 1。当计数器减为 0 时,那便代表该锁已经被释放掉了。
乐观锁、悲观锁、自旋锁、非公平锁、公平锁
乐观锁:乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是 否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。 设计上总是乐观的认为数据修改大部分场景都是没有线程并发修改,少量情况下才存在。线程安全上采取版本号来控制------用户自己判断版本号,并处理。
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样 别人想拿这个数据就会阻塞直到它拿到锁。 悲观的认为总是有其他线程并发修改,每次都是加锁操作
自旋锁:如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁 的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋), 等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
非公平锁:**JVM 按随机、就近原则分配锁的机制则称为不公平锁,**ReentrantLock 在构造函数中提供了 是否公平锁的初始化方式,默认为非公平锁。非公平锁实际执行的效率要远远超出公平锁,除非 程序有特殊需要,否则最常用非公平锁的分配机制。
-
非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护一个队列
-
Java 中的 synchronized 是非公平锁,ReentrantLock 默认的 lock()方法采用的是非公平锁
公平锁:指的是锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁,
如何实现实现原子操作
CAS
CAS: 全称Compare and swap,字面意思:”比较并交换“,CAS操作(又称为无锁操作)是一种乐观锁策略, 它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的 操作。
操作过程:
CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:V 内存地址存放的实际值;O
预期的值(旧值);N 更新的新值。
- 当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没 有被其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。
- 当V 和O不相同时,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给 V,返回V即可。
- 当多个线程使用CAS操作一个变量时,只有一个线程会成功,并成功更新,其余会失 败。失败的线程会重新尝试,当然也可以选择挂起线程。
应用场景:
CAS只适用于线程冲突较少的情况下使用
采取CAS的API:
- java.util.concurrent.atomic,原子性的并发包下的api
- synchronized中,多个线程不同时间点执行代码块时,jdk优化会采取CAS
- 其他的,如jdk1.8版本中ConcurrentHashMap实现,put操作室,节点为空,采取CAS
继续CAS实现的类:
public class AtomicInteger {
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
}
public class Unsafe {
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
}
CAS的问题
1.ABA问题 :因为CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。比如一个旧值A变为了B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了 变化。
解决:添加一个版本号可以解决。在JDK1.5后的 atomic包中提供了AtomicStampedReference来解决ABA问题
2.自旋会浪费大量的处理器资源
解决:JVM给出的方案是自适应自旋,根据以往自旋等待时能否获取锁,来动态调整自旋的时间(循环
数)。
synchronized优化
JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 (状态级别从低到高)
- 锁可以升级但不能降级
- 目的是为了提高获得锁和释放锁的效率
无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改 失败的线程会不断重试直到修改成功。
偏向锁对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,降低获取锁 带来的性能开销。 针对同一个线程再次申请已持有的对象锁。实现原理: CAS
轻量级锁:轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B 会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。
- 当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量 级锁;
- 当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁。
- 大概率在同一个时间点,只有线程申请对象锁。实现原理: CAS
重量级锁指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。 重量级锁通过对象内部的监视器(monitor)实现,而**其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,**操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。
- 大概率是在同一个时间点,多个线程竞争同-个对象锁。
- 缺点:涉及到操作系统的调度、 用户态到内核态切换,开销非常大,线程会阻塞、唤醒
比较:
其他优化方案:
锁粗化:一个线程对同一个对象锁反复获取释放的操作,中间没有其他受影响代码时,可以合并为一个锁
public class Test{
private static StringBuffer sb = new StringBuffer();
public static void main(String[] args) {
sb.append("a");
sb.append("b");
sb.append("c");
}
}
锁消除:临界区代码中,没有对象逃逸出当前线程,说明本身是线程安全的操作,可以直接删除锁(不使用锁)
public class Test{
public static void main(String[] args) {
StringBuffer sb = new StringBuffer();
sb.append("a").append("b").append("c");
}
}
死锁
概念:
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
死锁产生的四个必要条件:
1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了 一个等待环路
总结:至少有两个线程,互相持有对方申请的对象锁,造成互相等待。导致没法继续执行
解决:
① 资源一次性分配(破坏请求与保持条件)
② 可剥夺资源:在线程满足条件时,释放掉已占有的资源
③ 资源有 序分配:系统为每类资源赋予一个编号,每个线程按照编号递 请求资源,释放则相反
检测死锁的手段:
使用jdk的监控工具,比如jconsole、jstack查看线程状态
Lock锁
- 失去了像synchronize关键字隐式加锁解锁的便捷性,但是却拥有了锁获取和释放的可操作性,可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。
- synchronized同步块执行完成或者遇到异常是锁会自动释放,而lock必须调用unlock()方法释放锁,因此在finally块中释放锁
- 实现原理:AQS(队列式的同步器)
public static Lock LOCK = new ReentrantLock();
public static void t3(){
try {
LOCK.lock();//=synchronized()加锁的代码
//执行业务
while(库存达到上限){
CONDITION.await();//=synchronized锁对象.wait()
}
System.out.println("t3");
} finally {
LOCK.unlock();
}
}
synchronized与Lock的区别
- synchronized是关键字,而Lock是一个接口。
- synchronized会自动释放锁,而Lock必须手动释放锁。
- synchronized是不可中断的,Lock可以中断也可以不中断。
- 通过Lock可以知道线程有没有拿到锁,而synchronized不能。
- synchronized能锁住方法和代码块,而Lock只能锁住代码块。
- Lock可以使用读锁提高多线程读效率。
- synchronlzed是非公平锁,ReentrantLock可以控制是否是公平锁。
API
Semaphore:
Semaphore 是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信 号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。Semaphore 可以用来 构建一些对象池,资源池之类的,比如数据库连接池
public static void main4(String[] args) throws InterruptedException {
Semaphore s=new Semaphore(0);
for(int i=0;i<20;i++){
Thread t=new Thread(()->{
System.out.println(Thread.currentThread().getName());
s.release();//资源量+1
});
t.start();
}
s.acquire(20);//获取到20个资源后,才会继续往下执行
System.out.println("执行完毕"+Thread.currentThread().getName());
}
public static void main(String[] args) throws InterruptedException {
//模拟服务端接收客户端黄体酮请求:只有1000个并发
// (在一个时间点,客户端任务数达到1000,再有客户端请求,将阻塞等待)
Semaphore s=new Semaphore(1000);
for(;;){
Thread t=new Thread(()->{
try {
s.acquire();
//模拟每个线程处理客户端http请求
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
s.release();
}
});
t.start();
}
}
CountDownLatch
好像跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩。
public static void main3(String[] args) throws InterruptedException {
CountDownLatch cd1=new CountDownLatch(20);//计数器的初始值
for(int i=0;i<20;i++){
Thread t=new Thread(()->{
System.out.println(Thread.currentThread().getName());
cd1.countDown();//计数器的值-1
});
t.start();
}
cd1.await();//当前线程阻塞等待,直到计数器的值为0
System.out.println("执行完毕"+Thread.currentThread().getName());
}
Condition
作用线程间通信:
public static Lock LOCK = new ReentrantLock();
public static Condition CONDITION = LOCK.newCondition();
public static void t3(){
try {
LOCK.lock();//=synchronized()加锁的代码
//执行业务
while(库存达到上限){
CONDITION.await();//=synchronized锁对象.wait()
}
System.out.println("t3");
CONDITION.signal();//=synchronized锁对象.notify()
CONDITION.signalAll();//=synchronized锁对象.notifyAll()
} finally {
LOCK.unlock();
}
}
AQS
-
AQS则实现了对同步状态的管理,以及对阻塞线程进行排队,等待通知等等一些底层的实现处理
-
AQS的核心也包括了这些方面:同步队列,独占式锁的获取和释放,共享锁的获取和释放以及可中断
锁,超时等待锁获取这些特性的实现,
同步队列:
AQS中的同步队列则是通过链式方式进行实现 一个双向队列
AQS实际上通过头尾指针来管理同步队列,同时实现包括获取锁失败的线程进行入队,释放锁 时对同步队列中的线程进行通知等核心方法。其示意图如下:
读写锁
读写锁允许同一时刻被多个读线程访问,但是在写线程 访问时,所有的读线程和其他的写线程都会被阻塞
写锁:
- 在同一时刻写锁是不能被多个线程所获取,很显然写锁 是独占式锁,而实现写锁的同步语义是通过重写AQS中的tryAcquire方法实现的
- 写锁释放通过重写AQS的tryRelease方法
读锁:
-
读锁不是独占式锁,即同一时刻该锁可以被多个读线程获取也就是一种共享式锁,通过重写AQS的
tryAcquireShared方法和tryReleaseShared方法
-
读锁释放的实现主要通过方法tryReleaseShared
ThreadLocal
- ThreadLocal 用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程里的变 量。也就是说 ThreadLocal 可以为每个线程创建一个【单独的变量副本】,相当于线程的 private static 类型变量。
- ThreadLocal 的作用和同步机制有些相反:同步机制是为了保证多线程环境下数据的一致性;而 ThreadLocal 是保证了多线程环境下数据的独立性。
private static String commStr;
private static ThreadLocal<String> threadStr = new ThreadLocal<String>();
public static void main(String[] args) {
commStr = "main";
threadStr.set("main");
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
commStr = "thread";
threadStr.set("thread");
}
});
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(commStr);
System.out.println(threadStr.get());
}
从运行结果可以看出,对于 ThreadLocal 类型的变量,在一个线程中设置值,不影响其在其它线程中的值。也就是说 ThreadLocal 类型的变量的值在每个线程中是独立的。