继承Thread类创建线程

优点

使用继承方式创建 线程 的好处是方便传参,你可以在子类里面添加成员变量,通过 set 方法设置参数或者通过构造函数进行传递

缺点

Java 不支持多继承,如果继承了 Thread 类,那么就不能再继承其他类。
另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码,而 Runable 则没有这个限制。
任务没有返回值,可以 使用 FutureTask创建线程

public class ThreadTest {  
    //继承Thread类并重写run方法  
    public static class MyThread extends Thread {  
        @Override  
        public void run() {  
            System.out.println("I am a child thread");  
        }  
  
    }  
  
    public static void main(String[] args) {  
        // 创建线程  
        MyThread thread = new MyThread();  
        // 启动线程  
        thread.start();  
    }  
}
Tip

  • 📌 当创建完thread对象后该线程并没有被启动执行,直到调用了start方法后才真正启动了线程。

元数据

Java并发编程之美

  •  Java并发编程之美|200
  • 书名: Java并发编程之美
  • 作者: 翟陆续 薛宾田
  • 简介: Java并发编程无处不在,涉及的知识点多,要掌握并用好它并非易事。作者加多拥有在大型互联网公司阿里巴巴的丰富工作经验,遇到并解决了业务场景中很多实际的并发问题。本书是他对自己实践经验的总结与升华。为帮助读者解决学习中的各类痛点,作者将全书明确地分为基础篇、高级篇和实践篇,脉络清晰;全书以代码说话,辅以图表,让初学者能一步一步地深入堂奥,掌握并发编程的精髓。
  • 出版时间 2018-10-01 00:00:00
  • ISBN: 9787121349478
  • 分类: 计算机-编程设计
  • 出版社: 电子工业出版社

Warning

这里的内容仅为读书笔记,如果您需要阅读原版书籍,请购买正版以支持原创。感谢您的理解和支持。

高亮划线

第1章 并发编程线程基础

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

    • ⏱ 2023-05-02 16:23:54
  • 📌 操作系统在分配资源时是把资源分配给进程的,但是CPU资源比较特殊,它是被分配到线程的,因为真正要占用CPU运行的是线程,所以也说线程是CPU分配的基本单位。 ^25462418-7-644-722

    • ⏱ 2023-05-02 16:26:43
  • 📌 一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域 ^25462418-7-1134-1181

    • ⏱ 2023-05-02 16:42:38
  • 📌 程序计数器是一块内存区域,用来记录线程当前要执行的指令地址。 ^25462418-7-1211-1241

    • ⏱ 2023-05-02 16:42:49
  • 📌 如果执行的是native方法,那么pc计数器记录的是undefined地址,只有执行的是Java代码时pc计数器记录的才是下一条指令的地址。 ^25462418-7-1433-1503

    • ⏱ 2023-05-02 16:43:38
  • 📌 堆是一个进程中最大的一块内存,堆是被进程中的所有线程共享的,是进程创建时分配的,堆里面主要存放使用new操作创建的对象实例。 ^25462418-7-1632-1694

    • ⏱ 2023-05-02 16:44:17
  • 📌 方法区则用来存放JVM加载的类、常量及静态变量等信息,也是线程共享的 ^25462418-7-1723-1757

    • ⏱ 2023-05-02 16:44:26

1.2 线程创建与运行

  • 📌 Java中有三种线程创建方式,分别为实现Runnable接口的run方法,继承Thread类并重写run的方法,使用FutureTask方式。 ^25462418-8-440-511

    • ⏱ 2023-05-02 16:44:36
  • 📌 当创建完thread对象后该线程并没有被启动执行,直到调用了start方法后才真正启动了线程。 ^25462418-8-1096-1143

    • ⏱ 2023-05-02 16:46:58
  • 📌 使用继承方式的好处是,在run()方法内获取当前线程直接使用this就可以了,无须使用Thread.currentThread()方法;不好的地方是Java不支持多继承,如果继承了Thread类,那么就不能再继承其他类。 ^25462418-8-1304-1414

    • ⏱ 2023-06-01 09:46:42
  • 📌 使用继承方式的好处是方便传参,你可以在子类里面添加成员变量,通过set方法设置参数或者通过构造函数进行传递,而如果使用Runnable方式,则只能使用主线程里面被声明为final的变量。不好的地方是Java不支持多继承,如果继承了Thread类,那么子类不能再继承其他类,而Runable则没有这个限制。前两种方式都没办法拿到任务的返回结果,但是Futuretask方式可以。 ^25462418-8-2931-3119

    • ⏱ 2023-05-02 20:13:17

