[Java]并发编程(一)多线程相关的基础知识及概念

By | 2020年1月4日

1 并发与并行

并发是指在某个时间段内,多任务交替处理的能力。并行是指同一时刻处理多任务的能力。在并发环境下,由于程序的封闭性被打破,出现了以下特点
1)并发程序之间有相互制约的关系。 直接制约体现为一个程序需要另一个程序的计算结果,间接制约体现为多个程序竞争共享资源,如处理器、缓冲区等。
2)并发程序的执行过程是断断续续的。 程序需要记忆现场指令及执行点。
3)当并发数设置合理并且CPU拥有足够的处理能力时,并发会提高程序的运行效率。

2 线程的五种状态

为方便理解,可以认为线程在其生命周期中有五种状态:新建状态(New)就绪状态(Runnable)运行状态(Running)阻塞状态(Blocked)终止状态(Terminated)。他们之间的关系可以表示为:

3 创建线程的三种方式

1)继承Thread类;2)实现Runnable接口;3)实现Callable接口

4 线程的内存分配

每个线程拥有自己的虚拟机栈(Java Stacks)、本地方法栈(Native Method Stacks)和程序计数器(Program Counter Register)。虚拟机栈包括局部变量表、操作栈、动态链接和方法返回地址等。

5 线程安全

在一个多线程环境的程序中,当程序结束时能够得到预期的结果,那么称这种状态为线程安全。单线程中,永远是线程安全的。

6 线程交互的可见性

6.1 指令重排(Instruction Reordering)

编译器在编译代码时,为了提升CPU的工作效率,会对指令进行重新排序。在并发执行的情况下,重排序可能导致意想不到的运行结果。

6.2 原子性

一个不能被分割的操作叫做原子操作。非原子操作在并发条件下会得到意想不到的结果,如下例:

public class Increment {
    private int value;
    private static final int TIMES = 10000;
    private static final int THREAD_NUMBERS = 20;

    public void increase() {
        value++;
    }

    public int getValue() {
        return value;
    }

    public static void test() {
        Increment increment = new Increment();

        Thread[] threads = new Thread[THREAD_NUMBERS];

        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < TIMES; i++) {
                        increment.increase();
                    }
                }
            });
            threads[i].start();
        }

        for (Thread t : threads) {
            try {
                t.join();
            } catch (InterruptedException e) {

            }
        }

        System.out.println("result:" + increment.getValue());
    }

    public static void main(String[] args) {
        test();
        /*
        输出(一个小于200,000的值):
        
        result:71641
         */
    }
}

6.3 可见性

可见性是指,当一个线程修改了共享变量的值,其他线程也能够感知这个修改。因为每个线程拥有自己的工作内存,所以一个线程的操作不是对其他线程都可见的(每个线程在运行时会有自己的工作内存,工作内存的值会不停写回到主内存)。

6.4 volatile关键字与Happends-before原则

volatile关键字的作用有两点:1)保证volatile修饰的变量的可见性,即每次有线程更新共享变量时,其他线程也可以感知到此更新操作;2)禁止指令重排序优化。由此可见,volatile只有在多线程程序中,才有用处。

Happends-before原则是一种偏序关系:有两个操作A和B,如果A操作happends-before操作B,那么A操作就会在B操作之前发生,且A的所有改动对B都是可见的。记做hb(A, B)

Happends-before原则包含以下几条:
1)程序次序规则(Pragram Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作happends-before于书写在后面的操作。准确地说应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环结构。
2)管程锁定规则(Monitor Lock Rule):一个unlock操作happends-before于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。
3)volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作happends-before于后面对这个变量的读取操作,这里的”后面“同样指时间上的先后顺序。
4)线程启动规则(Thread Start Rule):Thread对象的start()方法happends-before于此线程的每一个动作。
5)线程终止规则(Thread Termination Rule):线程中的所有操作都happends-before于对此线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值等作段检测到线程已经终止执行。
6)线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用happends-before于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否有中断发生。
7)对象终结规则(Finalizer Rule):一个对象初始化完成(构造方法执行完成)happends-before于它的finalize()方法的开始。
8)传递性(Transitivity):对于操作A,B和C,如果有hb(A, B)hb(B, C)那就可以得出hb(A, C)的结论。

理解Happends-before原则有什么用处呢?先来看下面的例子:

T1
x = 5;  // 语句1
y = 6;  // 语句2
T2
if (y == 6) System.out.println(x);  // 语句3

x, y初始值都为0,那么T2的输出可能是5,也可能是0。因为在T1线程中可能会对两条语句进行重排。从上面的happends-before原则1)可能会推测,x = 5这条指令一定会在y = 6之前运行,实际上是错误的。对于T1来说,语句1和语句2谁先执行谁后执行不影响最终的一致性,所以可能因为指令重排导致先执行语句2,在执行语句1,那么语句3的输出就是0了。

参考:

发表评论

电子邮件地址不会被公开。 必填项已用*标注