Java 多线程并发线程基础

进程&线程

线程是进程中的一个实体,线程 本身是不会独立存在的 。

进程是代码在数据集合上的 一 次运行活动 , 是系统进行资源分配 和调度的基本单位, 线程则是进程的一个执行路径, 一个进程中至少有一个线程,进程中 的多个线程共享进程的资源。

操作系统在分配资源时是把资源分配给进程的, 但是 CPU 资源比较特殊, 它是被分 配到线程的 , 因为真正要占用 CPU 运行的是线程 , 所以也说线程是 CPU 分配的基本单位。

在Java中,当我们启动main函数时其实就启动了一个JVM的进程, 而main函数所 在的线程就是这个进程中的一个线程,也称主线程 。

 

Java 中有三种线程创建方式:

  1. 实现 Runnable接口的 run方法
  2. 继承 Thread类 并重写 run 的方法
  3. 使用 FutureTask方式

 

 

 

 

 

 

当一 个 线程调用 yield 方法 时, 当前线程会让出 CPU 使用权,然后处于就绪状态,线 程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到 刚刚让出 CPU 的那个线程来获取 CPU执行权。

sleep 与 yield 方法的区别在于,当线程调用 sl巳ep 方法时调用线程 会被阻塞挂 起指定的时间,在这期间线程调度器不会去调度该线程 。 而调用 yield 方法时,线程只是 让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下 一次调度 时就有可能调度到当前线程执行 。

 

 

理解线程上下文切换

在多线程编程中,线程个数一般都大于 CPU个数,而每个 CPU 同一时刻只能被一个 线程使用,为了让用户感觉多个线程是在同时执行的, CPU 资源的分配采用了时间片轮转 的策略 ,也就是给每个线程分配 一个时间片,线程在时间片内占用 CPU 执行任务 。当 前 线程使用完时间片后,就会处于就绪状态并让出 CPU 让其他线程占用 , 这就是上下文切换 , 从当前线程的上下文切换到了其他线程 。 那么就有 一 个问题,让出 CPU 的线程等下次轮 到自己占有 CPU 时如何知道自己之前运行到哪里了?

所以在切换线程上下文时需要保存 当前线程的执行现场 , 当再次执行时根据保存的执行现场信息恢复执行 现场 。

线程上下文切换时机有 : 当前线程的 CPU 时 间片 使用完处于就绪状态 时-,当前线程 被其他线程中断时 。

 

 

线程死锁

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

 

死锁的产生必须具备以 下四个条件     (破坏死锁:   请求并持有环路等待条件 )。

· 互斥条件: 指线程对己经获取到的资源进行排它性使用 , 即该资源同时只由 一个线 程占用。如果 此时 还有其 他 线程请求获取该资源 ,则 请求者只能等待,直至占有资 源 的 线程释放该资源。

· 请求并持有条件 : 指一个线程己经持有了至少一个资源, 但又提出了新的资源请求, 而新资源己被其 他 线程占有,所 以 当前线程会被阻塞 ,但 阻塞 的同时 并不释放自 己 己经获取的资源。

· 不可剥夺条件 : 指线程获取到的资源在自己使用完之前不能被其他线程抢占 , 只有 在自己使用完 毕后才 由 自 己释放该资源。

· 环路等待条件 : 指在发生死锁时 , 必然存在一个线程→资源的环形链 , 即线程集合 {TO,TLT2,...,Tn}中的TO正在等待一个Tl占用的资源, Tl正在等待T2占 用的资源,......Tn正在等待己被 TO 占用的资源。

 

 

 