1.3 线程通知与等待

  • 📌 当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回:(1)其他线程调用了该共享对象的notify()或者notifyAll()方法;(2)其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。 ^25462418-9-588-738

    • ⏱ 2023-05-06 15:35:59
  • 📌 notify() 函数一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。此外,被唤醒的线程不能马上从wait方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回,也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行。类似wait系列方法,只有当前线程获取到了共享变量的监视器锁后,才可以调用共享变量的notify()方法,否则会抛出IllegalMonitorStateException异常。 ^25462418-9-8627-9050

    • ⏱ 2023-06-01 10:23:38
  • 📌 notifyAll() 函数不同于在共享变量上调用notify()函数会唤醒被阻塞到该共享变量上的一个线程,notifyAll()方法则会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程。 ^25462418-9-9086-9215

    • ⏱ 2023-06-01 10:23:46

1.11 ThreadLocal

  • 📌 ThreadLocal是JDK包提供的,它提供了线程本地变量,也就是如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。创建一个ThreadLocal变量后,每个线程都会复制一个变量到自己的本地内存 ^25462418-17-966-1132

    • ⏱ 2023-06-02 11:05:50
  • 📌 Thread类中有一个threadLocals和一个inheritableThreadLocals,它们都是ThreadLocalMap类型的变量,而ThreadLocalMap是一个定制化的Hashmap。在默认情况下,每个线程中的这两个变量都为null,只有当前线程第一次调用ThreadLocal的set或者get方法时才会创建它们 ^25462418-17-4239-4408

    • ⏱ 2023-06-02 11:16:54
  • 📌 每个线程的本地变量不是存放在ThreadLocal实例里面,而是存放在调用线程的threadLocals变量里面。也就是说,ThreadLocal类型的本地变量存放在具体的线程内存空间中。 ^25462418-17-4411-4505

    • ⏱ 2023-06-02 11:16:29
  • 📌 如果调用线程一直不终止,那么这个本地变量会一直存放在调用线程的threadLocals变量里面,所以当不需要使用本地变量时可以通过调用ThreadLocal变量的remove方法,从当前线程的threadLocals里面删除该本地变量。 ^25462418-17-4613-4731

    • ⏱ 2023-06-02 11:17:36
  • 📌 同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。 ^25462418-17-9161-9200

    • ⏱ 2023-06-02 11:29:02

2.5 Java中的synchronized关键字

  • 📌 synchronized块是Java提供的一种原子性内置锁,Java中的每个对象都可以把它当作一个同步锁来使用,这些Java内置的使用者看不到的锁被称为内部锁,也叫作监视器锁。线程的执行代码在进入synchronized代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起。拿到内部锁的线程会在正常退出同步代码块或者抛出异常后或者在同步块内调用了该内置锁资源的wait系列方法时释放该内置锁。内置锁是排它锁,也就是当一个线程获取这个锁后,其他线程必须等待该线程释放锁后才能获取该锁。 ^25462418-22-538-788

    • ⏱ 2023-05-27 10:12:35
  • 📌 由于Java中的线程是与操作系统的原生线程一一对应的,所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,这是很耗时的操作,而synchronized的使用就会导致上下文切换 ^25462418-22-820-911

    • ⏱ 2023-05-27 10:32:24
  • 📌 其实这也是加锁和释放锁的语义,当获取锁后会清空锁块内本地内存中将会被用到的共享变量,在使用这些共享变量时从主内存进行加载,在释放锁时将本地内存中修改的共享变量刷新到主内存。 ^25462418-22-1294-1380

    • ⏱ 2023-05-27 10:37:42

