并发编程(2):synchronized原理、线程通讯

1、javap -v xxx.class可以查看class的字节码。

2、Java中i++ 非线程安全:
   字节码指令如下:
        14: getstatic     //访问变量
        17: iconst_1      //压栈
        18: iadd          //执行i+1
        19: putstatic     //赋值给i变量。
        
        这4条指令不是原子性执行,会存在线程安全问题。

 

3、锁(synchronized):

     使用方式一:同步方法上,其实就等于synchronized(this)。

     public void synchronized test(){
       ...
     }


     使用方式二:同步代码块。

     Object obj = new Object();
     public void test(){
        synchronized(obj){
           ...
        }
     }

      锁的范围:
       1、对象锁:以对象来作为锁,锁资源就是当前对象。
       2、静态方法、类对象、类锁,锁资源就是class实例,只有一份,是多线程的共享资源。

       案例演示如下:

public class Synchronizedtest2 {
    public static int count = 0;

    //对象锁
    public synchronized void test1(){
        count ++;
    }

    Object object = new Object();
    public void test2(){
        synchronized (object){
            count ++;
        }
    }

    //对象锁
    public static void main(String[] args) {
        //对象锁
        Synchronizedtest2 synchronizedtest2 = new Synchronizedtest2();
        //两条线程公用一个synchronizedtest2实例,而且test1()方法就是使用当前实例来作为锁,所以存在互斥。
        new Thread(()->synchronizedtest2.test1()).start();
        new Thread(()->synchronizedtest2.test1()).start();


        Synchronizedtest2 synchronizedtest22 = new Synchronizedtest2();
        Synchronizedtest2 synchronizedtest23 = new Synchronizedtest2();
        //两条线程没有公用实例,所以不存在访问共享的资源,所以不存在互斥。
        new Thread(()->synchronizedtest22.test1()).start();
        new Thread(()->synchronizedtest23.test1()).start();
    }

    //类锁
    public static synchronized void test03(){

    }
    public void test04(){
       synchronized (Synchronizedtest2.class){

       }
    }

    //类锁
    public static void main(String[] args) {
        //类锁
        Synchronizedtest2 synchronizedtest2 = new Synchronizedtest2();
        //由于是类锁,锁资源就是类的class实例,只有一份,所以存在互斥。
        new Thread(()->synchronizedtest2.test03()).start();
        new Thread(()->synchronizedtest2.test03()).start();

        Synchronizedtest2 synchronizedtest22 = new Synchronizedtest2();
        Synchronizedtest2 synchronizedtest23 = new Synchronizedtest2();
        //由于是类锁,锁资源就是类的class实例,只有一份,所以存在互斥。
        new Thread(()->synchronizedtest22.test1()).start();
        new Thread(()->synchronizedtest23.test1()).start();
    }
}

 

4、Java对象头

      

 

      4.1 jol-core 工具可以打印jvm中对象的布局

        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.10</version>
        </dependency>

 

        public class ClassLayoutDemo {
           public static void main(String[] args) {
             ClassLayoutDemo classLayoutDemo = new ClassLayoutDemo();
             System.out.println(ClassLayout.parseInstance(classLayoutDemo).toPrintable());
           }
        }

      4.2、对象头信息

        com.wzy.threadstudy.day02.ClassLayoutDemo object internals:
         OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
              0     4字节        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
              4     4字节        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
              8     4字节        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
             12     4        (loss due to the next object alignment)
        Instance size: 16 bytes
        Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


             可以看出64位机器中的对象布局的对象头有12个字节共96位,但是其实这是压缩过的,我们可以设置不压缩,使用jvm参数:-XX:-UseCompressedOops 来解除压缩。


              我们解除压缩后输出的布局中对象头将会有128位。

        com.wzy.threadstudy.day02.ClassLayoutDemo object internals:
         OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
              0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
              4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
              8     4        (object header)                           28 30 a5 25 (00101000 00110000 10100101 00100101) (631582760)
             12     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
        Instance size: 16 bytes
        Space losses: 0 bytes internal + 0 bytes external = 0 bytes total


               可以看出对象头一共有16个字节128位。不管是输出96位是128位,对于我们来说看前面64位,即

              0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
              4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)

    
      4.3、大小端储存-确定锁标记存储位
          大端方式将高位存放在低地址,小端方式将高位存放在高地址。
          
          jvm采用的是大端存储,所以对象头前64位的地址为:
          16进制:0x 00 00 00 00 00 00 00 01
          2进制: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001  二进制中倒数第三位用于                         存储 "是否偏向锁" 最后两位用于储存 "锁标志"。

      4.4、对象头结构以及锁标记规则

 

