JUC并发编程 Ⅱ -- 共享模型之管程(上)

共享带来的问题

两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

static int counter = 0;
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            counter++;
        }
    }, "t1");
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            counter--;
        }
    }, "t2");
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    log.debug("{}",counter);
}

问题分析

以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析

例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i

而对应 i-- 也是类似:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i

而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:

在这里插入图片描述

如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:

在这里插入图片描述

但多线程下这 8 行代码可能交错运行: 出现负数的情况:

在这里插入图片描述

出现正数的情况:

在这里插入图片描述

临界区 Critical Section

  • 一个程序运行多个线程本身是没有问题的

  • 问题出在多个线程访问共享资源

    • 多个线程读共享资源其实也没有问题

    • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题

  • 一段代码块内如果存在对共享资源的(多线程)读写操作,称这段代码块为临界区

static int counter = 0;
static void increment()
    // 临界区
{
    counter++;
}
static void decrement()
    // 临界区
{
    counter--;
}

竞态条件 Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

synchronized 解决方案

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案:原子变量

这里使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁 的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

注意

虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:

  • 互斥是保证临界区的竞态条件不发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

synchronized语法

synchronized(对象) // 线程1, 线程2(blocked)
{
    临界区
}

注意:多个线程这里使用的是同一个对象

解决方案

static int counter = 0;
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            synchronized (room) {
                counter++;
            }
        }
    }, "t1");
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            synchronized (room) {
                counter--;
            }
        }
    }, "t2");
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    log.debug("{}",counter);
}

图示流程
在这里插入图片描述

理解:
在这里插入图片描述
你可以做这样的类比:

  • synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人进行计算,线程 t1,t2 想象成两个人
  • 当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行count++ 代码
  • 这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切换,阻塞住了
  • 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外 (不要错误理解为锁住了对象就能一直执行下去哦),这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才能开门进入
  • 当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥匙给他(如果还有其他多个线程,那么就将这多个线程都唤醒,然后这些线程再去竞争那把锁)。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码

思考

synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。

为了加深理解,请思考下面的问题

  • 如果把 synchronized(obj) 放在 for 循环的外面,如何理解?
    在这里插入图片描述

    • 放在外面相当于给整个循环加上了锁。原来是保护四行字节码指令的原子性,现在就是保护两万行字节码的原子性,也就是整个5000次循环不会被其他线程所干扰执行顺序
  • 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?

    • 不可以,t1,t2如果锁的不是一个对象,那么当发生上下文切换的时候,别的线程不会被阻塞
  • 如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?

    • t2不加锁的话他可以直接拿到共享变量,不会被阻塞住

面向对象改进

把需要保护的共享变量放入一个类

class Room {
    int value = 0;
    public void increment() {
        synchronized (this) {
            value++;
        }
    }
    public void decrement() {
        synchronized (this) {
            value--;
        }
    }
    public int get() {
        synchronized (this) {
            return value;
        }
    }
}

@Slf4j
public class Test1 {

    public static void main(String[] args) throws InterruptedException {
        Room room = new Room();
        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 5000; j++) {
                room.increment();
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 5000; j++) {
                room.decrement();
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("count: {}" , room.get());
    }
}

方法上的 synchronized

加在方法上

class Test{
    public synchronized void test() {

    }
}
//等价于
class Test{
    public void test() {
        synchronized(this) {

        }
    }
}

加在静态方法上

class Test{
    public synchronized static void test() {
    }
}
等价于
class Test{
    public static void test() {
        synchronized(Test.class) {

        }
    }
}

线程八锁

其实就是考察 synchronized 锁住的是哪个对象

情况1:12 或 21

@Slf4j(topic = "c.Number")
class Number{
    public synchronized void a() {
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }
}
public static void main(String[] args) {
    Number n1 = new Number();
    new Thread(()->{ n1.a(); }).start();
    new Thread(()->{ n1.b(); }).start();
}

情况2:1s后12,或 2 1s后 1

@Slf4j(topic = "c.Number")
class Number{
    public synchronized void a() {
        sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }
}
public static void main(String[] args) {
    Number n1 = new Number();
    new Thread(()->{ n1.a(); }).start();
    new Thread(()->{ n1.b(); }).start();
}

情况3:3 1s 12 或 23 1s 1 或 32 1s 1