2.6 Java中的volatile关键字

2.7 Java中的原子性操作

  • 📌 简单的++value由2、5、6、7四步组成,其中第2步是获取当前value的值并放入栈顶,第5步把常量1放入栈顶,第6步把当前栈顶中两个值相加并把结果放入栈顶,第7步则把栈顶的结果赋给value变量。因此,Java中简单的一句++value被转换为汇编后就不具有原子性了。 ^25462418-24-1167-1304
    • ⏱ 2023-05-27 11:08:07

2.8 Java中的CAS操作

  • 📌 CAS有四个操作数,分别为:对象内存位置、对象中的变量的偏移量、变量预期值和新的值 ^25462418-25-888-929

    • ⏱ 2023-05-27 15:24:00
  • 📌 如果对象obj中内存偏移量为valueOffset的变量值为expect,则使用新的值update替换旧的值expect。这是处理器提供的一个原子性指令。 ^25462418-25-937-1014

    • ⏱ 2023-05-27 15:26:58
  • 📌 JDK中的AtomicStampedReference类给每个变量的状态值都配备了一个时间戳,从而避免了ABA问题的产生。 ^25462418-25-1394-1455

    • ⏱ 2023-05-28 15:45:48

2.9 Unsafe类

  • 📌 JDK的rt.jar包中的Unsafe类提供了硬件级别的原子性操作,Unsafe类中的方法都是native方法,它们使用JNI的方式访问本地C++ 实现库。 ^25462418-26-522-600
    • ⏱ 2023-05-28 15:48:33

2.10 Java指令重排序

  • 📌 重排序在多线程下会导致非预期的程序执行结果,而使用volatile修饰ready就可以避免重排序和内存可见性问题。 ^25462418-27-2069-2126

    • ⏱ 2023-05-28 16:07:21
  • 📌 写volatile变量时,可以确保volatile写之前的操作不会被编译器重排序到volatile写之后。读volatile变量时,可以确保volatile读之后的操作不会被编译器重排序到volatile读之前。 ^25462418-27-2155-2261

    • ⏱ 2023-05-28 16:07:30

2.11 伪共享

  • 📌 在Cache内部是按行存储的,其中每一行称为一个Cache行 ^25462418-28-890-920

    • ⏱ 2023-05-21 19:07:18
  • 📌 Cache行(如图2-7所示)是Cache与主内存进行数据交换的单位,Cache行的大小一般为2的幂次数字节。 ^25462418-28-921-976

    • ⏱ 2023-05-21 19:07:32
  • 📌 当CPU访问某个变量时,首先会去看CPU Cache内是否有该变量,如果有则直接从中获取,否则就去主内存里面获取该变量,然后把该变量所在内存区域的一个Cache行大小的内存复制到Cache中 ^25462418-28-1239-1334

    • ⏱ 2023-05-21 19:09:09
  • 📌 由于存放到Cache行的是内存块而不是单个变量,所以可能会把多个变量存放到一个Cache行中 ^25462418-28-1335-1381

    • ⏱ 2023-05-21 19:09:50
  • 📌 当多个线程同时修改一个缓存行里面的多个变量时,由于同时只能有一个线程操作缓存行,所以相比将每个变量放到一个缓存行,性能会有所下降,这就是伪共享 ^25462418-28-1382-1453

    • ⏱ 2023-05-21 19:10:22
  • 📌 如果CPU只有一级缓存,则会导致频繁地访问主内存。 ^25462418-28-1920-1945

    • ⏱ 2023-05-21 19:19:34
  • 📌 伪共享的产生是因为多个变量被放入了一个缓存行中,并且多个线程同时去写入缓存行中不同的变量。 ^25462418-28-2051-2096

    • ⏱ 2023-05-21 19:21:54
  • 📌 地址连续的多个变量才有可能会被放到一个缓存行中。 ^25462418-28-2366-2390

    • ⏱ 2023-05-21 19:22:43
  • 📌 所以在单个线程下顺序修改一个缓存行中的多个变量,会充分利用程序运行的局部性原则,从而加速了程序的运行。而在多线程下并发修改一个缓存行中的多个变量时就会竞争缓存行,从而降低程序运行性能。 ^25462418-28-4192-4284

    • ⏱ 2023-05-21 21:46:18
  • 📌 JDK 8提供了一个sun.misc.Contended注解,用来解决伪共享问题。 ^25462418-28-4855-4896

    • ⏱ 2023-05-21 21:47:32
  • 📌 在默认情况下,@Contended注解只用于Java核心类,比如rt包下的类。如果用户类路径下的类需要使用这个注解,则需要添加JVM参数:-XX:-RestrictContended。填充的宽度默认为128,要自定义宽度则可以设置-XX:ContendedPaddingWidth参数。 ^25462418-28-5686-5829

    • ⏱ 2023-05-21 21:48:27