Java 中的 线程分为两类,分别为 daemon 线程(守护线程〉user 线程(用户线程)。

 

 

并发编程

并发是指同 一个时间段内多个任务同时都在执行,并 且都没有执行结束

并行是说在单位时间 内 多个任务同时在执行 。

 

共享变量的内存可见性

Java 内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内 存里面的变量复制到自己的工作空间或者叫作工作内存,线程读写变量时操作的是自己工 作内存中的变量。

synchronized 关键字

synchronized块是 Java提供的一种原子性内置锁, Java中的每个对象都可以把它当作 一个同步锁来使用 , 这些 Java 内置的使用者看不到的锁被称为内部锁,也叫作监视器锁。

线程的执行代码在进入 synchronized 代码块前会自动获取内部锁,这时候其他线程访 问 该 同步代码块时会被阻塞挂起 。

拿到内部锁的线程会在正常退出同步代码块或者抛出 异常后 或者在同步块内调用了该内置锁资源的wait系列方法时释放该内置锁。 内置锁是排它锁, 也就是当一个线程获取这个锁后 , 其他线程必须等待该线程释放锁后才能获取该锁 。

由于 Java 中的线程是与操作系统的原生线程 一一 对应的,所以当阻塞 一个线 程时伞,需要从用户态切换到内核态执行阻塞操作,这是很耗时的操作,而 synchronized 的 使用就会导致上下文切换。

 

 

volatile 关键字

确保对 一 个变 量 的更新对其他线程马 上可见, 禁止指令重排 。

当一个变量被声明为 volatile 时,线程在写入变量时不会把值缓存在寄存器或者 其他地方,而是会把值刷新回主内存 。

当其 他 线程读取该共享变 量 时-,会从主内存重新获 取最新值,而不是使用当前线程的工作内存中的值。

 

写入变量值不依赖、变量的当前值时 。 因为如果依赖当前值,将是     获取一计算一写入 三步操作,

这三步操作不是原子性的,而 volatile不保证原子性。
读写 变量值时 没有加锁 。 因为加锁本身已经保证了内存可见性,这时候不 需要把变 量声 明为 volatile 的 。

 

原子性操作

所谓原子性操作,是指执行一系列操作时,这些操作要么全部执行 , 要么全部不执行, 不存在只执行其中一部分的情况。

 

CAS操作

Java 中 , 锁在并发处理中占据了 一 席之地,但是使用锁有 一 个不好的地方,就 是当 一个线程没有获取到锁时会被阻塞挂起 , 这会导致线程上下文的切换和重新调度开 销 。 Java 提供了非阻塞的 volatile 关键字来解决共享变量的可见性问题 , 这在 一 定程度 上弥补 了 锁带来的开销 问 题,但是 volatile 只能保 证 共享变 量 的 可见性,不能解决读 改一写等的原子性问题。 CAS即CompareandSwap,其是JDK提供的非阻塞原子性操 作 , 它通过硬件保证了 比较 更新操作的原子 性 。 JDK 里面的 Unsafe 类提供了一系列的 compareAndSwap*方法 , 下面以 compareAndSwapLong 方法为例进行简单介绍 。

boolean compareAndSwapLong(Object obj,long valueOffset,long expect, long update)

其中compareAndSwap的意思是比较并交换。

CAS有四个操作数, 分别为:对象内存位置对象中 的变量的偏移量变量预期值新的值

其操作含义是 , 如果 对象 obj 中内存偏移量为 valueOffset的变量值为 expect,则使用新的值 update替换 旧的值 expect。 这是处理器提供的一个原子性指令。

关于CAS操作有个经典的ABA问题, 具体如下: 假如线程I使用CAS修改初始值 为A的变量X, 那么线程I会首先去获取当前变量X的值(为A〕, 然后使用CAS操作尝 试修改 X 的值为 B,如果使用 CAS 操作成功了,那么程序运行一定是正确的吗?其实未必, 这是因为有可能在线程 I 获取变 量 X 的值 A 后,在执行 CAS 前,线程 II 使用 CAS 修改 了变量X的值为B,然后又使用CAS修改了变量X的值为A。 所以虽然线程I执行CAS 时X的值是A, 但是这个A己经不是线程I获取时的A了。 这就是ABA问题。

ABA 问题 的产生是因为变量 的状态值产生 了环形转换,就是变量的值可 以从 A 到 B, 然后再从 B 到 A。如果变量的值只能朝着一个方向转换 ,比如 A 到 B, B 到 C, 不构成环 形,就不会存在 问题。JDK 中 的 AtomicStampedReference 类给每个变量 的状态值都配备了 一个时间戳, 从而避免了ABA问题的产生。

 

Unsafe 类

JDK的此jar包中的Unsafe类提供了硬件级别的原子性操作, Unsafe类中的方法都是 native方法,它们使用JNI的方式访问本地C++实现库。

 


import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.util.concurrent.locks.ReentrantLock;

public class UnsafeTest implements java.io.Serializable  {

    private static  Unsafe unsafe  ;
    private static  long valueOffset;

    private volatile int value;

    static {
        try {

            Field field = Unsafe.class.getDeclaredField("theUnsafe") ;
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);


            valueOffset = unsafe.objectFieldOffset
                    (UnsafeTest.class.getDeclaredField("value"));

           


        } catch (Exception e) {
            e.printStackTrace();
        }




    }


    public static void main(String[] args) throws Exception {


        UnsafeTest unsafeTest = new UnsafeTest ();

        ReentrantLock reentrantLock = new ReentrantLock() ;
        long valueOffset = unsafe.objectFieldOffset
                (UnsafeTest.class.getDeclaredField("value"));


        reentrantLock.lock();




        boolean res = unsafe.compareAndSwapInt(unsafeTest,valueOffset,0,1);


        System.out.println("res: "+res+" this value : "+ unsafeTest.value);


        reentrantLock.unlock();


        


    }

}

 

伪共享

为了解决计算机系统中主内存与 CPU 之间运行速度差问题,会在 CPU 与主内存之间 添加一级或者多级高速缓冲存储器( Cache)。这个 Cache 一般是被集成到 CPU 内部的, 所以也叫 CPU Cache,如图所示是两级 Cache 结构。

在 Cache 内部是按行存储的,其中每一行称为一个 Cache行。 Cache行 是 Cache 与主内存进行数据交换的单位,

Cache 行的大小一般为 2 的幕次数字节。

CPU 访问某个变 量 时,首先会去看 CPU Cache 内是否有该变量,如果有则直接从 中获取,否则就去主内存里面获取该变 量 ,然后把该变量所在内存区域的一个 Cache 行大 小的内存复制到 Cache 中。 由于存放到 Cache行的是内存块而不是单个变量,所以可能会把多个变量存放到一个 Cache行中。 当多个线程同时修改一个缓存行里面的多个变量时, 由于同时只能有 一 个线程操作 缓存行 ,所以相比将 每个变 量放到 一 个缓存行,性能会有所 下降,这就是伪共享,如图 2-8 所示 。

伪共享的产生是因为 多个变量被放入了一个缓存行中,并且多个线程 同时去写入缓存 行中不同的变量 。

 

那么 为何 多个变量会被放入 一个缓存行呢?

其实是 因为缓存与内存交换 数据 的单位就是缓存行 , 当 CPU 要访问的变量没有在缓存中找到时,根据程序运行的局部性原理, 会把该变量所在内存中大小为缓存行的内存放入缓存行。

 

也就是地址连续的多个变量才有可能会被放到一个缓存行中。当创建数组时, 数组里面的多个元素就会被放入同一 个 缓存 行 。

实在正常情况下单线程访 问时将数组元素放入一个或者多个缓存 行对代码执行是有利的,因为数据都在缓存 中 ,代码执行会更快

 

如何避免伪共享

在 JDK 8 之前 一般都是通过字节填充的方 式来避免该问题,也就是创建 一个变量时使 用填充字段填充该变量所在的缓存行,这样就避免了将多个变量存放在同 一个缓存行中,

JDK 8提供了一个 sun.misc.Contended注解,用来解决伪共享问题。将上面代码修改 为如下。

 

Thread.java 类

Thread 类里面这三个变量默认被初始化为 0,这三个变量会在 ThreadLocalRandom 类 中使用.

    /** The current seed for a ThreadLocalRandom */
    @sun.misc.Contended("tlr")
    long threadLocalRandomSeed;

    /** Probe hash value; nonzero if threadLocalRandomSeed initialized */
    @sun.misc.Contended("tlr")
    int threadLocalRandomProbe;

    /** Secondary seed isolated from public ThreadLocalRandom sequence */
    @sun.misc.Contended("tlr")
    int threadLocalRandomSecondarySeed;

 

@Contended 注解只用于 Java 核心 类, 比如此包下的 类。

如果用户类路径下的类需要使用这个注解, 则 需要添加 NM 参数 :-XX:-RestrictContended。

填充的宽度默认为 128,要自定义宽度 则可以设 置 -XX:Con nd巳dPaddingWidth 参数。

 

乐观锁与悲观锁

悲观锁指对 数据被外界修改持保守态度 ,认为数据 很容易就会被其 他线 程修改 ,所以 在数据被处理前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态 。 悲 观锁的实现往往依靠数据库提供的锁机制,即在数据 库 中 ,在对数据记录操作前给记录 加 排它锁。 如果获取锁失败, 则说明数据正在被其他线程修改, 当前线程则等待或者抛出异 常。 如果 获取 锁成功,则对记 录进行操作 ,然 后提交事务后释放 排它锁 。

乐观锁是相对悲观锁来说的,它认为数据在一般情况下不会造成冲突,所以在访问记 录前不会加排它锁,而是在进行数据提交更新时,才会正式对数据冲突与否进行检测 。具 体来说,根据 update 返回的行数让用户决定如何去做 。

 

公平锁与非公平锁

根据线程获取锁的抢占机制,锁可以分为公平锁非公平锁

公平锁表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁。

非公平锁 则在运行时闯入,也就是先来不一定先得 。 ( 避免了上下文切换!!!!!!!!!!)

在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销  ( 避免了上下文切换!!!!!!!!!!)

 

独占锁与共享锁

根据锁只能被单个线程持有还是能被 多 个线程共同持有,锁可以分为独占锁共享锁

 

独占锁保证任何时候都只有 一个线程能得到锁, ReentrantLock 就是以独占方式实现 的。

共享锁则可以同时由多个线程持有 ,例如 ReadWriteLock 读写锁,它允许一个资源可 以被 多 线程同时进行读操作 。

 

独占锁是 一种悲观锁,由于每次访问资源都先加上互斥锁,这限制了并发性,因为读 操作并不会影响数据的 一 致性 ,而独占 锁只允许在同 一 时间由 一 个线程读取数据,其他线 程必须等待当前线程释放锁才能进行读取 。

共享锁则是一种乐观锁,它放宽了加锁的条件,允许多个线程同时进行读操作 。

 

可重入锁

当一个线程要获取 一个被其他线程持有的独占锁时,该线程会被阻塞, 当 一个线 程再次获取它自己己经获取的锁, 如果不被阻塞,那么我们说该锁是可 重入的,也就是只要该线程获取了该锁,那么可以无限次数(严格来说是有限次数)地进入被该锁锁住的代码 。

synchronized 内 部锁是可重入锁 。 可重入锁的原理是在锁内部维护 一 个线程 标示,用来标示该锁目前被哪个线程占用,然后关联一个计数器。一开始计数器值为0, 说明该锁没有被任何线程占用 。 当一个钱程获取了该锁时,计数器的值会变成 1,这时其他线程再来获取 该锁时会发现锁的所有者不是自己而被阻塞挂起。

但是当获取了该锁的线程再次获取锁时发现锁拥有者是自己,就会把计数器值加+ 1, 当释放锁后计数器值 -1。 当计数器值为 0 时-,锁里面的线程标示被重置为 null, 这时候被 阻塞的线程会被唤醒来竞争获取该锁 。

 

自旋锁

由于 Java 中的线程是与操作系统中的线程一一对应的,所以当 一个线程在获取锁(比 如独占锁)失败后,会被切换到内核状态而被挂起 。 当该线程获取到锁时又需要将其切换 到内核状态而唤醒该线程 。 而从用户状态切换到内核状态的开销是比较大的,在 一 定程度 上会影响并发性能。自旋锁则是,当前线程在获取锁时,如果发现锁已经被其他线程占有, 它不马上阻塞自己,在不放弃 CPU使用权的情况下,多次尝试获取(默认次数是 10,可 以使用 -XX:PreBlockSpinsh 参数设置该值),很有可能在后面几次尝试中其他线程己经释 放了锁 。 如果尝试指定的次数后仍没有获取到锁则当前线程才会被阻塞挂起 。 由此看来自 旋锁是使用 CPU 时间换 取线程阻塞与调度的开销,但是很有可能这些 CPU 时间 白白浪费 了。

 

 

 

 

 

 

 

 

 

来源:  Java并发编程之美 -- 翟陆续 薛宾田

 


 

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 鲸 设计师:meimeiellie 返回首页