4、Synchronized
共享:资源可以由多个线程同时访问。
可变:资源可以在其生命周期内被修改。
一、synchronized
同步实例方法,锁是当前实例对象。
//代码块1(对象)this指的是当前对象
public void accessResources1(){
synchronized(this){
try {
TimeUnit.MINUTES.sleep(2);
System.out.println(Thread.currentThread().getName()+" is runing");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}同步类方法(静态方法),锁是当前类对象。
//修饰静态方法
public synchronized static void accessResources0(){
try {
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+" is runing");
} catch (InterruptedException e) {
e.printStackTrace();
}
}同步代码块,锁是括号里面的对象。
//代码块1(CLASS类)
public void accessResources4(){
synchronized(SynchroDemo01.class){
//有Class对象的所有的对象都共同使用这一个锁
try {
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+" is runing");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}1.1、synchronized底层原理
public class SynchroDemo01 {
/**
* 代码块1(对象)this指的是当前对象
*/
public void accessResources1(){
synchronized(this){
try {
//2分钟的超时等待时间
TimeUnit.MINUTES.sleep(2);
System.out.println(Thread.currentThread().getName()+" is runing");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final SynchroDemo01 deno01=new SynchroDemo01();
for (int i = 0; i < 5; i++) {
new Thread(deno01::accessResources1).start();
}
}
}1.1.1、线程的堆栈分析
jconsole分析
其他线程状态为:
每2分一个线程走完自己的生命周期,后销毁线程。最后执行完成后结束(线程个数归0)。
1.1.2、jstack分析
public enum State {
//NEW状态表示线程刚刚被定义,还未实际获得资源以启动,也就是还未调用start()方法。
NEW,
//RUNNABLE表示线程当前处于运行状态,当然也有可能由于时间片使用完了而等待CPU重新的调度。
RUNNABLE,
//BLOCKED表示线程在竞争某个锁失败时被置于阻塞状态。
BLOCKED,
//WAITING和TIMED_WAITING表示线程在运行中由于缺少某个条件而不得不被置于条件等待队列,等待需要的条件或资源。
WAITING,
TIMED_WAITING,
//TERMINATED表示线程运行结束,当线程的run方法结束之后,该线程就会是TERMINATED状态。
TERMINATED;
}1.1.3、JVM指令反编译字节码分析
cmd窗口进入编译成class文件的目录,使用Javap -v查看字节码,accessResources1方法完整字节码如下:
public void accessResources1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=4, args_size=1 0: aload_0 1: dup 2: astore_1 3: monitorenter // 进入监视器,获取锁 4: getstatic #17 // Field java/util/concurrent/TimeUnit.MINUTES:Ljava/util/concurrent/TimeUnit; 7: ldc2_w #3 // long 2l 10: invokevirtual #5 // Method java/util/concurrent/TimeUnit.sleep:(J)V 13: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream; 16: new #7 // class java/lang/StringBuilder 19: dup 20: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V 23: invokestatic #9 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread; 26: invokevirtual #10 // Method java/lang/Thread.getName:()Ljava/lang/String; 29: invokevirtual #11 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 32: ldc #12 // String is runing 34: invokevirtual #11 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 37: invokevirtual #13 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 40: invokevirtual #14 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 43: goto 51 46: astore_2 47: aload_2 48: invokevirtual #16 // Method java/lang/InterruptedException.printStackTrace:()V 51: aload_1 52: monitorexit // 监视器退出,释放锁 53: goto 61 56: astore_3 57: aload_1 58: monitorexit // 异常退出,释放锁 59: aload_3 60: athrow 61: return
每个同步对象都有一个自己的Monitor(监视器锁),加锁过程如下图所示:
1.2、Monitor监视器锁
MonitorEnter指令
如果Monitor的进入数为0,则该线程进入Monitor,然后将进入数设置为1,该线程即为Monitor的所有者。
如果线程已经占有该Monitor,则重新进入,则进入Monitor的进入数加1(锁重入)。
如果其他线程已经占用了Monitor,则该线程进入阻塞状态,直到Monitor的进入数为0,再重新尝试获取Monitor的所有权。
MonitorExit指令
方法加锁
//修饰静态方法
public synchronized static void accessResources0(){
try {
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+" is runing");
} catch (InterruptedException e) {
e.printStackTrace();
}
}反编译:
1.3、什么是Monitor
ObjectMonitor() {
_header = NULL;
_count = 0; // 记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet队列
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该集合
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}首先会进入_EntryList集合,当线程获取到对象的Monitor的后,并把Monitor中的_owner变量设置为当前线程,同时Monitor中的计数器_count加1。
若线程调用 wait() 方法,将释放当前持有的Monitor,_owner变量恢复为null,_count自减1,同时该线程进入WaitSet集合中等待被唤醒。
若当前线程执行完毕,也将释放Monitor并复位count的值,以便其他线程进入获取Monitor。
三、对象的内存布局
对象头(Header):对象头存储一些信息,比如hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象)等。Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit),但是如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
实例数据(Instance Data):存放类的属性数据信息,包括父类的属性信息。
对齐填充(Padding):由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,方便维护。
3.1、对象头
64位
3.1.1、unused
3.1.2、identity_hashcode(大小31bits)
3.1.3、age(大小4bits)
3.1.4、biased_lock(大小1bits)
3.1.5、lock(大小2bits)
3.1.6、thread(大小54bits)
3.1.7、epoch(大小2bits)
3.1.8、ptr_to_lock_record(大小62bits)
3.1.8、ptr_to_heavyweight_monitor(大小62bits)
对象的全局静态变量(即类属性)。
对象头信息:64位平台下,原生对象头大小为16字节,压缩后为12字节。
对象的引用类型:64位平台下,引用类型本身大小为8字节,压缩后为4字节。
对象数组类型:64位平台下,数组类型本身大小为24字节,压缩后16字节。
四、锁的膨胀升级过程
4.1、偏向锁
默认开启偏向锁 开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 关闭偏向锁:-XX:-UseBiasedLocking
4.2、轻量级锁
4.3、自旋锁
4.5、锁消除
private void method(){
Object o = new Object();
synchronized(o){
System.out.printlen(o);
}
}-XX:+DoEscapeAnalysis // 开启逃逸分析 -XX:+EliminateLocks // 表示开启锁消除。
逃逸分析测试
public class T0_ObjectStackAlloc {
/**
* 进行两种测试
* 关闭逃逸分析,同时调大堆空间,避免堆内GC的发生,如果有GC信息将会被打印出来
* VM运行参数:-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
*
* 开启逃逸分析
* VM运行参数:-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
*
* 执行main方法后
* jps 查看进程
* jmap -histo 进程ID
*/
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 500000; i++) {
alloc();
}
long end = System.currentTimeMillis();
//查看执行时间
System.out.println("cost-time " + (end - start) + " ms");
try {
Thread.sleep(1000000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
private static TulingStudent alloc() {
//Jit对编译时会对代码进行 逃逸分析
//并不是所有对象存放在堆区,有的一部分存在线程栈空间
TulingStudent student = new TulingStudent();
return student;
}
static class TulingStudent {
private long id;
private int age;
}
}jps查看当前运行的代码
情况1,未开启逃逸分析
情况2,开启逃逸分析
同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不堆分配。
分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
版权声明
非特殊说明,本文由Zender原创或收集发布,欢迎转载。
ZENDER




















发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。