@Slf4j(topic = "c.Number")
class Number{
    public synchronized void a() {
        sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }
    public void c() {
        log.debug("3");
    }
}
public static void main(String[] args) {
    Number n1 = new Number();
    new Thread(()->{ n1.a(); }).start();
    new Thread(()->{ n1.b(); }).start();
    new Thread(()->{ n1.c(); }).start();
}

情况4:2 1s 后 1

@Slf4j(topic = "c.Number")
class Number{
    public synchronized void a() {
        sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }
}
public static void main(String[] args) {
    Number n1 = new Number();
    Number n2 = new Number();
    new Thread(()->{ n1.a(); }).start();
    new Thread(()->{ n2.b(); }).start();
}

情况5:2 1s 后 1

@Slf4j(topic = "c.Number")
class Number{
    public static synchronized void a() {
        sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }
}
public static void main(String[] args) {
    Number n1 = new Number();
    new Thread(()->{ n1.a(); }).start();
    new Thread(()->{ n1.b(); }).start();
}

情况6:1s 后12, 或 2 1s后 1

@Slf4j(topic = "c.Number")
class Number{
    public static synchronized void a() {
        sleep(1);
        log.debug("1");
    }
    public static synchronized void b() {
        log.debug("2");
    }
}
public static void main(String[] args) {
    Number n1 = new Number();
    new Thread(()->{ n1.a(); }).start();
    new Thread(()->{ n1.b(); }).start();
}

情况7:2 1s 后 1

@Slf4j(topic = "c.Number")
class Number{
    public static synchronized void a() {
        sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }
}
public static void main(String[] args) {
    Number n1 = new Number();
    Number n2 = new Number();
    new Thread(()->{ n1.a(); }).start();
    new Thread(()->{ n2.b(); }).start();
}

情况8:1s 后12, 或 2 1s后 1

@Slf4j(topic = "c.Number")
class Number{
    public static synchronized void a() {
        sleep(1);
        log.debug("1");
    }
    public static synchronized void b() {
        log.debug("2");
    }
}
public static void main(String[] args) {
    Number n1 = new Number();
    Number n2 = new Number();
    new Thread(()->{ n1.a(); }).start();
    new Thread(()->{ n2.b(); }).start();
}

变量的线程安全分析

成员变量和静态变量是否线程安全?

  • 如果它们没有共享,则线程安全
  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?

  • 局部变量是线程安全的
  • 但局部变量引用的对象则未必
    • 如果该对象没有逃离方法的作用访问,它是线程安全的
    • 如果该对象逃离方法的作用范围,需要考虑线程安全

从jvm的层面理解
局部变量处于栈帧中的局部变量表,而虚拟机栈属于线程独有的,所以一般局部变量是线程安全的。但是如果局部变量是一个对象的引用则线程不安全。因为对象存在与堆中,而堆是所有线程共有的一块区域。而所谓的逃离方法的作用范围是说,在方法中存在指针指向这个堆中的对象可以对其进行状态的改变,这就造成了临界区内发生竞态条件,而在方法结束之后这个对象也不会回收(因为被其它对象引用着,被可达性分析算法标记为存活对象)。

局部变量线程安全分析

public static void test1() {
    int i = 10;
    i++;
}

多个线程调用 test1() 方法时,局部变量 i会在每个线程的栈帧内存中被创建多份,实际上就是在每个线程之内操作,因此不存在共享。

public static void test1();
 descriptor: ()V 
 flags: ACC_PUBLIC, ACC_STATIC
 Code:
 stack=1, locals=1, args_size=0
 0: bipush 10
 2: istore_0
 3: iinc 0, 1
 6: return
 LineNumberTable:
 line 10: 0
 line 11: 3
 line 12: 6
 LocalVariableTable:
 Start Length Slot Name Signature
 3        4     0   i      I

注意:
这里的i++只对应iinc 0, 1这一条字节码指令,因此不存在原子性问题,和我们前面看到的静态变量自增不一样。

如图

在这里插入图片描述

局部变量的引用稍有不同

先看一个成员变量的例子

class ThreadUnsafe {
    ArrayList<String> list = new ArrayList<>();
    public void method1(int loopNumber) {
        for (int i = 0; i < loopNumber; i++) {
            // { 临界区, 会产生竞态条件
            method2();
            method3();
            // } 临界区
        }
    }
    private void method2() {
        list.add("1");
    }
    private void method3() {
        list.remove(0);
    }
}

执行

static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
    ThreadUnsafe test = new ThreadUnsafe();
    for (int i = 0; i < THREAD_NUMBER; i++) {
        new Thread(() -> {
            test.method1(LOOP_NUMBER);
        }, "Thread" + i).start();
    }
}

