Java并发学习笔记

最近都在看极客时间的《Java并发编程》这一课程,看了好一阵,有些明白,有些混沌。于是想着自己整理一版,根据自己的理解做一点笔记。

先罗列了一个笔记大纲,从整体去看,如何去学习。

大纲.png
并发定义

并发经常和另一个概念被一同提起-并行。
简单点说,并发与并行的区别就是,并行是真正的同一时刻干了几件事,并发只是看上去一段时间内干了几件事。两者的时间维度不一样,并发是“一段时间”这一维度。
并发是通过线程切换可以在同一段时间内干几件事。而线程切换就会带来一些问题。

带来的问题
  • 可见性问题
    可见性问题:一个线程对一个变量的修改对于另一个线程是可见的。而并发可能导致一个线程对变量的修改对于另一个线程是不可见的。
    具体见下面这个常见的例子-计数器:
public class Test {
  private long count = 0;
  private void add10K() {
    int idx = 0;
    while(idx++ < 10000) {
      count += 1;
    }
  }
  public static long calc() {
    final Test test = new Test();
    // 创建两个线程,执行 add() 操作
    Thread th1 = new Thread(()->{
      test.add10K();
    });
    Thread th2 = new Thread(()->{
      test.add10K();
    });
    // 启动两个线程
    th1.start();
    th2.start();
    // 等待两个线程执行结束
    th1.join();
    th2.join();
    return count;
  }
}

直觉count是20000,但是结果是10000~20000之间的一个数。
导致可见性问题的原因:


cpu内存与主内存图.png

关于可见性问题,今天刚刚细细看了一下,是由于cpu缓存与内存不一致导致的,那么单核应该是不会存在可见性问题,另外多个线程如果在同一个cpu上运行应该也是不会产生可见性问题,因为大家都是读取的同一个cpu缓存。

  • 原子性问题
    原子性问题:一个操作或多个操作在cpu执行过程中被执行不被中断
    我们知道I/O的读取是很慢的,cpu相对来说快很多,于是我们在做一个I/O操作时可能会要等待很久,这段时间内cpu是空闲的,于是有人提出多线程分时复用这个概念,可以在等待I/O的同时,让出cpu执行别的操作,就好像早上热牛奶这一操作要等很久,于是同时你可以洗漱。你就是cpu,热牛奶就是读取I/O。于是呢,就会出现线程切换,这样就会导致操作被中断。

例如:count+=1这一操作


原子性问题例子.png

如上图如果有两个线程同时执行count+=1这一操作,有可能出现线程切换,那么会导致数据错乱。

  • 有序性问题
    例如程序中:“a=6;b=7;”编译器优化后可能变成“b=7;a=6" 。这个主要是编译器优化导致的,关于编译器优化这一点我还是不是很理解。
以上就是关于右边并发定义,问题及问题产生的原因这三部分的叙述。
解决方案

如上所知,可见性问题是缓存导致,原子性问题是线程切换导致,有序性问题是编译器优化导致。那么解决方案也是从这三点入手,按需禁用缓存/编译器优化。至于线程切换这一点,我们不能禁止,因为这个是解决慢的问题提出的方案呀。
我们先看按需禁用缓存/编译器优化问题的解决方案:

给出了volatile,final,synchronized,Happens-Before规则这几个概念。

  • volatile
    就是禁用缓存的意思,只能从内存读取。
    那这样就可以解决可见性问题啦
  • final
    就不用说啦,不可修改,但是我有一个疑惑,对于基本类型是不可修改,但如果是一个实体,那么还是可以set去修改里面的值,那应该还是有可见性问题。
  • Happens-Before原则
    这个是重点,大概有6点规则是属于这里面的。
    先明白这个单词的含义,他规则的核心意思是指:前面的操作对后续的操作是可见的,也就说后面的操作是以前面的操作为前提。这点很重要!!!
    例子:
// 以下代码来源于【参考 1】
class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }
  public void reader() {
    if (v == true) {
      // 这里 x 会是多少呢?
    }
  }
}

答案是x=42,就是根据Happens-Before原则。

  1. 程序顺序性原则
  2. volatile 的写操作 Happens-Before后面的读操作
  3. 传递性
    以上三条就可以的得出 x=42
  4. 管程中的锁
    大意是说一个锁的解锁会Happens-Before后面对这个锁的加锁
  5. 线程start()
    大意是说主线程里开启线程B,线程B能看到主线程在启动子线程B之前的操作
  6. 线程join()
    大意是说主线程能看到子线程的操作
  • 原子性问题的解决方案
    有一个经典的例子,在32位cpu上对long写操作,long是64位,写操作会被分为两步,高32位和低32位。如果被两个线程去写,就会有问题啦。

所以呢,我们要保证同一时刻只有一个线程被执行,称之为互斥。这个是解决问题的思路~

写了好久,先去吃块饼干,休息会~

你可能感兴趣的:(Java并发学习笔记)