java线程-synchronized详解

这篇具有很好参考价值的文章主要介绍了java线程-synchronized详解。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

解决线程原子性问题,最常见的手段就是加锁,Java提供了两种加锁的方式,一个synchronized隐式锁,另外一个是通过J.U.C框架提供的Lock显式加锁。本文主要介绍一个Synchronized的实现方式。

synchronized概述

synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized 翻译为中文的意思是同步,也称之为”同步锁“。

synchronized的作用是保证在同一时刻, 被修饰的代码块或方法只会有一个线程执行,以达到保证并发安全的效果。

synchronized的使用方式

基本语法

synchronized有两个作用范围:方法和局部代码块,代码示例如下

public class SynchronizedDemo {
​
  private int v;
  private static int a;
  private final Object lock = new Object();
​
  // 修饰非静态方法 对象锁
  public synchronized void add(int value) {
    v += value; // 临界区
  }
​
  public void sub(int value) {
    // 修饰局部代码块 对象锁
    synchronized (lock) {
      v -= value; // 临界区
    }
  }
​
  // 修饰静态方法 类锁
  public static synchronized void multi(int value) {
    a *= value; // 临界区
  }
​
  public static void div(int value) {
    // 修饰局部代码块 类锁
    synchronized (SynchronizedDemo.class) {
      a /= value; // 临界区
    }
  }
} 
复制代码

java编译器会在synchronized修饰的方法或代码块前后自动Lock,unlock。

  1. synchronized修饰代码块,锁定是个obj对象,或者是一个类,sychronized(this.class)
  2. synchronized修饰静态方法,锁定是当前类的class对象
  3. synchronized修饰非静态方法,锁定的是当前实例对象this。

synchronized底层实现原理

synchronized对应的字节码

使用javap -verbose SynchronizedDemo查看字节码文件

  public synchronized void add(int);
    descriptor: (I)V
    flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=2, args_size=2
         0: aload_0
         1: dup
         2: getfield      #4                  // Field v:I
         5: iload_1
         6: iadd
         7: putfield      #4                  // Field v:I
        10: return
      LineNumberTable:
        line 10: 0
        line 11: 10
        
  public void sub(int);
    descriptor: (I)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=3, locals=4, args_size=2
         0: aload_0
         1: getfield      #3                  // Field lock:Ljava/lang/Object;
         4: dup
         5: astore_2
         6: monitorenter
         7: aload_0
         8: dup
         9: getfield      #4                  // Field v:I
        12: iload_1
        13: isub
        14: putfield      #4                  // Field v:I
        17: aload_2
        18: monitorexit
        19: goto          27
        22: astore_3
        23: aload_2
        24: monitorexit
        25: aload_3
        26: athrow
        27: return
      Exception table:
         from    to  target type
             7    19    22   any
            22    25    22   any
      LineNumberTable:
        line 14: 0
        line 15: 7
        line 16: 17
        line 17: 27
        
  public static synchronized void multi(int);
    descriptor: (I)V
    flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #5                  // Field a:I
         3: iload_0
         4: imul
         5: putstatic     #5                  // Field a:I
         8: return
      LineNumberTable:
        line 20: 0
        line 21: 8
​
  public static void div(int);
    descriptor: (I)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #6                  // class com/shawn/study/deep/in/java/concurrency/thread/SynchronizedDemo
         2: dup
         3: astore_1
         4: monitorenter
         5: getstatic     #5                  // Field a:I
         8: iload_0
         9: idiv
        10: putstatic     #5                  // Field a:I
        13: aload_1
        14: monitorexit
        15: goto          23
        18: astore_2
        19: aload_1
        20: monitorexit
        21: aload_2
        22: athrow
        23: return
      Exception table:
         from    to  target type
             5    15    18   any
            18    21    18   any
      LineNumberTable:
        line 24: 0
        line 25: 5
        line 26: 13
        line 27: 23        
