Java高并发之JMM的先行发生原则、Java线程实现、线程优先级

3. 先行发生原则(happens-before)
① 先行发生原则概述
  • 先行发生原则happens-before)可以用于判断数据是否存在竞争线程是否安全的主要依据。
  • 先行发生: JMM中定义的两项操作的偏序关系,如果操作A先行发生于操作B,则操作A产生的影响能被操作B观察到。影响包括修改共享变量的值发送了消息调用了方法等。
  • 先行发生的简单示例:
  1. 线程A的i = 12的操作先行发生于线程B的j = i操作,则变量j的值一定为12。
  2. 如果加入线程C的操作i = 24,它与线程B的j = i操作不存在先行发生关系,则变量j的值就变得不确定,可能是12,也可能是24。
// 在线程A中执行的操作
i = 12;
// 在线程B中执行的操作
j = i;
// 在线程C中执行的操作
i= 24;
  • 除了可以使用synchronizedvolatile关键字保证有序性,其中synchronized关键字,要求一个变量同一时刻只允许一个线程对其进行lock操作; volatile关键字,运算结果不依赖于当前变量的值,可以保证线程安全。
  • 先行发生原则,可以让一个操作无须控制就能先于另一个操作发生,即无须任何同步措施就能成立的操作的先后关系。
② 先行发生原则
  • 程序顺序规则Programma Order Rule):一个线程内,书写在前面的操作先行发生于书写在后面的操作。
    在这里插入图片描述
  • 监视器锁规则Monitor Lock Rule):对一个锁的unlock操作先行发生于后面对该锁的lock操作。后面是指时间上的先后顺序。
    在这里插入图片描述
  • volatile变量规则Volatile Variable Rule):对一个volatile变量的写操作先行发生于 后面对变量的读操作。
    在这里插入图片描述
  • 线程启动规则Thread Start Rule):线程A通过threadB.start()启动线程B,则线程A中的threadB.start()操作先行发生于线程B中的任意操作。
    在这里插入图片描述
  • 线程加入规则Thread Join Rule):线程A调用threadB.join()操作并成功返回,则线程B中的任意操作都先行发生于线程A从threadB.join()操作成功返回。
    在这里插入图片描述
  • 线程中断规则Thread Interruption Rule):对一个线程的中断操作先行发生于该线程的代码检测到中断事件的发生。中断操作通过调用interrupt()方法实现,中断检测通过调用interrupted()方法实现。
    在这里插入图片描述
public class InterruptRule {
    public static void main(String[] args) {
        MyThread threadB = new MyThread("threadB");
        threadB.start();
        System.out.println("主线程调用 threadB.interrupt()方法,中断threadB");
        threadB.interrupt();
    }
}
class MyThread extends Thread {
    private String name;
    public MyThread(String name) {
        this.name = name;
    }
    @Override
    public void run() {
        while (!interrupted()) {
            System.out.println("线程" + name + "已被中断");
        }
    }
}

在这里插入图片描述

  • 对象终结规则Finalizer Rule):一个对象的初始化完成(即构造函数的结束)先行发生于它的finalize()方法的开始。
    在这里插入图片描述
  • 传递性Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,则操作A先行发生于操作C。
  • 时间上的先后顺序先行发生的关系:
  1. 时间上的先后顺序并不保证先行发生。
    ① 下面的代码,线程A先调用了setValue()方法,线程B再调用getValue()方法。看起来应该是线程B再调用getValue()方法返回1,实际却很有可能返回0.
    ②因为setValue()方法和getValue()方法,并不满足任一的先行发生规则,无法保证线程安全。
public class TimeBefore {
    private int value;
    public void setValue(int value) {
        this.value = value;
    }
    public int getValue() {
        return value;
    }
    public static void main(String[] args) {
        TimeBefore test = new TimeBefore();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                    test.setValue(1);
                    System.out.println("setValue:" + 1);
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                    System.out.println("getValue:" + test.getValue());
            }
        });
        thread1.start();
        thread2.start();
    }
}

在这里插入图片描述
2. 同样,先行发生并不保证时间上的先后,如int i =1; int j = 2;,按照程序顺序规则,int i =1操作先行发生于int j = 2操作。但实际执行时,可能 int j = 2操作可能先被处理器执行。
3. 衡量多线程并发访问的安全问题时,不能受时间先后的迷惑,一切必须以先行发生原则为判断依据