2.12 锁的概述

  • 📌 悲观锁指对数据被外界修改持保守态度,认为数据很容易就会被其他线程修改,所以在数据被处理前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态 ^25462418-29-595-669

    • ⏱ 2023-05-28 16:08:35
  • 📌 乐观锁是相对悲观锁来说的,它认为数据在一般情况下不会造成冲突,所以在访问记录前不会加排它锁,而是在进行数据提交更新时,才会正式对数据冲突与否进行检测。 ^25462418-29-1945-2020

    • ⏱ 2023-05-28 16:11:41
  • 📌 根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁,公平锁表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁。而非公平锁则在运行时闯入,也就是先来不一定先得。 ^25462418-29-4257-4358

    • ⏱ 2023-05-28 16:20:34
  • 📌 ReentrantLock提供了公平和非公平锁的实现。● 公平锁:ReentrantLock pairLock = new ReentrantLock(true)。● 非公平锁:ReentrantLock pairLock = new ReentrantLock(false)。如果构造函数不传递参数,则默认是非公平锁。 ^25462418-29-4387-4607

    • ⏱ 2023-05-28 16:20:47
  • 📌 在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销。 ^25462418-29-4797-4831

    • ⏱ 2023-05-28 16:21:43
  • 📌 根据锁只能被单个线程持有还是能被多个线程共同持有,锁可以分为独占锁和共享锁。 ^25462418-29-4936-4974

    • ⏱ 2023-05-28 16:22:06
  • 📌 独占锁保证任何时候都只有一个线程能得到锁,ReentrantLock就是以独占方式实现的。共享锁则可以同时由多个线程持有,例如ReadWriteLock读写锁,它允许一个资源可以被多线程同时进行读操作。 ^25462418-29-5003-5104

    • ⏱ 2023-05-28 16:22:23
  • 📌 独占锁是一种悲观锁,由于每次访问资源都先加上互斥锁,这限制了并发性,因为读操作并不会影响数据的一致性,而独占锁只允许在同一时间由一个线程读取数据,其他线程必须等待当前线程释放锁才能进行读取。共享锁则是一种乐观锁,它放宽了加锁的条件,允许多个线程同时进行读操作。 ^25462418-29-5133-5292

    • ⏱ 2023-05-28 16:22:36
  • 📌 当一个线程要获取一个被其他线程持有的独占锁时,该线程会被阻塞,那么当一个线程再次获取它自己已经获取的锁时是否会被阻塞呢?如果不被阻塞,那么我们说该锁是可重入的,也就是只要该线程获取了该锁,那么可以无限次数(在高级篇中我们将知道,严格来说是有限次数)地进入被该锁锁住的代码。 ^25462418-29-5397-5533

    • ⏱ 2023-05-28 16:23:11
  • 📌 synchronized内部锁是可重入锁 ^25462418-29-5965-5985

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

    • ⏱ 2023-05-28 16:25:48
  • 📌 但是当获取了该锁的线程再次获取锁时发现锁拥有者是自己,就会把计数器值加+1,当释放锁后计数器值-1。当计数器值为0时,锁里面的线程标示被重置为null,这时候被阻塞的线程会被唤醒来竞争获取该锁。 ^25462418-29-6140-6237

    • ⏱ 2023-05-28 16:26:18
  • 📌 自旋锁则是,当前线程在获取锁时,如果发现锁已经被其他线程占有,它不马上阻塞自己,在不放弃CPU使用权的情况下,多次尝试获取(默认次数是10,可以使用-XX:PreBlockSpinsh参数设置该值),很有可能在后面几次尝试中其他线程已经释放了锁。如果尝试指定的次数后仍没有获取到锁则当前线程才会被阻塞挂起。由此看来自旋锁是使用CPU时间换取线程阻塞与调度的开销,但是很有可能这些CPU时间白白浪费了。 ^25462418-29-6464-6664

    • ⏱ 2023-05-28 16:32:32

