Java并发

Wesley13
• 阅读 439

最简单的东西,往往包含了最复杂的实现,因为需要为上层的存在提供一个稳定的基础,Object作为java中所有对象的基类,其存在的价值不言而喻,其中wait和notify方法的实现多线程协作提供了保证。

案例

public class WaitTestDemo {

    public static void main(String[] args) {
        Message msg = new Message("process it");
        Waiter waiter = new Waiter(msg);
        new Thread(waiter,"waiterThread").start();

        Waiter waiter1 = new Waiter(msg);
        new Thread(waiter1, "waiter1Thread").start();
        
        Notifier notifier = new Notifier(msg);
        new Thread(notifier, "notifierThread").start();
 
        System.out.println("All the threads are started");
    }

    public static class Message {
        private String msg;
        public Message(String str){
            this.msg=str;
        }
        public String getMsg() {
            return msg;
        }
        public void setMsg(String str) {
            this.msg=str;
        }
    }

    public static class Waiter implements Runnable{
        private Message msg;
        public Waiter(Message m){
            this.msg=m;
        }

        @Override
        public void run() {
            String name = Thread.currentThread().getName();
            synchronized (msg) {
                try{
                    System.out.println(name+" waiting to get notified at time:"+System.currentTimeMillis());
                    msg.wait();
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println(name+" waiter thread got notified at time:"+System.currentTimeMillis());
                //process the message now
                System.out.println(name+" processed: "+msg.getMsg());
            }
        }
    }

    public static class Notifier implements Runnable {
        private Message msg;

        public Notifier(Message msg) {
            this.msg = msg;
        }

