JUC学习03 共享模型(01)

共享模型

        线程安全问题的Java体现

        

 /**
     * 线程安全问题的Java体现
     */
    //如果两个线程做自增自减500次,那结果会是0吗
    static class JavaThreadSecurity{
        static int num = 0;
        public static void main(String[] args) throws InterruptedException {
            Thread thread1 = new Thread(()->{
                for (int i = 0; i < 5000; i++) {
                    num++;
                }
            },"t1");
            Thread thread2 = new Thread(()->{
                for (int i = 0; i < 5000; i++) {
                    num--;
                }
            },"t2");
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();
            System.out.println(num);
        }
    }

运行结果:2023

问题分析

以上的结果可能是正数、负数、零。为什么呢?因为 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

 像计算这种工作都是在线程内部运行,计算完后线程将结果写回内存内,此时的num可能已经被另外一个线程给修改过了值,造成新写入的值与之前上下文不符合。造成了脏读。

临界区 Critical Section

一个程序运行多个线程本身是没有问题的
问题出在多个线程访问 共享资源
  •         多个线程读共享资源其实也没有问题
  •         在多个线程对共享资源读写操作时发生指令交错,就会出现问题
一段代码块内如果存在对 共享资源 的多线程读写操作,称这段代码块为 临界区
 

 synchronized 解决方案

* 应用之互斥
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
  •         阻塞式的解决方案:synchronizedLock
  •         非阻塞式的解决方案:原子变量
本次课使用阻塞式的解决方案: synchronized ,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁 的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

利用synchronize解决基本的多线程操作变量问题

static Object room =  new Object();
        static int count = 0;
        /**
         * Synchronized的基本使用方法
         */
        public static void testSynchronized() throws InterruptedException {
            Thread thread1 = new Thread(()->{
                for (int i = 0; i < 5000; i++) {
                    synchronized (room){
                        count++;
                    }
                }
            });
            Thread thread2 = new Thread(()->{
                for (int i = 0; i < 5000; i++) {
                    synchronized (room){
                        count--;
                    }
                }
            });
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();
            System.out.println(count);
        }
    }

结果无论怎么运行都会是0

原理分析

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

思考

synchronized 实际是用 对象锁 保证了 临界区内代码的原子性 ,临界区内的代码对外是不可分割的,不会被线程切换所打断。
为了加深理解,请思考下面的问题
  • 如果把 synchronized(obj) 放在 for 循环的外面,如何理解?-- 原子性 代码会在运行完for循环后释放锁
  • 如果 t1 synchronized(obj1) t2 synchronized(obj2) 会怎样运作?-- 锁对象 由于锁的不同,所以还是会造成线程安全问题
  • 如果 t1 synchronized(obj) t2 没有加会怎么样?如何理解?-- 锁对象 由于t2没有上锁,所以还是会造成线程安全问题

利用面向对象的方式改造代码

 public static void testSynchronized2() throws InterruptedException {
            Room room = new Room();
            Thread thread1 = new Thread(()->{
                for (int i = 0; i < 5000; i++) {
                    synchronized (room){
                        room.increment();
                    }
                }
            });
            Thread thread2 = new Thread(()->{
                for (int i = 0; i < 5000; i++) {
                    synchronized (room){
                        room.decrement();
                    }
                }
            });
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();
            System.out.println(room.getCount());
        }
        /**
         * 利用面向对象的思想对上一段代码进行改造
         */
        static class Room{
            public int getCount() {
                //为保证读取到的值是正确的,所以也要加锁
                synchronized (this){
                    return count;
                }
            }

            private int count = 0;
            public void increment(){
                synchronized (this){
                    count++;
                }
            }
            public void decrement(){
                synchronized (this){
                    count--;
                }
            }
        }

在这里room对象里的锁是使用自己为锁

方法上添加synchronize

不加 synchronized 的方法
不加 synchronzied 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的),无法保证代码的原子性