其中一种情况是,如果线程2 还未 add,线程1 remove 就会报错:

Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 
 at java.util.ArrayList.rangeCheck(ArrayList.java:657) 
 at java.util.ArrayList.remove(ArrayList.java:496) 
 at cn.itcast.n6.ThreadUnsafe.method3(TestThreadSafe.java:35) 
 at cn.itcast.n6.ThreadUnsafe.method1(TestThreadSafe.java:26) 
 at cn.itcast.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14) 
 at java.lang.Thread.run(Thread.java:748) 

分析:

  • 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
  • method3 与 method2 分析相同

在这里插入图片描述

将 list 修改为局部变量

class ThreadSafe {
    public final void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            method2(list);
            method3(list);
        }
    }
    private void method2(ArrayList<String> list) {
        list.add("1");
    }
    private void method3(ArrayList<String> list) {
        list.remove(0);
    }
}

那么就不会有上述问题了

分析:

  • list 是局部变量,每个线程调用时会创建其不同实例,没有共享
  • 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象
  • method3 的参数分析与 method2 相同

在这里插入图片描述

方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?

  • 情况1:有其它线程调用 method2 和 method3
  • 情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,
class ThreadSafe {
    public final void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            method2(list);
            method3(list);
        }
    }
    private void method2(ArrayList<String> list) {
        list.add("1");
    }
    private void method3(ArrayList<String> list) {
        list.remove(0);
    }
}
class ThreadSafeSubClass extends ThreadSafe{
    @Override
    public void method3(ArrayList<String> list) {
        new Thread(() -> {
            list.remove(0);
        }).start();
    }
    //线程不安全
}

因为子类重写的method3中又新开了一个线程,这也就是说在method1方法中存在临界区(对共享资源list有读写操作)和竞态条件(多线程操作),所以说会产生线程安全问题。

我们控制不了子类的行为,这就容易造成局部变量的引用会暴露给其他线程。这就引起了线程安全问题。

从这个例子可以看出 private 或 final 可以为程序的线程安全问题提供一定的支持。

常见线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent 包下的类

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为

Hashtable table = new Hashtable();
new Thread(()->{
    table.put("key", "value1");
}).start();
new Thread(()->{
    table.put("key", "value2");
}).start();
  • 它们的每个方法是原子的
  • 但注意它们多个方法的组合不是原子的

线程安全类方法的组合

分析下面代码是否线程安全?

Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
	table.put("key", value);
}

在这里插入图片描述

不可变类线程安全性

String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?

这是因为这些方法的返回值都创建了一个新的对象,而不是直接改变String、Integer对象本身。

实例分析

例1:

public class MyServlet extends HttpServlet {
    // 是否安全?
    Map<String,Object> map = new HashMap<>();
    // 是否安全?
    String S1 = "...";
    // 是否安全?
    final String S2 = "...";
    // 是否安全?
    Date D1 = new Date();
    // 是否安全?
    final Date D2 = new Date();

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        // 使用上述变量
    }
}
  • map线程不安全,线程安全的Map实现有HashTable
  • S1是字符串是不可变得,所以线程安全。S2更不用说也是线程安全的
  • Date不在常见的线程安全类中,作为成员变量,如果有多个线程对他进行读写并不是线程安全的
  • 使用final修饰Date类也是线程不安全的。因为final只能限定其引用不发生变化,他Date对象的状态是可以发生改变的。

例2:

public class MyServlet extends HttpServlet {
    // 是否安全?
    private UserService userService = new UserServiceImpl();

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}
public class UserServiceImpl implements UserService {
    // 记录调用次数
    private int count = 0;

    public void update() {
        // ...
        count++;
    }
}

线程不安全。count 作为共享数据,update方法里面是一个临界区,如果多线程参与存在竞态条件。

例3:

@Aspect
@Component
public class MyAspect {
    // 是否安全?
    private long start = 0L;

    @Before("execution(* *(..))")
    public void before() {
        start = System.nanoTime();
    }

    @After("execution(* *(..))")
    public void after() {
        long end = System.nanoTime();
        System.out.println("cost time:" + (end-start));
    }
}

此类不是线程安全的,MyAspect切面类只有一个实例,成员变量start 会被多个线程同时进行读写操作

例4:

public class MyServlet extends HttpServlet {
    // 是否安全
    private UserService userService = new UserServiceImpl();

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}
public class UserServiceImpl implements UserService {
    // 是否安全
    private UserDao userDao = new UserDaoImpl();

