从青铜到王者的路线,来聊聊Synchronized底层实现原理,java面试不问基础,只问项目

System.out.println(getName() + “进入了同步代码块1”);
synchronized (MyThread.class) {
System.out.println(getName() + “进入了同步代码块2”);
}
}
}
}

运行结果如下,我们可以很明细的看出在输出“同步代码块1”之后,不需要等待锁释放,即可进入第二个同步代码块。这样的一个特性可以避免死锁的发生,也可以更好的封装代码(即:同步代码块中的代码,可以分成多个方法来写)。

输入结果如下:

Thread-0进入了同步代码块1
Thread-0进入了同步代码块2

4.2 sync不可中断特性

不可中断只指,线程二在等待线程一释放锁的时候,是不可被中断的。

当一个线程获得锁之后,另外一个线程一直处于堵塞或者等待状态,前一个线程不释放锁,后一个线程会一直被阻塞或等待,所以sync是不可中断锁。

public class SyncExample9 {

private static Object lock = new Object();

public static void main(String[] args) throws InterruptedException {
Runnable run = () -> {
synchronized (lock) {
String name = Thread.currentThread().getName();
System.out.println(name + “进入同步代码块”);
try {
// 让线程一持有锁
Thread.sleep(888888L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};

// 创建线程一先执行同步代码快
Thread t1 = new Thread(run);
t1.start();

// 主线程睡眠一下,保证上面线程先执行
Thread.sleep(1000L);

// 后开启线程取执
Thread t2 = new Thread(run);
t2.start();

System.out.println(“开始中断线程二”);
// 强行线程二中断
t2.interrupt();
System.out.println(“线程一状态” + t1.getState());
System.out.println(“线程二状态” + t2.getState());

}

}

当我们线程一进入同步代码之后,一直持有锁,并且睡眠了(也证实了sleep方法睡眠不会释放锁对象)。

此时线程二启动去尝试获取锁,获取失败之后就变成堵塞状态,哪怕我们强行中断线程二,最后看到线程二的状态仍是堵塞的。

Thread-0进入同步代码块
开始中断线程二
线程一状态TIMED_WAITING
线程二状态BLOCKED

4.3 反汇编学习sync原理

使用javap反汇编java代码,引入monitor概念。

public class SyncExample10 {

private static Object lock = new Object();

public static void main(String[] args) throws InterruptedException {
synchronized (lock) {
System.out.println(“1”);
}
}

public synchronized void test() {
System.out.println(“1”);
}

}

我们使用javac、javap 两个命令对SyncExample10来进行编译

javac SyncExample10.java

javap -v -p  SyncExample10.class

编译后的指令就如下啦,我们主要看main方法里面的内容,着重看 monitorenter、monitorexit 两个指令

public static void main(java.lang.String[]) throws java.lang.InterruptedException;
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: getstatic     #2                 
3: dup
4: astore_1
5: monitorenter    // 这里
6: getstatic     #3                 
9: ldc           #4                
11: invokevirtual #5                  
14: aload_1
15: monitorexit  // 这里
16: goto          24
19: astore_2
20: aload_1
21: monitorexit  // 这里
22: aload_2
23: athrow
24: return

monitorenter 指令

当我们进入同步代码块的时候会先执行monitorenter指令,每一个对象都会和一个monitor监视器关联,监视器被占用时会被锁住,其他线程无法来获取该monitor。当其他线程执行monitorente指令时,它会尝试去获取当前对象对应的monitor的所有权。

monitor里面有两个很重要成员变量:

owner: 当一个线程获取到该对象的锁,就把线程当前赋值给owner。

recursions:会记录线程拥有锁的次数,重复获取锁当前变量也会+1,当一个线程拥有monitor后,其他线程只能等待。

monitorenter执行流程如下:

1)若monitor的进入次数为0时,线程可以进入monitor,并将monitor进入的次数(recursions)+1,当前线程成为montiro的owner(所有者);

2)若线程已拥有monitor的所有权,允许它重入monitor,进入一次次数+1 (可重复特性);

3)若其他线程已经占有monitor,那么当前尝试获取monitor的线程会被阻塞,一直到monitor进入次数为变0,才能重新被再次获取。

monitorexit 指令