线程八锁(部分)

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();
            }

  这是一个特殊的锁,由于a方法添加了static关键字。所以实际上锁住的是Number类对象,而相反b对象锁住的只是n1。导致执行是没有占据同一个锁,这段代码执行的结果显而易见的是: 2 1s后 1

变量线程安全分析

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

如果它们没有共享,则线程安全

如果它们被共享了,根据它们的状态是否能够改变,又分两种情况

  1.         如果只有读操作,则线程安全
  2.         如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量是否线程安全?
局部变量是线程安全的 但局部变量引用的对象则未必
  1. 如果该对象没有逃离方法的作用访问,它是线程安全的
  2. 如果该对象逃离方法的作用范围,需要考虑线程安全

局部变量线程安全分析

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

Code:
    stack=1, locals=1, args_size=0 
    0: bipush 10
    2: istore_0
    3: iinc 0, 1 
//这里是局部变量的i++操作,实际上和静态变量的i++有些许不同。局部变量的i++只有这一条操作,具有原子性。
    6: return
LineNumberTable:
    line 10: 0
    line 11: 3
    line 12: 6
LocalVariableTable:
Start Length Slot Name Signature

        多线程操作test1方法,实际上在JVM里,为每一个线程都创造了独立的栈内存。而test1被创造一块栈帧后,局部变量i也会被创建一份,多线程里这些i都是相互独立的,是一块私有的内存,并不是共享的。所以并不存在线程安全问题。

        而局部变量如果引用的是对象则稍有不同

public static void main(String[] args) {
        ThreadUnsafe test = new ThreadUnsafe();
        for (int i = 0; i < 2; i++) {
            //在这里创建两个线程去操作ThreadUnsafe类
            new Thread(()->{
               test.method1(200);
            },"Thread"+(i+1)).start();
            //最后结果是出现了异常
            //Exception in thread "Thread2" Exception in thread "Thread1" java.lang.ArrayIndexOutOfBoundsException: -1
        }
    }
    static class ThreadUnsafe{
        ArrayList<String> list = new ArrayList<>();
        public void method1(int loopNumber){
            for (int i = 0; i < loopNumber; i++) {
                method2();
                method3();
            }
        }

        private void method3() {
            list.remove(0);
        }

        private void method2() {
            list.add("1");
        }
    }

 

为什么会报错呢?

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

 实际上演变成了多个线程对共享资源list的操作

而改成局部变量后程序可以正常运行了。原理和之前说的差不多。list不再是共享资源

方法访问修饰符带来的思考,如果把 method2 method3 的方法修改为 public 会不会代理线程安全问题?
 
        情况1 :有其它线程调用 method2 method3
        情况2 :在 情况 1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 method3 方法
public class Node03 {
    public static void main(String[] args) {
        Threadsafe test = new ThreadSafeSubClass();
        for (int i = 0; i < 2; i++) {
            //在这里创建两个线程去操作ThreadUnsafe类
            new Thread(()->{
                test.method1(200);
            },"Thread"+(i+1)).start();
            //最后结果是出现了异常
            //Exception in thread "Thread2" Exception in thread "Thread1" java.lang.ArrayIndexOutOfBoundsException: -1
        }
    }
}
class Threadsafe{
    public void method1(int loopNumber){
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            method2(list);
            method3(list);
            //System.out.println(Thread.currentThread().getName()+"运行正常:"+i);
        }
    }
    public void method2(ArrayList<String> list) {
        list.add("1");
    }
    public void method3(ArrayList<String> list) {
        list.remove(0);
    }
}
class ThreadSafeSubClass extends Threadsafe{
    @Override
    public void method3(ArrayList<String> list) {
       new Thread(()->{
           //在调用method2方法时,由于传参传进来的时其它线程传来的list,所以不会影响到method1中创建的list
           //但我们创建了一个子类,这个子类里我们重写了method3方法。
           //我们知道子类和父类可以共享资源,method3重启了一个新的线程来执行remove操作,这就会出现问题了。
           //我们假设Thread1调用method3时,内部又有一个新的线程访问到了list对象。意味着这个list和新线程的list是一个共享资源。
            list.remove(0);
       }).start();
    }
}

