java并发基础知识

跟風遠走 提交于 2020-01-28 20:44:52

  这几天全国都是关键时候,放假了,还是要学习啊!很久没有写博客了,最近看了一本书,有关于java并发编程的,书名叫做“java并发编程之美”,讲的很有意思,这里就做一个笔记吧!  

  有需要openjdk8源码的,可以直接下载 链接:https://pan.baidu.com/s/1_uT99PLxH-STcs0zl0Mhuw  提取码:ov5b

一.了解并发和并行

  并发:指的是同一时间段内多个任务在执行,并且没有执行结束;就好像你用一个手机看视频,你一下子想看熊出没,一下子又想看喜羊羊,那么你会怎么办?可以这个视频看几秒钟,然后那个视频看几秒钟,最关键的是当你从熊出没->喜羊羊,然后再跳到熊出没的时候,可以直接跳到上次看的记录的位置,这个才是并发最重要的地方;

  在这里,你的眼睛就相当于一个cpu,在来回的切换视频,每个视频就是一个线程,然后可以跳转回上次的最后观看的位置,这功能就是类似PC计数器的作用,可以保存每个线程切换时候的上下文;

  并行:理解了上面的并发,那么并行就很好理解!记得看过火影没有,可以知道鸣人用影分身修炼的情节,这个就是并行!如果你会影分身,那么你和你的分身分别用一个手机去看熊出没和喜羊羊,然后解除影分身,那么你就一下子同时看完两集了!哈哈哈,这个比喻还行吧!其实就是有两个CPU分别去执行一个线程,再把运行后的结果汇总

  在多线程编程实践中,线程的个数往往多余CPU的个数,往往一般称为多线程并发编程而不是多线程并行编程

 

二.线程安全问题

  根据上面的比较应该可以知道,现在的计算机多核CPU的效率更高,打破了单核CPU对多线程效能的限制,每个线程可以使用自己的CPU运行,而不用去频繁的切换线程浪费时间了。

  那么什么是线程安全问题呢?在回答这个问题之前,我们要先知道多线程是在一个java程序里面运行的,而在一个java程序中一些共享的资源是每个线程都需要的,比如多个线程都要对一个全局变量+1,而这个+1的操作不是一瞬间能完成的,多个线程同时+1肯定就会有冲突,这里涉及到缓冲区,后面再说;有冲突的话就会产生不可预料的后果,看看下面的很简单的代码

package com.example.demo.study;

public class Study0127 {
    //这是一个全局变量
    public int num = 0;

    //每次调用这个方法,都会对全局变量加一操作,执行10000次
    public void sum() {
        for (int i = 0; i < 10000; i++) {
            num++;
            System.out.println("当前num的值为num= "+ num);
        }       
    }

    public static void main(String[] args) throws InterruptedException {
        Study0127 demo = new Study0127();
        //下面就是新建两个线程,分别调用一次sum方法
        new Thread(new Runnable() {
            @Override
            public void run() {
                demo.sum();
            }
        }).start();

        new Thread(new Runnable() {

            @Override
            public void run() {
                demo.sum();

            }
        }).start();   
    }

}

 

  这里就是简单的新建两个线程,按理来说的最后的结果是20000,然而每次运行的结果都不一样,随便截一个图,如下,最后的结果是19997,其实这就是缓存的作用。

  每个线程都有自己的私有的空间,首先是将全局变量复制一份到私有空间,然后CPU再进行计算,再赋值;这三步中,如果第一个线程在进行到CPU计算的时候,然后第二个线程去赋值然后进行CPU计算,再赋值到全局变量中去了,此时线程1再去赋值,此时就重复赋值了,这就是问题的所在。

 

   我们可以自己猜想一下,怎么处理这种问题呢?一般有几种方式:

    第一种就是一个代码块同一时间只能被一个线程执行,其他线程等待;具体代码中是加锁的形式,可以使用synchronized,Lock等方式

    第二种就是将那三步打包成一个整体,在一个线程执行这三步任何一步没有执行完的时候,有其他线程修改了全局变量,那么第一个线程就会去重新获取全局变量;这种方式叫做原子性,其实就是CAS;

 