既然我们同步代码块进入时计数器会执行+1操作,那么我们退出的时候,计数器当然要执行-1;

要注意,能够执行monitorexit指令的线程,一定是拥有当前对象的monitor所有权的线程。 当我们执行monitorexit指令计数器减到为0时,当前线程就不再拥有monitor所有权。其他被阻塞的线程即可再一次去尝试获取这个monitor的所有权。

大家仔细看看上面编译出来的指令,其实monitoreexit是有两个的,为什么呢?

因为需要保证如果同步代码块执行抛出了异常,则也需要释放锁对象。等到下次面试官问你,synchronized如果抛异常了,会不会释放锁对象,答案是:会的。

ACC_SYNCHRONIZED 修饰

刚刚我们所看到的是mian方法中同步代码块所编译后的指令,以下是同步方法编译后指令

可以看到同步方法在反汇编后,会增加ACC_SYNCHRONIZED修饰,会隐式调用monitorenter、mointorexit,在执行同步方法前会调用monitorenter,在方法结束之后会调用monitorexit。

public synchronized void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc           #4                  // String 1
5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 19: 0
line 20: 8

五、尊贵铂金

5.1 montior 监视器锁

刚刚上文有提到每一个对象都会和一个monitor监视器关联,真正的锁都是靠monitor监视器来完成,

那monitor到底是个啥玩意呢? 小编偷偷告诉你,其实monitor是用C++所写。

http://hg.openjdk.java.net/jdk8/jdk8/hotspot/ 网址都给你们找好了,点击左边zip、gz下载都行。 网速不好的同学可以在网上“hotspot 源码下载” ,下载之后文件如下图:

image

下载之后为了方便浏览,小编建议你们可以去下载一个CLion工具来看代码,或者直接用文本编辑器打开也行。

java对象怎么和monitor关联的呢?

这里就牵扯到另外一个知识点,我们每一个对象在内存中分为三块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。而这个对象头就包含了一个monitor的引用地址,指向了一个具体的monitor对象。

image

monitor里面包含了什么?

我们先找到monitor对象对应的源文件:/src/share/vm/runtime/objectMonitor.hpp,往下翻可以看到ObjectMonitor的构造方法,里面有一系列成员属性。

ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0; // 记录线程的重入次数
_object = NULL;
_owner = NULL; // 标识拥有该monitor的线程
_WaitSet = NULL; // 存储正处于wait状态的线程
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; // 存放竞争失败线程的单向链表
FreeNext = NULL ;
_EntryList = NULL ; // 存储等待锁block状态的线程
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}

挑几个比较重要的来说一下:

_recursions:这个在上文讲monitorenter指令的时候有提到,就是记录线程线程获取锁的次数,获取到锁该属性则会+1,退出同步代码块则-1;

_owner:当一个线程获得了monitor的所有权,则该对象会保存到_owner中。

_WaitSet:当线程入wait状态,则会存储到_WaitSet当中。

_cxq :当线程之间开始竞争锁,如果锁竞争失败后,则会加入_cxq链表中。

_EntryList:当新线程进来尝试去获取锁对象,又没有获取到对象的时候,则会存储到_EntryList当中。

5.2 monitor 竞争

什么情况下会竞争?

当多个线程执行同步代码块的时候,这个时候就会出现锁竞争。

当线程执行同步代码块时,先执行monitorenter指令, 这个时候会调用interpreterRuntime.cpp中的函数

源文件如下:src/share/vm/interpreter/interpreterRuntime.cpp,搜索:monitorenter

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
// 代码省略

// 是否用偏向锁
if (UseBiasedLocking) {
// Retry fast entry if bias is revoked to avoid unnecessary inflation
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
// 重量级锁
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}

// 代码省略
IRT_END

线程之间如何竞争锁的?

对于重量级锁,monitorenter函数中会调用 :ObjectSynchronizer::slow_enter,

最终调用到这个函数上:ObjectMonitor::enter,源码位于:/src/share/vm/runtime/objectMonitor.cpp