5、锁升级

     5.1、偏向锁

             在大多数情况下,锁不仅仅不存在多线程的竞争,而且总是由同一个线程多次获得。在这个背景下就设 计了偏向锁。偏向              锁,顾名思义,就是锁偏向于某个线程。
              当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的ID,后续这个线程进入和退出这 段加了同步锁的代              码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线 程的偏向锁。如果相等表示偏向锁是偏向            于当前线程的,就不需要再尝试获得锁了,引入偏向锁是为了 在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。          (偏向锁的目的是消除数据在无竞争情 况下的同步原语,进一步提高程序的运行性能。

     5.2、轻量级锁

              如果偏向锁被关闭或者当前偏向锁已经已经被其他线程获取,那么这个时候如果有线程去抢占同步锁 时,锁会升级到轻量               级锁。

     5.3、重量级锁

              多个线程竞争同一个锁的时候,虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒 这些线程; Java 线程的              阻塞以及唤醒,都是依靠操作系统来完成的:os pthread_mutex_lock() ; 升级为重量级锁时,锁标志的状态值变为                      “10”,此时Mark Word中存储的是指向重量级锁的指 针,此时等待锁的线程都会进入阻塞状态。

     5.4、锁升级

             锁升级是锁优化的过程,由于直接竞争重量级锁会代劳比较大的开销,所以jvm的开发人员进行了锁升级的优化锁,升级的
            过程是 偏向锁-->轻量级锁-->重量级锁。

            偏向锁-->轻量级锁 流程如下:

                         

          偏向锁的获取流程说明:

            1、首先获取锁对象头中的 Mark Word,判断当前对象是否处于可偏向状态(即当前没有对象获得偏向锁)。

            2、如果是可偏向状态,则通过CAS原子操作,把当前线程的ID 写入到 MarkWord,如果CAS成功,表示获得偏向锁成功,会将偏向锁标记设置为1,且将当前线程的ID写入Mark Word;如果CAS失败则说明当前有其他线程获得了偏向锁,同时也说明当前环境存在锁竞争,这时候就需要将已获得偏向锁的线程中的偏向锁撤销掉,并升级为轻量级锁(偏向锁的撤销,需要等待全局安全点,即在这个时间点上没有正在执行的字节码)。

            3、如果当前线程是已偏向状态,需要检查Mark Word中的ThreadID是否和自己相等,如果相等则不需要再次获得锁,可以直接执行同步代码块,如果不相等,说明当前偏向的是其他线程,需要撤销偏向锁并升级到轻量级锁。

          偏向锁的撤销流程说明:

              偏向锁的撤销并不是把对象恢复到无锁可偏向状态(因为偏向锁并不存在锁释放的概念),而是在获取偏向锁的过程中,发现CAS失败也就是存在线程竞争时,直接把被偏向的锁对象升级到被加了轻量级锁的状态。对原持有偏向锁的线程进行撤销时,原获得偏向锁的线程 有两种情况:

             1、原获得偏向锁的线程如果已经退出了临界区,也就是同步代码块执行完了,那么这个时候会把对象头设置成无锁状态,同时正在争抢锁的线程可以基于 CAS 重新偏向当前线程。

             2、如果原获得偏向锁的线程的同步代码块还没执行完,处于临界区之内,这个时候会把原获得偏向锁的线程升级为轻量级锁后继续执行同步代码块。原获得偏向锁的线程升级为轻量级锁的时候会执行轻量级锁的加锁过程即-->在原获得偏向锁的线程的栈帧中创建lock record 然后将锁对象的mark word 复制到lock record 中,然后使用 CAS将锁对象头中的Mark Word替换为指向lock record的指针。

           

          轻量级锁-->重量级锁 流程如下:

                        

                   轻量级锁加锁流程说明:

                      1、线程A 将 Mark Word 拷贝到线程栈的 Lock Record中,这个位置叫 displayced hdr。

                          如下图所示:

                             

                      2、将锁记录中的Owner指针指向锁的对象(存放锁对象地址)。

                      3、将锁对象的对象头的MarkWord替换为指向Lock Record的指针,此步骤会使用cas操作,如果成功表示此线程获                               取了轻量级锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁,自旋一定次数后还是没有获                             取到锁那就锁膨胀到重量级锁。

                              如下图所示:

                                      

 

                  轻量级锁解锁流程说明:

                          轻量级解锁时,会使用原子的CAS操作将displayced hdr替换回到对象头,如果成功,则表示没有竞争发                                      生,如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

 

6、重量级锁(synchronized的锁升级的最终的锁)

     当锁升级到重量级锁后,就会有线程被阻塞,我们通过查看synchronized关键字的字节码指令,案例代码如下:


              public static synchronized void test03(){

              }

              public void test04(){
                  synchronized (Synchronizedtest2.class){
 
                  }
              }

                    javap -v  Synchronizedtest2.class 输出如下:

                      

               public static synchronized void test03();
                   descriptor: ()V
                   flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
                   Code:
                     stack=0, locals=0, args_size=0
                        0: return
                     LineNumberTable:
                        line 36: 0


               public void test04();
                   descriptor: ()V
                   flags: ACC_PUBLIC
                   Code:
                     stack=2, locals=3, args_size=1
                        0: ldc           #5                  // class com/wzy/threadstudy/synchronizedStudy/Synchronizedtest2
                        2: dup
                        3: astore_1
                        4: monitorenter
                        5: aload_1
                        6: monitorexit
                        7: goto          15
                       10: astore_2
                       11: aload_1
                       12: monitorexit
                       13: aload_2
                       14: athrow
                       15: return

                   从class字节码来看有如下两点:                 

                      1、synchronized方法反编译后,字节码中有ACC_SYNCHRONIZED标识。
                      2、synchronized代码块反编译后,字节码中有monitorentermonitorexit语句。

                    由此我们有如下猜测:
                      1、synchronized的作用域不同,JVM底层实现原理也不同
                      2、synchronized代码块是通过monitorenter和monitorexit来实现其语义的
                      3、synchronized方法是通过ACC_SYNCRHONIZED来实现其语义的

                    monitorentermonitorexit实现原理:       

                      每一个对象都会和一个监视器monitor关联(后面会详细说明)。监视器被占用时会被锁住,其他线程无法来获取该                          monitor。当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有                          权。

                      monitorenter过程如下:

                            1、若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1,当前线程成为monitor的                                         owner(所有者)。
                            2、若线程已拥有monitor的所有权,允许它重入monitor,并递增monitor的进入数
                            3、若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor                                      的进入数变为0,才能重新尝试获取monitor的所有权。

                      monitorexit过程如下:

                            1、能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。
                            2、执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当
                                 前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞
                                 的线程可以尝试去获取这个monitor的所有权。

                      ACC_SYNCRHONIZED原理:

                             当JVM执行引擎执行某一个方法时,其会从方法区中获取该方法的access_flags,
                             检查其是否有ACC_SYNCRHONIZED标识符,若是有该标识符,则说明当前方法是同
                             步方法,需要先获取当前对象的monitor,再来执行方法。

 

7、 监视器monitor详细

       7.1、每一个JAVA对象都会与一个监视器monitor关联,我们可以把它理解成为一把锁,当一个线程想要执行一段被                                   synchronized圈起来的同步方法或者代码块时,该线程得先获取到synchronized修饰的对象对应的monitor。

       7.2、我们的java代码里不会显示地去创造这么一个monitor对象,我们也无需创建,事实上可以这么理解:我们是通过                             synchronized修饰符告诉JVM需要为我们的某个对象创建关联的monitor对象。

       7.3、在hotSpot虚拟机中,monitor是由ObjectMonitor实现的。其源码是用c++来实现的,位于hotSpot虚拟机源码                               ObjectMonitor.hpp文件中。

       7.4、java对象对应的monitor也是一个临界资源,其线程安全由虚拟机自身代码来保证,开发者无需考虑。

                ObjectMonitor主要数据结构如下:

             ObjectMonitor() {
                _header       = NULL;
                _count        = 0; //monitor进入数
                _waiters      = 0,
                _recursions   = 0;  //线程的重入次数
                _object       = NULL;
                _owner        = NULL; //标识拥有该monitor的线程
                _WaitSet      = NULL; //等待线程组成的双向循环链表,_WaitSet是第一个节点
                _WaitSetLock  = 0 ;
                _Responsible  = NULL ;
                _succ         = NULL ;
                _cxq          = NULL ; //多线程竞争锁进入时的单项链表
                FreeNext      = NULL ;
                _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
                _SpinFreq     = 0 ;
                _SpinClock    = 0 ;
                OwnerIsThread = 0 ;
             }

                  owner:初始时为NULL。当有线程占有该monitor时,owner标记为该线程的唯一标识。当线程释放monitor时,
                               owner又恢复为NULL。owner是一个临界资源,JVM是通过CAS操作来保证其线程安全的。

                  _cxq    :竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链接),_cxq是一个临界资源,JVM通过CAS原                                    子指令来修改_cxq队列。修改前_cxq的旧值填入了node的next字段,_cxq指向新值(新线程),因此_cxq是                                    一个后进先出的stack(栈)。

                  _EntryList:_cxq队列中有资格成为候选资源的线程会被移动到该队列中。

                  _WaitSet  :  因为调用wait方法而被阻塞的线程会被放在该队列中。

 

       7.5、synchronized是非公平锁,因为后面才开始等待的线程,反而会先获得锁。因为释放锁时,默认策略是:如果EntryList为空,则将cxq中的元素按原有顺序插入到到EntryList,并唤醒第一个线程。也就是当EntryList为空时,是后来的线程先获取锁

        monitor的整个流程可以使用一张图说明(图是拿来主义): 

 

