JAVA-多线程-常识

 

Atomic 原子类

并发包 java.util.concurrent 的原子类都存放在java.util.concurrent.atomic

 

所谓原子类说简单点就是具有原子/原子操作特征的类。

Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰

JUC 包中的原子类是哪 4 类

基本类型  数组类型  引用类型 对象的属性修改类型

AtomicInteger

使用 AtomicInteger 之后,不用对 increment() 方法加锁也可以保证线程安全 

原理

private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;

 

 

 

 

简述happens-before八大原则

在Java内存模型中,happens-before的意思是前一个操作的结果可以被后续操作获取。

https://zhuanlan.zhihu.com/p/434045508

程序次序规则:

在一个线程中,按照代码的顺序,前面的操作先行发生于后面的任意操作。在同一个线程中,程序在前面对某个变量的修改一定是对后续操作可见的。

volatile变量规则:

对 volatile 变量的写操作先行发生于后面的读操作

传递性规则:

如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C

锁定规则:

对一个锁的解锁操作先行发生于后续对这个锁的加锁操作。

例如,下面的代码,在进入synchronized代码块之前,会自动加锁,在代码块执行完毕后,会自动释放锁。

 

 

 我们可以这样理解这段程序:假设变量x的值为10,线程A执行完synchronized代码块之后将x变量的值修改为10,并释放synchronized锁。

当线程B进入synchronized代码块时,能够获取到线程A对x变量的写操作,也就是说,线程B访问到的x变量的值为10。

线程启动规则:

线程的 start 方法先行发生于线程的每个动作

 

 上述代码是在线程A中执行的一个代码片段,根据线程的启动规则,线程A启动线程B之后,线程B能够看到线程A在启动线程B之前的操作,在线程B中访问到的x变量的值为100。

线程中断规则

对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。

例如,下面的程序代码。在线程A中中断线程B之前,将共享变量x的值修改为100,则当线程B检测到中断事件时,访问到的x变量的值为100。

 

 

线程终结规则

线程中所有操作先行发生于对线程的终止检测。

线程A等待线程B完成(在线程A中调用线程B的join()方法实现),当线程B完成后(线程A调用线程B的join()方法返回),则线程A能够访问到线程B对共享变量的操作。

例如,在线程A中进行的如下操作。

 

 

 

对象终结原则

一个对象的初始化完成Happens-Before于它的finalize()方法的开始。

import java.util.*;

public class Main {
    public Main() {
        System.out.println("gou zao");
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("xiao hui");
    }

    public static void main(String[] args) {
        new Main();
        System.gc();
    }
}

gou zao
xiao hui

 

 

 一个 Java 程序的运行是 main 线程和多个其他线程同时运行

 

《Java并发编程实践》中对线程安全的定义:

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。

并发编程的三个重要特性

  1. 原子性 : 一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。synchronized 可以保证代码片段的原子性。
  2. 可见性 :当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。volatile 关键字可以保证共享变量的可见性。
  3. 有序性 :代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。volatile 关键字可以禁止指令进行重排序优化。

为什么要使用多线程

先从总体上来说:

  • 从计算机底层来说: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
  • 从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。

 

计算机底层

 在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。当使用多线程的时候,一个线程被 IO 阻塞,其他线程还可以继续使用 CPU。从而提高了 Java 进程利用系统资源的整体效率。

多核时代多线程主要是为了提高进程利用多核 CPU 的能力 

使用多线程可能带来什么问题

并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、死锁、线程不安全等等。

多线程就一定能提高处理速度吗

https://blog.csdn.net/xiaoxinshuaiga/article/details/104429573

多线程能提高应用吞吐量和处理速度

使用多线程来提高程序处理速度,其本质是提高对CPU的利用率。主要是两个方面

  • 柱塞等待时充分利用CPU
    当程序发生阻塞的操作时候,例如IO等待,CPU将就空闲下来了。而使用多线程,当一些线程发生阻塞的时候,另一些线程则仍能利用CPU,而不至于让CPU一直空闲。
  • 利用CPU的多核并行计算能力
    现在的CPU基本上都是多核的。使用多线程,可以利用多核同时执行多个线程,而不至于单线程时一个核心满载,而其他核心空闲。

多线程就一定能提高处理速度吗?显示着不一定。当程序偏计算型的时候,盲目启动大量线程来并发,并不能提高处理速度,反而会降低处理速度。因为在多个线程进行切换执行的时候会带会一定的开销。其中有 上下文切换开销,CPU调度线程的开销,线程创建和消亡的开销等。其中主要是上下文切换带来的开销。

线程的生命周期和状态   6

 

 

 

 在操作系统中层面线程有 READY 和 RUNNING 状态,而在 JVM 层面只能看到 RUNNABLE 状态(所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 

为什么 JVM 没有区分这两种状态呢? 

现在的时分(time-sharing)多任务(multi-task)操作系统架构通常都是用所谓的“时间分片(time quantum or time slice)”方式进行抢占式(preemptive)轮转调度(round-robin式)。这个时间分片通常是很小的,一个线程一次最多只能在 CPU 上运行比如 10-20ms 的时间(此时处于 running 状态),也即大概只有 0.01 秒这一量级,时间片用后就要被切换下来放入调度队列的末尾等待再次调度。(也即回到 ready 状态)。线程切换的如此之快,区分这两种状态就没什么意义

当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。

 

sleep() 方法和 wait() 方法区别和共同点

 

 

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法

 

 总结: 调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。

 

上下文切换

当出现如下情况的时候,线程会从占用 CPU 状态中退出

 

 

前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换

上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下

线程死锁

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去

预防和避免线程死锁

如何预防死锁? 破坏死锁的产生的必要条件即可:

  1. 破坏请求与保持条件 :一次性申请所有的资源。
  2. 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  3. 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

如何避免死锁?

避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。

安全状态 指的是系统能够按照某种线程推进顺序(P1、P2、P3.....Pn)来为每个线程分配所需资源,直到满足每个线程对资源的最大需求,使每个线程都可顺利完成。称<P1、P2、P3.....Pn>序列为安全序列。

 

posted on 2022-02-03 16:34  cltt  阅读(45)  评论(0编辑  收藏  举报

导航