0
点赞
收藏
分享

微信扫一扫

#yyds干货盘点# Educoder头歌—Java高级特性 - 多线程基础(3)线程同步 第1关:并发编程的三个概念

任务描述

在我们进行应用开发的时候,常常会关注网站的并发,如果网站的用户量很多,当这些用户同时访问一个服务的时候,我们的服务器就会接收到大量的并发请求,处理好这些并发请求是一个合格程序员必须要完成的工作。

理解并发编程的三个概念对于我们更好的开发高并发的Web应用有很大的帮助。

本关的任务就是理解并发编程的三个重要概念并完成右侧选择题。

相关知识

1.原子性

原子性:即一个操作或者多个操作,要么全部执行并且在执行过程中不会被任何因素打断,要么就不执行。

我们来看看下面这段代码:

    x = 10;        //语句1
    y = x;         //语句2
    x++;           //语句3
    x = x + 1;        //语句4

现在请你判断,这段代码哪些是原子操作。

可能你会觉得四个语句都是原子操作,可是实际上只有语句1是原子性操作。 语句1是直接将10的值赋值给x,所以也就是说执行这个语句会直接将数值10写入到内存中,所以这是原子性的,语句2其实是两个操作,先读取x的值,然后赋值给y,这两个步骤是原子性的,但是他们合起来就不是原子性操作了,后面两个语句也是同样的道理。

也就是说,只有简单的读取,赋值(必须是将数字赋值给某个变量,变量之间的赋值不是原子性操作),才是原子性操作。

从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现大范围的原子性,可以通过synchronizedlock来实现,lock(锁)和synchronized(同步)在后面的关卡会介绍。

synchronizedlock可以保证在任何时候只有一个线程执行该代码块,所以就保证了原子性。

2.可见性

可见性是当多个线程访问一个变量时,一个线程改变了变量的值,其他线程立马可以知道这个改变。

举个例子:

    //线程1执行的代码
    int i = 0;
    i = 10;

    //线程2执行的代码
    j = i;

假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。

此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10

这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。

对于可见性,Java提供了Volatile关键字来保证,当一个变量被Volatile修饰时,它会保证修改的值会被立即重新写入到主内存,当其他线程要调用该共享变量时,会去主内存中重新读取。

但是普通的共享变量是不能保证可见性的,因为普通变量会被读入到线程自己的内存,当一个线程修改了之后,可能还没来得及刷新到主内存,其他线程就从主存中读取了该变量。所以其他线程读取的时候可能还是原来的值,所以普通共享变量是无法保证可见性的。

关于保证可见性还可以通过Synchronizedlock的方式来实现。

3.有序性

有序性:即程序的执行是按照代码编写的先后顺序执行的,例如下面这个例子:

    int a;              
    boolean flag;
    i = 10;                //语句1  
    flag = false;            //语句2

上述代码定义了一个整形的变量a,布尔类型变量flag,使用语句1和语句2对变量iflag进行赋值,看起来语句1是在语句2之前的,但是在程序运行的时候语句1一定会在语句2之前执行吗?是不一定的,因为这里可能会发生指令重排序Instruction Reorder)。

什么是指令重排序呢?

一般来说,处理器为了提升执行效率,会对输入代码进行优化,它不保证代码执行的顺序和代码编写的顺序一致,但是它会保证程序的输出结果和代码的顺序执行结果是一致的

比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。

但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例子:

    int a = 3;        //语句1
    int b = 5;        //语句2
    a = a + 3;        //语句3
    b = b + a + 4;       //语句4

上述程序的执行结果就可能是:语句2 => 语句1 => 语句3 => 语句4

那有没有可能是:语句2 => 语句1 => 语句4 => 语句3呢?

不可能,因为处理器在执行语句的时候会考虑数据之间的依赖性,上述代码语句4是要依赖语句3的结果的,所以处理器会保证语句3在语句4之前执行。

如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。

虽然指令重排序不会影响单个线程的最终执行结果,但是多线程的情况下会不会影响呢?我们来看一个例子:

    //线程1:
    context = loadContext();   //语句1
    inited = true;             //语句2

    //线程2:
    while(!inited ){
      sleep();
    }
    doSomethingwithconfig(context);

可以发现语句1和语句2并没有数据依赖性,所以按照指令重排序的规则,可能语句2在语句1之前执行,语句2执行完之后,语句1还没开始执行,可能线程2就开始执行了,这个时候initedtrue,会跳出while循环转而执行doSomethingwithconfig(context)而这个时候语句1还没执行,context还没有初始化,就会造成程序报错。

从上述例子可以看出,指令重排序不会影响单个线程的执行,但是会影响多线程的执行。

也就是说,要保证多线程程序执行的正确性,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

编程要求

测试说明

  • 1、在并发编程中,我们需要以下哪几个特性来保持多线程程序执行正确( A

    A、可见性

    B、原子性

    C、并发性

    D、有序性

  • 2、请分析以下语句哪些是原子操作( AB

    A、int a = 3;

    B、boolean flag = false;

    C、a–;

    D、a =a *a

  • 3、以下代码的执行结果是(E

    public class Test {
      public  int inc = 0;
    
      public void increase() {
          inc++;
      }
    
      public static void main(String[] args) {
          final Test test = new Test();
          for(int i=0;i<10;i++){
              new Thread(){
                  public void run() {
                      for(int j=0;j<1000;j++)
                          test.increase();
                  };
              }.start();
          }
    
          while(Thread.activeCount()>1)  //保证前面的线程都执行完
              Thread.yield();
          System.out.println(test.inc);
      }
    }

    A、10000

    B、9870

    C、大于10000

    D、小于10000

    E、不一定,大概率小于一万

举报

相关推荐

Java多线程:并发编程(三)

0 条评论