StringBuffer、StringBuilder区别以及Synchronized原理

1.为什么StringBuffer是线程安全的StringBuilder是线程不安全的

StringBuffer、StringBuilder区别以及Synchronized原理_第1张图片
StringBuffer、StringBuilder区别以及Synchronized原理_第2张图片
这里我只是列举了几个方法来对比,其他方法对比可以查看两个类的源码,从上面的截图可以看到StringBuffer实现线程安全是通过Synchronized字段来实现的,那么Synchronized的原理又是怎样实现的呢?下文将对此讨论。

2.Synchronized原理

2.1synchronized的三种应用方式
  • 修饰实例方法 对当前实例加锁
  • 修饰静态方法 对当前类对象加锁
  • 修饰代码块 对指定对象加锁

修饰实例方法


  public synchronized StringBuffer append(int i) {
        toStringCache = null;
        super.append(i);
        return this;
    }

修饰静态方法


 public static synchronized void test(){
        i++;
    }

修饰代码块

    public boolean contentEquals(CharSequence cs) {
        // Argument is a StringBuffer, StringBuilder

      synchronized(cs) {
                   return nonSyncContentEquals((AbstractStringBuilder)cs);
         } 
    }
2.2motitorenter、monitorexit指令

示例代码

public class Test
{
   public void test(String str) {
    synchronized (str) {
    System.out.println(str);
    }
  }
}

执行javac Test.java 生成Test.class文件
执行 javap -verbose -p Test.class,生成如下信息(只选取关键部分):
StringBuffer、StringBuilder区别以及Synchronized原理_第3张图片
上面的字节码中包含一个 monitorenter 指令以及多个monitorexit指令,分别表示了JVM获取锁以及JVM解除锁。

  public synchronized void test2(String str) {
    System.out.println(str);
  }

执行 javap -verbose -p Test.class,生成如下信息(只选取关键部分):
StringBuffer、StringBuilder区别以及Synchronized原理_第4张图片
当用 synchronized 标记方法时,这里会生成ACC_SYNCHRONIZED标记,这个标记表示在进入这个方法的时候JVM要进行motitorenter操作,当方法退出的时候,JVM 进行monitorexit操作。所以这里可以将ACC_SYNCHRONIZED标记理解为隐式的motitorenter、monitorexit操作。

2.3锁算法实现原理

在 Java 虚拟机中,每个 Java 对象都有一个对象头(object header),这个由标记字段和类型
指针所构成。其中,标记字段用以存储 Java 虚拟机有关该对象的运行数据,如哈希码、GC 信息
以及锁信息,而类型指针则指向该对象的类。

当JVM对对象加锁的时候,如果锁计数器为0,表示没有被任何线程持有,这时当前线程会持有该锁对象,会将锁计数器器加1,锁对象的持有该锁的线程指针会指向当前线程。如果锁对象的计数器不为0,如果持有该所对象的线程是当前线程,计数器加1,否则需要等待直到持有锁对象的线程释放该锁。
当JVM释放锁的时候会将计数减1,当计数器等于0,表示该锁被释放。

采取这种锁计数器方式,是考虑到同一线程重复获取锁的场景,举个例子一个类可能有多个方法被
synchronized 修饰,这些synchronized方法之间互相调用就是重复获取锁。

上面是对锁算法原理的描述,在HotSpot虚拟器中锁的具体实现主要涉及到以下几个概念:
重量级锁、轻量级锁、偏向锁,下面会逐一介绍这几个概念,在介绍前先看下下面这个表格(就是对象头里面的锁信息描述),结合这个表格理解会更加容易一些:

StringBuffer、StringBuilder区别以及Synchronized原理_第5张图片
重量级锁
JVM会阻塞加锁失败的线程,当目标锁被释放的时候,再唤醒这些被阻塞的线程。阻塞线程、唤醒线程是一个比较重的操作,所以这正为什么被叫做“重量级锁”的原因。当然JVM也不是加锁失败就一味地阻塞唤醒,对此是有做优化的,JVM在线程进入阻塞之前和唤醒后任然竞争不到锁的时候,进入自旋状态——执行无用指令并且轮询锁是否放开,如果此时所释放线程能竞争到锁就无需进入阻塞状态了。当然自选会有一个副作用,那就是导致不公平,处于阻塞的线程没有办法立刻竞争释放的锁,自旋下的线程可能优先获得锁。

轻量级锁
多个线程在不同的时间段请求同一把锁,也就是说没有锁竞争。针对这种情形,Java 虚拟机采用了轻量级锁,来避免重量级锁的阻塞以及唤醒。

偏向锁
从始至终只有一个线程请求某一把锁

上面对几个概念做了介绍,那么线程在进行加锁,对象头信息又是如何变化的呢?
线程加锁的时,JVM会比较标记位最后两位是不是01(表示无锁或者偏向锁),如果时的话,则修改锁记录地址,最后两位被修改成00(表示轻量级锁),这时候线程已经持有该锁了。如果不是01结尾,则说明该锁已经被持有,可能时别的线程也可能时当前线程自己持有,如果是该线程重复获取同一把锁,则清空所记录,如果是其他线程持有锁,该锁会被膨胀成重量级锁,当前线程被阻塞。

你可能感兴趣的:(java)