复制代码

从上述展示的字节码可以看出:

add()函数对应的字节码如下所示。实际上,编译器只不过是在函数的flags中添加了ACC_SYNCHRONIZED标记而已,其他部分跟没有添加synchronized的add()函数的字节码相同。

add()函数对应的字节码如下所示。字节码通过monitorenter和monitorexit来标记synchronized的作用范围。除此之外,对于以下字节码,我们有点需要解释。其一,以下字节码中有两个monitorexit,添加第二个monitorexit的目的是为了在代码抛出异常时仍然能解锁。其二,前面讲到,synchronized可以选择指定使用哪个对象的Monitor锁。具体使用哪个对象的Monitor锁,在字节码中,通过monitorenter前面的几行字节码来指定。

synchronized关键字底层使用的锁叫做Monitor锁。但是,我们无法直接创建和使用Monitor锁。Monitor锁是寄生存在的,每个对象都会拥有一个Monitor锁。如果我们想要使用一个新的Monitor锁,我们只需要使用一个新的对象,并在synchronized关键字后,附带声明要使用哪个对象的Monitor锁即可。

  • 当使用sychronized修饰方法的时候,编译器只不过是在函数的flags中添加了ACC_SYNCHRONIZED标记而已,其他部分跟没有添加synchronized的函数的字节码相同。

  • 当使用synchronized修饰局部代码块的时候,字节码通过monitorenter和monitorexit来标记synchronized的作用范围。但有两点需要再解释一下

    • synchronized关键字底层使用的锁叫做Monitor锁。但是,我们无法直接创建和使用Monitor锁。Monitor锁是寄生存在的,每个对象都会拥有一个Monitor锁,在字节码中,通过monitorenter前面的几行字节码来指定。
    • 以下字节码中有两个monitorexit,添加第二个monitorexit的目的是为了在代码抛出异常时仍然能解锁。

monitor锁实现原理

synchronized在底层使用不同的锁来实现,重量级锁,轻量级锁,偏向锁等。

实际上,synchronized使用的重量级锁,就是前面提到的对象上的Monitor锁。JVM有不同的实现版本,因此,Monitor锁也有不同的实现方式。在Hotspot JVM实现中,Monitor锁对应的实现类为ObjectMonitor类。因为Hotspot JVM是用C++实现的,所以,ObjectMonitor也是用C++代码定义的。ObjectMonitor包含的代码很多,我们只罗列一些与其基本实现原理相关的成员变量,如下所示。

class ObjectMonitor {
  void*  volatile _object; // 该Monitor锁所属的对象
  void*  volatile _owner;  // 获取到该Monitor锁的线程
  ObjectWaiter* volatile _EntryList; // 存储等待被唤醒的线程
  ObjectWaiter* volatile _cxq ; // 没有获取到锁的线程
  ObjectWaiter* volatile _WaitSet; // 存储调用了wait()的线程
}
复制代码

monitor如何与对象关联

_object表示该Monitor锁所属的对象,但是如何通过对象来找到对应的Monitor锁呢?对象的存储结构如下:

java线程-synchronized详解

其中Mark Word是个可变字段,根据不同的场景记录不同的信息,monitor锁的信息就是记录在此。

monitor如何实现加锁,解锁

ObjectMonitor Enter方法

互斥锁的基本功能:

  • 多个线程竞争获取锁;
  • 没有获取到锁的线程排队等待获取锁;
  • 锁释放之后会通知排队等待锁的线程去竞争锁;
  • 没有获取锁的线程会阻塞,并且对应的内核线程不再分配时间片;
  • 阻塞线程获取到锁之后取消阻塞,并且对应的内核线程恢复分配时间片。

其中加锁源代码如下:

void ObjectMonitor::EnterI(TRAPS) {
  ...
  // Try the lock - TATAS
  if (TryLock (Self) > 0) {
    assert(_succ != Self, "invariant");
    assert(_owner == Self, "invariant");
    assert(_Responsible != Self, "invariant");
    return;
  }
  ...
  // We try one round of spinning *before* enqueueing Self.
  //
  // If the _owner is ready but OFFPROC we could use a YieldTo()
  // operation to donate the remainder of this thread's quantum
  // to the owner.  This has subtle but beneficial affinity
  // effects.
​
  if (TrySpin (Self) > 0) {
    assert(_owner == Self, "invariant");
    assert(_succ != Self, "invariant");
    assert(_Responsible != Self, "invariant");
    return;
  }
  ...
  ObjectWaiter node(Self);
  // Push "Self" onto the front of the _cxq.
  // Once on cxq/EntryList, Self stays on-queue until it acquires the lock.
  // Note that spinning tends to reduce the rate at which threads
  // enqueue and dequeue on EntryList|cxq.
  ObjectWaiter * nxt;
  for (;;) {
    node._next = nxt = _cxq;
    if (Atomic::cmpxchg(&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;
    ...
    if ((SyncFlags & 2) && _Responsible == NULL) {
      Atomic::replace_if_null(Self, &_Responsible);
    }
    // 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 > MAX_RECHECK_INTERVAL) {
        recheckInterval = MAX_RECHECK_INTERVAL;
      }
    } else {
      TEVENT(Inflated enter - park UNTIMED);
      Self->_ParkEvent->park();
    }
​
    if (TryLock(Self) > 0) break;
    ...
  }
  ...
  if (_Responsible == Self) {
    _Responsible = NULL;
  }
  // 善后处理,比如将当前线程从等待队列 CXQ 中移除
  ...
}
​
复制代码

多个线程竞争获取锁

多个线程同时请求获取Monitor锁时,JVM会通过CAS操作,先检查_owner是否是null,如果_owner是null,再将自己的Thread对象的地址赋值给_owner,那么谁就获取到了monitor锁。

源代码:

int ObjectMonitor::TryLock (Thread * Self) {
   for (;;) {
      void * own = _owner ;
      if (own != NULL) return 0 ;
      if (Atomic::cmpxchg_ptr (Self, &_owner, NULL) == NULL) {
         // Either guarantee _recursions == 0 or set _recursions = 0.
         assert (_recursions == 0, "invariant") ;
         assert (_owner == Self, "invariant") ;
         // CONSIDER: set or assert that OwnerIsThread == 1
         return 1 ;
      }
      // The lock had been free momentarily, but we lost the race to the lock.
      // Interference -- the CAS failed.
      // We can either return -1 or retry.
      // Retry doesn't make as much sense because the lock was just acquired.
      if (true) return -1 ;
   }
}
复制代码

源码中有段需要注意的是:先检查再执行这类复合操作是非线程安全的,那么就会存在多个线程有可能同时检查到_owner为null的情况,然后都去改变_owner。为了解决这个问题,JVM采用CPU提供的cmpxchg_ptr指令,通过给总线加锁的方式,来保证了以上CAS操作的线程安全性。

没有获取到锁的线程排队等待获取锁

多个线程竞争Monitor锁,成功获取到锁的线程就去执行代码,没有获取到锁的线程会放入ObjectMonitor的_cxq单向链表中等待锁

锁释放之后会通知排队等待锁的线程去竞争锁

当持有Monitor锁的线程释放了锁之后,JVM会从_EntryList中取出一个线程,再取竞争Monitor锁。

如果_EntryList中没有线程,JVM会先将_CXQ中所有线程全部搬移到_EntryList中,然后再从_EntryList中取线程。

没有获取锁的线程会阻塞,并且对应的内核线程不再分配时间片

一个java线程会对应一个内核线程。应用程序会将java线程要执行的代码,交给其对应的内核线程来执行。内核线程在执行过程中,如果没有竞争到锁,则内核线程会调用park()函数将自己阻塞,这样CPU就不再分配时间片给它。