方法的访问修饰符也是可以一定程度上保护线程安全的。他限制了子类不能够随意覆盖父类的方法。因此就会有一些因为继承产生的线程安全问题。

常见的线程安全类

  • String
  • Integer (代指包装类)
  • StringBuffffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent 包下的类
        这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为
 
  1. 它们的每个方法是原子的
  2. 注意它们多个方法的组合不是原子的,见后面分析

线程安全类方法的组合
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
    table.put("key", value);
}

这种是线程安全的吗

虽然get和put方法在hashtable里都是线程安全的,但是组合在一起用就不是了。他只能保证get和put方法内的的内容是原子的。而其中的逻辑代码则不是。

例如接下来这张图。线程1判断get是不是null,但是还没有执行put的时候上下文切换了。线程2在判断完后put了key,此时又发生上下文切换,线程1put了key。这时候就发生了线程不安全了。

不可变类线程安全性

String Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
可是 String replace substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安
全的呢?
例如subString方法:
public String substring(int beginIndex, int endIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        if (endIndex > value.length) {
            throw new StringIndexOutOfBoundsException(endIndex);
        }
        int subLen = endIndex - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
//在这里其实subString方法没有对原对象进行修改操作,而是返回了一个newString,返回了一个新对象,所以说事线程安全的
        return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);
    }

实例分析

例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) {

// 使用上述变量
 }
}
  1. map不是线程安全的,因为hashmap不是线程安全的。
  2. S1属于不可变类,所以他是线程安全的
  3. final修饰过的S2自然也是线程安全的。
  4. D1不是线程安全的,Date并不是线程安全类。
  5. D2不是线程安全的,因为D2只是D2这个引用值固定了不能变,但是其他的属性值还是可以变的,所以他还是线程不安全的。

例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++;
 }
}

userService是不是线程安全的呢?不是。userService只是MyServerlet的一个成员变量,具有共享属性。

count也不是线程安全的。在调用userService时count也同样会被共享。

例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));
 }
}

那这段代码有没有线程安全问题呢?

答案是有的。

因为Spring里某一个对象都是单例模式,只要是单例模式就会被共享,里面的成员变量也不例外。所以他不是线程安全的。

解决方法就是用环绕通知中的局部变量,这样就保证了线程安全。

例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) {
// ...
            }
        }
    }

这个从底向上看。

首先DAOImpl是没有成员变量的,可以判断他们是线程安全的。因为他没有共享属性。connection也同样,不同的线程回创造不同的connection对象。

那Service是不是线程安全的。是的。虽然他内部有成员变量,但他内部已经没有可以修改的对象了,所以他是线程安全的。这有点类似先前说的不可变类型,都是无状态的。

Serverlet里使用的Service是不是线程安全的?是的。因为Service里虽然有成员变量,但是这个成员变量是私有的,也没有可以修改的属性,所以是线程安全的。

例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

在这里Connection已经是一个局部变量了,肯定是会被多个线程共享的。所以他是线程不安全的。

例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();
        }
    }

这段代码的不同与例5的点在于,Service实现类里线程调用update方法时,都会新建一个Dao对象,而不是当成一个实现类的成员变量。这样就不会有线程安全问题了。

因为线程一调用update时,每次都是一个新的DAO对象,这个DAO对象里的Connection每次都是一个NULL的状态,独立与之前创建的Connection对象。所以不会有线程安全问题。

例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();
        }
    }

所以面对这种不可预料的情况,把可能会出现线程安全的方法设置成final显得很有必要了。

习题:卖票

public class Node03 {
    static Random random = new Random();
    public static int RandomAmount(){return random.nextInt(5)+1;}

