2.管程

前言

  在1.多线程基础 - 求知律己 - 博客园 (cnblogs.com)这篇博客中,我已经简要介绍了多线程的三种创建方式以及常用的方法,本篇博客我将讲解一下管程,提到管程,咋们首先要知道它是什么,管程即一个操作系统的资源管理模块,细化点说就是由共享数据结构和操作该结构的过程所组成的资源管理程序。它可以有效地解决死锁,这个后面会讲如何解决死锁。

1.管程之共享

1.1 共享问题

 问题:两个线程对初始值为 0 的静态变量(临界区)一个做自增,一个做自减,各做 5000 次,结果是 0 吗?我们通过代码来验证下

代码实现

复制代码
static Logger log = LoggerFactory.getLogger(ShareProblem.class);
    private 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.info("counter:{}", counter);
    }
测试两个线程对静态共享变量5000加减
复制代码

运行结果

   这是我测试之后截取的运行结果,几次结果都是负数,一次结果是正数,也有可能是0。我们是不是都期待结果是0,事实却不是如此,为什么呢,因为我们的代码里面的线程时并发执行的,是互相争夺资源的,所以它的运行在我们没有添加控制的时候,是不受我们控制的即共享变量的自增和自减不是原子操作,什么是原子操作呢?即不会被线程调度打断,也就是说不受线程状态变更影响。如果要分析上述问题的话,我们需要从代码执行的字节码开始分析。

对i++字节码操作为:

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

对i--亦是如此:

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

  而这些操作可以总结为:1)首先获取静态变量值,2)准备常量,3)进行操作,4)将修改值存入静态变量;其操作的过程都是在java的内存模型中间进行的,java的内存模型分为两种,一种是包含静态变量和成员变量的主存,另外一种是包含局部变量的工作内存。而完成i自增和自减是在主存和工作内存之间进行数据交换,交换过程如下图

 继续分析上述操作,如果我们的代码变换成单线程执行上述加减操作的话,得到的结果就是如我们所想的0,执行过程图如下

 但是如果我们是多线程的话就会出现交错运行,首先是出现负数的情况:

这是出现正数的情况:

   一个程序执行多个线程是没有问题的,问题出在多个线程去访问同一个共享资源;多个线程区访问多个共享资源的时候容易出现上述指令交错的情况,即i++和i--是在进行完赋值操作之后进行了上下文切换导致改变的值并没有存入到主存中,引起了结果的错误。

  一个代码块中如果存在对共享资源的多线程读写操作,我们将其称之为临界区

复制代码
static int counter = 0;
static void increment()
// 临界区
{
counter++;
}
static void decrement()
// 临界区
{
    counter--;
}
上述加减代码的临界区
复制代码
  多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

2.管程之synchronized互斥

2.1 synchronized的概念和使用

  为了避免临界区的竞态条件发生,有很多手段可以处理这个问题,常用的阻塞式解决方法就有synchronized、ReenTrantLock;非阻塞式的解决方案由原子变量。本节通过synchronized来解决上述问题。synchronized也被称为对象锁,它采用互斥的方式允许同一时刻最多一个线程能够持有对象锁,其它线程想获得这个对象锁时就会被阻塞住。对象锁去被爱了拥有锁的线程可以安全的执行临界区的代码,不用担心线程上下文切换所造成的数据错误。

  注意:synchronized关键字可以用来处理互斥合同部,但它们还是有所区别的:

  1)互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码

  2)同步是由于线程执行的先后顺序不同,需要一个线程等待其它线程运行到某一步

  在使用synchronized锁对象的时候,我们要先找出临界区也就是读写的操作的代码块,我们既可以在该代码块上面加锁,也可以在代码块所在的方法前面加锁,还可以锁住该代码块所在的类对象

  下面我们通过本篇文章的静态变量加减5000次来分析synchronized的用法和流程

代码实现

复制代码
static Logger log = LoggerFactory.getLogger(RoomCounter.class);
    private static int counter;
    private static final Object room = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread addThread = new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                synchronized (room){
                    counter++;
                }
            }
        },"自增线程");

        Thread subThread = new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                synchronized (room){
                    counter--;
                }
            }
        },"自减线程");
        addThread.start();
        subThread.start();
        addThread.join();
        subThread.join();
        log.info("counter: {}", counter);
    }