3.1 Random类及其局限性

  • 📌 每个Random实例里面都有一个原子性的种子变量用来记录当前的种子值,当要生成新的随机数时需要根据当前种子计算新的种子并更新回原子变量。在多线程下使用单个Random实例生成随机数时,当多个线程同时计算随机数来计算新的种子时,多个线程会竞争同一个原子变量的更新操作,由于原子变量的更新是CAS操作,同时只有一个线程会成功,所以会造成大量线程进行自旋重试,这会降低并发性能,所以ThreadLocalRandom应运而生。 ^25462418-33-2932-3142
    • ⏱ 2023-05-28 16:37:35

3.2 ThreadLocalRandom

  • 📌 ThreadLocal通过让每一个线程复制一份变量,使得在每个线程对变量进行操作时实际是操作自己本地内存里面的副本,从而避免了对共享变量进行同步。实际上ThreadLocalRandom的实现也是这个原理,Random的缺点是多个线程会使用同一个原子性种子变量,从而导致对原子变量更新的竞争 ^25462418-34-1013-1158
    • ⏱ 2023-05-28 16:39:28

3.3 源码分析

  • 📌 ThreadLocalRandom类继承了Random类并重写了nextInt方法,在ThreadLocalRandom类中并没有使用继承自Random类的原子性种子变量。在ThreadLocalRandom中并没有存放具体的种子,具体的种子存放在具体的调用线程的threadLocalRandomSeed变量里面。ThreadLocalRandom类似于ThreadLocal类,就是个工具类。当线程调用ThreadLocalRandom的current方法时,ThreadLocalRandom负责初始化调用线程的threadLocalRandomSeed变量,也就是初始化种子。 ^25462418-35-743-1035

    • ⏱ 2023-05-28 16:45:01
  • 📌 当调用ThreadLocalRandom的nextInt方法时,实际上是获取当前线程的threadLocalRandomSeed变量作为当前种子来计算新的种子,然后更新新的种子到当前线程的threadLocalRandomSeed变量,而后再根据新种子并使用具体算法计算随机数。这里需要注意的是,threadLocalRandomSeed变量就是Thread类里面的一个普通long变量,它并不是原子性变量。其实道理很简单,因为这个变量是线程级别的,所以根本不需要使用原子性变量 ^25462418-35-1064-1303

    • ⏱ 2023-05-28 16:45:55
  • 📌 seeder和probeGenerator是两个原子性变量,在初始化调用线程的种子和探针变量时会用到它们,每个线程只会使用一次。 ^25462418-35-1363-1427

    • ⏱ 2023-05-28 16:48:02
  • 📌 变量instance是ThreadLocalRandom的一个实例,该变量是static的。当多线程通过ThreadLocalRandom的current方法获取ThreadLocalRandom的实例时,其实获取的是同一个实例。但是由于具体的种子是存放在线程里面的,所以在ThreadLocalRandom的实例里面只包含与线程无关的通用算法,所以它是线程安全的 ^25462418-35-1459-1641

    • ⏱ 2023-05-28 16:47:38

4.1 原子变量操作类

  • 📌 这些原子操作类都使用CAS非阻塞算法,性能更好。但是在高并发情况下AtomicLong还会存在性能问题。JDK 8提供了一个在高并发下性能更好的LongAdder类 ^25462418-38-5883-5965
    • ⏱ 2023-05-28 17:09:24

