并发编程3

并发编程之synchronized同步

多线程造成的计算错误—线程切换

  • package BingFaBianCheng.bingFaBianCheng3.test;
    
    import BingFaBianCheng.bingFaBianCheng3.entity.L;
    import BingFaBianCheng.bingFaBianCheng3.util.CASLock;
    import lombok.extern.slf4j.Slf4j;
    
    @Slf4j(topic = "enjoy")
    public class Test1 {
           
        Thread t1;
        Thread t2;
        CASLock c =new CASLock();
        int k =0;
    
        L l = new L();
    
        public static void main(String[] args) throws InterruptedException {
           
            Test1 test1 = new Test1();
            test1.start();
        }
    
        public void start() throws InterruptedException {
           
                t1 = new Thread(){
           
                    @Override
                    public void run() {
           
                        sum();
                    }
                };
    
             t2 = new Thread(){
           
                @Override
                public void run() {
           
                    sum();
                }
            };
    
             t1.start();
             t2.start();
            //阻塞
             t1.join();
             t2.join();
             //两个线程执行10000次加一,最后正确的结果应该是两万
            System.out.println(k);
        }
    
        public void sum(){
           
            /**
             * 锁 对象当中的一个标识
             * 加锁:就是去改变这个对象的标识的值
             * 加锁成功:让方法正常返回
             * 加锁失败:让失败的这个线程 死循环 阻塞
             */
            // c.lock();
                
            for (int i = 0; i <= 9999; i++) {
           
                k = k + 1;
            }
                
    
            // c.unlock();
    
        }
        //12月份之前 都不能有多个线程访问 sun
    }
    
    
  • // 当t1线程来时,k=9,中k=9+1
    // 但是t1线程还没有把结果更新到主存中时,t2线程来了,也加一,最后实际只加了一次
    // 所以最后的结果一定是小于20000的
    k = k + 1;
    

caslock

  • package BingFaBianCheng.bingFaBianCheng3.util;
    
    import sun.misc.Unsafe;
    
    import java.lang.reflect.Field;
    import java.util.concurrent.atomic.AtomicBoolean;
    
    public class CASLock {
           
        private volatile int status=0;//标识---是否有线程在同步块-----是否有线程上锁成功
        //获取Unsafe对象,只能这么获取,Unsafe这个类比较难有兴趣同学可以自己研究研究
        //窃以为Unsafe是java里面最牛逼的一个对象,没有之一
        private static final Unsafe unsafe = getUnsafe();
    
        //定义一个变量来记录 volatile int status的地址
        //因为CAS需要的是一个地址,于是就定义这个变量来标识status在内存中的地址
        private static long valueOffset = 0;
    
        /**
         * 初始化的获取status在内置的偏移量
         * 说白了就是status在内存中的地址
         * 方便后面对他进行CAS操作
         */
        static {
           
            try {
           
                valueOffset = unsafe.objectFieldOffset
                        (CASLock.class.getDeclaredField("status"));
            } catch (Exception ex) {
            throw new Error(ex); }
        }
    
    
        /**
         * 加锁方法
         */
        public void lock(){
           
            /**
             * 判断status是否=0;如果等于0则改变成为1
             * 而判断赋值这两个操作可以通过一个叫做CAS的技术来完成
             * 通过cas去改变status的值,如果是0就改成1
             * 思考一下为什么要用CAS
             * 关于CAS如果你不了解可以先放放,后面我们讲
             * 目前就认为CAS用来赋值的和 = 的效果一样
             */
            while(!compareAndSet(0,1)){
           
                //加锁失败会进入到这里空转
            }
    
           // 如果加锁成功则直接正常返回
        }
    
        //为什么unlock不需要CAS呢?可以自己考虑一下,如果不懂可以讨论
        public void unlock(){
           
            //只有修改值这个操作,就是原子性的
            status=0;
        }
    
        boolean compareAndSet(int except,int newValue){
           
            // 这行代码是原子性的,只有一条被操作系统执行,不会被拆分成几条指令
            //如果 valueOffset或者 status这个变量 = except 那么改成 newValue
          return unsafe.compareAndSwapInt(this,valueOffset,except,newValue);
        }
    
    
        /**
         * 获取Unsafe对象
         * @return
         */
        public static Unsafe getUnsafe() {
           
            try {
           
                Field field = Unsafe.class.getDeclaredField("theUnsafe");
                field.setAccessible(true);
                return (Unsafe)field.get(null);
    
            } catch (Exception e) {
           
            }
            return null;
        }
    
    }
    
    