void ATTR ObjectMonitor::enter(TRAPS) {
// The following code is ordered to check the most common cases first
// and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.
Thread * const Self = THREAD ;
void * cur ;

// 1、通过CAS操作尝试把monitor的_owner设置成当前线程
cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
if (cur == NULL) {
assert (_recursions == 0 , “invariant”) ;
assert (_owner == Self, “invariant”) ;
return ;
}

// 2、重入锁
if (cur == Self) {
// 重入锁计数器也需要+1
_recursions ++ ;
return ;
}

// 3、如果是当前线程第一次进入该monitor
if (Self->is_lock_owned ((address)cur)) {
assert (_recursions == 0, “internal state error”);
// 计数器+1
_recursions = 1 ;
// 把当前线程设置赋值给_owner
_owner = Self ;
OwnerIsThread = 1 ;
return ;
}

// TODO-FIXME: change the following for(;😉 loop to straight-line code.
for (;😉 {
jt->set_suspend_equivalent();

// 4、获取锁失败,则等待锁释放
EnterI (THREAD) ;

if (!ExitSuspendEquivalent(jt)) break ;

_recursions = 0 ;
_succ = NULL ;
exit (false, Self) ;

jt->java_suspend_self();
}
}

此处省略了锁的自旋优化等操作,文章后面会讲到

以上代码具体的操作流程如下:

1)通过CAS尝试把monitor的_owner属性设置为当前线程

2)如果之前设置的owner等于当前线程,说明当前线程再次进入monitor,即重入锁,执行_recursions ++ ; 记录重入次数。

3)如果当前线程是第一次进入monitor,设置_recursions = 1,_owner = 当前线程,该线程成功获得锁并返回。

4、如果获取锁失败,等待锁释放

5.3. monitor 等待

上文有提到,如果锁竞争失败后,会调用EnterI (THREAD) 函数,还是在objectMonitor.cpp源码中搜索:::EnterI

以下代码小编省略了部分:

void ATTR ObjectMonitor::EnterI (TRAPS) {
Thread * Self = THREAD ;
assert (Self->is_Java_thread(), “invariant”) ;
assert (((JavaThread *) Self)->thread_state() == _thread_blocked , “invariant”) ;

// 尝试获取锁
if (TryLock (Self) > 0) {
assert (_succ != Self , “invariant”) ;
assert (_owner == Self , “invariant”) ;
assert (_Responsible != Self , “invariant”) ;
return ;
}

// 自旋操作尝试获取锁
if (TrySpin (Self) > 0) {
assert (_owner == Self , “invariant”) ;
assert (_succ != Self , “invariant”) ;
assert (_Responsible != Self , “invariant”) ;
return ;
}

// 当前线程封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ
ObjectWaiter node(Self) ;
Self->_ParkEvent->reset() ;
node._prev = (ObjectWaiter *) 0xBAD ;
node.TState = ObjectWaiter::TS_CXQ ;

// 通过CAS把node节点push到_cxq队列中
ObjectWaiter * nxt ;
for (;😉 {
node._next = nxt = _cxq ;
if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;

// Interference - the CAS failed because _cxq changed. Just retry.
// As an optional optimization we retry the lock.
// 再次尝试获取锁
if (TryLock (Self) > 0) {
assert (_succ != Self , “invariant”) ;
assert (_owner == Self , “invariant”) ;
assert (_Responsible != Self , “invariant”) ;
return ;
}
}

// 挂起线程
for (;😉 {
// 挂起之前再次尝试获取锁
if (TryLock (Self) > 0) break ;
assert (_owner != Self, “invariant”) ;

if ((SyncFlags & 2) && _Responsible == NULL) {
Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
}

// park self
if (_Responsible == Self || (SyncFlags & 1)) {
TEVENT (Inflated enter - park TIMED) ;
Self->_ParkEvent->park ((jlong) RecheckInterval) ;
// Increase the RecheckInterval, but clamp the value.
RecheckInterval *= 8 ;
if (RecheckInterval > 1000) RecheckInterval = 1000 ;
} else {
TEVENT (Inflated enter - park UNTIMED) ;
// 通过park将当前线程挂起,等待锁释放
Self->_ParkEvent->park() ;
}
// 尝试获取锁
if (TryLock(Self) > 0) break ;
}

return ;
}

以上代码具体流程概括如下:

1)进入EnterI后,先会再次尝试获取锁对象

2)把当前线程封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ ;