4.2 JDK 8新增的原子操作类LongAdder

  • 📌 使用AtomicLong时,在高并发下大量线程会同时去竞争更新同一个原子变量,但是由于同时只有一个线程的CAS操作会成功,这就造成了大量线程竞争失败后,会通过无限循环不断进行自旋尝试CAS的操作,而这会白白浪费CPU资源。 ^25462418-39-612-723

    • ⏱ 2023-05-28 17:09:51
  • 📌 使用LongAdder时,则是在内部维护多个Cell变量,每个Cell里面有一个初始值为0的long型变量,这样,在同等并发量的情况下,争夺单个变量更新操作的线程量会减少,这变相地减少了争夺共享资源的并发量。另外,多个线程在争夺同一个Cell原子变量时如果失败了,它并不是在当前Cell变量上一直自旋CAS重试,而是尝试在其他Cell的变量上进行CAS尝试,这个改变增加了当前线程重试CAS成功的可能性。最后,在获取LongAdder当前值时,是把所有Cell变量的value值累加后再加上base返回的。 ^25462418-39-1513-1766

    • ⏱ 2023-05-28 17:14:45
  • 📌 LongAdder维护了一个延迟初始化的原子性更新数组(默认情况下Cell数组是null)和一个基值变量base。由于Cells占用的内存是相对比较大的,所以一开始并不创建它,而是在需要时创建,也就是惰性加载。 ^25462418-39-1795-1900

    • ⏱ 2023-05-28 17:15:36
  • 📌 当一开始判断Cell数组是null并且并发线程较少时,所有的累加操作都是对base变量进行的。保持Cell数组的大小为2的N次方,在初始化时Cell数组中的Cell元素个数为2,数组里面的变量实体是Cell类型。Cell类型是AtomicLong的一个改进,用来减少缓存的争用,也就是解决伪共享问题。 ^25462418-39-1929-2107

    • ⏱ 2023-05-28 17:16:55
  • 📌 对于大多数孤立的多个原子操作进行字节填充是浪费的,因为原子性操作都是无规律地分散在内存中的(也就是说多个原子性变量的内存地址是不连续的),多个原子变量被放入同一个缓存行的可能性很小。但是原子性数组元素的内存地址是连续的,所以数组内的多个元素能经常共享缓存行,因此这里使用@sun.misc.Contended注解对Cell类进行字节填充,这防止了数组中多个元素共享一个缓存行,在性能上是一个提升。 ^25462418-39-2136-2334

    • ⏱ 2023-05-28 17:17:53
  • 📌 LongAdder类继承自Striped64类,在Striped64内部维护着三个变量。LongAdder的真实值其实是base的值与Cell数组里面所有Cell元素中的value值的累加,base是个基础值,默认为0。cellsBusy用来实现自旋锁,状态值只有0和1,当创建Cell元素,扩容Cell数组或者初始化Cell数组时,使用CAS操作该变量来保证同时只有一个线程可以进行其中之一的操作 ^25462418-39-3020-3219

    • ⏱ 2023-05-28 17:19:41

读书笔记

第1章 并发编程线程基础

划线评论

  • 📌 每个线程都有自己的栈资源,用于存储该线程的局部变量,这些局部变量是该线程私有的,其他线程是访问不了的,除此之外栈还用来存放线程的调用栈帧。 ^506504175-7IDE8BOMk
    • 💭 线程中的栈资源就好像是工作中的git分支,一个任务一个分支,最后完成时再将处理好的结果刷回主分支,当然这个比喻不是很恰当.
    • ⏱ 2023-06-01 09:42:43

划线评论

  • 📌 其实程序计数器就是为了记录该线程让出CPU时的执行地址的,待再次分配到时间片时线程就可以从自己私有的计数器指定地址继续执行。 ^506504175-7IDDVNoSp
    • 💭 就好比阅读时在要结束的时候添加一个书签在当前位置,这样下次再看这本书的时候就不用花时间去回想上次的进度了,只需要从书签的位置开始继续阅读
    • ⏱ 2023-06-01 09:39:34