8、线程通讯(wait/notify)

      1、调用wait() 首先会获取监视器锁,获得成功后,会让线程进入等待状态并且进入等待队列释放锁
      2、然后当其他线程调用notify或者notifyall以后,会选择从等待队列中唤醒任意一个线程
      3、而执行完notify方法以后,并不会立马唤醒线程,原因是当前线程仍然持有这把锁,处于等待状态的线程无法获得锁。必须要             等到当前的线程执行完按monitorexit指令之后,也就是被释放之后,处于等待队列的线程就可以开始竞争锁了。

       案例:生产者消费者模型实现:

           producer代码:

public class Producer implements Runnable {

    private Queue<String> msg;
    private int maxSize;

    @Override
    public void run() {
        int i = 0;
        synchronized (msg) {
            i++;
            while (true) {
               while (msg.size() == maxSize){
                   //如果生产满了
                   try {
                       msg.wait();
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }

               //生产消息
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                msg.add("消息:" + i);
                System.out.println("生产了:"+ "消息:" + i);

                /*
                保证了生产完成maxSize个消息才会通知消费者去生产,此处notify不会释放锁,
                只有等到下一次循环的时候调用到msg.wait();才会释放锁。
                 */
               if (msg.size() == maxSize){
                   msg.notify();
               }
            }
        }
    }

    public Producer(Queue<String> msg, int maxSize) {
        this.msg = msg;
        this.maxSize = maxSize;
    }
}

         

           consumer代码:

public class Consumer implements Runnable {

    private Queue<String> msg;
    private int maxSize;

    @Override
    public void run() {
        synchronized (msg) {
            while (true) {
               while (msg.size()==0){
                   //如果消费完了
                   try {
                       msg.wait();
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
               //生产消息
                String remove = msg.remove();
                System.out.println("消费:" + remove);

                /*
                保证了消费完成才会通知生产者去生产,此处notify不会释放锁,
                只有等到下一次循环的时候调用到msg.wait();才会释放锁。
                 */
                if (msg.size()==0){
                   msg.notify();
               }
            }
        }
    }

    public Consumer(Queue<String> msg, int maxSize) {
        this.msg = msg;
        this.maxSize = maxSize;
    }
}

           

            测试代码client:

public class Client {

    public static void main(String[] args) {
         Queue<String> msg = new LinkedList<>();
         int maxsize = 5;

        Producer producer = new Producer(msg, maxsize);
        Consumer consumer = new Consumer(msg, maxsize);

        Thread t1 = new Thread(producer);
        Thread t2 = new Thread(consumer);

        t1.start();
        t2.start();
    }
}


 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值