使用synchronized互斥访问静态变量共享资源
复制代码

运行结果

  从上面的代码,我们是通过synchronized来锁住room这个类对象来互斥访问静态变量counter的

下面通过简要描述一下synchronized加锁过程:

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

   可以看出在字节码实现中,当我们给subThread上了room对象锁后,它在执行自减操作的时候,addThread线程想对counter进行操作被阻塞住了,当它执行完后,才进行addThread的自增操作。

2.2 理解synchronized锁

  为了更好地理解synchronized对象锁,到底锁的那个对象,我们通过以下几个示例来说明

复制代码
情况1:
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();
}
说明:结果为12或21,他们synchronized锁的都是this对象,由于它们都是由n1调用,故锁的同一对象。
情况2:
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();
}
说明:结果为1 1秒后2或2一秒后1,他们synchronized锁的都是this对象,由于它们都是由n1类对象调用,故锁的同一对象,但是1线程要休息1s,分配资源之前它们都是相互竞争的
情况3:
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();
}
说明:结果为32 1秒后1,23 1秒后 1,3 1秒后12,c锁的是Numberl类中一个对象,a和b锁的都是各自的this对象且都由n1对象调用,故锁的都是n1对象,但是1线程要休息1s,分配资源之前它们都是相互竞争的
情况4:
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();
}
说明:结果为2 1秒后1。a和b锁的都是各自的this对象,但是a和b各自被n1和n2调用,故a锁的n1对象,b锁的n2对象,不是同一个对象,b不会等待a执行完。
情况5:
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();
}
说明:结果为2 1秒后1,a锁因为static所以锁的是Number类其中一个对象,b锁的自己的this对象,由于被n1对象调用,故锁的n1对象。
情况6:
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();
}
说明:1 1秒后2或2 1秒后1(a和b锁的都是static类对象)
情况7:
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 n1 = new Number();
    new Thread(()->{ n1.a(); }).start();
    new Thread(()->{ n2.b(); }).start();
}
说明:2 一秒后1,a方法上面与static故锁的Number类对象,而b锁的是自己的this对象,但是是由n2对象调用,故锁的n2对象。
情况8:
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();
}    
说明: 结果1 一秒后2,或2一秒后1,它们的方法前面都有static,故锁的都是Number同一个对象。
synchronized的“线程八锁”
复制代码

小结:

  不看创建的线程是否同一个对象,只看锁谁,锁相同对象那么就可能分配给其中一个,互斥进行,如果锁不同对象,就并发执行。synchronize锁this对象,锁调用自己的线程,static锁类对象

2.3 变量的线程安全分析

  在我们的类中以及方法中都存在着变量,在类中的变量由静态变量、成员变量,它们是能够被共享数据的,不一定安全;在我们方法中的变量是局部变量,它们私有的,在没有被外界引用的话,它是安全的,被外界引用它就不是绝对地安全。

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

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

2.3.2 局部变量是否线程安全

  • 局部变量是线程安全的(每次线程调用某个方法的局部变量时,会在每个线程的栈帧内存中被创建多份,因此不存在共享;因为它是私有的,不共享)。
  • 局部变量引用的对象则未必
  • 如果该对象没有逃离方法的作用访问,它是线程安全的。
  • 如果该对象逃离方法的作用范围,需要考虑线程安全。

  下面我们通过一个示例来测试一下静态变量和局部变量的安全性

代码实现

复制代码
public class SafeAndUnsafeVariable {
    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;

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

class UnsafeThread{
    ArrayList<String>list = new ArrayList<>();
    public void method1(int loopNumber){
        for (int i = 0; i < loopNumber; i++) {
            method2();
            method3();
        }
    }

    public void method2(){
        list.add("1");//添加字符串1
    }

    public void method3(){
        list.remove(0);//移除列表中索引为0的元素
    }
}
不安全的成员变量
复制代码
复制代码
public class SafeAndUnsafeVariable {
    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;

