java并发笔记四之synchronized 锁的膨胀过程(锁的升级过程)深入剖析
警告⚠️:本文耗时很长,先做好心理准备,建议PC端浏览器浏览效果更佳。
-
当没有竞争出现时,默认会使用偏向锁。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏向锁可以降低无竞争开销。
-
如果有另外的线程试图锁定某个已经被偏向过的对象,JVM 就需要撤销(revoke)偏向锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用轻量级锁;否则,进一步升级为重量级锁
1 2 3 4 5 6 7 8 9 10 11 12 13 | public class TestDemo { } public class DemoExample1 { static TestDemo testDemo; public static void main(String[] args) throws Exception { testDemo= new TestDemo(); synchronized (testDemo){ System.out.println( "lock ing" ); testDemo.hashCode(); System.out.println(ClassLayout.parseInstance(testDemo).toPrintable()); } } } |
1 | javap -c DemoExample1. class |
- 当执行 monitorenter 时,如果目标锁对象的计数器为 0,那么说明它没有被其他线程所持有。在这个情况下,Java 虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加 1。
- 在目标锁对象的计数器不为 0 的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加 1,否则需要等待,直至持有线程释放该锁。当执行 monitorexit 时,Java 虚拟机则需将锁对象的计数器减 1。当计数器减为 0 时,那便代表该锁已经被释放掉了。
- 之所以采用这种计数器的方式,是为了允许同一个线程重复获取同一把锁。举个例子,如果一个 Java 类中拥有多个 synchronized 方法,那么这些方法之间的相互调用,不管是直接的还是间接的,都会涉及对同一把锁的重复加锁操作。因此,我们需要设计这么一个可重入的特性,来避免编程里的隐式约束。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | public class DemoExample3 { public int sharedState; public void nonSafeAction() { while (sharedState < 100000 ) { int former = sharedState++; int latter = sharedState; if (former != latter - 1 ) { System.out.println( "Observed data race, former is " + former + ", " + "latter is " + latter); } } } public static void main(String[] args) throws InterruptedException { final DemoExample3 demoExample3 = new DemoExample3(); Thread thread1 = new Thread() { @Override public void run() { demoExample3.nonSafeAction(); } }; Thread thread2 = new Thread() { @Override public void run() { demoExample3.nonSafeAction(); } }; thread1.start(); thread2.start(); thread1.join(); thread2.join(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | public class DemoExample3 { public int sharedState; public void nonSafeAction() { while (sharedState < 100000 ) { synchronized ( this ) { int former = sharedState++; int latter = sharedState; if (former != latter - 1 ) { System.out.println( "Observed data race, former is " + former + ", " + "latter is " + latter); } } } } public static void main(String[] args) throws InterruptedException { final DemoExample3 demoExample3 = new DemoExample3(); Thread thread1 = new Thread() { @Override public void run() { demoExample3.nonSafeAction(); } }; Thread thread2 = new Thread() { @Override public void run() { demoExample3.nonSafeAction(); } }; thread1.start(); thread2.start(); thread1.join(); thread2.join(); } } |
这次看下加上synchronized关键字的打印出来的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | // Handles the uncommon case in locking, i.e., contention or an inflated lock. JRT_BLOCK_ENTRY( void , SharedRuntime::complete_monitor_locking_C(oopDesc* _obj, BasicLock* lock, JavaThread* thread )) // Disable ObjectSynchronizer::quick_enter() in default config // on AARCH64 and ARM until JDK-8153107 is resolved. if (ARM_ONLY((SyncFlags & 256) != 0 &&) AARCH64_ONLY((SyncFlags & 256) != 0 &&) !SafepointSynchronize::is_synchronizing()) { // Only try quick_enter() if we're not trying to reach a safepoint // so that the calling thread reaches the safepoint more quickly. if (ObjectSynchronizer::quick_enter(_obj, thread , lock)) return ; } // NO_ASYNC required because an async exception on the state transition destructor // would leave you with the lock held and it would never be released. // The normal monitorenter NullPointerException is thrown without acquiring a lock // and the model is that an exception implies the method failed. JRT_BLOCK_NO_ASYNC oop obj(_obj); if (PrintBiasedLockingStatistics) { Atomic::inc(BiasedLocking::slow_path_entry_count_addr()); } Handle h_obj(THREAD, obj); //在 JVM 启动时,我们可以指定是否开启偏向锁 if (UseBiasedLocking) { // Retry fast entry if bias is revoked to avoid unnecessary inflation <strong> //fast_enter 是我们熟悉的完整锁获取路径</strong> ObjectSynchronizer::fast_enter(h_obj, lock, true , CHECK); } else { //slow_enter 则是绕过偏向锁,直接进入轻量级锁获取逻辑 ObjectSynchronizer::slow_enter(h_obj, lock, CHECK); } assert (!HAS_PENDING_EXCEPTION, "Should have no exception here" ); JRT_BLOCK_END JRT_END |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 | // ----------------------------------------------------------------------------- // Fast Monitor Enter/Exit // This the fast monitor enter. The interpreter and compiler use // some assembly copies of this code. Make sure update those code // if the following function is changed. The implementation is // extremely sensitive to race condition. Be careful. void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) { if (UseBiasedLocking) { if (!SafepointSynchronize::is_at_safepoint()) { //biasedLocking定义了偏向锁相关操作,revoke_and_rebias revokeatsafepoint 则定义了当检测到安全点时的处理 BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD); if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) { return ; } } else { assert (!attempt_rebias, "can not rebias toward VM thread" ); BiasedLocking::revoke_at_safepoint(obj); } assert (!obj->mark()->has_bias_pattern(), "biases should be revoked by now" ); } //如果获取偏向锁失败,则进入 slow_enter,锁升级 slow_enter(obj, lock, THREAD); } // ----------------------------------------------------------------------------- // Interpreter/Compiler Slow Case // This routine is used to handle interpreter/compiler slow case // We don't need to use fast path here, because it must have been // failed in the interpreter/compiler code. void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) { markOop mark = obj->mark(); assert (!mark->has_bias_pattern(), "should not see bias pattern here" ); if (mark->is_neutral()) { // Anticipate successful CAS -- the ST of the displaced mark must // be visible <= the ST performed by the CAS. // 将目前的 Mark Word 复制到 Displaced Header 上 lock->set_displaced_header(mark); // 利用 CAS 设置对象的 Mark Wo if (mark == obj()->cas_set_mark((markOop) lock, mark)) { return ; } // Fall through to inflate() … // 检查存在竞争 } else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) { assert (lock != mark->locker(), "must not re-lock the same lock" ); assert (lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock”); // 清除 lock->set_displaced_header(NULL); return ; } // The object header will never be displaced to this lock, // so it does not matter what the value is, except that it // must be non-zero to avoid looking like a re-entrant lock, // and must not look locked either. // 重置 Displaced Header lock->set_displaced_header(markOopDesc::unused_mark()); //锁膨胀 ObjectSynchronizer::inflate(THREAD, obj(), inflate_cause_monitor_enter)->enter(THREAD); } // This routine is used to handle interpreter/compiler slow case // We don't need to use fast path here, because it must have // failed in the interpreter/compiler code. Simply use the heavy // weight monitor should be ok, unless someone find otherwise. void ObjectSynchronizer::slow_exit(oop object, BasicLock* lock, TRAPS) { fast_exit(object, lock, THREAD); } //锁膨胀 ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) { // Inflate mutates the heap ... // Relaxing assertion for bug 6320749. assert (Universe::verify_in_progress() || !SafepointSynchronize::is_at_safepoint(), "invariant" ) ; for (;;) { //自旋 const markOop mark = object->mark() ; assert (!mark->has_bias_pattern(), "invariant" ) ; // The mark can be in one of the following states: // * Inflated - just return // * Stack-locked - coerce it to inflated // * INFLATING - busy wait for conversion to complete // * Neutral - aggressively inflate the object. // * BIASED - Illegal. We should never see this // CASE: inflated已膨胀,即重量级锁 if (mark->has_monitor()) { //判断当前是否为重量级锁 ObjectMonitor * inf = mark->monitor() ; //获取指向ObjectMonitor的指针 assert (inf->header()->is_neutral(), "invariant" ); assert (inf->object() == object, "invariant" ) ; assert (ObjectSynchronizer::verify_objmon_isinpool(inf), "monitor is invalid" ); return inf ; } // CASE: inflation in progress - inflating over a stack-lock.膨胀等待(其他线程正在从轻量级锁转为膨胀锁) // Some other thread is converting from stack-locked to inflated. // Only that thread can complete inflation -- other threads must wait. // The INFLATING value is transient. // Currently, we spin/yield/park and poll the markword, waiting for inflation to finish. // We could always eliminate polling by parking the thread on some auxiliary list. if (mark == markOopDesc::INFLATING()) { TEVENT (Inflate: spin while INFLATING) ; ReadStableMark(object) ; continue ; } // CASE: stack-locked栈锁(轻量级锁) // Could be stack-locked either by this thread or by some other thread. // // Note that we allocate the objectmonitor speculatively, _before_ attempting // to install INFLATING into the mark word. We originally installed INFLATING, // allocated the objectmonitor, and then finally STed the address of the // objectmonitor into the mark. This was correct, but artificially lengthened // the interval in which INFLATED appeared in the mark, thus increasing // the odds of inflation contention. // // We now use per-thread private objectmonitor free lists. // These list are reprovisioned from the global free list outside the // critical INFLATING...ST interval. A thread can transfer // multiple objectmonitors en-mass from the global free list to its local free list. // This reduces coherency traffic and lock contention on the global free list. // Using such local free lists, it doesn't matter if the omAlloc() call appears // before or after the CAS(INFLATING) operation. // See the comments in omAlloc(). if (mark->has_locker()) { ObjectMonitor * m = omAlloc (Self) ; //获取一个可用的ObjectMonitor // Optimistically prepare the objectmonitor - anticipate successful CAS // We do this before the CAS in order to minimize the length of time // in which INFLATING appears in the mark. m->Recycle(); m->_Responsible = NULL ; m->OwnerIsThread = 0 ; m->_recursions = 0 ; m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ; // Consider: maintain by type/class markOop cmp = (markOop) Atomic::cmpxchg_ptr (markOopDesc::INFLATING(), object->mark_addr(), mark) ; if (cmp != mark) { //CAS失败//CAS失败,说明冲突了,自旋等待//CAS失败,说明冲突了,自旋等待//CAS失败,说明冲突了,自旋等待 omRelease (Self, m, true ) ; //释放监视器锁 continue ; // Interference -- just retry } // We've successfully installed INFLATING (0) into the mark-word. // This is the only case where 0 will appear in a mark-work. // Only the singular thread that successfully swings the mark-word // to 0 can perform (or more precisely, complete) inflation. // // Why do we CAS a 0 into the mark-word instead of just CASing the // mark-word from the stack-locked value directly to the new inflated state? // Consider what happens when a thread unlocks a stack-locked object. // It attempts to use CAS to swing the displaced header value from the // on-stack basiclock back into the object header. Recall also that the // header value (hashcode, etc) can reside in (a) the object header, or // (b) a displaced header associated with the stack-lock, or (c) a displaced // header in an objectMonitor. The inflate() routine must copy the header // value from the basiclock on the owner's stack to the objectMonitor, all // the while preserving the hashCode stability invariants. If the owner // decides to release the lock while the value is 0, the unlock will fail // and control will eventually pass from slow_exit() to inflate. The owner // will then spin, waiting for the 0 value to disappear. Put another way, // the 0 causes the owner to stall if the owner happens to try to // drop the lock (restoring the header from the basiclock to the object) // while inflation is in-progress. This protocol avoids races that might // would otherwise permit hashCode values to change or "flicker" for an object. // Critically, while object->mark is 0 mark->displaced_mark_helper() is stable. // 0 serves as a "BUSY" inflate-in-progress indicator // fetch the displaced mark from the owner's stack. // The owner can't die or unwind past the lock while our INFLATING // object is in the mark. Furthermore the owner can't complete // an unlock on the object, either. markOop dmw = mark->displaced_mark_helper() ; assert (dmw->is_neutral(), "invariant" ) ; //CAS成功,设置ObjectMonitor的_header、_owner和_object等 // Setup monitor fields to proper values -- prepare the monitor m->set_header(dmw) ; // Optimization: if the mark->locker stack address is associated // with this thread we could simply set m->_owner = Self and // m->OwnerIsThread = 1. Note that a thread can inflate an object // that it has stack-locked -- as might happen in wait() -- directly // with CAS. That is, we can avoid the xchg-NULL .... ST idiom. m->set_owner(mark->locker()); m->set_object(object); // TODO-FIXME: assert BasicLock->dhw != 0. // Must preserve store ordering. The monitor state must // be stable at the time of publishing the monitor address. guarantee (object->mark() == markOopDesc::INFLATING(), "invariant" ) ; object->release_set_mark(markOopDesc::encode(m)); // Hopefully the performance counters are allocated on distinct cache lines // to avoid false sharing on MP systems ... if (ObjectMonitor::_sync_Inflations != NULL) ObjectMonitor::_sync_Inflations->inc() ; TEVENT(Inflate: overwrite stacklock) ; if (TraceMonitorInflation) { if (object->is_instance()) { ResourceMark rm; tty->print_cr( "Inflating object " INTPTR_FORMAT " , mark " INTPTR_FORMAT " , type %s" , ( void *) object, ( intptr_t ) object->mark(), object->klass()->external_name()); } } return m ; } // CASE: neutral 无锁 // TODO-FIXME: for entry we currently inflate and then try to CAS _owner. // If we know we're inflating for entry it's better to inflate by swinging a // pre-locked objectMonitor pointer into the object header. A successful // CAS inflates the object *and* confers ownership to the inflating thread. // In the current implementation we use a 2-step mechanism where we CAS() // to inflate and then CAS() again to try to swing _owner from NULL to Self. // An inflateTry() method that we could call from fast_enter() and slow_enter() // would be useful. assert (mark->is_neutral(), "invariant" ); ObjectMonitor * m = omAlloc (Self) ; // prepare m for installation - set monitor to initial state m->Recycle(); m->set_header(mark); m->set_owner(NULL); m->set_object(object); m->OwnerIsThread = 1 ; m->_recursions = 0 ; m->_Responsible = NULL ; m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ; // consider: keep metastats by type/class if (Atomic::cmpxchg_ptr (markOopDesc::encode(m), object->mark_addr(), mark) != mark) { m->set_object (NULL) ; m->set_owner (NULL) ; m->OwnerIsThread = 0 ; m->Recycle() ; omRelease (Self, m, true ) ; m = NULL ; continue ; // interference - the markword changed - just retry. // The state-transitions are one-way, so there's no chance of // live-lock -- "Inflated" is an absorbing state. } // Hopefully the performance counters are allocated on distinct // cache lines to avoid false sharing on MP systems ... if (ObjectMonitor::_sync_Inflations != NULL) ObjectMonitor::_sync_Inflations->inc() ; TEVENT(Inflate: overwrite neutral) ; if (TraceMonitorInflation) { if (object->is_instance()) { ResourceMark rm; tty->print_cr( "Inflating object " INTPTR_FORMAT " , mark " INTPTR_FORMAT " , type %s" , ( void *) object, ( intptr_t ) object->mark(), object->klass()->external_name()); } } return m ; } } |
1、整个膨胀过程在自旋下完成;
2、mark->has_monitor()方法判断当前是否为重量级锁,即Mark Word的锁标识位为 10,如果当前状态为重量级锁,执行步骤(3),否则执行步骤(4);
3、mark->monitor()方法获取指向ObjectMonitor的指针,并返回,说明膨胀过程已经完成;
4、如果当前锁处于膨胀中,说明该锁正在被其它线程执行膨胀操作,则当前线程就进行自旋等待锁膨胀完成,这里需要注意一点,虽然是自旋操作,但不会一直占用cpu资源,每隔一段时间会通过os::NakedYield方法放弃cpu资源,或通过park方法挂起;如果其他线程完成锁的膨胀操作,则退出自旋并返回;
5、如果当前是轻量级锁状态,即锁标识位为 00,膨胀过程如下:
- 通过omAlloc方法,获取一个可用的ObjectMonitor monitor,并重置monitor数据;
- 通过CAS尝试将Mark Word设置为markOopDesc:INFLATING,标识当前锁正在膨胀中,如果CAS失败,说明同一时刻其它线程已经将Mark Word设置为markOopDesc:INFLATING,当前线程进行自旋等待膨胀完成;
- 如果CAS成功,设置monitor的各个字段:_header、_owner和_object等,并返回;
6、如果是无锁,重置监视器值;
-
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
-
在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。
-
当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。
-
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态。撤销偏向锁后恢复到无锁(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
-
偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | //创建一个啥都没有的类: public class TestDemo {} public class DemoExample { static TestDemo testDemo; public static void main(String[] args) throws Exception { //此处睡眠50000ms,取消jvm默认偏向锁延迟4000ms Thread.sleep( 5000 ); testDemo= new TestDemo(); //hash计算? //testDemo.hashCode(); System.out.println( "befor lock" ); //无锁:偏向锁? System.out.println(ClassLayout.parseInstance(testDemo).toPrintable()); synchronized (testDemo){ System.out.println( "lock ing" ); System.out.println(ClassLayout.parseInstance(testDemo).toPrintable()); } System.out.println( "after lock" ); System.out.println(ClassLayout.parseInstance(testDemo).toPrintable()); } } |
-
具体来说,在线程进行加锁时,如果该锁对象支持偏向锁,那么 Java 虚拟机会通过 CAS操作,将当前线程的地址记录在锁对象的标记字段之中,并且将标记字段的最后三位设置为:1 01;
-
在接下来的运行过程中,每当有线程请求这把锁,Java 虚拟机只需判断锁对象标记字段中:最后三位是否为: 1 01,是否包含当前线程的地址,以及 epoch 值是否和锁对象的类的epoch 值相同。如果都满足,那么当前线程持有该偏向锁,可以直接返回;
-
我们先从偏向锁的撤销讲起。当请求加锁的线程和锁对象标记字段保持的线程地址不匹配时(而且 epoch 值相等,如若不等,那么当前线程可以将该锁重偏向至自己),Java 虚拟机需要撤销该偏向锁。这个撤销过程非常麻烦,它要求持有偏向锁的线程到达安全点,再将偏向锁替换成轻量级锁;
-
如果某一类锁对象的总撤销数超过了一个阈值(对应 jvm参数 -XX:BiasedLockingBulkRebiasThreshold,默认为 20),那么 Java 虚拟机会宣布这个类的偏向锁失效;(这里说的就是批量重偏向)
1 2 3 4 5 | product(intx, BiasedLockingBulkRebiasThreshold, 20, \ "Threshold of number of revocations per type to try to " \ "rebias all objects in the heap of that type" ) \ range(0, max_intx) \ constraint(BiasedLockingBulkRebiasThresholdFunc,AfterErgo) \ |
-
具体的做法便是在每个类中维护一个 epoch 值,你可以理解为第几代偏向锁。当设置偏向锁时,Java 虚拟机需要将该 epoch 值复制到锁对象的标记字段中;
-
在宣布某个类的偏向锁失效时,Java 虚拟机实则将该类的 epoch 值加 1,表示之前那一代的偏向锁已经失效。而新设置的偏向锁则需要复制新的 epoch 值;
-
为了保证当前持有偏向锁并且已加锁的线程不至于因此丢锁,Java 虚拟机需要遍历所有线程的 Java 栈,找出该类已加锁的实例,并且将它们标记字段中的 epoch 值加 1。该操作需要所有线程处于安全点状态;
-
如果总撤销数超过另一个阈值(对应 jvm 参数 -XX:BiasedLockingBulkRevokeThreshold,默认值为 40),那么 Java 虚拟机会认为这个类已经不再适合偏向锁。此时,Java 虚拟机会撤销该类实例的偏向锁,并且在之后的加锁过程中直接为该类实例设置轻量级锁(这里说的就是偏向批量撤销)
1 2 3 4 5 | product(intx, BiasedLockingBulkRevokeThreshold, 40, \ "Threshold of number of revocations per type to permanently " \ "revoke biases of all objects in the heap of that type" ) \ range(0, max_intx) \ constraint(BiasedLockingBulkRevokeThresholdFunc,AfterErgo) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | public class TestDemo { } public class DemoExample4 { public static void main(String[] args) throws InterruptedException { test1(); } public class DemoExample5 { public static void main(String[] args) throws InterruptedException { test1(); } /** * 仅证明批量重偏向 * @throws InterruptedException */ public static void test1() throws InterruptedException { List<TestDemo> list = new ArrayList<>(); for ( int i = 0 ; i < 100 ; i++) { list.add( new TestDemo()); } Thread t1 = new Thread(()->{ System.out.println( "加锁前 get(0) 应该是无锁可偏向 " + ClassLayout.parseInstance(list.get( 0 )).toPrintable()); for (TestDemo a:list ) { synchronized (a){ System.out.print( "加锁 >" ); } } System.out.println(); System.out.println( "加锁后 get(0) 应该是偏向锁" +ClassLayout.parseInstance(list.get( 0 )).toPrintable()); try { TimeUnit.SECONDS.sleep( 1000 ); //这里不让线程死,防止线程ID复用 } catch (InterruptedException e) { e.printStackTrace(); } }); t1.start(); TimeUnit.SECONDS.sleep( 5 ); Thread t2 = new Thread(()->{ for ( int i = 0 ; i < 40 ; i++) { TestDemo a = list.get(i); synchronized (a){ System.out.print( "加锁 >" ); } if (i== 18 ){ System.out.println(); System.out.println( "加锁后 get(18) 应该是无锁(轻量级锁释放) " +ClassLayout.parseInstance(list.get(i)).toPrintable()); } if (i== 19 ){ //开始重偏向 System.out.println(); System.out.println( "加锁后 get(19) 应该是偏向锁 " +ClassLayout.parseInstance(list.get(i)).toPrintable()); System.out.println( "加锁后 get(0) 应该是无锁(轻量级锁释放) " +ClassLayout.parseInstance(list.get( 0 )).toPrintable()); System.out.println( "加锁后 get(99) 应该是偏向锁 偏向t1 " +ClassLayout.parseInstance(list.get( 99 )).toPrintable()); } if (i== 20 ){ System.out.println(); System.out.println( "加锁后 get(20) 应该是偏向锁 " +ClassLayout.parseInstance(list.get(i)).toPrintable()); } } }); t2.start(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | public class TestDemo { } public class DemoExample7 { public static void main(String[] args) throws Exception { List<TestDemo> list = new ArrayList<>(); //初始化数据 for ( int i = 0 ; i < 100 ; i++) { list.add( new TestDemo()); } Thread t1 = new Thread() { String name = "1" ; public void run() { System.out.printf(name); for (TestDemo a : list) { synchronized (a) { if (a == list.get( 10 )) { System.out.println( "t1 预期是偏向锁" + 10 + ClassLayout.parseInstance(a).toPrintable()); } } } try { Thread.sleep( 100000 ); } catch (InterruptedException e) { e.printStackTrace(); } } }; t1.start(); Thread.sleep( 5000 ); System.out.println( "main 预期是偏向锁" + 10 + ClassLayout.parseInstance(list.get( 10 )).toPrintable()); Thread t2 = new Thread() { String name = "2" ; public void run() { System.out.printf(name); for ( int i = 0 ; i < 100 ; i++) { TestDemo a = list.get(i); // hack 为了在批量重偏向发生后再次加锁,前面使用了轻量级锁的对象 if (i == 20 ) { a = list.get( 9 ); } synchronized (a) { if (i == 10 ) { //已经经过偏向锁撤销,并使用轻量级锁的对象,释放后 状态依为001 无锁状态 System.out.println( "t2 i=10 get(1)预期是无锁" + ClassLayout.parseInstance(list.get( 1 )).toPrintable()); //因为和t1交替使用对象a 没有发生竞争,但偏向锁已偏向,另外不满足重偏向条件,所以使用轻量级锁 System.out.println( "t2 i=10 get(i) 预期轻量级锁 " + i + ClassLayout.parseInstance(a).toPrintable()); } if (i == 19 ) { //已经经过偏向锁撤销,并使用轻量级锁的对象,在批量重偏向发生后。不会影响现有的状态 状态依然为001 System.out.println( "t2 i=19 get(10)预期是无锁" + 10 + ClassLayout.parseInstance(list.get( 10 )).toPrintable()); //满足重偏向条件后,已偏向的对象可以重新使用偏向锁 将线程id指向当前线程,101 System.out.println( "t2 i=19 get(i) 满足重偏向条件20 预期偏向锁 " + i + ClassLayout.parseInstance(a).toPrintable()); //满足重偏向条件后,已偏向还为需要加锁的对象依然偏向线程1 因为偏向锁的撤销是发生在下次加锁的时候。这里没有执行到同步此对象,所以依然偏向t1 System.out.println( "t2 i=19 get(i) 满足重偏向条件20 但后面的对象没有被加锁,所以依旧偏向t1 " + i + ClassLayout.parseInstance(list.get( 40 )).toPrintable()); } if (i == 20 ) { //满足重偏向条件后,再次加锁之前使用了轻量级锁的对象,依然轻量级锁,证明重偏向这个状态只针对偏向锁。已经发生锁升级的,不会退回到偏向锁 System.out.println( "t2 i=20 满足偏向条件之后,之前被设置为无锁状态的对象,不可偏向,这里使用的是轻量级锁 get(9)预期是轻量级锁 " + ClassLayout.parseInstance(a).toPrintable()); } } } try { Thread.sleep( 100000 ); } catch (InterruptedException e) { e.printStackTrace(); } } }; t2.start(); Thread.sleep( 5000 ); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | public class TestDemo { } public class DemoExample6 { public static void main(String[] args) throws InterruptedException { test2(); } /** * 证明偏量偏向撤销 * @throws InterruptedException */ public static void test2() throws InterruptedException { List<TestDemo> list = new ArrayList<TestDemo>(); for ( int i = 0 ; i < 100 ; i++) { list.add( new TestDemo()); } Thread t1 = new Thread(()->{ System.out.println( "加锁前 get(0) 应该是无锁可偏向 " +ClassLayout.parseInstance(list.get( 0 )).toPrintable()); for (TestDemo a:list ) { synchronized (a){ System.out.print( "加锁 >" ); } } System.out.println(); System.out.println( "加锁后 get(0) 应该是偏向锁" +ClassLayout.parseInstance(list.get( 0 )).toPrintable()); try { TimeUnit.SECONDS.sleep( 1000 ); //这里不让线程死,防止线程ID复用 } catch (InterruptedException e) { e.printStackTrace(); } }); t1.start(); TimeUnit.SECONDS.sleep( 5 ); Thread t2 = new Thread(()->{ for ( int i = 0 ; i < 100 ; i++) { TestDemo a = list.get(i); synchronized (a){ System.out.println(Thread.currentThread().getId()+ "加锁 >" ); } try { TimeUnit.MILLISECONDS.sleep( 100 ); } catch (InterruptedException e) { e.printStackTrace(); } if (i== 9 ){ //这里刚好是第19个上锁的(同样是第19个偏向锁升级的) System.out.println(); System.out.println( "加锁后 get(9) 应该是无锁(轻量级锁释放) " +ClassLayout.parseInstance(list.get(i)).toPrintable()); } if (i== 10 ){ //这里刚好是第21个上锁的 System.out.println(); System.out.println( "加锁后 get(10) 应该是偏向锁 偏向t2 " +ClassLayout.parseInstance(list.get(i)).toPrintable()); } if (i== 50 ){ //50开始升级为轻量级锁(同样是第21个偏向锁升级的) System.out.println(); System.out.println( "加锁后 get(50) 无锁(轻量级锁释放) " +ClassLayout.parseInstance(list.get(i)).toPrintable()); } if (i== 59 ){ //60(同样是第39个偏向锁升级的) System.out.println(); System.out.println( "加锁后 get(59) 无锁(轻量级锁释放) " +ClassLayout.parseInstance(list.get(i)).toPrintable()); } if (i== 69 ){ //69(同样是第59个偏向锁升级的) System.out.println(); System.out.println( "加锁后 get(69) 无锁(轻量级锁释放) " +ClassLayout.parseInstance(list.get(i)).toPrintable()); TestDemo a1 = new TestDemo(); synchronized (a1){ System.out.println( "偏向撤销发生后的该类新建的对象都不会再偏向任何线程 " +ClassLayout.parseInstance(a1).toPrintable()); } } } }); Thread t3 = new Thread(()->{ for ( int i = 99 ; i >= 0 ; i--) { TestDemo a = list.get(i); synchronized (a){ System.out.println(Thread.currentThread().getId()+ "加锁 >" ); } try { TimeUnit.MILLISECONDS.sleep( 100 ); } catch (InterruptedException e) { e.printStackTrace(); } /** * 重点:重偏向撤销 */ if (i== 40 ){ //40升级为轻量级锁(同样是第40个偏向锁升级的,这时候发生偏向撤销) System.out.println(); System.out.println( "加锁后 get(" +i+ ") 应该是无锁(轻量级锁释放) " +ClassLayout.parseInstance(list.get( 0 )).toPrintable()); TestDemo a1 = new TestDemo(); synchronized (a1){ System.out.println( "偏向撤销发生后的该类新建的对象都不会再偏向任何线程 " +ClassLayout.parseInstance(a1).toPrintable()); } } if (i== 30 ){ //39升级为轻量级锁(同样是第42个偏向锁升级的) System.out.println(); System.out.println( "加锁后 get(" +i+ ") 应该是无锁(轻量级锁释放) " +ClassLayout.parseInstance(list.get( 0 )).toPrintable()); TestDemo a1 = new TestDemo(); synchronized (a1){ System.out.println( "偏向撤销发生后的该类新建的对象都不会再偏向任何线程 " +ClassLayout.parseInstance(a1).toPrintable()); } } } }); t2.start(); TimeUnit.MILLISECONDS.sleep( 50 ); t3.start(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | public class TestDemo { } public class DemoExample8 { public static void main(String[] args) throws Exception { List<TestDemo> list = new ArrayList<>(); List<TestDemo> list2 = new ArrayList<>(); List<TestDemo> list3 = new ArrayList<>(); for ( int i = 0 ; i < 100 ; i++) { list.add( new TestDemo()); list2.add( new TestDemo()); list3.add( new TestDemo()); } //偏向锁 System.out.println( "初始状态" + 10 + ClassLayout.parseClass(TestDemo. class ).toPrintable()); Thread t1 = new Thread() { String name = "1" ; public void run() { System.out.printf(name); for (TestDemo a : list) { synchronized (a) { if (a == list.get( 10 )) { //偏向锁 System.out.println( "t1 预期是偏向锁" + 10 + ClassLayout.parseInstance(a).toPrintable()); } } } try { Thread.sleep( 100000 ); } catch (InterruptedException e) { e.printStackTrace(); } } }; t1.start(); Thread.sleep( 5000 ); //偏向锁 System.out.println( "main 预期是偏向锁" + 10 + ClassLayout.parseInstance(list.get( 10 )).toPrintable()); Thread t2 = new Thread() { String name = "2" ; public void run() { System.out.printf(name); for ( int i = 0 ; i < 100 ; i++) { TestDemo a = list.get(i); synchronized (a) { if (a == list.get( 10 )) { System.out.println( "t2 i=10 get(1)预期是无锁" + ClassLayout.parseInstance(list.get( 1 )).toPrintable()); //偏向锁 System.out.println( "t2 i=10 get(10) 预期轻量级锁 " + i + ClassLayout.parseInstance(a).toPrintable()); //偏向锁 } if (a == list.get( 19 )) { System.out.println( "t2 i=19 get(10)预期是无锁" + 10 + ClassLayout.parseInstance(list.get( 10 )).toPrintable()); //偏向锁 System.out.println( "t2 i=19 get(19) 满足重偏向条件20 预期偏向锁 " + i + ClassLayout.parseInstance(a).toPrintable()); //偏向锁 System.out.println( "类的对象累计撤销达到20" ); } } } try { Thread.sleep( 100000 ); } catch (InterruptedException e) { e.printStackTrace(); } } }; t2.start(); Thread.sleep( 5000 ); Thread t3 = new Thread() { String name = "3" ; public void run() { System.out.printf(name); for (TestDemo a : list2) { synchronized (a) { if (a == list2.get( 10 )) { System.out.println( "t3 预期是偏向锁" + 10 + ClassLayout.parseInstance(a).toPrintable()); //偏向锁 } } } try { Thread.sleep( 100000 ); } catch (InterruptedException e) { e.printStackTrace(); } } }; t3.start(); Thread.sleep( 5000 ); Thread t4 = new Thread() { String name = "4" ; public void run() { System.out.printf(name); for ( int i = 0 ; i < 100 ; i++) { TestDemo a = list2.get(i); synchronized (a) { if (a == list2.get( 10 )) { System.out.println( "t4 i=10 get(1)预期是无锁" + ClassLayout.parseInstance(list2.get( 1 )).toPrintable()); //偏向锁 System.out.println( "t4 i=10 get(10) 当前不满足重偏向条件 20 预期轻量级锁 " + i + ClassLayout.parseInstance(a).toPrintable()); //偏向锁 } if (a == list2.get( 19 )) { System.out.println( "t4 i=19 get(10)预期是无锁" + 10 + ClassLayout.parseInstance(list2.get( 10 )).toPrintable()); //偏向锁 System.out.println( "t4 i=19 get(19) 当前满足重偏向条件 20 但A类的对象累计撤销达到40 预期轻量级锁 " + i + ClassLayout.parseInstance(a).toPrintable()); //偏向锁 System.out.println( "类的对象累计撤销达到40" ); } if (a == list2.get( 20 )) { System.out.println( "t4 i=20 get(20) 当前满足重偏向条件 20 预期轻量级锁 " + i + ClassLayout.parseInstance(a).toPrintable()); //偏向锁 } } } } }; t4.start(); Thread.sleep( 5000 ); System.out.println( "main 预期是偏向锁" + 10 + ClassLayout.parseInstance(list3.get( 0 )).toPrintable()); //偏向锁 Thread t5 = new Thread() { String name = "5" ; public void run() { System.out.printf(name); for (TestDemo a : list3) { synchronized (a) { if (a == list3.get( 10 )) { System.out.println( "t5 预期是轻量级锁,类的对象累计撤销达到40 不可以用偏向锁了" + 10 + ClassLayout.parseInstance(a).toPrintable()); //偏向锁 } } } try { Thread.sleep( 100000 ); } catch (InterruptedException e) { e.printStackTrace(); } } }; t5.start(); Thread.sleep( 5000 ); System.out.println( "main 预期是偏向锁" + 10 + ClassLayout.parseInstance(list.get( 10 )).toPrintable()); //偏向锁 Thread t6 = new Thread() { String name = "6" ; public void run() { System.out.printf(name); for ( int i = 0 ; i < 100 ; i++) { TestDemo a = list3.get(i); synchronized (a) { if (a == list3.get( 10 )) { System.out.println( "t6 i=10 get(1)预期是无锁" + ClassLayout.parseInstance(list3.get( 1 )).toPrintable()); //偏向锁 System.out.println( "t6 i=10 get(10) 预期轻量级锁 " + i + ClassLayout.parseInstance(a).toPrintable()); //偏向锁 } if (a == list3.get( 19 )) { System.out.println( "t6 i=19 get(10)预期是无锁" + 10 + ClassLayout.parseInstance(list3.get( 10 )).toPrintable()); //偏向锁 System.out.println( "t6 i=19 get(19) 满足重偏向条件20 但类的对象累计撤销达到40 不可以用偏向锁了 " + i + ClassLayout.parseInstance(a).toPrintable()); //偏向锁 } } } try { Thread.sleep( 100000 ); } catch (InterruptedException e) { e.printStackTrace(); } } }; t6.start(); Thread.sleep( 5000 ); System.out.println( "由于撤销锁次数达到默认的 BiasedLockingBulkRevokeThreshold=40 这里实例化的对象 是无锁状态" + ClassLayout.parseInstance( new TestDemo()).toPrintable()); //偏向锁 System.out.println( "撤销偏向后状态" + 10 + ClassLayout.parseInstance( new TestDemo()).toPrintable()); //偏向锁 } } |
撤销偏向后状态10com.boke.TestDemo 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) bf c3 00 f8 (10111111 11000011 00000000 11111000) (-134167617)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
- 当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
- 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后拷贝对象头中的Mark Word复制到锁记录中。
- 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word。
- 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。
- 如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。
- 若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
- 多个线程在不同的时间段请求同一把锁,也就是说没有锁竞争。针对这种情形,Java 虚拟机采用了轻量级锁,来避免重量级锁的阻塞以及唤醒
- 在没有锁竞争的前提下,减少传统锁使用OS互斥量产生的性能损耗
- 在竞争激烈时,轻量级锁会多做很多额外操作,导致性能下降
- 可以认为两个线程交替执行的情况下请求同一把锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | public class TestDemo { } public class DemoExample9 { public static void main(String[] args) throws Exception { TestDemo testDemo = new TestDemo(); //子线程 Thread t1 = new Thread(){ @Override public void run() { synchronized (testDemo){ System.out.println( "t1 lock ing" ); System.out.println(ClassLayout.parseInstance(testDemo).toPrintable()); } } }; t1.join(); //主线程 synchronized (testDemo){ System.out.println( "main lock ing" ); System.out.println(ClassLayout.parseInstance(testDemo).toPrintable()); } } } |
- 多个线程竞争同一个锁的时候,虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程;
- Java 线程的阻塞以及唤醒,都是依靠操作系统来完成的:os pthread_mutex_lock() ;
- 升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态
分析一个由轻量级锁膨胀成重量级锁的案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | public class TestDemo { } public class DemoExample9 { public static void main(String[] args) throws Exception { TestDemo testDemo = new TestDemo(); Thread t1 = new Thread(){ @Override public void run() { synchronized (testDemo){ System.out.println( "t1 lock ing" ); System.out.println(ClassLayout.parseInstance(testDemo).toPrintable()); } } }; t1.start(); synchronized (testDemo){ System.out.println( "main lock ing" ); System.out.println(ClassLayout.parseInstance(testDemo).toPrintable()); } } } |
运行结果:
-
当进行加锁操作时,Java 虚拟机会判断是否已经是重量级锁。如果不是,它会在当前线程的当前栈桢中划出一块空间,作为该锁的锁记录,并且将锁对象的标记字段复制到该锁记录中。
-
然后,Java 虚拟机会尝试用 CAS(compare-and-swap)操作替换锁对象的标记字段。这里解释一下,CAS 是一个原子操作,它会比较目标地址的值是否和期望值相等,如果相等,则替换为一个新的值。
-
假设当前锁对象的标记字段为 X…XYZ,Java 虚拟机会比较该字段是否为 X…X01。如果是,则替换为刚才分配的锁记录的地址。由于内存对齐的缘故,它的最后两位为 00。此时,该线程已成功获得这把锁,可以继续执行了。
-
如果不是 X…X01,那么有两种可能。第一,该线程重复获取同一把锁。此时,Java 虚拟机会将锁记录清零,以代表该锁被重复获取。第二,其他线程持有该锁。此时,Java 虚拟机会将这把锁膨胀为重量级锁,并且阻塞当前线程。
-
当进行解锁操作时,如果当前锁记录(你可以将一个线程的所有锁记录想象成一个栈结构,每次加锁压入一条锁记录,解锁弹出一条锁记录,当前锁记录指的便是栈顶的锁记录)的值为 0,则代表重复进入同一把锁,直接返回即可。
-
否则,Java 虚拟机会尝试用 CAS 操作,比较锁对象的标记字段的值是否为当前锁记录的地址。如果是,则替换为锁记录中的值,也就是锁对象原本的标记字段。此时,该线程已经成
-
功释放这把锁。
- 如果不是,则意味着这把锁已经被膨胀为重量级锁。此时,Java 虚拟机会进入重量级锁的释放过程,唤醒因竞争该锁而被阻塞了的线程

-
偏向锁只会在第一次请求时采用 CAS 操作,在锁对象的标记字段中记录下当前线程的地址。在之后的运行过程中,持有该偏向锁的线程的加锁操作将直接返回。它针对的是锁仅会被同一线程持有的情况。
-
轻量级锁采用 CAS 操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段。它针对的是多个线程在不同时间段申请同一把锁的情况。
-
重量级锁会阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况。Java 虚拟机采取了自适应自旋,来避免线程在面对非常小的 synchronized 代码块时,仍会被阻塞、唤醒的情况。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步