三.java内存可见性问题

  首先说说线程之间内存可见性的问题,下面这个图,这是一个双核CPU架构,每个核都有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辑运算,每一个核都有自己的以及缓存,在这里所有CPU共享二级缓存(有的架构可能不会共享二级缓存),在java中每一个线程的私有空间,就是对应于这里的一级缓存;

  缓存的作用就不用多说了吧,就是为了下一次读取的速度更快;

  那么在本架构中,如果线程A和B都要读取共享变量1,那么线程A首先会将共享变量1存到二级缓存中,再将共享变量1存到线程A的一级缓存中,此时线程A就可以计算了,此时线程B只需要在二级缓存中读取共享变量1就行了,这样速度就快多了!然后线程B再将共享变量1读取到线程B的一级缓存中,进行运算就ok了;

  举个例子,假设这里的共享变量1的值为0,每个线程进行的操作就是+1

  首先,线程A先会在自己的一级缓存中找有没有共享变量1,没有的话再到二级缓存中找,还没有的话就到主内存中找;于是就复制一份0到二级缓存,然后又把0复制一份到线程A的一级缓存,进行运算得到的值为1,然后将1复制给线程A的一级缓存,再复制给二级缓存,再写入到共享变量1中,此时共享变量的值是1

  然后,线程B再到自己的一级缓存中找共享变量1,没有就去二级缓存中找,发现找到了,值为1,于是就复制一份到线程B的一级缓存中,进行+1操作之后等于2,然后把2复制给线程B的一级缓存中,再复制到二级缓存中,再写入到主内存中的共享变量1中,此时共享变量的值为2

  注意:如果这个时候线程A又要进行操作,它会先在自己的一级缓存中找共享变量,发现找到了,值为1,然后进行+1操作等等....这就是坑的所在,线程A此时一级缓存中共享变量的值为1,而主内存和二级缓存中的共享变量的值已经被线程B写入为2,这就是共享变量的内存不可见问题,也就是线程B写入的值2对线程A不可见(线程A读取的还是1)

  看懂了上面,我们就知道volatile关键字的用处了,如果一个共享变量使用volatile关键字进行修饰,那么这个线程在修改该变量的时候是不会放到一级缓存或者二级缓存中,会直接写入到主内存中,例如上面的例子中,我们可以像下面这样修改一下使用,然而最后的结果不一定是20000,因为没有保证原子性!

 

   还有,使用了这个关键字之后,由于没有使用缓存了,效率略低一点,而且不能保证原子性!我们可以想象将上图中一级缓存和二级缓存去掉,那么每次还是会进行三步:从主内存中读取共享变量--->运算--->写入到主内存;这个时候就有一个问题,假设共享变量的值为1,双核CPU,线程A读取共享变量之后正在运算,此时另外一个线程同时也读取共享变量的值为1,也在计算,最后就会导致两个线程都把1写入到主内存的共享变量中,这不是我们想看到的!后面的CAS就可以解决这个问题

 

四.简单说说synchronized

  这个关键字应该很熟悉了,就简单说一下吧,这个关键字是java提供的原子性内置锁,java中的每一个对象都可以当做一个同步器锁,其实当前只能一个线程进入synchronized块中,其他线程进不来,只能阻塞,而阻塞需要从用户态切换到内核态执行阻塞操作(这是操作系统有关的知识),这是很花费时间的,所以一般都说synchronized锁是重量级的;

  其实后面要说的CAS就是避免线程阻塞的,更通俗的说CAS就是多个线程都不会阻塞,每个线程只会去无限循环,当满足条件之后就会更新,避免切换线程阻塞然后还要去切换线程上下文!

  使用synchronized关键字的话,在synchronized块中的变量都不会存到线程的局部空间中,就像volatile一样,可以看做是消除了一级缓存和二级缓存,读取直接从主内存中读取,写入也是直接写入到主内存中(在进入synchronized块中就是从主内存中读取需要用的变量,退出synchronized块就是将计算后的值写入到主内存中 )

  而且synchronized关键字也可以用来实现原子操作,但是会引起线程上下文切换从而导致线程调度开销;这里其实就是将i++这种非原子操作放进一个synchronized块或者synchronized方法中,没啥

 