阻塞线程获取到锁之后取消阻塞,并且对应的内核线程恢复分配时间片

持有锁的线程在释放锁之后,从_EntryList中取出一个线程时,就会调用unpark()函数,取消对应内核线程的阻塞状态,这样它才能有机会去竞争monitor锁

ObjectMonitor Enter方法总结:

  1. ObjectMonitor 内部通过一个 CXQ 队列保存所有的等待线程
  2. 在实际进入队列之前,会反复尝试 lock,在某些系统上会存在 CPU 亲和力的优化
  3. 入队的时候,通过 ObjectWaiter 对象将当前线程包裹起来,并且入到 CXQ 队列的头部
  4. 入队成功以后,会根据当前线程是否为第一个等待线程做不同的处理
  5. 如果是第一个等待线程,会根据一个简单的「退避算法」来有条件的 wait
  6. 如果不是第一个等待线程,那么会执行无限期等待
  7. 线程的 park 在 posix 系统上是通过 pthread_cond_wait() 实现的
  8. 当一个线程获得对象锁成功之后,就可以执行自定义的同步代码块了

ObjectMonitor exit方法

当前线程执行完代码块以后,会进入到ObjectMonitor exit方法,释放当前对象锁,方便下一个线程来获取这个锁,下面我们逐步分析下 exit 的实现过程。

exit函数方法较长,但是整体上的结构比较清晰。

void ObjectMonitor::exit(bool not_suspended, TRAPS) {
  for (;;) {
    //...
    ObjectWaiter * w = NULL;
    int QMode = Knob_QMode;
    if (QMode == 2 && _cxq != NULL) {
      //
    }
​
    if (QMode == 3 && _cxq != NULL) {
      //
    }
​
    if (QMode == 4 && _cxq != NULL) {
      //
    }
​
    // ...
​
    if (QMode == 1) {
      //
    } else {
      // QMode == 0 or QMode == 2 
    }
    // ...
  }
}
​
复制代码

上面的 exit 函数整体上分为如下几个部分:

  1. 根据 Knob_QMode 的值和 _cxq 是否为空执行不同策略
  2. 根据一定策略唤醒等待队列中的下一个线程

其中Knob_QMode这个变量主要用来指定在 exit 的时候 EntryList 和 CXQ 队列之间的唤醒关系,也就是说,当 EntryList 和 CXQ 中都有等待的线程时,因为 exit 之后只能有一个线程得到锁,这个时候选择唤醒哪个队列中的线程是一个值得考虑的事情。JVM默认值为0,我暂时没有找到可以修改Knob_QMode的方法,除了重新编译JVM源代码,所以,这里我们暂时只讨论Knob_QMode=0的情况。代码如下:

void ObjectMonitor::exit(bool not_suspended, TRAPS) {
    ...
  for(;;) {
    ObjectWaiter * w = NULL ;
    w = _EntryList  ;
    if (w != NULL) {
      assert (w->TState == ObjectWaiter::TS_ENTER, "invariant") ;
      ExitEpilog (Self, w) ;
      return ;
    }
    w = _cxq ;
    if (w == NULL) continue ;
    for (;;) {
      assert (w != NULL, "Invariant") ;
      ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ;
      if (u == w) break ;
      w = u ;
    }
    assert (w != NULL              , "invariant") ;
    assert (_EntryList  == NULL    , "invariant") ;
    // 抽离出来的QMode == 0 or QMode == 2情况下代码;
    _EntryList = w ;
    ObjectWaiter * q = NULL ;
    ObjectWaiter * p ;
    // 将单向链表构造成双向环形链表;
    for (p = w ; p != NULL ; p = p->_next) {
      guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
      p->TState = ObjectWaiter::TS_ENTER ;
      p->_prev = q ;
      q = p ;
    }
    // _succ表示已经存在唤醒的线程;
    if (_succ != NULL) continue;
    w = _EntryList  ;
    if (w != NULL) {
      guarantee (w->TState == ObjectWaiter::TS_ENTER, "invariant") ;
      ExitEpilog (Self, w) ;
      return ;
    }
  }
}
复制代码
  1. 若EntryList队列的头节点_EntryList不为null,那么直接唤醒该头节点封装的线程,然后返回;
  2. 1的条件不满足,程序继续向下执行,若cxq队列的头节点_cxq为null,则跳过当次循环;
  3. 若程序继续向下执行说明cxq队列不为空,EntryList队列为空。接下来是一个内嵌的for循环,目的是取出cxq队列中的所有元素,方法是通过一个临时变量指针获得构成队列的整个链表,然后将_cxq指针置为NULL;
  4. 第二个内嵌for循环是QMode == 0策略的内容,目的在于将第三步得到的单向链表倾倒(drain)进EntryList队列,具体方法是将_EntryList指针指向单向链表的头节点,然后通过for循环将单向链表构造成双向环形链表;
  5. 通过ExitEpilog函数释放monitor锁并唤醒EntryList队列的头节点;