    public static void main(String[] args) {
        SafeThread safeThread = new SafeThread();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                safeThread.method1(LOOP_NUMBER);
            }, "Thread"+i).start();
        }
    }
}
    class SafeThread{
        public void method1(int loopNumber){
            ArrayList<String>list = new ArrayList<>();
            for (int i = 0; i < loopNumber; i++) {
                method2(list);
                method3(list);
            }
        }

        public void method2(ArrayList<String>list){
            list.add("1");//添加字符串1
        }

        public void method3(ArrayList<String>list){
            list.remove(0);//移除列表中索引为0的元素
        }
}
安全的局部变量
复制代码

运行结果

 从结果上面可以看出,因为它是不安全数据共享的,执行顺序得不到控制,在我们还没有添加1的时候就移除列表中元素,但是此时列表中没有元素,故报错。下面一张图就是使用局部变量,通过传参来实现添加和移除并没有出现错误。可见局部变量得我安全性。

   从上面那张图,可以看出两个方法共享的是对中的同一个list对象,数据共享,并且它们的操作是都带有读写的,所以不安全,系统报错。而下面的是各自方法中的变量是私有的,不共享,故list对象是安全的。

2.3 常见线程的安全类

  在Java中有一些类通常是安全的,例如:String、Integer、StringBuffer、Random、vector、Hashtable、JUC类,它们是线程安全是指多个线程调用它们同一个实例的某个方法是线程安全的。即一个实例对象单独调用该存储对象的一个方法是安全的,但是调用该存储对象的组合方法就不是安全的。

复制代码
//单独的使用put()方法是安全的
Hashtable table = new Hashtable();
new Thread(()->{
    table.put("key", "value1");
}).start();
new Thread(()->{
    table.put("key", "value2");
}).start();

//组合使用put()和get()方法是不安全的
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
    table.put("key", value);
}
独立是安全的,组合是不安全的
复制代码

   从上图,我们可以看到,当两个线程都获取key为空的时候发生上下文切换吗,此时都会执行put操作,但是后面的put会覆盖前面的put,此时就会造成结果错误。

 注意:有很多人会疑惑String是不可类,那为什么会有replace、substring等方法来改变值呢?其实他们不是改变,而是创建一个新的字符串,将原字符串复制过来再进行修改,将新的字符串返回。

2.3.1 线程安全七测试

代码举例

复制代码
//测试一
public class MyServlet extends HttpServlet {多个线程共享
    // 是否安全?不是(HashTable是)
    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) {
        // 使用上述变量
    }
}

//测试二
public class MyServlet extends HttpServlet {
    // 是否安全?不是,因为共享使用count
    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++;
    }
}

//测试三
@Aspect
@Component
public class MyAspect {
    // 是否安全?不安全,因为spring是单例的,它是共享的,
     //我们可以通过环绕通知设置,它们为局部变量来保证线程安全
    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));
    }
}

//测试四
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) {
                // ...
            }
        }
}

//测试五
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();
        }
}

//测试六
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();
    }
}

//测试七
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();
    }
}