1.2 线程创建与运行

划线评论

  • 📌 其实调用start方法后线程并没有马上执行而是处于就绪状态,这个就绪状态是指该线程已经获取了除CPU资源外的其他资源,等待获取CPU资源后才会真正处于运行状态。一旦run方法执行完毕,该线程就处于终止状态 ^506504175-7IDElaZ3i
    • 💭 类比手头正在做的工作,已经把材料都准备好了,就等着自己将其他事情做完或者放下来处理这一件事情
    • ⏱ 2023-06-01 09:45:49

1.3 线程通知与等待

划线评论

  • 📌 在如上代码中假如生产者线程A首先通过synchronized获取到了queue上的锁,那么后续所有企图生产元素的线程和消费线程将会在获取该监视器锁的地方被阻塞挂起。线程A获取锁后发现当前队列已满会调用queue.wait()方法阻塞自己,然后释放获取的queue上的锁,这里考虑下为何要释放该锁?如果不释放,由于其他生产者线程和所有消费者线程都已经被阻塞挂起,而线程A也被挂起,这就处于了死锁状态。这里线程A挂起自己后释放共享变量上的锁,就是为了打破死锁必要条件之一的持有并等待原则。关于死锁后面的章节会讲。线程A释放锁后,其他生产者线程和所有消费者线程中会有一个线程获取queue上的锁进而进入同步块,这就打破了死锁状态。 ^506504175-7IDFPguab
    • 💭 简单来说就是生产者线程在获取到监视器锁后如果发现当前的人消费队列已满就会阻塞挂起并释放监视器锁,避免进入死锁状态
    • ⏱ 2023-06-01 10:08:30

划线评论

  • 📌 如果调用wait()方法的线程没有事先获取该对象的监视器锁,则调用wait()方法时调用线程会抛出IllegalMonitorStateException异常。 ^506504175-7IDEK8Rum
    • 💭 这是为了避免最后执行结果与预期不符
    • ⏱ 2023-06-01 09:51:58

1.11 ThreadLocal

划线评论

  • 📌 Thread类中有一个threadLocals和一个inheritableThreadLocals ^506504175-7IFgyKdGz
    • 💭 其中threadLocals中存放的的是由ThreadLocal通过set方法放入的value,因为它是ThreadLocalMap类型,是一个定制化的HashMap,它可一个关联多个ThreadLocal变量
    • ⏱ 2023-06-02 11:16:03

2.5 Java中的synchronized关键字

划线评论

  • 📌 进入synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到主内存。 ^506504175-7Iw6a0jNN
    • 💭 进入synchronize块时会将使用的的变量从工作线程中清除,使得获取变量需要从主内存获取。退出时将块内的共享变量的修改刷新的主内存中

    • ⏱ 2023-05-27 10:37:14

2.6 Java中的volatile关键字

划线评论

  • 📌 那么一般在什么时候才使用volatile关键字呢?● 写入变量值不依赖变量的当前值时。因为如果依赖当前值,将是获取—计算—写入三步操作,这三步操作不是原子性的,而volatile不保证原子性。● 读写变量值时没有加锁。因为加锁本身已经保证了内存可见性,这时候不需要把变量声明为volatile的。 ^506504175-7Iw7PqFOb
    • 💭 在单例模式下使用volatile关键字保证变量的可见性
    • ⏱ 2023-05-27 11:02:42

划线评论

  • 📌 那么一般在什么时候才使用volatile关键字呢?● 写入变量值不依赖变量的当前值时。因为如果依赖当前值,将是获取—计算—写入三步操作,这三步操作不是原子性的,而volatile不保证原子性。● 读写变量值时没有加锁。因为加锁本身已经保证了内存可见性,这时候不需要把变量声明为volatile的。 ^506504175-7Iw7FgUxJ
    • 💭 例如可以使用volatile关键字修饰用来表示状态的共享值,由需要使用这一变量的线程监控这个值,当值修改时线程根据逻辑进行相应的变化如退出等等。
    • ⏱ 2023-05-27 11:00:12