3)在for循环中,通过CAS把node节点push到_cxq(上文有提到这个属性)列表中,同一时刻可能有多个线程把自己到node节点push到_cxq列表中。

4)node节点push到_cxq 列表之后,通过自旋尝试获取锁,如果还是没有获取到锁,则通过park将当前线程挂起,等待唤醒。

5)当前线程被唤醒时,会从挂起到点继续执行,通过TryLock再次尝试锁。

5.4 monitor 释放

什么时候会释放monitor?

当线程执行完同步代码块时,调用monitorexit指令释放锁,这个时候锁就会被释放。

还是在objectMonitor.cpp源码中搜索:::exit

释放monitor过程是什么?

exit函数代码如下,当然小编也有大部分的删减,留下比较主要的代码部分。

void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) {

// 判断计数器,不等于0则执行-1
if (_recursions != 0) {
_recursions–; // this is simple recursive enter
TEVENT (Inflated exit - recursive) ;
return ;
}

// w = 最后被唤醒的线程
ObjectWaiter * w = NULL ;
int QMode = Knob_QMode ;

// QMode == 2,会绕过EntryList队列,从cxq队列中获取线程用于竞争锁
if (QMode == 2 && _cxq != NULL) {
w = _cxq ;
assert (w != NULL, “invariant”) ;
assert (w->TState == ObjectWaiter::TS_CXQ, “Invariant”) ;
// 唤醒线程
ExitEpilog (Self, w) ;
return ;
}

// QMode还有还好几种策略,小编就不一一列举了

// 最后拿到了要被唤醒的线程
w = _EntryList ;
if (w != NULL) {
guarantee (w->TState == ObjectWaiter::TS_ENTER, “invariant”) ;
// 唤醒线程
ExitEpilog (Self, w) ;
return ;
}

}

观察以上代码,都需要调用ExitEpilog函数来唤醒线程, 还是在objectMonitor.cpp源码中搜索:::ExitEpilog

void ObjectMonitor::ExitEpilog (Thread * Self, ObjectWaiter * Wakee) {
assert (_owner == Self, “invariant”) ;

_succ = Knob_SuccEnabled ? Wakee->_thread : NULL ;
ParkEvent * Trigger = Wakee->_event ;

Wakee = NULL ;

// Drop the lock
OrderAccess::release_store_ptr (&_owner, NULL) ;
OrderAccess::fence() ; // ST _owner vs LD in unpark()

if (SafepointSynchronize::do_call_back()) {
TEVENT (unpark before SAFEPOINT) ;
}

DTRACE_MONITOR_PROBE(contended__exit, this, object(), Self);

// 最重要的时候这里,调用unpark来进行唤醒
Trigger->unpark() ;

// Maintain stats and report events to JVMTI
if (ObjectMonitor::_sync_Parks != NULL) {
ObjectMonitor::_sync_Parks->inc() ;
}
}

以上代码具体流程概括如下:

1)退出同步代码块时会让_recursions - 1,当_recursions的值等于0的时候,说明线程释放了锁。

2)根据不同的策略(由QMode来指定),最终获取到需要被唤醒的线程(代码中是:w)

3)最后调用ExitEpilog函数中,最终由unpark来执行唤醒操作。

六、永恒钻石

6.1 CAS 介绍

CAS的英文单词CompareAndSwap的缩写,比较并替换。CAS需要有3个操作数:内存地址V、旧的预期值A、即将要更新的目标值B。

CAS指令执行时,当内存地址V的值与预期值A相等时,将目标值B保存到内存当中,否则就什么都不做。 整个比较并替换的操作是一个原子操作。

CAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都失败,失败的线程并不会挂起,而是被告知这次竞争失败,并可以再次尝试。

优点:可以避免优先级倒置和死锁等危险,竞争比较便宜,协调发生在更细的力度级别,允许更高程度的并行机制等等。

缺点:

1、循环时间长开销很大,如果CAS失败,会一直进行尝试,如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

2、只能保证一个共享的原子操作,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。

3、ABA问题,如果内存地址V初次读取的值是A,并且在准备赋值的时候检查仍然为A,那我们就能说它的值没有被其他线程改变过吗?