锁优化

锁的类型

对于一个synchronized锁,如果它只被一个线程使用,那么,synchronzied锁底层使用偏向锁来实现。如果它被多个线程交叉使用(你用完我再用),不存在竞争使用的情况,那么,synchronized锁底层使用轻量级锁来实现。如果它存在被竞争使用的情况,那么,synchronized锁底层使用重量级锁来实现。

上面再讲到重量级锁需要用到对象头的Mark Word,实际上,偏向锁和轻量级锁也要用到Mark Word。

无锁在Mark Word中的记录有unused(25bits)、hashCode(31bits)、cms_free、GC age、偏向1、锁标志位01

偏向锁

偏向锁在Mark Word中的记录有,threadId(51bits)、epoch(2bits)、cms_free(1bit)、GC age(4bits)、偏向1,锁标志位01

如果我们设置了JVM参数-XX:BiasedLockingStartupDelay=0,那么,Mark Word会在对象创建之后,直接进入偏向锁状态。

java线程-synchronized详解

如上图所示:

  1. 当一个对象被创建出来,还没有持有偏向锁,此时Mark Word字段中的threadID为0,当前线程会使用CPU提供的CAS原子操作来竞争这个偏向锁。
  2. 当threadID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中threadID设置为当前线程ID
  3. 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
  4. 如果CAS已经成功获取到偏向锁,那就开始执行代码,如果执行完代码,线程也不会立刻解锁偏向锁,也就是不会更改threadID为0。这是偏向锁有别于轻量级锁和重量级锁。这样做的目的是提高加锁的效率,当同一个线程再次请求这个偏向锁的时候,线程会查看Mark Word,发现还是处于偏向锁状态,并且threadID就是自己的threadID,线程不再需要做任何加锁操作,就可以直接执行业务代码。
  5. 偏向锁不会主动解锁,当其他线程尝试获取偏向锁时,持有偏向锁的线程才会释放锁,JVM需要暂停持有偏向锁的线程,然后查看它是否还在使用这个偏向锁,如果线程不再使用这个偏向锁,那么jvm就会将Mark Word设置为无锁状态。如果线程还在使用这个偏向锁,那么虚拟机就将偏向锁升级为轻量级锁。

jvm需要根据持有偏向锁的线程是否正在使用偏向锁,来决定将锁升级为无锁还是偏向锁,这是一个CAS的复合操作,存在线程安全问题,但又无法使用CPU提供的CAS指令来实现,所以解决方案就是jvm会复用垃圾回收器中的STW功能,来停止持有偏向锁的线程。

轻量级锁和自旋锁

当一个线程去竞争锁时,它会先检查Mark Word的的锁标志位,如果锁标志位是01并且相邻偏向位为0(无锁状态)或锁标志位是00(轻量级锁状态),那么,这就说明锁已经升级到了轻量级锁。