4. Java的线程
  • 线程是CPU调度的基本单位,又称为轻量级进程LWP)。
  • 线程主要有3种实现方式:使用内核线程实现使用用户线程实现使用用户线程加轻量级进程(LWP)混合实现
  • 几个常见概念:
  1. 内核线程(Kernel Level Thread,KLT): 由操作系统内核支持的线程,内核通过调度器(Scheduler)对内核线程进行调度,将其映射到各个CPU上进行执行。
  2. 轻量级进程(Light Weight Process,LWP):程序一般不直接使用内核线程,而是使用内核线程的高级接口——轻量级进程,即我们通常意义上所说的线程。
  3. 用户线程(User Level Thread,ULT):广义上讲,只要不是内核线程的线程都是用户线程,包括轻量级进程;狭义上讲,用户线程是指完全建立在用户空间线程库上,线程的建立、同步、销毁和调度都在用户态中完成,不需要内核的帮助。
① 使用内核线程实现
  • 内核线程实现的方式:
  1. 一个进程分解为若干个轻量级线程,每个轻量级线程与内核线程是1:1映射关系。
  2. 内核线程调度器对内核线程进行调度,将其映射到各个CPU上进行执行。
    在这里插入图片描述
  • 轻量级进程的局限性:
  1. 轻量级进程基于内核线程实现,任何线程操作都需要进行系统调用,而系统调用代价很高,需要在用户态内核态中来回切换。
  2. 每个轻量级进程都需要一个内核线程,因此轻量级进程需要消耗一定的内核资源,导致系统能支持的轻量级进程是有限的
② 使用用户线程实现
  • 使用用户线程的实现方式:
  1. 若干个用户线程构成一个进程,进程在CPU上执行。
  2. 用户线程和进程之间,是N:1的映射关系。
    在这里插入图片描述
  • 使用用户线程的优势在于不需要内核支援劣势也在于不需要内核支援
  1. 因为不需要使用内核支援,所有线程操作都需要自己处理,实现起来一般比价复杂。
  2. 使用用户线程的程序越来越少了,Java、Ruby语言之前都是使用用户线程的,后来又放弃使用它。
③ 使用用户线程加轻量级进程混合实现
  • 混合实现方式的特点:
  1. 既存在用户线程,又存在轻量级进程。
  2. 用户线程的执行还是完全建立在用户空间,可以支持大规模的用户线程并发
  3. 轻量级进程是用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度和CPU映射,还可以降低进程被阻塞的风险。
  4. 用户线程和轻量级进程之间是N:M的映射关系。
    在这里插入图片描述
④ java线程的实现方式
  • Sun JDKWindowsLinux版本都是使用内核线程方式实现的,即使用一对一的线程模型
  • 因为WindowsLinux系统提供的线程模型就是一对一的。
5. Java线程的优先级
① 线程调度两种方式
  • 线程的调度有协同式调度(Cooperative Threads-Schedule)和抢占式调度(Preemptive Threads-Schedule)
  • 协同式调度: 线程的执行时间由线程本身控制,当前线程将自己的任务执行完成,才会通知系统切换到另外一个线程上。
  1. 线程切换对线程自己是可知的,不存在同步问题。
  2. 由于线程的执行时间不可控的,很容易出现一个线程一直不通知系统进行线程切换,从而导致整个程序阻塞在当前线程。
  • 抢占式调度: 每个线程由系统进行时间分配,线程切换不由线程本身决定(Thread.yield()方法可以让出执行时间)。
  1. Java使用的就是抢占式调度。
  2. 抢占式调度模式,线程的执行时间是系统可控的,不会出现一个线程导致整个程序阻塞的情况。
② Java线程的优先级
  • Java线程提供优先级设置,优先级较高的线程被调度执行的机会更大。相比优先级较低的线程,系统为它分配的执行时间更长一点。
  1. 通过thread.setPriority(int priority)设置线程的优先级,priority可以是1 ~ 10的数字。
  2. Java提供10个级别的线程优先级,MIN_PRIORITYMAX_PRIORITY 。其中MIN_PRIORITY = 1NORM_PRIORITY = 5MAX_PRIORITY = 10
  • Java线程的优先级并不靠谱:
  1. 原因: Java线程最终会映射成系统的原生线程,线程的调度最终还是取决于操作系统
  2. 不靠谱的表现1: Java的线程优先级与某些平台的优先级并不是一一对应的,比如Windows平台有7种线程优先级,这就导致不同的Java线程优先级最终可能变得相同。
  3. 不靠谱的表现2: Java线程的优先级可能被系统自行更改
  4. 综上,不能通过Java线程的优先级准确的判断都处于Ready状态的线程,谁会先被系统调度执行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值