    public void update() {
        userDao.update();
    }
}
public class UserDaoImpl implements UserDao {
    public void update() {
        String sql = "update user set password = ? where username = ?";
        // 是否安全
        try (Connection conn = DriverManager.getConnection("","","")){
            // ...
        } catch (Exception e) {
            // ...
        }
    }
}

此例是典型的三层模型调用,MyServlet UserServiceImpl UserDaoImpl类都只有一个实例,UserDaoImpl类中没有成员变量,update方法里的变量引用的对象不是线程共享的,所以是线程安全的;UserServiceImpl类中只有一个线程安全的UserDaoImpl类的实例,那么UserServiceImpl类也是线程安全的,同理 MyServlet也是线程安全的

例5:

public class MyServlet extends HttpServlet {
    // 是否安全
    private UserService userService = new UserServiceImpl();

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}
public class UserServiceImpl implements UserService {
    // 是否安全
    private UserDao userDao = new UserDaoImpl();

    public void update() {
        userDao.update();
    }
}
public class UserDaoImpl implements UserDao {
    // 是否安全
    private Connection conn = null;
    public void update() throws SQLException {
        String sql = "update user set password = ? where username = ?";
        conn = DriverManager.getConnection("","","");
        // ...
        conn.close();
    }
}

跟示例二大体相似,UserDaoImpl类中有成员变量,那么多个线程可以对成员变量conn 同时进行操作,故是不安全的

例6:

public class MyServlet extends HttpServlet {
    // 是否安全
    private UserService userService = new UserServiceImpl();

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}
public class UserServiceImpl implements UserService {
    public void update() {
        UserDao userDao = new UserDaoImpl();
        userDao.update();
    }
}
public class UserDaoImpl implements UserDao {
    // 是否安全
    private Connection = null;
    public void update() throws SQLException {
        String sql = "update user set password = ? where username = ?";
        conn = DriverManager.getConnection("","","");
        // ...
        conn.close();
    }
}

跟示例三大体相似,UserServiceImpl类的update方法中 UserDao是作为局部变量存在的,所以每个线程访问的时候都会新建有一个UserDao对象,新建的对象是线程独有的,所以是线程安全的

例7:

public abstract class Test {

    public void bar() {
        // 是否安全
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        foo(sdf);
    }

    public abstract foo(SimpleDateFormat sdf);


    public static void main(String[] args) {
        new Test().bar();
    }
}

其中 foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法

