java线程-synchronized详解

3476 篇文章 105 订阅

解决线程原子性问题,最常见的手段就是加锁,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锁呢?对象的存储结构如下:

其中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会在对象创建之后,直接进入偏向锁状态。

如上图所示:

  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操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。

锁消除

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. 不能够实现公平锁
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 我可以简单解释一下多线程synchronized的含义。synchronizedJava 语言中的一个关键字,它修饰的代码块,表示当前线程独占对象,其他线程无法对这段代码进行访问,也就是只有一个线程可以执行这段代码,其他线程只能等待。这样做的目的是为了保证共享资源的完整性和一致性。 ### 回答2: 多线程synchronized是一种用于实现线程安全的技术。在多线程编程中,当多个线程同时访问共享资源时,可能会出现数据不一致或产生竞态条件等问题。为了解决这些问题,可以使用synchronized关键字对关键代码块或方法进行同步。 synchronized关键字可以用于修饰代码块或方法。当修饰代码块时,需要指定对象作为锁,即synchronized(obj){},这样只有获取到锁的线程才能执行代码块内的内容,其他线程需要等待。而当修饰方法时,表示整个方法是同步的,相当于synchronized(this){}。 通过synchronized关键字,可以保证在同一时间只有一个线程可以执行被锁定的代码块或方法,从而避免了并发访问共享资源时的冲突。当一个线程执行完synchronized代码块或方法后,会释放锁,其他线程才能获取到锁并执行相应的代码。 synchronized还具有可重入性,即一个线程在获取到锁后,可以再次获取到该锁,而不会造成死锁。这是因为synchronized是基于持有锁的机制,当线程再次进入被synchronized修饰的代码块或方法时,会判断当前线程是否已经持有锁,如果是则可以继续执行,否则需要等待获取锁。 然而,synchronized也存在一些限制和局限性。首先,synchronized只能保证同一时间只能有一个线程执行被锁定的代码块或方法,不能满足多个线程同时访问的需求。其次,如果一个线程获取到锁后进入了死循环或长时间的阻塞操作,会导致其他线程一直等待无法获得锁,从而造成线程的饥饿问题。 总的来说,多线程synchronized是一种常用的线程同步技术,通过保证同一时间只有一个线程访问共享资源,从而避免了数据不一致和竞态条件等问题。然而,在使用synchronized时需要注意避免死锁和饥饿问题的发生。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值