synchronized来加锁

  • package BingFaBianCheng.bingFaBianCheng3.test;
    
    import BingFaBianCheng.bingFaBianCheng3.entity.L;
    import BingFaBianCheng.bingFaBianCheng3.util.CASLock;
    import lombok.extern.slf4j.Slf4j;
    
    @Slf4j(topic = "enjoy")
    public class Test1 {
           
        Thread t1;
        Thread t2;
        int k =0;
    
        L l = new L();
    
        public static void main(String[] args) throws InterruptedException {
           
            Test1 test1 = new Test1();
            test1.start();
        }
    
        public void start() throws InterruptedException {
           
                t1 = new Thread(){
           
                    @Override
                    public void run() {
           
                        sum();
                    }
                };
    
             t2 = new Thread(){
           
                @Override
                public void run() {
           
                    sum();
                }
            };
    
             t1.start();
             t2.start();
            //阻塞
             t1.join();
             t2.join();
             //两个线程执行10000次加一,最后正确的结果应该是两万
            System.out.println(k);
        }
    
        public void sum(){
           
            /**
             * 锁 对象当中的一个标识
             * 加锁:就是去改变这个对象的标识的值
             * 加锁成功:让方法正常返回
             * 加锁失败:让失败的这个线程 死循环 阻塞
             */
           
            //业内一般叫做锁对象
            //synchronized究竟改变了l对象的什么东西
            synchronized (l) {
           
    
                for (int i = 0; i <= 9999; i++) {
           
                    k = k + 1;
                }
            }
    
    
    
        }
        
    }
    
    
  • synchronized锁的是L对象还是代码块?

    • 业内一般叫锁对象,很难理解,明明看上去锁的是代码块
    • 从结果看好像锁的是代码块,如果把synchronized理解成一把锁,锁住的就是L对象
  • synchronized究竟改变了L对象的什么东西?

    • 对象由对象头、实例数据、对齐填充组成

    • // 定义一个对象User
      // User这个对象有多大?
      // ClassLayout.parseInstance(u).toPrintable()这条命令可以得到u对象的大小(需要添加依赖openjdk.jol)
      // 
      User u = new User;
      
    • // 第一行
      A 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)
      // 第五行---对象头是12字节,1个字节等于8个byte,总共是96byte
      // 
            8     4           (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
      // 第六行---boolean类型对象是1个字节
           12     1   boolean A.f                                       false
      // 第七行---对齐填充,使得最后的结果是8的倍数
           13     3           (loss due to the next object alignment)
      // 第八行
      Instance size: 16 bytes
      // 第九行
      Space losses: 0 bytes internal + 3 bytes external = 3 bytes total
      

对象头

  • http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html【openjdk定义的对象头规范】
  • 任何对象开头的公共部分,包含type、GC state、synchronization state、identity hash code
    • type是对象的类型
    • GC state包含分代年龄、是否被GC标记
    • synchronization state是否被synchronized标记
    • identity hash code— hashcode
  • 对象头由两部分组成,一共12个字节
    • 对象头的第一部分是mark word,占8个字节;包含了GC state、synchronization state、identity hash code;
    • 第二部分是klass pointer,占4个字节;指向元数据(类的模板)的指针;运行时数据区的元空间,会存放class文件转码后生成的二进制文件,对应地址的首地址;长度可能是4,也有可能是8,取决于是否开启指针压缩,没有开始8位,开启了是4位;

markword

  • mark word的8位存储的内容不是固定的

  • 无锁可偏向(无hash)101与无锁不可偏向(有hash)001结构一样

    • 前25位没用,hash:31,前56(25+31)位存的是hashcode,hashcode()也是一个native方法,需要调用后才有hashcode值;

    • 5432(打印hashcode的16进制,刚好等于对象头第五字节+第四字节+第三字节+第二字节拼起来的结果)

    • 为什么会有前25位,是因为小端存储,小端存储指高字节存储到高地址,低字节存储到低地址,所以打印出来是反过来的,所以第八个字节+第七个字节+第六个字节+第五个字节第一位肯定都是0,

    • package BingFaBianCheng.bingFaBianCheng3.util;
      
      import sun.misc.Unsafe;
      
      import java.lang.reflect.Field;
      
      public class HashUtil {
               
      
          public static void countHash(Object object) throws NoSuchFieldException, IllegalAccessException {
               
              // 手动计算HashCode
              Field field = Unsafe.class.getDeclaredField("theUnsafe");
              field.setAccessible(true);
              Unsafe unsafe = (Unsafe) field.get(null);
              long hashCode = 0;
              for (long index = 7; index > 0; index--) {
               
                  hashCode |= (unsafe.getByte(object, index) & 0xFF) << ((index-1)*8);
          }
              String code = Long.toHexString(hashCode);
              System.out.println("util‐‐‐‐‐‐‐‐‐‐‐0x"+code);
      
          }
      }
      
    • 第一个字节共8位(01—十六进制表示法)— 第一位没有用到;第二位到第五位表示分代年龄;第六位表示bl,指偏向标识,即是否可偏向,0表示不可偏向;第七位和第八位记录锁的状态,指示是偏向锁(01)、轻量锁、重量锁、gc等等

    • 第一个字节最后三位是001时,表示当前对象锁状态是偏向锁,但是不可偏向;如果没有hash,但是前三位也是001时,说明开启了偏向延迟,jvm默认是4秒钟之内任何线程都不可偏向,关闭偏向延迟后,第一个字节的后三位变成101,同时没有hash值

    • 偏向延迟关闭命令 -XX:BiasedLockingStartupDelay=0

    • 第一个字节最后三位是001时,表示当前对象锁状态是偏向锁,但是不可偏向,第一个字节最后三位是101时,表示可偏向并且可持有的锁是偏向锁

    • 为什么加了hash就不可偏向呢?— 因为计算了hashcode,无法存放持有锁线程的线程ID,发生了冲突

  • 偏向锁已偏向(101)

    • 前54位存放持有锁的线程的线程ID
    • 第55位+第56位存的是偏向时间戳
    • 第57位未使用
    • 后面的与未偏向的两种无锁状态一致
      • 第58、59、60、61位----代表分代年龄
      • 第62位是偏向标识,此处是1
      • 第63、64位表示锁状态,此处是01
  • 轻量锁(00)

    • 第1-62位指向线程当中的一个地址:62位 lock record
    • 后两位表示锁状态
  • 重量锁(10)

    • 第1-62位指向线程当中的一个地址:62位
    • 后两位表示锁状态
  • gc标记(11)

  • 如果执行力hashcode方法,线程的对象头存的是001,01表示偏向锁,但是没有获得,第一个0表示不可偏向,即不可偏向的没有持有的偏向锁

加锁过程总结

  • 没有计算hashcode—偏向锁打开的情况下,当一把锁第一次被线程持有的时候,是偏向锁,如果这个线程再次加锁还是偏向锁,如果别的线程来执行(交替执行),是轻量锁,如果有资源竞争,是重量锁

  • package BingFaBianCheng.bingFaBianCheng3.test;
    
    
    import BingFaBianCheng.bingFaBianCheng3.entity.A;
    import lombok.extern.slf4j.Slf4j;
    import org.openjdk.jol.info.ClassLayout;
    
    @Slf4j(topic = "enjoy")
    public class TestJol {
           
    
        static A l = new A();
    
        static Thread t1;
        static  Thread t2;
        public static void main(String[] args) throws InterruptedException {
           
            // 计算hash值,如果获取偏向锁存储线程id时会产生冲突
            System.out.println(Integer.toHexString(l.hashCode()));
            log.debug("线程还未启动----无锁");
            log.debug(ClassLayout.parseInstance(l).toPrintable());
            System.out.println(ClassLayout.parseInstance(l).toPrintable());
    
            t1 = new Thread(){
           
                @Override
                public void run() {
           
                    for (int i = 0; i < 3; i++) {
           
                        testLock();
                    }
    
                }
            };
    
            t2 = new Thread(){
           
                @Override
                public void run() {
           
                    testLock();
                }
            };
            t1.setName("t1");
    
            t1.start();
            //等待t1执行完后再启动t2
            //是线程是交替进行的,防止直接变成重量锁
            t1.join();
    
            t2.setName("t2");
            t2.start();
    
    
        }
    
        /**
         * synchronized 如果是同一个线程加锁
         * 交替执行 轻量锁
         * 资源竞争----mutex
         *
         *
         */
    
        public static void testLock(){
           
            //偏向锁  首选判断是否可偏向  判断是否偏向了 拿到当前的id 通过cas 设置到对象头
            synchronized (l){
           //t1 locked  t2 ctlock
                log.debug("name:"+Thread.currentThread().getName());
                //有锁  是一把偏向锁
               log.debug(ClassLayout.parseInstance(l).toPrintable());
            }
    
        }
    }
    
    
  • 如果不加t1.join(),synchronized直接变成重量锁了,因为有资源竞争,t1和t2打印出来都是010

  • 如果加了t1.join(), 打印出来是000,synchronized变成了轻量锁

  • 如果加了t1.join(),同时注释掉t2.setName(“t2”)和t2.start(), 打印出来是001,这里的01不是偏向锁,任何对象初始化出来都是01,表示它是偏向锁但是没有偏向,并且是不可偏向的。之后走到synchronized里面变成000,因为不可以偏向,直接膨胀成轻量锁

  • 如果再注释掉第一行hash方法,先打印出来的是101,后打印的也是101,第一个101是一把偏向锁,其余54位什么都没存,但是没有人偏向它,第二个101执行到synchronized里面,可偏向,变成了偏向锁,同时保存线程ID

偏向锁效率高

  • 偏向锁会膨胀成轻量锁,轻量锁再膨胀成重量锁。
  • 当一把锁第一次被线程持有的时候,是偏向锁,如果这个线程再次加锁还是偏向锁,如果别的线程来执行(交替执行),是轻量锁,如果有资源竞争,是重量锁。
    • 如果一个线程重复加锁,始终是101
    • 为什么效率高?— 加锁时发现是可偏向的,就不会去操作系统做互斥操作,只会做一个操作,首先判断是否可偏向,如果偏向了,拿到当前线程的id,通过cas设置到对象头,没有偏向就没有操作,没有跟底层操作系统发生任何交互

synchronized是一把重量锁吗?

  • 如果是同一个线程加锁,就是偏向锁

  • 如果是两个线程交替直系,是轻量锁

  • 如果两个线程有资源竞争,底层是从pthread_mutex_t实现的重量锁

  • 膨胀过程不可逆,都是在某些极苛刻条件下可偏向

2.操作系统知识补充

虚拟地址映射

  • 要说明虚拟地址,需要说清进程,

  • 4g的内存 ---- 代码块、数据块、堆(全局变量)、栈(局部变量)

  • gcc -g -c xx.c----汇编指令,生成一个.o文件

  • Objdump -d xx.o ---- 生成汇编代码,

    • 0,4,5,8是编排地址
    • 在代码里面通过&l 打印出来的是虚拟地址
    • Objdump -s -d xx.o显示详细代码块的内容,包括文本段、数据段、代码段

执行下面两行,打印保存的虚拟地址

  • gcc -c xx.c xx.out — 将.c文件编译成.out文件(printf("%d",&l))

  • ./xx.out — 运行.out文件

  • cat /proc/进程号/maps — 就可以看到相应的虚拟地址,java中打印的对象地址也是虚拟地址

你可能感兴趣的:(并发编程,多线程)