如果是无锁状态,jvm会将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方称之为Displaced Mark Word),其目的主要是为了轻量级锁解锁时快速恢复到无锁状态。

拷贝成功后,JVM将使用CAS操作尝试将Mark Word中的Lock Record指针更新为指向自己的Lock Record。

如果更新成功,那么这个线程就拥有了该对象的锁,并将对象的Mark Word的标志位设置为“00”,表示此对象已经是轻量级锁状态。

如果更新失败,按理来说应该要升级成重量级锁,但是JVM对此做了优化,JVM首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数(默认是10次,可以使用-XX:PreBlockSpin来更改),或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。

这里有个问题,自旋多少次合适?如果自旋次数太少,有可能刚升级为重量级锁,另一个线程就释放了轻量级锁,这样就很可惜。如果自旋次数很多,CPU就会做了很多无用功。针对这个问题,JVM发明了自适应自旋锁。

自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK 6中变为默认开启,并且引入了自适应的自旋锁)。如果上次自旋之后成功等到了另一个线程释放轻量级锁,那么下次自旋的次数就增加,如果上次自旋没有等到等到另一个线程释放轻量级锁,那么下次自旋的次数就减少。

如果线程自旋等待轻量级锁失败,只能将轻量级线程升级为重量级线程。跟偏向锁升级不同的是,轻量级锁升级不需要STW,因为所有的CAS操作都是由硬件提供的原子CAS指令来完成的。

重量级锁

升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。这个就是创建Monitor锁的过程

总结

综上,偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。

java线程-synchronized详解

锁消除

JVM在执行JIT编译的时候,会根据对代码的逃逸分析,去掉某些没有必要的锁。例如:

public String concat(String s1, String s2){
  StringBuffer buffer = new StringBuffer(); 
  buffer.append(s1).append(s2);
  return buffer.toString();
}
复制代码

StringBuffer中的append函数使用了Sychronized修饰,加了锁,但是,buffer是局部变量,不会被多线程共享,更不会在多线程环境下调用它的append()函数,所以append函数的锁可以被优化消除。

锁粗化

JVM在执行JIT编译时,可能会扩大锁的范围,对多个小范围代码的加锁,合并成一个对大范围代码加锁的操作叫做锁粗化。例如:

private StringBuffer buffer = new StringBuffer();
​
public void reproduce(String s){
  for(int i = 0; i < 10000; i ++){
    buffer.append(s);
  }
}
复制代码

执行10000次append,会加锁解锁10000次,通过锁粗化,编译器将append函数的锁去掉,移到for循环外面,这样只需要加锁解锁一次就可以了。

Synchronized的缺点

  1. 无法判断获取锁的状态。
  2. 虽然会自动释放锁,但如果如果锁的那个方法执行时间较长就会一直占用着不去释放,不能让使用同一把锁的方法继续执行,影响程序的运行。不能设置超时。
  3. 当多个线程尝试获取锁时,未获取到锁的线程会不断的尝试获取锁,而不会发生中断,这样会造成性能消耗。
  4. 不能够实现公平锁

作者:Shawn_Shawn
原文链接:https://juejin.cn/post/7195569817940672572
 文章来源地址https://www.toymoban.com/news/detail-494795.html