如果在这段期间它的值曾被改成了B,后来又被改回A,那CAS就会误认为它从来没有被改变过,这个漏洞称之为CAS操作的ABA问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类 “AtomicStampendReference”,它可以通过控制变量值的版本来保证CAS的正确性。

因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发性的正确性,如果需要解决ABA问题,改用传统的互斥同步可能比原子类更高效

介绍完CAS,那么肯定就多多少少介绍以下实现原理,我们以AtomicInteger为例,它是JDK中提供能够保障原子性操作的类。

/**

  • Atomically increments by one the current value.
  • @return the updated value
    */
    public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

我们点进去看它里面的方法,拿incrementAndGet方法为例子,这个方法是在原有值的基础上进行+1操作,它的实现调用Unfafe类的方法,我们再点进去看。

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;
}

Unfafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针问题,过度的使用Unsafe类会使得出错的几率变大。因此Java官方不建议使用的,Unsafe对象也不能直接调用,只能通过放射来获取。

小编这里说一下getandAddInt方法的执行流程,

var1:传进来的是this,也就是AtomicInteger实例对象;

var2:偏移量,通过结合var1就能够获得在内存中的最新值;

var4:要进行累加的值,也就是 1 ;

先通过var1+var2 获取到内存中最新的值,然后再调用compareAndSwapInt方法,这个方法又会通过var1+var2参数获取内存中最新的值,与var5的值进行比较,如果比较成功,这把var5+var4的结果更新到内存中去。如果不成功,则继续循环操作。也就是我们刚刚介绍CAS所说,比较并替换。

6.2 sync 锁升级过程

在JDK1.5以前,sync是一个重量级的锁,在1.6以后,对sync做了大量的各种优化,包含偏向锁、轻量级锁、适应性自旋、锁消除、锁粗化等等,这些技术都是为了线程之间更加高效的共享数据,以及解决竞争问题,从而达到程序的执行效率。

当然锁肯定升级的过程:无锁 —— 偏向锁 —— 轻量级锁 —— 重量级锁。

每个不同的锁都有不同的使用藏场景,在了解各种锁的特性之前,我们还需要搞清楚对象在内存中的布局!

6.3 对象的布局

我们每一个对象在内存中分为三块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。

对象头:

当一个线程尝试访问sync修饰的代码块时,它先要获得锁,这个锁对象是存在对象头中的。

以Hotspot虚拟机为例,对象头里面主要包含了Mark Word(字段标记)、Klass Pointer (指针类型),如果对象是数组类型,还包含了数组的长度。

怎么又扯到Hotspot虚拟机呢? 小伙伴可以这样理解,JVM可以理解为一套规范,而Hotspot是具体的虚拟机产品。 就好比如你们要找女朋友、或者男朋友,既然找朋友是不是就要有一定的要求或者规范,JVM就可以看作这个规范,而Hotspot就是具体的男朋友或者女朋友了。

你不信? System.out.println(System.getProperties());  运行这个代码吧,找找你们java.vm.name等于什么。

java.vm.name=Java HotSpot™ 64-Bit Server VM

**Mark Word :**里默认存储对象的HashCode、分代年龄和锁位标记。 这个也是sync锁实现的重要部分了,在运行期间,Mark Word 里存储的数据会随着锁标位置的变化而变化。 在64位虚拟机下,Mark Word是64bit大小的,其存储结构如图:

Mark Word 64位虚拟机存储结构
image.png

以上这个表格数据不能乱来对不对,我们可以查看源码:src/share/vm/oops/markOop.hpp

里面注释写的很清楚了,对照以下注释反映出上面的表格,更加直观。

// 32 bits:
// --------
// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
// size:32 ------------------------------------------>| (CMS free block)
// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
// size:64 ----------------------------------------------------->| (CMS free block)

**Klass Pointer **:用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定是哪个对象的实例。

对象头 = Mark Word + Klass Point 在未开启指针压缩对情况下所占大小:

以64位系统为例:Mark Word = 8 bytes,指针类型 = 8 bytes ,对象头 = 16 bytes = 128bits;

实例数据:

类中定义的成员变量

对齐填充:

对齐填充并不是必然存在的,也没有什么特殊的意义,它仅仅只是占位符的作用。由于HotPort VM的自动内存管理系统要求对象起始地址必须是8字节的整倍数,当对象的实例数据部分没有对齐时,就需要通过对齐填充来不补齐。