public void foo(SimpleDateFormat sdf) {
    String dateStr = "1999-10-11 00:00:00";
    for (int i = 0; i < 20; i++) {
        new Thread(() -> {
            try {
            	//sdf作为共享资源可以被多个线程读写
                sdf.parse(dateStr);
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

请比较 JDK 中 String 类的实现
在这里插入图片描述
final就是为了防止别人继承写出一些不安全的方法

Monitor

Monitor 被翻译为监视器,是由JVM提供,C++语言实现

在代码中想要体现monitor需要借助javap命令查看clsss的字节码,比如以下代码:

public class SyncTest {

    static final Object lock = new Object();
    static int counter = 0;
    public static void main(String[] args) {
        synchronized (lock) {
            counter++;
        }
    }
}

找到这个类的class文件,在class文件目录下执行javap -v SyncTest.class,反编译效果如下:

在这里插入图片描述

  • monitorenter 上锁开始的地方
  • monitorexit 解锁的地方
  • 其中被monitorenter和monitorexit包围住的指令就是上锁的代码
  • 有两个monitorexit的原因,第二个monitorexit是为了防止锁住的代码抛异常后不能及时释放锁

在使用了synchornized代码块时需要指定一个对象,所以synchornized也被称为对象锁,monitor主要就是跟这个对象产生关联,如下图

在这里插入图片描述

Monitor内部具体的存储结构:

  • Owner:存储当前获取锁的线程,只能有一个线程可以获取

  • EntryList:关联没有抢到锁的线程,处于Blocked状态的线程

  • WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程

具体的流程:

  • 当线程执行到临界区代码时,如果使用了synchronized,会先查询synchronized中所指定的对象(lock)是否绑定了Monitor。

    • 如果没有绑定,则会先去去与Monitor绑定,并且将Owner设为当前线程。
    • 如果已经绑定,则会去查询该Monitor是否已经有了Owner
      • 如果没有,则Owner与将当前线程绑定
      • 如果有,则放入EntryList,进入阻塞状态(blocked)
  • 当Monitor的Owner将临界区中代码执行完毕后,Owner便会被清空,此时EntryList中处于阻塞状态的线程会被叫醒并竞争,此时的竞争是非公平的

    不公平的锁竞争指的是锁的获取顺序并不依赖线程请求锁的顺序,可能导致某些线程长期等待锁
    公平锁和不公平锁的主要区别是:

    • 公平锁:线程获取锁的顺序严格按照线程请求锁的顺序。先请求锁的线程先获取锁,后请求的线程后获取锁。
    • 不公平锁:线程获取锁的顺序并不依赖于请求锁的顺序。后请求锁的线程有可能在先请求的线程之前获取锁。

    相比而言,不公平锁可以在性能上略微超过公平锁,原因是锁的分配过程简单一些。但是,不公平锁也存在某些线程可能长期得不到锁的情况,影响程序的公平性。

  • 如果代码块中调用了wait()方法,则会进去WaitSet中进行等待

注意

  • synchronized 必须是进入同一个对象的 monitor 才有上述的效果
  • 不加 synchronized 的对象不会关联监视器,不遵从以上规则
  • 对象在使用了synchronized后与Monitor绑定时,会将对象头中的Mark Word置为Monitor指针。
  • 每个对象都会绑定一个唯一的Monitor,如果synchronized中所指定的对象不同,则会绑定不同的Monitor

Monitor原理字节码实现

static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
    synchronized (lock) {
        counter++;
    }
}

对应的字节码为

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
              flags: ACC_PUBLIC, ACC_STATIC
              Code:
              stack=2, locals=3, args_size=1
              0: getstatic #2 // <- lock引用 (synchronized开始)
              3: dup
              4: astore_1 // lock引用 -> slot 1
              5: monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针
              6: getstatic #3 // <- i
              9: iconst_1 // 准备常数 1
              10: iadd // +1
              11: putstatic #3 // -> i
              14: aload_1 // <- lock引用
              15: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
              16: goto 24
              19: astore_2 // e -> slot 2 
              20: aload_1 // <- lock引用
              21: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
              22: aload_2 // <- slot 2 (e)
              23: athrow // throw e
              24: return
              Exception table:
              from to target type
              6    16  19    any
              19   22  19    any
              LineNumberTable:
              line 8: 0
              line 9: 6
              line 10: 14
              line 11: 24
              LocalVariableTable:
              Start Length Slot Name Signature
              0     25     0    args [Ljava/lang/String;
              StackMapTable: number_of_entries = 2
              frame_type = 255 /* full_frame */
              offset_delta = 19
              locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
              stack = [ class java/lang/Throwable ]
              frame_type = 250 /* chop */
              offset_delta = 4

注意

方法级别的 synchronized 不会在字节码指令中有所体现

synchronized原理进阶

Monitor实现的锁属于重量级锁,你了解过锁升级吗?

  • Monitor实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。

  • 在JDK 1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题

在HotSpot虚拟机中,对象在内存中存储的布局可分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充

在这里插入图片描述
我们需要重点分析MarkWord对象头

MarkWord

在这里插入图片描述

  • hashcode:25位的对象标识Hash码

  • age:对象分代年龄占4位

  • biased_lock:偏向锁标识,占1位 ,0表示没有开始偏向锁,1表示开启了偏向锁

  • thread:持有偏向锁的线程ID,占23位

  • epoch:偏向时间戳,占2位

  • ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针,占30位

  • ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针,占30位

我们可以通过lock的标识,来判断是哪一种锁的等级

  • 后三位是001表示无锁
  • 后三位是101表示偏向锁
  • 后两位是00表示轻量级锁
  • 后两位是10表示重量级锁

我们前面说过Monitor实现的锁都是重量级锁,而每一个Monitor都会关联一个对象,在这的体现就是:该对象头的Mark Word 中就被设置指向 Monitor 对象的指针

轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。

轻量级锁对使用者是透明的,即语法仍然是 synchronized

假设有两个方法同步块,利用同一个对象加锁

static final Object obj = new Object();
public static void method1() {
    synchronized( obj ) {
        // 同步块 A
        method2();
    }
}
public static void method2() {
    synchronized( obj ) {
        // 同步块 B
    }
}

这个地方是锁的重入,所以都是同一个线程持有的锁,所以不存在锁的竞争,也没必要使用Monitor来实现了,而是使用轻量级锁。

我们来看看轻量级锁的原理:

  • 创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word

    这个地方省略了一下细节,Lock Record作为一个对象是不会在栈中的,这个地方只是他的一个引用。

    在这里插入图片描述

  • 让锁记录中 Object reference 指向锁对象,并尝试用 cas操作 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录

    CAS(compare and swap):比较和替换,它里面有三个值,当前
    原值,预期原值和目的值,只有当当前原值和预期原值还保持一样的时候才会将当前原值替换为目的值,具体后面会讲到。

    在这里插入图片描述

  • 如果 cas 替换成功,对象头中存储了 Lock Record地址和状态 00(轻量级锁状态) ,表示由该线程给对象加锁,这时图示如下

    在这里插入图片描述

  • 如果 cas 失败,有两种情况

    • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
    • 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数

    在这里插入图片描述

  • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一

    在这里插入图片描述

  • 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用cas将Mark Word的值恢复给对象头

    • 成功,则解锁成功
    • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

static Object obj = new Object();
public static void method1() {
    synchronized( obj ) {
        // 同步块
    }
}
  • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

在这里插入图片描述

  • 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程 (使用Monitor)
    • 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
    • 然后自己进入 Monitor 的 EntryList BLOCKED

在这里插入图片描述

  • 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁 流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

自旋优化

重量级锁(如互斥锁)在获取和释放期间都需要进行用户态和内核态的切换。

获取锁:

  1. 线程在用户态调用lock()方法获取锁。
  2. lock()方法会调用内核的lock_acquire()函数,切换到内核态。
  3. lock_acquire()函数会检查锁是否已经被其他线程持有。如果是,将线程加入等待队列,然后切换出内核态。
  4. 等待队列中的线程会被锁的释放唤醒。唤醒后再次切换到内核态,获取锁,然后切换出内核态,lock()方法返回。

释放锁:

  1. 线程在用户态调用unlock()方法释放锁。
  2. unlock()方法会调用内核的lock_release()函数,切换到内核态。
  3. lock_release()函数会唤醒等待队列中的线程之一(如果有的话)。唤醒线程后,切换出内核态。
  4. 被唤醒的线程得到CPU执行时间后,会再次切换到内核态获取锁,然后返回用户态继续运行。

所以,整个过程中内核态和用户态的切换如下:

  • 获取锁:用户态->内核态(检查锁并挂起线程)->用户态(挂起)…->内核态(获取锁)->用户态
  • 释放锁:用户态->内核态(唤醒等待线程)->用户态
  • 被唤醒线程:用户态->内核态(获取锁)->用户态

可以看出,互斥锁的获取和释放都需要进行比较频繁的内核态切换,这也是重量级锁性能开销较大的主要原因之一。

为了减少这种开销,许多重量级锁的实现都提供了自旋优化。自旋意味着当线程尝试获取锁失败时,它会进行一定的自旋操作,即在用户态进行循环尝试获取锁,避免直接进入内核态挂起线程。如果在自旋操作期间获取锁成功,就可以避免一次内核态切换,提高性能。

自旋重试成功的情况

线程1 ( core 1上)对象Mark线程2 ( core 2上)
-10(重量锁)-
访问同步块,获取monitor10(重量锁)重量锁指针-
成功(加锁)10(重量锁)重量锁指针-
执行同步块10(重量锁)重量锁指针-
执行同步块10(重量锁)重量锁指针访问同步块,获取 monitor
执行同步块10(重量锁)重量锁指针自旋重试
执行完毕10(重量锁)重量锁指针自旋重试
成功(解锁)01(无锁)自旋重试
-10(重量锁)重量锁指针成功(加锁)
-10(重量锁)重量锁指针执行同步块
-

自旋重试失败的情况

线程1 ( core 1上)对象Mark线程2( core 2上)
-10(重量锁)-
访问同步块,获取monitor10(重量锁)重量锁指针-
成功(加锁)10(重量锁)重量锁指针-
执行同步块10(重量锁)重量锁指针-
执行同步块10(重量锁)重量锁指针访问同步块,获取monitor
执行同步块10(重量锁)重量锁指针自旋重试
执行同步块10(重量锁)重量锁指针自旋重试
执行同步块10(重量锁)重量锁指针自旋重试
执行同步块10(重量锁)重量锁指针阻塞
-
  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势
  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会 高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
  • Java 7 之后不能控制是否开启自旋功能

偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入(该线程执行的方法中再次锁住该对象)仍然需要执行 CAS 操作。

Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

例如:

static final Object obj = new Object();
public static void m1() {
    synchronized( obj ) {
        // 同步块 A
        m2();
    }
}
public static void m2() {
    synchronized( obj ) {
        // 同步块 B
        m3();
    }
}
public static void m3() {
    synchronized( obj ) {
        // 同步块 C
    }
}

加锁的流程

  1. 在线程栈中创建一个Lock Record,将其obj字段指向锁对象。
    在这里插入图片描述

  2. 通过CAS指令将Lock Record的线程id存储在对象头的mark word中,同时也设置偏向锁的标识为101,如果对象处于无锁状态则修改成功,代表该线程获得了偏向锁。
    在这里插入图片描述

  3. 如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。与轻量级锁不同的时,这里不会再次进行cas操作,只是判断对象头中的线程id是否是自己,因为缺少了cas操作,性能相对轻量级锁更好一些
    在这里插入图片描述

轻量级锁
偏向锁
用ThreadID替换MarkWord
检查ThreadID是否是自己
检查ThreadID是否是自己
生成锁记录
生成锁记录
生成锁记录
用锁记录替换markword
用锁记录替换markword
用锁记录替换markword
m1内调用synchronized(obj)
m2内调用synchronized(obj)
m2内调用synchronized(obj)
对象
m1内调用synchronized(obj)
m2内调用synchronized(obj)
m2内调用synchronized(obj)
对象

偏向状态

回忆一下对象头格式:
在这里插入图片描述

  • Normal:一般状态,没有加任何锁,前面62位保存的是对象的信息,最后2位为状态(01),倒数第三位表示是否使用偏向锁(未使用:0)
  • Biased:偏向状态,使用偏向锁,前面54位保存的当前线程的ID,最后2位为状态(01),倒数第三位表示是否使用偏向锁(使用:1)
  • Lightweight:使用轻量级锁,前62位保存的是锁记录的指针,最后两位为状态(00)
  • Heavyweight:使用重量级锁,前62位保存的是Monitor的地址指针,后两位为状态(10)

一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 thread、epoch、age 都为 0
  • 偏向锁默认是延迟的,不会在程序启动时立即生效
    • 如果想避免延迟,可以加 VM 参数- XX:BiasedLockingStartupDelay=0来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、 age 都为 0,第一次用到 hashcode 时才会赋值
    • 添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁

我们可以试验一下:
测试偏向锁:

class Dog {}

利用 jol 第三方工具来查看对象头信息;

// 添加虚拟机参数 -XX:BiasedLockingStartupDelay=0 
public static void main(String[] args) throws IOException {
    Dog d = new Dog();
    ClassLayout classLayout = ClassLayout.parseInstance(d);
    new Thread(() -> {
        log.debug("synchronized 前");
        System.out.println(classLayout.toPrintableSimple(true));
        synchronized (d) {
            log.debug("synchronized 中");
            System.out.println(classLayout.toPrintableSimple(true));
        }
        log.debug("synchronized 后");
        System.out.println(classLayout.toPrintableSimple(true));
    }, "t1").start();
}

结果;

11:08:58.117 c.TestBiased [t1] - synchronized00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 
11:08:58.121 c.TestBiased [t1] - synchronized00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101 
11:08:58.121 c.TestBiased [t1] - synchronized00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101 

注意
处于偏向锁的对象解锁后,线程 id 仍存储于对象头中

我们将偏向锁禁用之后再进行测试,结果如下

11:13:10.018 c.TestBiased [t1] - synchronized00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
11:13:10.021 c.TestBiased [t1] - synchronized00000000 00000000 00000000 00000000 00100000 00010100 11110011 10001000 
11:13:10.021 c.TestBiased [t1] - synchronized00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 

正常状态对象一开始是没有 hashCode 的,第一次调用才生成

撤销偏向

以下几种情况会使对象的偏向锁失效:

  • 调用对象的hashCode方法
  • 多个线程使用该对象
  • 调用了wait/notify方法(调用wait方法会导致锁膨胀而使用重量级锁)

我们还是来试验一下;

撤销 - 调用对象 hashCode

调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,没有位置再放hashcode了,于是干脆重置变为normal状态

因为调用 hashCode 会导致偏向锁被撤销 ,所以:

  • 轻量级锁会在锁记录中记录 hashCode
  • 重量级锁会在 Monitor 中记录 hashCode

在调用 hashCode 后使用偏向锁,会变为轻量级锁。

输出

11:22:10.386 c.TestBiased [main] - 调用 hashCode:1778535015 
11:22:10.391 c.TestBiased [t1] - synchronized00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001 
11:22:10.393 c.TestBiased [t1] - synchronized00000000 00000000 00000000 00000000 00100000 11000011 11110011 01101000 
11:22:10.393 c.TestBiased [t1] - synchronized00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001 
撤销 - 其它线程使用对象

轻量级锁和偏向锁都是在多线程错开的情况下说的,因为一旦有冲突就会发生锁膨胀成为重量级锁。

当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁

private static void test2() throws InterruptedException {
    Dog d = new Dog();
    Thread t1 = new Thread(() -> {
        synchronized (d) {
            log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
        }
        synchronized (TestBiased.class) {
            TestBiased.class.notify();
        }
        // 如果不用 wait/notify 使用 join 必须打开下面的注释
        // 因为:t1 线程不能结束,否则底层线程可能被 jvm 重用作为 t2 线程,底层线程 id 是一样的
        /*try {
 System.in.read();
 } catch (IOException e) {
 e.printStackTrace();
 }*/
    }, "t1");
    t1.start();
    Thread t2 = new Thread(() -> {
        synchronized (TestBiased.class) {
            try {
                TestBiased.class.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
        synchronized (d) {
            log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
        }
        log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
    }, "t2");
    t2.start();
}

输出

[t1] - 00000000 00000000 00000000 00000000 00011111 01000001 00010000 00000101 
[t2] - 00000000 00000000 00000000 00000000 00011111 01000001 00010000 00000101 
[t2] - 00000000 00000000 00000000 00000000 00011111 10110101 11110000 01000000 
[t2] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
撤销 - 调用 wait/notify

wait/notify这种等待通知的机制只有重量级锁才有,所以这种方法会将轻量级锁或者偏向锁都升级成为重量级锁。

public static void main(String[] args) throws InterruptedException {
    Dog d = new Dog();
    Thread t1 = new Thread(() -> {
        log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
        synchronized (d) {
            log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
            try {
                d.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
        }
    }, "t1");
    t1.start();
    new Thread(() -> {
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (d) {
            log.debug("notify");
            d.notify();
        }
    }, "t2").start();
}

输出

[t1] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 
[t1] - 00000000 00000000 00000000 00000000 00011111 10110011 11111000 00000101 
[t2] - notify 
[t1] - 00000000 00000000 00000000 00000000 00011100 11010100 00001101 11001010 

批量重偏向

如果对象被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象 的 Thread ID

当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程

实验:

private static void test3() throws InterruptedException {
    Vector<Dog> list = new Vector<>();
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 30; i++) {
            Dog d = new Dog();
            list.add(d);
            synchronized (d) {
                log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
            }
        }
        synchronized (list) {
            list.notify();
        } 
    }, "t1");
    t1.start();

    Thread t2 = new Thread(() -> {
        synchronized (list) {
            try {
                list.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.debug("===============> ");
        for (int i = 0; i < 30; i++) {
            Dog d = list.get(i);
            log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
            synchronized (d) {
                log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
            }
            log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
        }
    }, "t2");
    t2.start();
}

输出:

[t1] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 1 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 2 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 3 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 4 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 5 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 6 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 7 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 8 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 9 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 10 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 11 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 12 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 13 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 14 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 15 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 16 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 17 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 18 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - ===============> 
[t2] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 0 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 0 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 1 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 1 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 1 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 2 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 2 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 2 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 3 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 3 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 3 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 4 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 4 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 4 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 5 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 5 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 5 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 6 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 6 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 6 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 7 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 7 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 7 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 8 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 8 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 8 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 9 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 9 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 9 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 10 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 10 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 10 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 11 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 11 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 11 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 12 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 12 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 12 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 13 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 13 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 13 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 14 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 14 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 14 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 15 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 15 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 15 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 16 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 16 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 16 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 17 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 17 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 17 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 18 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 18 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 18 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
****************从此处开始批量重偏向
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 

批量撤销

当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的

锁消除

jvm中的JIT即时编译器在发现你对一个未共享的资源加锁的时候,会进行相应的优化,将锁消除。

锁粗化

对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化,这不同于之前讲的细分锁的粒度。

三锁总结

Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。

描述
重量级锁底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
轻量级锁线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性
偏向锁一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令

一旦锁发生了竞争,都会升级为重量级锁

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

十八岁讨厌编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值