2.管程

时间:2021-02-25 01:06:22

前言

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

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加减

运行结果

2.管程

2.管程

   这是我测试之后截取的运行结果,几次结果都是负数,一次结果是正数,也有可能是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自增和自减是在主存和工作内存之间进行数据交换,交换过程如下图

2.管程

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

2.管程

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

2.管程

这是出现正数的情况:

2.管程

   一个程序执行多个线程是没有问题的,问题出在多个线程去访问同一个共享资源;多个线程区访问多个共享资源的时候容易出现上述指令交错的情况,即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互斥访问静态变量共享资源

运行结果

2.管程

  从上面的代码,我们是通过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-- 代码。

2.管程

   可以看出在字节码实现中,当我们给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的元素
        }
}

安全的局部变量

运行结果

2.管程

2.管程

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

2.管程

2.管程

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