五.原子操作

  什么叫做原子操作呢?简单来说就是多个操作要么全部成功,要么全部失败,有点类似事务。这里基本的知识就不说了,有兴趣的可以去查查资料!

  直奔主题CAS,全称compareAndSwap,我翻译的意思是:比较xx然后把xx换成xxx,实例的话可以看看AtomicInteger类的compareAndSet方法,这方法内部就是调用unsafe.compareAndSwapInt(this, valueOffset, expect, update);

  可以看到compareAndSwapInt方法有四个参数,第一个参数表示对象的内存地址,第二个参数表示一个偏移量,可以根据偏移量找到变量的位置(假如对象的内存地址是112,该对象有个变量num偏移量为8,那么我们可以知道num的内存地址是120),第三个参数表示根据偏移量找到的变量的值为expect,第四个参数表示将expect更新为update;

  随便举个例子,假设有伪代码unsafe.compareAndSwapInt(this, 3, 99, 100);表示在当前对象内存地址往后移动3个位置的值是99,那么就把99更新为100,我感觉更类似sql语句中的update xxx t set t.a=100 where t.a = 99

  我们可以知道CAS是通过一个Unsafe来实现的,而这个类在rt.jar包中,而且该类几乎所有的方法都是native方法,可以知道是通过JNI调用本地的C艹库实现的,提供了硬件级别的原子操作,所以我们看不到其中的源码,就简单使用一下吧!

package com.example.demo.study;

import java.lang.reflect.Field;

import sun.misc.Unsafe;
public class Study0128 {
    private static final Unsafe unsafe;

    private static final long stateOffset;//偏移量

    private volatile long state = 0;

    static {
        try {
            //通过反射获取Unsafe类中的实例变量,其实openjdk源码中private static final Unsafe theUnsafe = new Unsafe();
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            //获取Unsafe对象
            unsafe = (Unsafe)field.get(null);
            //获取state变量在类Study0128中的偏移值
            stateOffset = unsafe.objectFieldOffset(Study0128.class.getDeclaredField("state"));
        } catch (Exception ex) {
            throw new Error(ex);
        }
    }

    public static void main(String[] args) {
        Study0128 demo = new Study0128();
        System.out.println("之前的state的值为:"+demo.state);
        boolean andSwapInt = unsafe.compareAndSwapInt(demo, stateOffset, 0, 1);
        System.out.println("CAS之后的state的值为:"+demo.state);
        System.out.println(andSwapInt);
    }

}

 

  注意,如果想要看Unsafe的源码的话只能在openjdk中看,至于为什么这里要用反射获取Unsafe对象,以为这个类可以直接操作内存,是不安全的,如果直接跟AtomicInteger类中一样直接用Unsafe unsafe = Unsafe.getUnsafe();就会报错,主要是getUnsafe方法中进行了一个判断,源码如下:

 @CallerSensitive
  public static Unsafe getUnsafe() {
   //获取调用类的Class对象,这指的是Study0128.class
   Class<?> caller = Reflection.getCallerClass();
   //判断我们自己的Study0128这个类的类加载器是不是Bootstrap,肯定不是啊,我们自己定义的类使用AppClassLoader加载的,所以直接会进入到这里,抛错!这样做是为了不让我们开发接触太底层的操作,   //只在rt.jar里面使用这个类,应该就是这样吧
   if (!VM.isSystemDomainLoader(caller.getClassLoader()))
         throw new SecurityException("Unsafe");
   return theUnsafe;
    }       

 

 public static boolean isSystemDomainLoader(ClassLoader loader) {
      //判断类加载器是不是Bootstrap加载器,因为Bootstrap加载器使用c写的,我们直接判断为null就行了
        return loader == null;
    }

 

  也不能直接使用new Unsafe()这种的,因为构造器私有化了

   其实Unsafe还有一些方法挺有意思的,正在看,后面如果用到再说吧,嘿嘿!

  最后说一句,武汉加油!

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!