并行与并发

  • 并行
    这个概念很好理解。所谓并行,就是同时执行的意思,无需过度解读。判断程序是否处于并行的状态,就看同一时刻是否有超过一个“工作单位”在运行就好了。所以,单线程永远无法达到并行状态。
    图片说明

  • 并发
    要理解“并发”这个概念,必须得清楚,并发指的是程序的“结构”。当我们说这个程序是并发的,实际上,这句话应当表述成“这个程序采用了支持并发的设计”。好,既然并发指的是人为设计的结构,那么怎样的程序结构才叫做支持并发的设计?

正确的并发设计的标准是:使多个操作可以在重叠的时间段内进行(two tasks can start, run, and complete in overlapping time periods)。

这句话的重点有两个。我们先看“(操作)在重叠的时间段内进行”这个概念。它是否就是我们前面说到的并行呢?是,也不是。并行,当然是在重叠的时间段内执行,但是另外一种执行模式,也属于在重叠时间段内进行。这就是协程。

使用协程时,程序的执行看起来往往是这个样子:
图片说明
经常看到这样一个说法,叫做并发执行。现在我们可以正确理解它。有两种可能:

  • 原本想说的是“并行执行”,但是用错了词
  • 指多个操作可以在重叠的时间段内进行,即,真的并行。

并发与并行的关系
这句话来自著名的talk: Concurrency is not parallelism。它足够concise,以至于不需要过多解释。但是仅仅引用别人的话总是不太好,所以我再用之前文字的总结来说明:<font color="red" size=3>并发设计让并发执行成为可能,而并行是并发执行的一种模式。 并行指物理上同时执行,并发指能够让多个任务在逻辑上交织执行的程序设计</font>

多线程是并发还是并行

多进程是并行还是并发取决于你的CPU核心数量。如果是单核CPU,多线程也没用。如果是多核心CPU,那么就可以并行了。CPU多一个核心,这个多出来的核心就可以多处理一个线程。
异步对应的概念是同步。多线程是实现异步的方法。多线程并行(同时进行)(这属于异步)依赖于多核心。多线程并发(把核心比作勺子,把每个线程比作人,把线程的任务比作喝汤。只有一个勺子(单核),一人一口轮着喝(并发))(也属于异步)不依赖与多核心。把一个马桶比作一个CPU核心,线程比做人,那么一个马桶,蹲这个马桶就属于同步,不能拉一半换另一个人拉,然后再换回来接着拉。必须上一个人拉完了,下一个人才能进来拉。

多线程的代价

  • 线程切换的开销大
  • 增加资源消耗
  • 设计更复杂

线程的竞态条件与临界区

当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。下例中add()方法就是一个临界区,它会产生竞态条件。在临界区中使用适当的同步就可以避免竞态条件。

public class Counter {
        protected long count = 0;
        public void add(long value){
            this.count = this.count + value;  
        }
    }

想象下线程A和B同时执行同一个Counter对象的add()方法,我们无法知道操作系统何时会在两个线程之间切换。JVM并不是将这段代码视为单条指令来执行的,而是按照下面的顺序:

从内存获取 this.count 的值放到寄存器
将寄存器中的值增加value
将寄存器中的值写回内存

线程A和B交错执行会发生什么:

this.count = 0;
A:   读取 this.count 到一个寄存器 (0)
B:   读取 this.count 到一个寄存器 (0)
B:   将寄存器的值加2
B:   回写寄存器值(2)到内存. this.count 现在等于 2
A:   将寄存器的值加3
A:   回写寄存器值(3)到内存. this.count 现在等于 3

线程控制逃逸规则

线程控制逃逸规则可以帮助你判断代码中对某些资源的访问是否是线程安全的。

如果一个资源的创建,使用,销毁都在同一个线程内完成,
且永远不会脱离该线程的控制,则该资源的使用就是线程安全的。

线程安全的不可变性

当多个线程同时访问同一个资源,并且其中的一个或者多个线程对这个资源进行了写操作,才会产生竞态条件。多个线程同时读同一个资源不会产生竞态条件。

我们可以通过创建不可变的共享对象来保证对象在线程间共享时不会被修改,从而实现线程安全。如下示例:

public class ImmutabilityTest{
        private int value = 0;     
        public ImmutabilityTest(int value){
            this.value = value;
        }

        public int getValue(){
            return this.value;
        }
    }

请注意ImmutableValue类的成员变量value是通过构造函数赋值的,并且在类中没有set方法。这意味着一旦ImmutableValue实例被创建,value变量就不能再被修改,这就是不可变性。

线程的运行与创建

线程的创建

  • 继承 Thread 类创建线程对象
  • 实现 Runnable 接口类创建线程对象
  • 实现Runnable接口和继承Thread接口的区别:

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。

实现Runnable接口比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源

2):可以避免java中的单继承的限制

3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
1.继承Thread的demo

class Thread1 extends Thread{
    private int count=5;
    private String name;
    public Thread1(String name) {
       this.name=name;
    }
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(name + "运行  count= " + count--);
            try {
                sleep((int) Math.random() * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

public class Main {

    public static void main(String[] args) {
        Thread1 mTh1=new Thread1("A");
        Thread1 mTh2=new Thread1("B");
        mTh1.start();
        mTh2.start();
    }
}

2、实现Runnable的demo

class Thread2 implements Runnable{
    private int count=15;
    @Override
    public void run() {
          for (int i = 0; i < 5; i++) {
              System.out.println(Thread.currentThread().getName() + "运行  count= " + count--);
                try {
                    Thread.sleep((int) Math.random() * 10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    }

}
public class Main {

    public static void main(String[] args) {
        Thread2 mTh = new Thread2();
            new Thread(mTh, "C").start();//同一个mTh,但是在Thread中就不可以,如果用同一个实例化对象mt,就会出现异常  
            new Thread(mTh, "D").start();
            new Thread(mTh, "E").start();
    }
}
//这里要注意每个线程都是用同一个实例化对象,如果不是同一个,效果就和上面的一样了!

在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个jvm,每一个jvm实际上就是在操作系统中启动了一个进程。

run 方法和 start 方法的区别是什么呢?

  • run 方法就是跑的意思,线程启动后,会调用 run 方法。
  • start 方法就是启动的意思,就是启动新线程实例。启动线程后,才会调线程的 run 方法。
    图片说明

    线程的运行

    在运行上面两个小 demo 后,JVM 执行了 main 函数线程,然后在主线程中执行创建了新的线程。正常情况下,所有线程执行到运行结束为止。除非某个线程中调用了 System.exit(1) 则被终止。
    在实际开发中,一个请求到响应式是一个线程。但在这个线程中可以使用线程池创建新的线程,去执行任务。
    图片说明

    线程的状态

  • new(新建)
  • runnnable(可运行)
  • blocked(阻塞)
  • waiting(等待)
  • time waiting (定时等待)
  • terminated(终止)

状态转换图如下:
图片说明
线程状态流程大致如下:

  • 线程创建后,进入 new 状态
  • 调用 start 或者 run 方法,进入 runnable 状态
  • JVM 按照线程优先级及时间分片等执行 runnable 状态的线程。开始执行时,进入 running 状态
  • 如果线程执行 sleep、wait、join,或者进入 IO 阻塞等。进入 wait 或者 blocked 状态
  • 线程执行完毕后,线程被线程队列移除。最后为 terminated 状态。