划线评论

  • 📌 当线程写入了volatile变量值时就等价于线程退出synchronized同步块(把写入工作内存的变量值同步到主内存),读取volatile变量值时就相当于进入同步块(先清空本地内存变量值,再从主内存获取最新值)。 ^506504175-7Iw7ay94o
    • 💭 但是synchronize是独占锁同时只有一个线程能够读取或者写入修饰的共享变量,同时其他的调用线程会被阻塞,同时也存在着线程上下文切换和线程重新调度的开销。而volatile关键字是非阻塞的,不会造成线程上下文切换的开销,但是volatile关键字不保证操作的原子性。
    • ⏱ 2023-05-27 10:52:38

划线评论

  • 📌 当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。 ^506504175-7Iw6MXAmd
    • 💭 由volatile关键字修饰的变量在线程写入时会直接将值刷入主内存中,而当其他线程读取这个共享变量时会直接从主内存中获取最新值,而不是当前线程工作内存中的值
    • ⏱ 2023-05-27 10:46:49

2.7 Java中的原子性操作

划线评论

  • 📌 所谓原子性操作,是指执行一系列操作时,这些操作要么全部执行,要么全部不执行,不存在只执行其中一部分的情况。 ^506504175-7Iw82ejAG
    • 💭 除不可控制因素外
    • ⏱ 2023-05-27 11:05:51

2.8 Java中的CAS操作

划线评论

  • 📌 CAS即Compare and Swap,其是JDK提供的非阻塞原子性操作,它通过硬件保证了比较—更新操作的原子性 ^506504175-7IxWBXDXw
    • 💭 CAS是通过CPU提供的原子性操作指令实现的,当一个线程执行CAS操作时它会先读取内存中的值,然后将需要修改的值与内存中的值进行比较。如果两个值相等就会将新值写入内存并返回操作成功,否则失败不进行任何修改。当多个线程同时执行CAS操作时只有一个会成功,而其他的均失败且不会改变目标的值。因此CAS通过不可中断的原子性CPU指令和等待策略保证了非阻塞原子性操作
    • ⏱ 2023-05-28 15:44:46

2.10 Java指令重排序

划线评论

  • 📌 Java内存模型允许编译器和处理器对指令重排序以提高运行性能,并且只会对不存在数据依赖性的指令重排序。 ^506504175-7IxXNgaCL
    • 💭 指令重排可以保证单线程下最终执行结果与预想结果一致,但是无法保证多线程最终结果与执行结果一致
    • ⏱ 2023-05-28 16:02:49

2.12 锁的概述

划线评论

  • 📌 乐观锁并不会使用数据库提供的锁机制,一般在表中添加version字段或者使用业务状态来实现。乐观锁直到提交时才锁定,所以不会产生任何死锁。 ^506504175-7IxYUtHlK
    • 💭 乐观锁通过添加version字段来保证并发状态下只有一个线程能够成功执行,在线程修改数据库内容前会先查询相应数据并获取到version字段,在提交修改时通过检查version字段是否正确来判断数据是否已被修改,如果被修改则执行失败或者重试
    • ⏱ 2023-05-28 16:19:52

3.2 ThreadLocalRandom

划线评论

  • 📌 如果每个线程都维护一个种子变量,则每个线程生成随机数时都根据自己老的种子计算新的种子,并使用新种子更新老的种子,再根据新种子计算随机数,就不会存在竞争问题了,这会大大提高并发性能 ^506504175-7Iy0uuWU1
    • 💭 ThreadLocalRandom通过每个线程都维护一个种子的方式解决了Random因为原子性种子变量导致的竞争问题,大大提高了并发性能
    • ⏱ 2023-05-28 16:44:00

4.2 JDK 8新增的原子操作类LongAdder

划线评论

  • 📌 JDK 8新增了一个原子性递增或者递减类LongAdder用来克服在高并发下使用AtomicLong的缺点 ^506504175-7Iy2soger
    • 💭 LongAdder通过将变量分解为多个变量,让多个线程去竞争多个资源解决性能问题
    • ⏱ 2023-05-28 17:14:01

本书评论