        @Override
        public void run() {
            String name = Thread.currentThread().getName();
            System.out.println(name+" started");
            try {
                Thread.sleep(1000);
                synchronized (msg) {
                    msg.setMsg(name+" Notifier work done");
                    msg.notify();
                    msg.notify();
                    //msg.notifyAll();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Output:

All the threads are started
waiterThread waiting to get notified at time:1572344152693
waiter1Thread waiting to get notified at time:1572344152693
notifierThread started
waiterThread waiter thread got notified at time:1572344153705
waiterThread processed: notifierThread Notifier work done
waiter1Thread waiter thread got notified at time:1572344153706
waiter1Thread processed: notifierThread Notifier work done

也可以使用notifyAll,输出为:

All the threads are started
waiterThread waiting to get notified at time:1572344222162
waiter1Thread waiting to get notified at time:1572344222162
notifierThread started
waiter1Thread waiter thread got notified at time:1572344223175
waiter1Thread processed: notifierThread Notifier work done
waiterThread waiter thread got notified at time:1572344223177
waiterThread processed: notifierThread Notifier work done

发现最后唤醒的顺序颠倒了

执行完notify方法,并不会立马唤醒等待线程,在notify方法后面加一段sleep代码就可以看到效果,如果线程执行完notify方法之后sleep 5s,在这段时间内,线程waiterThread1依旧持有monitor,线程waiterThread只能继续等待;

为什么要使用synchronized?

在Java中,synchronized有两种使用形式,同步方法和同步代码块。代码如下:

public class SynchronizedTest {

    public synchronized void doSth(){
        System.out.println("Hello World");
    }

    public void doSth1(){
        synchronized (SynchronizedTest.class){
            System.out.println("Hello World");
        }
    }
}

我们先来使用Javap来反编译以上代码,结果如下(部分无用信息过滤掉了):

public synchronized void doSth();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return

  public void doSth1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #5                  // class com/hollis/SynchronizedTest
         2: dup
         3: astore_1
         4: monitorenter
         5: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #3                  // String Hello World
        10: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        13: aload_1
        14: monitorexit
        15: goto          23
        18: astore_2
        19: aload_1
        20: monitorexit
        21: aload_2
        22: athrow
        23: return

反编译后,我们可以看到Java编译器为我们生成的字节码。在对于doSthdoSth1的处理上稍有不同。也就是说。JVM对于同步方法和同步代码块的处理方式不同。

对于同步方法,JVM采用ACC_SYNCHRONIZED标记符来实现同步。 对于同步代码块。JVM采用monitorentermonitorexit两个指令来实现同步。

关于这部分内容,在JVM规范中也可以找到相关的描述。

同步方法

方法级的同步是隐式的。同步方法的常量池中会有一个ACC_SYNCHRONIZED标志。当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。

同步代码块

同步代码块使用monitorentermonitorexit两个指令实现。 The Java® Virtual Machine Specification 中有关于这两个指令的介绍:

大致内容如下: 可以把执行monitorenter指令理解为加锁,执行monitorexit理解为释放锁。 每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程获得锁(执行monitorenter)后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行monitorexit指令)的时候,计数器再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁。

归总

同步方法通过ACC_SYNCHRONIZED关键字隐式的对方法进行加锁。当线程要执行的方法被标注上ACC_SYNCHRONIZED时,需要先获得锁才能执行该方法。

同步代码块通过monitorentermonitorexit执行来进行加锁。当线程执行到monitorenter的时候要先获得所锁,才能执行后面的方法。当线程执行到monitorexit的时候则要释放锁。

每个对象自身维护这一个被加锁次数的计数器,当计数器数字为0时表示可以被任意线程获得锁。当计数器不为0时,只有获得锁的线程才能再次获得锁。即可重入锁。

底层原理

对象头和内置锁(ObjectMonitor)

每个对象分为三块区域:_对象头、实例数据和对齐填充_。

  • 对象头包含两部分,第一部分是Mark Word,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等,这一部分占一个字节。第二部分是Klass Pointer(类型指针),是对象指向它的类元数据的指针,_虚拟机通过这个指针来确定这个对象是哪个类的实例_,这部分也占一个字节。(如果对象是数组类型的,则需要3个字节来存储对象头,因为还需要一个字节存储数组的长度)
  • 实例数据存放的是类属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,_这部分内存按4字节对齐_。
  • 填充数据是因为虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。

级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下表所示。 Java并发

Synchronized通常被称为重量级锁,但是1.6之后对其进行优化,_新增了轻量级锁和偏向锁_,这里重点说下重量级锁,随后对Synchronized的优化简单介绍下。

从对象头的存储内容可以看出锁的状态都保存在对象头中,Synchronized也不例外,当其从轻量级锁膨胀为重量级锁时,锁标识位为10,其中_指针指向的是monitor对象_(也称为管程或监视器锁)的起始地址。

关于Synchronized的实现在java对象头里较为简单,只是改变一下标识位,并将指针指向monitor对象的起始地址,其实现的重点是monitor对象。

在HotSpot虚拟机中,monitor采用ObjectMonitor实现。

内置锁(ObjectMonitor)

通常所说的对象的内置锁,是对象头Mark Word中的重量级锁指针指向的monitor对象,该对象是在HotSpot底层C++语言编写的(openjdk里面看),简单看一下代码:

//结构体如下
ObjectMonitor::ObjectMonitor() {  
  _header       = NULL;  
  _count       = 0;  
  _waiters      = 0,  
  _recursions   = 0;       //线程的重入次数
  _object       = NULL;  
  _owner        = NULL;    //标识拥有该monitor的线程
  _WaitSet      = NULL;    //等待线程组成的双向循环链表,_WaitSet是第一个节点
  _WaitSetLock  = 0 ;  
  _Responsible  = NULL ;  
  _succ         = NULL ;  
  _cxq          = NULL ;    //多线程竞争锁进入时的单向链表
  FreeNext      = NULL ;  
  _EntryList    = NULL ;    //_owner从该双向循环链表中唤醒线程结点,_EntryList是第一个节点
  _SpinFreq     = 0 ;  
  _SpinClock    = 0 ;  
  OwnerIsThread = 0 ;  
}  

ObjectMonitor队列之间的关系转换可以用下图表示: Java并发

既然提到了_waitSet和_EntryList(_cxq队列后面会说),那就看一下底层的wait和notify方法

wait方法的实现过程:

  //1.调用ObjectSynchronizer::wait方法
void ObjectSynchronizer::wait(Handle obj, jlong millis, TRAPS) {
  /*省略 */
  //2.获得Object的monitor对象(即内置锁)
  ObjectMonitor* monitor = ObjectSynchronizer::inflate(THREAD, obj());
  DTRACE_MONITOR_WAIT_PROBE(monitor, obj(), THREAD, millis);
  //3.调用monitor的wait方法
  monitor->wait(millis, true, THREAD);
  /*省略*/
}
  //4.在wait方法中调用addWaiter方法
  inline void ObjectMonitor::AddWaiter(ObjectWaiter* node) {
  /*省略*/
  if (_WaitSet == NULL) {
    //_WaitSet为null,就初始化_waitSet
    _WaitSet = node;
    node->_prev = node;
    node->_next = node;
  } else {
    //否则就尾插
    ObjectWaiter* head = _WaitSet ;
    ObjectWaiter* tail = head->_prev;
    assert(tail->_next == head, "invariant check");
    tail->_next = node;
    head->_prev = node;
    node->_next = head;
    node->_prev = tail;
  }
}
  //5.然后在ObjectMonitor::exit释放锁,接着 thread_ParkEvent->park  也就是wait

总结:通过object获得内置锁(objectMonitor),通过内置锁将Thread封装成OjectWaiter对象,然后addWaiter将它插入以_waitSet为首结点的等待线程链表中去,最后释放锁。

notify方法的底层实现

  //1.调用ObjectSynchronizer::notify方法
    void ObjectSynchronizer::notify(Handle obj, TRAPS) {
    /*省略*/
    //2.调用ObjectSynchronizer::inflate方法
    ObjectSynchronizer::inflate(THREAD, obj())->notify(THREAD);
}
    //3.通过inflate方法得到ObjectMonitor对象
    ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) {
    /*省略*/
     if (mark->has_monitor()) {
          ObjectMonitor * inf = mark->monitor() ;
          assert (inf->header()->is_neutral(), "invariant");
          assert (inf->object() == object, "invariant") ;
          assert (ObjectSynchronizer::verify_objmon_isinpool(inf), "monitor is inva;lid");
          return inf 
      }
    /*省略*/ 
      }
    //4.调用ObjectMonitor的notify方法
    void ObjectMonitor::notify(TRAPS) {
    /*省略*/
    //5.调用DequeueWaiter方法移出_waiterSet第一个结点
    ObjectWaiter * iterator = DequeueWaiter() ;
    //6.后面省略是将上面DequeueWaiter尾插入_EntrySet的操作
    /**省略*/
  }

总结:通过object获得内置锁(objectMonitor),调用内置锁的notify方法,通过_waitset结点移出等待链表中的首结点,将它置于_EntrySet中去,等待获取锁。注意:notifyAll根据policy不同可能移入_EntryList或者_cxq队列中,此处不详谈。

参考

JVM源码分析之Object.wait/notify实现

[深入理解多线程(四)—— Moniter的实现原理]

从jvm源码看synchronized

并发编程之 wait notify 方法剖析

点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
1年前
java中面向对象的一点学习总结
最近开始看java的一些东西,感觉比python麻烦些,今天学习了面向对象的一些东西,觉得挺多挺复杂,这里做个知识总结以一个简单的例子来说明java面向对象的三大特性,封装,继承,多态,有一个动物(Animal)基类,定义了run与eat方法,然后有一个猫(Cat)与狗(Dog)的子类继承了动物这个父类,子类重写(override)了父类的run与eat
Wesley13 Wesley13
1年前
java多线程管理 concurrent包用法详解
我们都知道,在JDK1.5之前,Java中要进行业务并发时,通常需要有程序员独立完成代码实现,当然也有一些开源的框架提供了这些功能,但是这些依然没有JDK自带的功能使用起来方便。而当针对高质量Java多线程并发程序设计时,为防止死蹦等现象的出现,比如使用java之前的wait()、notify()和synchronized等,每每需要考虑性能、死锁、公平性、
Wesley13 Wesley13
1年前
Java日期时间API系列31
  时间戳是指格林威治时间1970年01月01日00时00分00秒起至现在的总毫秒数,是所有时间的基础,其他时间可以通过时间戳转换得到。Java中本来已经有相关获取时间戳的方法,Java8后增加新的类Instant等专用于处理时间戳问题。 1获取时间戳的方法和性能对比1.1获取时间戳方法Java8以前
Wesley13 Wesley13
1年前
Java16周作业
题目1:编写一个应用程序,利用Java多线程机制,实现时间的同步输出显示。/\使用Runnable接口使用类创建线程对象,重写run()方法\\/代码publicclasstimetext{publicstaticvoidmain(Stringargs){Threa
Wesley13 Wesley13
1年前
Java并发编程之Synchronized
引子目前在Java中存在两种锁机制:synchronized和Lock,今天我们先来介绍一下synchronizedsynchronized可以保证方法或代码块在运行时,同一时刻只有一个线程可以进入到临界区,同时它还保证了共享变量的内存可见性。用法Java中的每个对象都可以作为锁。每一个Object类及其子类
Wesley13 Wesley13
1年前
JAVA设计模式之单例设计模式
    单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例。  在JAVA中实现单例,必须了解JAVA内存机制,JAVA中实例对象存在于堆内存中,若要实现单例,必须满足两个条件:  1.限制类实例化对象。即只能产生一个对象。
Wesley13 Wesley13
1年前
Java基础学习总结(7)——Object类
一、Object类介绍  Object类在JAVA里面是一个比较特殊的类,JAVA只支持单继承,子类只能从一个父类来继承,如果父类又是从另外一个父类继承过来,那他也只能有一个父类,父类再有父类,那也只能有一个,JAVA为了组织这个类组织得比较方便,它提供了一个最根上的类,相当于所有的类都是从这个类继承,这个类就叫Object。所以
Wesley13 Wesley13
1年前
ABA问题的本质及其解决办法
点击上方的蓝字关注我吧_程序那些事_简介CAS的全称是compareandswap,它是java同步类的基础,java.util.concurrent中的同步类基本上都是使用CAS来实现其原子性的。CAS的原理其实很简单,为了保证在多线程环境下我们的更新是符合预期的,或者说一个线程在更新某个对象的时
Wesley13 Wesley13
1年前
Java并发编程指南
  多线程是实现并发机制的一种有效手段。在Java中实现多线程有两种手段,一种是继承Thread类,另一种就是实现Runnable/Callable接口。  java.util.concurrent包是专为Java并发编程而设计的包。类图如下:!(https://oscimg.oschina.net/oscnet/29ddbb
Wesley13 Wesley13
1年前
Java多线程下的协同控制,这些你都知道了吗?
协同控制是并发程序必不可少的重要手段。主要分为两大控制方法,一个是JDK提供的最基础的协同控制方法,一个是java.util.concurrent包下的拓展类控制,接下来我们将会介绍这两种方法有哪些操作可以进行同步控制。一、基础的协同控制线程基础知识因为加锁涉及到多线程,所以有必要先说一下线程的基础知识(定义那