    public static void main(String[] args) throws InterruptedException {
        //用这个统计卖出去的票
        List<Integer> list = new Vector<>();
        //放所有的线程来调用join方法
        List<Thread> threads = new Vector<>();
        TicketWindow ticketWindow = new TicketWindow(1000);
        for (int i = 0; i < 300; i++) {
            Thread thread = new Thread(()->{
                int amount = ticketWindow.sell(RandomAmount());
                list.add(amount);
            },"旅客"+(i+1));
            threads.add(thread);
            thread.start();
        }
        for (Thread thread : threads) {

            thread.join();
        }

        System.out.println("余票:"+ticketWindow.getCount());
        System.out.println("卖出去的票数:"+list.stream().mapToInt(i -> i).sum());
    }
}

class TicketWindow{
    private int count;

    public TicketWindow(int count) {
        this.count = count;
    }

    public int getCount() {
        return count;
    }

    public int sell(int amount){
        if (this.count >= amount){
            this.count -= amount;
            System.out.println(Thread.currentThread().getName()+"买了"+amount+"张票成功!余票:"+this.count+"张");
            return amount;
        }else {
            return 0;
        }
    }
}

其实有点难测出来。。但确实出现了卖出1000多张票的情况。

        这段代码显然是线程不安全的,对于共享变量count没有加锁,在多线程的操作下肯定是会出现线程安全问题。哪怕Vector是线程安全的,但在多线程没有保护临界区的情况下,依旧会存爱线程安全的问题。

所以最好给sell方法加上synchronize关键字,这样就可以解决线程安全问题了。

习题:转账

public class Node03 {
    static Random random = new Random();
    public static int RandomAmount(){return random.nextInt(5)+1;}

    public static void main(String[] args) throws InterruptedException {
        Account account1 = new Account(2000);
        Account account2 = new Account(1000);

        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 1000; i++) {
                account1.transfer(account2,RandomAmount());
            }
        },"t1");
        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 1000; i++) {
                account2.transfer(account1,RandomAmount());
            }
        },"t2");
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("钱相加:"+(account1.getMoney()+account2.getMoney()));
    }
}
class Account{
    private int money;

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    Account(int money) {
        this.money = money;
    }
    public synchronized void transfer(Account target,int amount){
        if(this.money >= amount){
            this.setMoney(this.getMoney() - amount);
            target.setMoney(this.getMoney() + amount);
        }
    }


}

 这次的结果就比较离谱,参差不齐的。

这题的难点在于要保护的共享变量有两个,一个是this.money,一个是target.money

像代码里只给方法加synchronize是没用的,因为synchronize加上去也只是加在this这个对象上,只保护到this.money,但没法保护到target.money,起不到一个保护的作用。我们得找一个this.money和target.money共享的地方。

用关键字锁住Account类即可。

Monitor概念

        Java对象头

        由于Java面向对象的思想,在JVM中需要大量存储对象,存储时为了实现一些额外的功能,需要在对象中添加一些标记字段用于增强对象功能,这些标记字段组成了对象头。

1.对象头形式

通常写的一个Java对象都是由两部分组成,一部分是成员变量,一部分就是对象头。

JVM中对象头的方式有以下两种(以32位JVM为例):

        普通类对象

|--------------------------------------------------------------|
|                     Object Header (64 bits)                  |
|------------------------------------|-------------------------|
|        Mark Word (32 bits)         |    Klass Word (32 bits) |
|------------------------------------|-------------------------|

        数组对象

|---------------------------------------------------------------------------------|
|                                 Object Header (96 bits)                         |
|--------------------------------|-----------------------|------------------------|
|        Mark Word(32bits)       |    Klass Word(32bits) |  array length(32bits)  |
|--------------------------------|-----------------------|------------------------|

        一个对象头在32位下是8个字节,其中KlassWord是一个指针,它指向了对象所从属的class,也就是找到了它的类对象。MarkWord在后面详解。其中数组对象中还有4个字节的数组长度。

MarkWord

这部分主要用来存储对象自身的运行时数据,如hashcode、gc分代年龄等。mark word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word为32位,64位JVM为64位。
为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下:

|-------------------------------------------------------|--------------------|
|                  Mark Word (32 bits)                  |       State        |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 |       Normal       |
|-------------------------------------------------------|--------------------|
|  thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 |       Biased       |
|-------------------------------------------------------|--------------------|
|               ptr_to_lock_record:30          | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
|               ptr_to_heavyweight_monitor:30  | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
|                                              | lock:2 |    Marked for GC   |
|-------------------------------------------------------|--------------------|

其中各部分的含义如下:
lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个mark word表示的含义不同。

biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。
thread:持有偏向锁的线程ID。
epoch:偏向时间戳。
ptr_to_lock_record:指向栈中锁记录的指针。
ptr_to_heavyweight_monitor:指向管程Monitor的指针。

Monitor

        Monitor被翻译为监视器或者管程。

一、monitor概念

        在操作系统中,存在着semaphore和mutex,为了更好的编写并发程序,在mutex和semaphore基础上,提出了更高层次的同步原语,实际上,monitor属于编程语言的范畴,具体的实现模式,不同的编程语言都有可能不一样,C语言不支持monitor,而java支持monitor机制。

        一个重要特点是,在同一时间,只有一个线程/进程能进入monitor所定义的临界区,这使得monitor能够实现互斥的效果。无法进入monitor的临界区的进程/线程,应该被阻塞,并且在适当的时候被唤醒。显然,monitor作为一个同步工具,也应该提供这样管理线程/进程的机制。

        monitor这个机制之所以被称为:更高级的原语,它不可避免的需要对外屏蔽这些机制,并且在内部实现这些机制。

二、monitor基本元素

  •     临界区
  •     monitor对象和锁
  •     条件变量,以及定义在monitor对象上的wait,notify操作

        使用monitor主要是为了互斥进入临界区,为了能够阻塞无法进入临界区的进程,线程,需要一个monitor object来协助,这个object内部会有相应的数据结构,例如列表,用来保存被阻塞的线程;同时由于monitor机制本质是基于mutex原语的,所以object必须维护一个基于mutex的锁。

        此外,为了在适当的时候能够阻塞和唤醒 进程/线程,还需要引入一个条件变量,这个条件变量用来决定什么时候是“适当的时候”,这个条件可以来自程序代码的逻辑,也可以是在 monitor object 的内部,总而言之,程序员对条件变量的定义有很大的自主性。不过,由于 monitor object 内部采用了数据结构来保存被阻塞的队列,因此它也必须对外提供两个 API 来让线程进入阻塞状态以及之后被唤醒,分别是 wait 和 notify。

三、monitor在java中的实现

每个Java对象都可以关联一个Moniror对象。如果使用synchronize给对象上锁(重量级)之后,该对象的MarkWord中就被设置指向Monitor对象的指针。(ptr_to_heavyweight_monitor:指向管程Monitor的指针。)

临界区:被synchronized关键字修饰的方法,代码块,就是monitor机制的临界区。

monitor object:在上述synchronized关键字被使用时,往往需要指定一个对象与之关联,例如synchronized(this),总之,synchronized需要管理一个对象,这个对象就是monitor object。

Java 对象存储在内存中,分别分为三个部分,即对象头、实例数据(对象体)和对齐填充,而在其对象头中,保存了锁标识;同时,java.lang.Object 类定义了 wait(),notify(),notifyAll() 方法,这些方法的具体实现,依赖于一个叫 ObjectMonitor 模式的实现,这是 JVM 内部基于 C++ 实现的一套机制,基本原理如下所示:

        在Java中,一个对象对应了一个momitor对象,而synchronized关键字也需要关联一个对象,这个对象需要天生就支持monitor,所以在Java中,可以就是Java 中的 java.lang.Object 类,便是满足这个要求的对象,任何一个 Java 对象都可以作为 monitor 机制的 monitor object。这也就是wait(),notify(),notifyAll(),是在Object类中的原因。

需要注意的是,synchronize的代码块需要关联到同一对象才能产生锁的效果。

 
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值