并发编程的三大特性——原子性,可见性,有序性

一.原子性

  
  
火车售票系统例子:
并发编程的三大特性——原子性,可见性,有序性_第1张图片
  当客户端A检测到还有票时,执行卖票操作,还没有执行更新票数的操作时客户端B检查了票数,发现大于0,于是又进行了一次卖票操作。这就出现了同一张票被卖了两次的情况。

什么是原子性?
  可以将一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果A进入到这个房间后,还没有出来,B也是可以进入此房间的,打断A在房间里的隐私。此时就是不具备原子性的。
  怎么去解决这个问题呢?此时如果给房间加上一把锁,A拿到钥匙进去之后就把门锁上,那么其他人就进不来了。这样就保证了这段代码的原子性。有时将这种现象叫做同步互斥。



参考代码1:

public class Test {
    private static int num;

    public static void main(String[] args) {
    
          //用循环方式创建20个线程
        for(int i=0;i<20;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                  //每个线程对num循环++
                    for(int j=0;j<10000;j++){
                        num++;
                    }
                }
            }).start();
        }

        while(Thread.activeCount()>1){
            Thread.yield();
        }

        System.out.println(num);
    }
}

运行结果:
并发编程的三大特性——原子性,可见性,有序性_第2张图片
  上述代码申请了20个线程,在每个线程中都对num进行循环++操作,我们希望最终输出num的结果是200000,但是实际debug运行之后每次结果都小于200000且每次结果都不一样。这就是线程不安全以及没有保证原子性造成的。实际上一个线程在进行num++操作时别的线程也可以对num进行操作。所以这时解决办法就是利用原子性给类对象加上锁,在得到锁钥匙的线程对内部代码进行操作时,别的没有得到锁的线程是操作不了这段代码的,接下来加上锁来看看。
  
  
参考代码2:

public class Test {
    private static int num;

    public static void main(String[] args) {
        for(int i=0;i<20;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                //加上对象锁
                    synchronized (Test.class) {
                        for (int j = 0; j < 10000; j++) {
                            num++;
                        }
                    }
                }
            }).start();
        }

        while(Thread.activeCount()>1){
            Thread.yield();
        }

        System.out.println(num);
    }
}

运行结果:
并发编程的三大特性——原子性,可见性,有序性_第3张图片
  
  加上锁之后就可以解决这个问题了,获得锁的线程继续执行代码,而没有竞争到锁的线程处于阻塞态,做到了同步互斥。
  
  

各个线程之间竞争锁的过程:

并发编程的三大特性——原子性,可见性,有序性_第4张图片  
  
  
  
  

二.可见性

  
主内存——工作内存
  
Java内存模型:
并发编程的三大特性——原子性,可见性,有序性_第5张图片

  为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,这个就是可见性问题。
  Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,在这里,无论是普通变量还是volatile变量都是如此。两者之间的区别就是:volatile的特殊规则保证了新值可以立即同步到主内存,以及每次使用前立即从主内存刷新。

  

volatile变量只能保证可见性,在不符合以下规则是还是需要通过加锁来保证原子性。
  1.运算结果并不依赖变量的当前值,或者可以确保只有单一的线程修改变量的值。
  2.变量不需要与其他的状态变量共同参与不变约束。

  
  
  
  
  
  

三.有序性

  
有这样的情况:

比如你的老板分配给你3个任务:
1.去前台拿文件
2.去买杯咖啡
3.去前台取下u盘

  老板分配给你任务之后,其实自己是可以对这3个任务进行优化重排,可以去买咖啡之后再去前台拿文件和u盘,这样就会节省很多时间,效率也会大大提升。同样CPU,JVM也是会对指令代码进行优化重排的,这种叫做指令重排序。当然上述指令正确执行的前提是在单线程情况下。如果是在多线程场景下就有问题了,试想,如果在你去买咖啡的时候,文件或者u盘被另一个人拿走了,那此时就会出错了。

  Java语言也是提供了volatile和synchronized两个关键字来保证线程之间的操作的有序性。volatile关键字本身就包含了禁止指令重排序的语义,synchronized则是通过“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则来实现的。
  
  
  

总结:
 这就是并发编程的3种特性,而synchronized关键字在需要这3种特性时都可以作为一种解决方案,看起来很“万能”,但是越这样“万能”的并发控制,也是会伴随着一些性能的影响的(如线程越多,性能下降也越快,因为归还对象锁时,也是会伴随着大量的线程唤醒,阻塞状态的切换等)。

你可能感兴趣的:(JavaWeb总结笔记)