public void foo(SimpleDateFormat sdf) {
    String dateStr = "1999-10-11 00:00:00";
    for (int i = 0; i < 20; i++) {
        new Thread(() -> {
            try {
                sdf.parse(dateStr);
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }).start();
    }
}
安全七测试
复制代码

2.3.2 小练习

例题1:以下代码模仿一个窗口卖票,我们需要做的就是找出临界区,并使其互斥访问即可

代码实现

复制代码
package com.ku.juc.synchronize.exercise;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Vector;

public class ExerciseSell {
    public static void main(String[] args) {
        TicketWindow ticketWindow = new TicketWindow(2000);
        List<Thread> list = new ArrayList<>();
        // 用来存储买出去多少张票
        List<Integer> sellCount = new Vector<>();
        for (int i = 0; i < 2000; i++) {
            Thread t = new Thread(() -> {
                // 分析这里的竞态条件
                int count = ticketWindow.sell(randomAmount());
                sellCount.add(count);
            });
            list.add(t);
            t.start();
        }
        list.forEach((t) -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        // 买出去的票求和
        System.out.println("selled count:"+ sellCount.stream().mapToInt(c -> c).sum());
        // 剩余票数
        System.out.println("remainder count:" + ticketWindow.getCount());

    }
    // Random 为线程安全
    static Random random = new Random();
    // 随机 1~5
    public static int randomAmount() {
        return random.nextInt(5) + 1;
    }
}
class TicketWindow {
    private int count;
    public TicketWindow(int count) {
        this.count = count;
    }
    public int getCount() {
        return count;
    }

    //这里买票的共享资源amount只有一个共享对象,所以锁住这个对象即可
    public synchronized int sell(int amount) {
        if (this.count >= amount) {
            this.count -= amount;
            return amount;
        } else {
            return 0;
        }
    }
}
窗口卖票
复制代码

运行结果

  代码中,我们频繁进行读写操作的方法就是改代码的临界区,我们使用synchronized锁住该代码就可以实现多线程互斥访问了。

例题2:以下代码模仿一个人存钱,一个人取钱,但是账户类只有一个,我们也是需要找出临界区,并使其互斥访问即可

代码实现

复制代码
package com.ku.juc.synchronize.exercise;

import java.util.Random;

public class ExerciseTransfer {
    public static void main(String[] args) throws InterruptedException {
        Account a = new Account(1000);
        Account b = new Account(1000);
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                a.transfer(b, randomAmount());
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                b.transfer(a, randomAmount());
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        // 查看转账2000次后的总金额
        System.out.println("total:"+(a.getMoney() + b.getMoney()));
    }
    // Random 为线程安全
    static Random random = new Random();
    // 随机 1~100
    public static int randomAmount() {
        return random.nextInt(100) +1;
    }
}
class Account {
    private int money;
    public Account(int money) {
        this.money = money;
    }
    public int getMoney() {
        return money;
    }
    public void setMoney(int money) {
        this.money = money;
    }

    //这里的转账不同于之前的卖票,卖的资源只有一个共享对象
    //而这里的转账是两个对象互持自己的账户,如何锁呢,分析到这里可以锁住Account这个对象是唯一的
    public void transfer(Account target, int amount) {
        synchronized (Account.class){//
            if (this.money > amount) {
                this.setMoney(this.getMoney() - amount);
                target.setMoney(target.getMoney() + amount);
            }
        }
    }
}
两个人转账
复制代码

运行结果

   在上述代码中,虽然属于两个账户之间相互转账,但是他们之间账户类只有一个,这就是改代码的临界区,两个线程互斥访问就不会有安全问题导致结果出错。

小结:一定要找出代码中的共享资源所属的类(临界区所操作的资源类),锁住该共享资源的类对象就可以实现多线程互斥进行资源操作。

3.管程之Monitor机制原理

3.1 Mark Word和Monitor内存结构

  在谈到Monitor管程监控之前,首先我们要了解对象的结构,如下图所示是32位虚拟机中普通对象的对象头(占64bits)内部结构:

   该对象头是由Mark Word(占32bits)、Klass Word(32 bits)组成,Mark Word包含GC年龄(即多久被垃圾回收器回收)、锁标记(与本文后面所讲紧密及相关)、hashCOde(对象创建时不更新到对象头的Mark Word中,只有调用hashCode()方法才更新到对象头中);Klass Word表示类型指针。

  下面是数组对象的对象头,它比普通对象多了一个数组长度array length(占32bits):

   在分析完对象后,我们来到本节重点也就是与Monitor相关的Mark Word,它里面包含的锁标记与Monitor紧密相关:

  在上图中Mark Word在不同状态下,它所包含的结构不同。

  1. 在正常情况下,它是由hashcode(31bits)、GC age(4 bits) 、biased_block锁标记(此时锁标记为0)组成的;
  2. 在偏向锁状态,它是由thread(54bits)、GC age(4bits)、biased_block锁标记(此时锁标记为1)组成;
  3. 在轻量级锁状态,它被置换成ptr_tolock_record(占62bits)指向锁记录的地址;
  4. 在重量级锁状态,它被置换成ptr_to_heavyweight_monitor(占62字节)指向重量级锁的monitor;
  5. 最后就是关于对象的GC 

  在了解了Mark Word之后,再来了解一下给临界区资源对象加上synchronized锁时的monitor过程:

  下图是Monitor与一个对象的Mark Word关联图:

  下图是Monitor与多个对象的Mark Word关联图:

Monitor是由Owner、EntryList、WaitSet三部分组成,

  1. 当某个线程Thread-2执行synchronized(obj)时,就将Monitor的Owner之为t,Monitor中只能有一个Owner;
  2. 当Thread-2在上锁的过程中,其它线程也来执行synchronized,就会进入EntryList进行阻塞;
  3. 当Thread-2执行完临界区代码时,会唤醒EntryList中等待的线程竞争synchronized(obj),但是此时竞争是非公平的;
  4. 当线程Thread-0,Thread-1之前获得过锁,但是资源条件不满足时会进入Waiting状态等待资源,当获取等待的资源时再去竞争synchronized(obj)锁。

 小结:Owner存放正在执行的Synchronized(obj),EntryList中存放获取锁阻塞的线程,WaitSet中存放获取过锁,但因资源不足而等待的线程。

 3.2 轻量级锁

  轻量级锁使用场景:当一个对象被多个线程访问时,多线程访问的时间是错开的,即没有竞争,此时使用轻量级锁。语法还是使用synchronized。

  在说到加锁之前,我们首先来认识一下什么是锁记录对象?即我们每个线程的栈帧(实现过程或函数调用的一种数据结构)中都会包含一个锁记录对象,该对象存储锁定对象的Mark Word

  加锁:

  1.当线程Thread-0给某个对象Object加锁时,此时线程Thread-0中的Object reference指向要锁的对象Object,并尝试使用CAS(无锁机制,通过获取之前的值与当前值比较,相同则使用修改后的值替换之前的值)替换Object中的Mark Word,再将Mark Word值存入到线程的lock record中;

  2.如果CAS替换成功,Object对象头中存储线程Thread-0的lock record地址和状态00,表示此时由线程Thread-0给对象Object加锁。

   重入锁:即对同一个对象多次加锁

  当线程Thread-0给对象Object加锁时,如果CAS失败,可能有两种原因:

  1.其它线程已经持有对象Object的轻量级锁,这时候表明竞争,进入锁膨胀过程;

  2.线程Thread-0自己执行了synchronized锁重入,此时在该线程的lock record中添加一个lockrecord作为重入的计数。

   解锁:

  1.当线程Thread-0退出synchronized代码块时,如果lock record为null表明有重入,这是重置lock record,表示重入计数减一;

  2.当线程Thread-0退出代码块时,lock record不为null,此时使用CAS将Mark Word的值恢复给Object的对象头。

  如果成功,即解锁;失败表明轻量级锁进行了锁膨胀或已经升级为重量级锁,此时应该进入重量级解锁流程。

 3.3 锁膨胀

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

  1.当线程Thread-1对对象Object尝试加上轻量级锁时,线程Thread-0已经对该对象加上了轻量级锁;

  2.这时Thread-1加锁失败,进入锁膨胀了流程;

  3.此时为对象Object申请Monitor锁,使用Monitor的地址替换掉对象的Mark Word

  4.Thread-1进入EntryList进行阻塞;

  5.当Thread-0执行完同步代码块时,使用CAS将Mark Word恢复给对象Object的对象头;将Monitor地址返回给Monitor对象,Owner设置为null,唤醒EntryList中的BLOCKED线程,失败则进入重量级锁流程。

 小结:

  无竞争(时间错开)轻量级锁时,线程的Object reference的lock record存储对象的hashcode age bias0,对象的Mark Word被置换成指向线程的lock record地址和状态。有竞争重量级锁,使用Monitor,对象中MarkWord换成monitor地址,Owner指向该线程。

posted @   求知律己  阅读(218)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
· 零经验选手,Compose 一天开发一款小游戏!
点击右上角即可分享
微信分享提示