● Synchronization的底层实现概述
Java虚拟机的同步(Synchronized)是基于进入和退出管理对象(monitor)实现的。同步方法并不是由monitor enter和monitor exit指令来实现同步的,而是由方法调用指令读取运行时常量池中的方法的ACC_SYNCHRONIZED标志来隐式实现的。
注:monitor enter 和 monitor exit 指令是C语言的内容。
● 对象的内存模型
● 对象组成介绍
对象头:存储对象的 hashCode、锁信息或分代年龄或 GC 标志,类型指针指向对象的类元数据,JVM 通过这个指针确定该对象是哪个类的实例等信息。
实例变量:存放类的属性数据信息,包括父类的属性信息。
填充数据:由于虚拟机要求对象起始地址必须是 8 字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
● 加锁时的流程
当执行 synchronized 同步方法或同步代码块时,会在对象头中记录锁标记,锁标记指向的是 monitor 对象(也称为管程或监视器锁)的起始地址来记录锁信息。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如 monitor 可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。
另外的线程想获取对象头中的锁信息的时候,会发现对象头中已经记录一把锁(monitor),他就获取不到。monitor是互斥的,对象头记录的monitor就不会分配给其他线程了,此时这个线程就会进入阻塞状态。当执行中的线程发生异常,或者是释放锁标记,对象头的锁信息就会释放它记录的monitor。阻塞状态的线程就会弹出来一同争夺,重新在锁信息中记录monitor。
ObjectMonitor(java的monitor的实现)中有两个队列,_WaitSet 和 _EntryList,以及_Owner标记。其中WaitSet是用于管理等待队列(wait)线程的,EntryList 是用于管理锁池阻塞线程的,_Owner 标记用于记录当前执行线程。
● 线程状态流程
当多线程并发访问同一个同步代码时,首先会进入EntryList,当线程获取锁标记后,monitor 中的Owner 记录此线程,并在 monitor 中的计数器执行递增计算(+1),代表锁定,其他线程在EntryList 中继续阻塞。若执行线程调用 wait 方法,则 monitor 中的计数器执行赋值为 0 计算,并将Owner 标记赋值为 null,代表放弃锁,执行线程进入WaitSet 中阻塞。若执行线程调用 notify/notifyAll 方法,WaitSet 中的线程被唤醒,进入EntryList 中阻塞,等待获取锁标记。若执行线程的同步代码执行结束,同样会释放锁标记,monitor 中的Owner标记赋值为 null,且计数器赋值为 0 计算。
● 什么是锁的重入
在 Java 中,同步锁是可以重入的。只有同一线程调用同步方法或执行同步代码块,对同一个对象加锁时才可重入。当线程持有锁时,会在 monitor 的计数器中执行递增计算,若当前线程调用其他同步代码,且同步代码的锁对象相同时,monitor 中的计数器继续递增。每个同步代码执行结束,monitor 中的计数器都会递减,直至所有同步代码执行结束,monitor 中的计数器为 0 时,释放锁标记,_Owner 标记赋值为 null。
ThreadLocal介绍
ThreadLocal叫做线程本地变量,也有些地方叫做线程本地存储,ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
● Thread与ThreadLocal源码的解析
public void set(T value) {
Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null)
map.set(this, value); else createMap(t, value);}
ThreadLocalMap getMap(Thread t) { return t.threadLocals;}void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue);}
我们以set方法为例子,当线程第一次调用ThreadLocal的set方法时候会创建Thread对象里面的threadLocals成员。也就是说ThreadLocal的每次set和get操作,都是对Thread的threadLocals进行操作,ThreadLocals类似于threadLocals变量的管理者。
threadLocals类似一个特殊的Map,它的key就是threadLocal的实例,而value就是我们设置的value值。
● ThreadLocal使用注意事项
在一个操作系统中,线程和进程是有数量上限的。在操作系统中,确定线程和进程唯一性的唯一条件就是线程或进程 ID。操作系统在回收线程或进程的时候,不是一定杀死线程或进程,在繁忙的时候对线程或进程栈数据的操作,可能重复使用线程或进程。
使用ThreadLocal的时候,一定注意回收资源问题,每个线程结束之前,将当前线程保存的线程变量一定要删除 ,调用ThreadLocal.remove(),要不会发生泄露。run方法的finally代码块。
在并发量高的时候,可能有内存溢出。
● ThreadLocal使用实例
public class Test_ThreadLocal { volatile static String name = "zhangsan"; static ThreadLocal<String> tl = new ThreadLocal<>(); public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } name = "lisi"; tl.set("wangwu"); } }).start(); new Thread(new Runnable() { @Override public void run() { try { // 等待前一条线程运行结束 TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(name); System.out.println(tl.get()); } }).start(); }}
运行结果是:lisi 和 null。
就是说虽然第二条线程的打印在第一条线程set之后,当时它在ThreadLocad容器中取不到相关的值。
锁的种类
Java 中锁的种类包括偏向锁,自旋锁,轻量级锁,重量级锁。锁的使用方式先提供偏向锁,如果不满足的时候,升级为轻量级锁,再不满足,升级为重量级锁。自旋锁是一个过渡的锁状态,不是一种实际的锁类型。锁只能升级,不能降级。
偏向锁
偏向锁是一种编译解释锁。如果代码中不可能出现多线程并发争抢同一个锁的时候,JVM 编译代码,解释执行的时候,会自动的放弃同步信息。消除 synchronized 的同步代码结果。可以避免锁的争抢和锁池状态的维护,提高JVM解释效率。
Object o = new Object();public void m() { o = new Object(); synchronized (o) { }}
轻量级锁
当偏向锁不满足,也就是有多线程并发访问,锁定同一个对象的时候,先提升为轻量级锁。也是使用标记 ACC_SYNCHRONIZED 标记记录的。ACC_UNSYNCHRONIZED 标记记录未获取到锁信息的线程。就是只有两个线程争抢锁标记的时候,优先使用轻量级锁。A线程和monitor有直接关联的。B线程不记录monitor,是monitor记录B线程,线程A结束后,B两个线程才找到monitor。但也可能有时候还是会出现重量级锁。
自旋锁
自旋锁是一个过渡锁,是偏向锁和轻量级锁的过渡。当获取锁的过程中,未获取到。为了提高效率,JVM 自动执行若干次空循环,再次申请锁,而不是进入阻塞状态的情况。故称为自旋锁。自旋锁提高效率就是避免线程状态的变更。
重量级锁
自旋锁不会一直持续自旋下去,当自旋一定次数的时候,还没获取到锁就会进入阻塞,该锁膨胀为重量级锁。重量级会让其他申请线程阻塞,性能降低。