文章最前: 我是Octopus,这个名字来源于我的中文名--章鱼;我热爱编程、热爱算法、热爱开源。所有源码在我的个人github ;这博客是记录我学习的点点滴滴,如果您对 Python、Java、AI、算法有兴趣,可以关注我的动态,一起学习,共同进步。
相关文章:
- 多线程的应用与原理分析1
- 多线程的应用与原理分析2(线程的状态)
- 多线程的应用与原理分析3(原子性、可见性、有序性)
- 多线程的应用与原理分析4(synchronized)
- 多线程的应用与原理分析5(ReentrantLock)
- 多线程的应用与原理分析6(ReentrantLock)
- 多线程的应用与原理分析7(Condition)
- 多线程的应用与原理分析8(countdownlatch)
- 多线程的应用与原理分析9(原子操作)
- 多线程的应用与原理分析10(Semaphore)
- 多线程的应用与原理分析11(线程池)
synchronized的使用
在多线程并发编程中synchronized一直是元老级角色,很多人都会称呼它为重量级锁。但是,随着Java SE 1.6对synchronized进行了各种优化之后,有些情况下它就并不那么重了,Java SE 1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程。我们仍然沿用前面使用的案例,然后通过synchronized关键字来修饰在inc的方法上,再看看执行结果。
package lock;
/**
* @author zhangyu
* @version V1.0
* @ClassName: SynchronizedDemo
* @Description: 测试synchronized保证原子性
* @date 2019/1/23 11:51
**/
public class SynchronizedDemo {
private static int count = 0;
public static void inc() {
synchronized (SynchronizedDemo.class) {
try {
Thread.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
count++;
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(() -> SynchronizedDemo.inc()).start();
}
Thread.sleep(3000);
System.out.println(count);
}
}
synchronized的三种应用方式
synchronized有三种方式来加锁,分别是:
1. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
2. 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
synchronized括号后面的对象
synchronized扩后后面的对象是一把锁,在java中任意一个对象都可以成为锁,简单来说,我们把object比喻是一个key,拥有这个key的线程才能执行这个方法,拿到这个key以后在执行方法过程中,这个key是随身携带的,并且只有一把。如果后续的线程想访问当前方法,因为没有key所以不能访问只能在门口等着,等之前的线程把key放回去。所以,synchronized锁定的对象必须是同一个,如果是不同对象,就意味着是不同的房间的钥匙,对于访问者来说是没有任何影响的。
synchronized的字节码指令
通过javap -v 来查看对应代码的字节码指令,对于同步块的实现使用了monitorenter和monitorexit指令,前面我们在讲JMM的时候,提到过这两个指令,他们隐式的执行了Lock和UnLock操作,用于提供原子性保证。monitorenter指令插入到同步代码块开始的位置、monitorexit指令插入到同步代码块结束位置,jvm需要保证每个monitorenter都有一个monitorexit对应。这两个指令,本质上都是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有一个线程获取到synchronized所保护对象的监视器线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁;而执行monitorexit,就是释放monitor的所有权。
synchronized的锁的原理
jdk1.6以后对synchronized锁进行了优化,包含偏向锁、轻量级锁、重量级锁; 在了解synchronized锁之前,我们需要了解两个重要的概念,一个是对象头、另一个是monitor;
Java对象头
在Hotspot虚拟机中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充;Java对象头是实现synchronized的锁对象的基础,一般而言,synchronized使用的锁对象是存储在Java对象头里。它是轻量级锁和偏向锁的关键。
Mawrk Word
Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit)。
synchronized的锁升级和获取过程
了解了对象头以及monitor以后,接下来去分析synchronized的锁的实现,就会非常简单了。前面讲过synchronized的锁是进行过优化的,引入了偏向锁、轻量级锁;锁的级别从低到高逐步升级, 无锁->偏向锁->轻量级锁->重量级锁。
自旋锁(CAS)
自旋锁就是让不满足条件的线程等待一段时间,而不是立即挂起。看持有锁的线程是否能够很快释放锁。怎么自旋呢?其实就是一段没有任何意义的循环。
虽然它通过占用处理器的时间来避免线程切换带来的开销,但是如果持有锁的线程不能在很快释放锁,那么自旋的线程就会浪费处理器的资源,因为它不会做任何有意义的工作。所以,自旋等待的时间或者次数是有一个限度的,如果自旋超过了定义的时间仍然没有获取到锁,则该线程应该被挂起。
偏向锁
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步快并获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,以后该线程进入和退出同步快时不需要进行cas操作来加锁和解锁,只需要测试一下对象头的Mark Word里是否存储了指向当前线程的偏向锁。
轻量级锁
引入轻量级锁的主要目的是在多没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁。
重量级锁
重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
前面我们在讲Java对象头的时候,讲到了monitor这个对象,在hotspot虚拟机中,通过ObjectMonitor类来实现monitor。他的锁的获取过程的体现会简单很多:
wait和notify
wait和notify是用来让线程进入等待状态以及使得线程唤醒的两个操作:
package lock;
/**
* @author zhangyu
* @version V1.0
* @ClassName: ThreadWait
* @Description: 线程等待
* @date 2019/1/23 14:10
**/
public class ThreadWait extends Thread {
private Object lock;
public ThreadWait(Object lock) {
this.lock = lock;
}
public void run(){
System.out.println("开始执行 thread wait");
try{
lock.wait();
}catch (Exception e){
e.printStackTrace();
}
System.out.println("执行结束 thread wait");
}
}
package lock;
/**
* @author zhangyu
* @version V1.0
* @ClassName: ThreadNotify
* @Description: 唤醒线程
* @date 2019/1/23 14:16
**/
public class ThreadNotify extends Thread {
private Object lock;
public ThreadNotify(Object lock) {
this.lock = lock;
}
public void run() {
System.out.println("开始执行 thread notify");
try {
lock.notify();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("执行结束 thread nofity");
}
}
wait和notify的原理
调用wait方法,首先会获取监视器锁,获得成功以后,会让当前线程进入等待状态进入等待队列并且释放锁;然后当其他线程调用notify或者notifyall以后,会选择从等待队列中唤醒任意一个线程,而执行完notify方法以后,并不会立马唤醒线程,原因是当前的线程仍然持有这把锁,处于等待状态的线程无法获得锁。必须要等到当前的线程执行完按monitorexit指令以后,也就是锁被释放以后,处于等待队列中的线程就可以开始竞争锁了;
wait和notify为什么需要在synchronized里面
wait方法的语义有两个,一个是释放当前的对象锁、另一个是使得当前线程进入阻塞队列, 而这些操作都和监视器是
相关的,所以wait必须要获得一个监视器锁。
而对于notify来说也是一样,它是唤醒一个线程,既然要去唤醒,首先得知道它在哪里?所以就必须要找到这个对
象获取到这个对象的锁,然后到这个对象的等待队列中去唤醒一个线程。