到了这里,关于java线程-synchronized详解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【JavaEE】多线程之线程安全(synchronized篇),死锁问题

    线程安全问题 观察线程不安全 线程安全问题的原因  从原子性入手解决线程安全问题 ——synchronized synchronized的使用方法  synchronized的互斥性和可重入性 死锁 死锁的三个典型情况  死锁的四个必要条件  破除死锁 在前面的章节中,我们也了解到多线程为我们的程序带来了

    2024年02月01日
    浏览(55)
  • Synchronized与Java线程的关系

    ​ Java多线程处理任务时,为了线程安全,通常会对共享资源进行加锁,拿到锁的线程才能进行访问共享资源。而加锁方式通过都是Synchronized锁或者Lock锁。 ​ 那么多线程在协同工作的时候,线程状态的变化都与锁对象有关系。 ​ Java采用synchronized、以互斥同步的方式

    2024年02月11日
    浏览(31)
  • 线程的状态,多线程带来的风险,synchronized关键字及死锁问题

    目录 状态  线程的意义 多线程带来的风险——线程安全✅ 线程安全的概念 线程不安全的原因 抢占式执行,随机性调度 修改共享数据 原子性-加🔒 可见性 指令重排序 解决线程不安全问题(学完线程再总结) synchronized——监视器锁monitor lock​编辑   互斥 使用示例

    2024年02月06日
    浏览(51)
  • Java多线程篇(1)——深入分析synchronized

    synchronized实现原理的由浅入深依次为 字节码层面:monitorenter/monitorexit 指令 java对象层面: Mark Word 对象头 JVM层面: CAS、自旋 、 ObjectMonitor(MESA管层模型:cxq,entryList,wait三个队列) 操作系统层面: mutex 锁 其中 mark word 对象头如下图: 说到锁升级,我相信很多人都错

    2024年02月09日
    浏览(36)
  • Java多线程(4)---死锁和Synchronized加锁流程

    目录 前言 一.synchronized 1.1概念  1.2Synchronized是什么锁? 1.3Synchronized加锁工作过程 1.4其他优化操作 二.死锁 2.1什么是死锁 2.2死锁的几个经典场景 2.3死锁产生的条件 2.4如何解决死锁 🎁个人主页:tq02的博客_CSDN博客-C语言,Java,Java数据结构领域博主 🎥 本文由 tq02 原创,首发于

    2024年02月13日
    浏览(40)
  • 【创作赢红包】Java多线程:synchronized锁方法块

    synchronized同步代码块 用synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法执行一个较长时间的任务,那么B线程必须等待比较长的时间。这种情况下可以尝试使用synchronized同步语句块来解决问题。看一下例子:     运行结果,分两部分来看: synchr

    2023年04月09日
    浏览(38)
  • Java并发编程(三)线程同步 上[synchronized/volatile]

    当使用多个线程来访问同一个数据时,将会导致数据不准确,相互之间产生冲突,非常容易出现线程安全问题,比如多个线程都在操作同一数据,都打算修改商品库存,这样就会导致数据不一致的问题。 所以我们通过线程同步机制来保证线程安全,加入同步锁以避免在该线程没有完成

    2024年02月13日
    浏览(41)
  • 【并发多线程】java并发中的Synchronized关键词

    如果在多线程的环境中,我们经常会遇到资源竞争的情况,比如多个线程要去同时修改同一个共享变量,这时候,就需要对资源的访问方法进行一定的处理,保证同一时间只有一个线程访问。 java提供了synchronized,方便我们实现上述操作。 我们举个例子,我们创建一个

    2023年04月13日
    浏览(44)
  • java八股文面试[多线程]——synchronized锁升级过程

    速记:偏向-轻量-重量 上面讲到锁有四种状态,并且会因实际情况进行膨胀升级,其膨胀方向是: 无锁——偏向锁——轻量级锁——重量级锁 ,并且膨胀方向 不可逆 一.锁升级理论. 在synchronized锁升级过程中涉及到以下几种锁.先说一下这几种锁是什么意思. 偏向锁: 只有一个

    2024年02月10日
    浏览(42)
  • 多线程系列(十六) -常用并发原子类详解

    在 Java 的 java.util.concurrent 包中,除了提供底层锁、并发同步等工具类以外,还提供了一组原子操作类,大多以 Atomic 开头,他们位于 java.util.concurrent.atomic 包下。 所谓原子类操作,顾名思义,就是这个操作要么全部执行成功,要么全部执行失败,是保证并发编程安全的重要一

    2024年03月11日
    浏览(50)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包