说了这么多,都是概念性的东西,说谁不会说对不对,接下来我们尝试在把一个对象在内存中都布局输出看下:

先引入这个jar包,它能够提供我们想要看到的东西,使用方式如下:

org.openjdk.jol jol-core 0.10

public class SyncExample4 {

static Apple apple = new Apple();

public static void main(String[] args) {
// 这里使用ClassLayout来查看
System.out.println(ClassLayout.parseInstance(apple).toPrintable());
}
}

class Apple {
private int count;
private boolean isMax;

}

以下内容就是我们Java对象内存分布所查看到的内容,我们能直接看到内容有object header 翻译过来就是对象头呀, 再往下看就是loss due to the next object alignment,这个就是对齐填充,由于Apple 有一个boolean的属性,占了一个字节,所以计算机为了提高执行效率和GC垃圾回收的效率,进行了7个字节的填充(这里涉及到CPU运行小编就不多扯了)。

com.example.concurrency.sync.Apple object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c0 00 f8 (01000011 11000000 00000000 11111000) (-134168509)
12 4 int Apple.count 0
16 1 boolean Apple.isMax false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

看到这里我们确实能够确定对象头的存在,那么对象头里面不是说用31 bit存储了HashCode吗? 怎么没看见

我们再来执行一段代码, 计算一下apple的HashCode是多少,看运行结果可知,本次运行apple的HashCode是7ea987ac,我们再看看对应VALUE值也发生了改变。这里有一个概念,由于存在大小端存储方式,我们需要从后往前看。

public class SyncExample4 {

static Apple apple = new Apple();

public static void main(String[] args) {
// 查看HashCode
System.out.println(Integer.toHexString(apple.hashCode()));
System.out.println(ClassLayout.parseInstance(apple).toPrintable());
}
}

class Apple {
private int count;
private boolean isMax;
}

7ea987ac

WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope

com.example.concurrency.sync.Apple object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 ac 87 a9 (00000001 10101100 10000111 10101001) (-1450726399)
4 4 (object header) 7e 00 00 00 (01111110 00000000 00000000 00000000) (126)
8 4 (object header) 43 c0 00 f8 (01000011 11000000 00000000 11111000) (-134168509)
12 4 int Apple.count 0

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
img

最后

我想问下大家当初选择做程序员的初衷是什么?有思考过这个问题吗?高薪?热爱?

既然入了这行就应该知道,这个行业是靠本事吃饭的,你想要拿高薪没有问题,请好好磨练自己的技术,不要抱怨。有的人通过培训可以让自己成长,有些人可以通过自律强大的自学能力成长,如果你两者都不占,还怎么拿高薪?

架构师是很多程序员的职业目标,一个好的架构师是不愁所谓的35岁高龄门槛的,到了那个时候,照样大把的企业挖他。为什么很多人想进阿里巴巴,无非不是福利待遇好以及优质的人脉资源,这对个人职业发展是有非常大帮助的。

如果你也想成为一名好的架构师,那或许这份Java核心架构笔记你需要阅读阅读,希望能够对你的职业发展有所帮助。

中高级开发必知必会:

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img

小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!**

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-NBcaogrl-1712807487178)]

最后

我想问下大家当初选择做程序员的初衷是什么?有思考过这个问题吗?高薪?热爱?

既然入了这行就应该知道,这个行业是靠本事吃饭的,你想要拿高薪没有问题,请好好磨练自己的技术,不要抱怨。有的人通过培训可以让自己成长,有些人可以通过自律强大的自学能力成长,如果你两者都不占,还怎么拿高薪?

架构师是很多程序员的职业目标,一个好的架构师是不愁所谓的35岁高龄门槛的,到了那个时候,照样大把的企业挖他。为什么很多人想进阿里巴巴,无非不是福利待遇好以及优质的人脉资源,这对个人职业发展是有非常大帮助的。

如果你也想成为一名好的架构师,那或许这份Java核心架构笔记你需要阅读阅读,希望能够对你的职业发展有所帮助。

中高级开发必知必会:

[外链图片转存中…(img-JYUtE0UX-1712807487178)]

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-xkWyeTEw-1712807487178)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值