多线程学习1.2

一些常见技术点

什么是上下⽂切换?

多线程编程中⼀般线程的个数都⼤于 CPU 核⼼的个数,⽽⼀个 CPU 核⼼在任意时刻只能被⼀个线程使⽤,为了让这些线程都能得到有效执⾏,CPU 采取的策略是为每个线程分配时间⽚并轮转的形式。当⼀个线程的时间⽚⽤完的时候就会重新处于就绪状态让给其他线程使⽤,这个过程就属于⼀次上下⽂切换。

概括来说就是:当前任务在执⾏完 CPU 时间⽚切换到另⼀个任务之前会先保存⾃⼰的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。 任务从保存到再加载的过程就是⼀次上下⽂切换。

上下⽂切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒⼏⼗上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下⽂切换对系统来说意味着消耗⼤量的CPU 时间,事实上,可能是操作系统中时间消耗最⼤的操作。

Linux 相⽐与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有⼀项就是,其上下⽂切换和模式切换的时间消耗⾮常少

说说 sleep() ⽅法和 wait() ⽅法区别和共同点?

  • 两者最主要的区别在于:sleep() ⽅法没有释放锁,⽽ wait() ⽅法释放了锁。
  • 两者都可以暂停线程的执⾏。
  • wait() 通常被⽤于线程间交互/通信,sleep()通常被⽤于暂停执⾏。
  • wait() ⽅法被调⽤后,线程不会⾃动苏醒,需要别的线程调⽤同⼀个对象上的 notify()或者 notifyAll() ⽅法。sleep()⽅法执⾏完成后,线程会⾃动苏醒。或者可以使⽤ wait(long timeout) 超时后线程会⾃动苏醒

synchronized 关键字最主要的三种使⽤⽅式

  • 总结:
    • synchronized 关键字加到 static 静态⽅法和 synchronized(class) 代码块上都是是Class
    • 类上锁。
    • synchronized 关键字加到实例⽅法上是给对象实例上锁。
    • 尽量不要使⽤ synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能

关于双重校验锁实现对象单例(线程安全)

public class Singleton {
 
    private volatile static Singleton uniqueInstance;
 
    private Singleton() {
    }
 
    public static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进⼊加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

volatile 关键字

volatile 关键字 除了防⽌ JVM 的指令重排 ,还有⼀个重要的作⽤就是保证变量的可⻅性。

volatile 关键字可以禁⽌ JVM 的指令重排

  • 总结
    • 另外,需要注意 uniqueInstance 采⽤ volatile 关键字修饰也是很有必要。
    • uniqueInstance 采⽤ volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执⾏:
      1. 为 uniqueInstance 分配内存空间
      2. 初始化 uniqueInstance
      3. 将 uniqueInstance 指向分配的内存地址
    • 但是由于 JVM 具有指令重排的特性,执⾏顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致⼀个线程获得还没有初始化的实例。例如,线程 T1 执⾏了 1 和 3,此时 T2 调⽤ getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
    • 使⽤ volatile 可以禁⽌ JVM 的指令重排,保证在多线程环境下也能正常运⾏。

为什么要弄⼀个 CPU ⾼速缓存呢?

  • 总结:
    CPU Cache 缓存的是内存数据⽤于解决 CPU 处理速度和内存不匹配的问题,内存缓存的
    是硬盘数据⽤于解决硬盘访问速度过慢的问题。

  • CPU Cache 的⼯作⽅式:
    先复制⼀份数据到 CPU Cache 中,当 CPU 需要⽤到的时候就可以直接从 CPU Cache 中读取数
    据,当运算完成后,再将运算得到的数据写回 Main Memory 中。但是,这样存在 内存缓存不⼀
    致性的问题 !⽐如我执⾏⼀个 i++操作的话,如果两个线程同时执⾏的话,假设两个线程从 CPU
    Cache 中读取的 i=1,两个线程做了 1++运算完之后再写回 Main Memory 之后 i=2,⽽正确结果应该是 i=3。
    CPU 为了解决内存缓存不⼀致性问题可以通过制定缓存⼀致协议或者其他⼿段来解决。

讲⼀下 JMM(Java 内存模型)

在 JDK1.2 之前,Java 的内存模型实现总是从主存(即共享内存)读取变量,是不需要进⾏特别
的注意的。⽽在当前的 Java 内存模型下,线程可以把变量保存本地内存(⽐如机器的寄存器)
中,⽽不是直接在主存中进⾏读写。这就可能造成⼀个线程在主存中修改了⼀个变量的值,⽽另
外⼀个线程还继续使⽤它在寄存器中的变量值的拷⻉,造成数据的不⼀致。

要解决这个问题,就需要把变量声明为volatile,这就指示 JVM,这个变量是共享且不稳定的,每次使⽤它都到主存中进⾏读取。
所以,volatile 关键字 除了防⽌ JVM 的指令重排 ,还有⼀个重要的作⽤就是保证变量的可⻅性。

synchronized 关键字和 volatile 关键字的区别

  • synchronized 关键字和 volatile 关键字是两个互补的存在,⽽不是对⽴的存在!
    • volatile 关键字是线程同步的轻量级实现,所以volatile性能肯定⽐synchronized关键字
      要好。但是volatile 关键字只能⽤于变量⽽ synchronized 关键字可以修饰⽅法以及代码块。
    • volatile 关键字能保证数据的可⻅性,但不能保证数据的原⼦性。synchronized 关键字两
      者都能保证。
    • volatile关键字主要⽤于解决变量在多个线程之间的可⻅性,⽽ synchronized 关键字解决
      的是多个线程之间访问资源的同步性。

ThreadLocal 了解么?

  • 通常情况下,我们创建的变量是可以被任何⼀个线程访问并修改的。如果想实现每⼀个线程都有⾃⼰的专属本地变量该如何解决呢? JDK 中提供的ThreadLocal类正是为了解决这样的问题。
    ThreadLocal类主要解决的就是让每个线程绑定⾃⼰的值,可以将ThreadLocal类形象的⽐喻成
    存放数据的盒⼦,盒⼦中可以存储每个线程的私有数据。

  • 如果你创建了⼀个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副
    本,这也是ThreadLocal变量名的由来。他们可以使⽤ get()和 set()⽅法来获取默认值或将其值更改为当前线程所存的副本的值,从⽽避免了线程安全问题。

ThreadLocal 内存泄露问题了解不?

ThreadLocalMap 中使⽤的 key 为 ThreadLocal 的弱引⽤,⽽ value 是强引⽤。所以,如果
ThreadLocal 没有被外部强引⽤的情况下,在垃圾回收的时候,key 会被清理掉,⽽ value 不会
被清理掉。这样⼀来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远⽆法被 GC 回收,这个时候就可能会产⽣内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调⽤ set()、get()、remove() ⽅法的时候,会清理掉 key 为null的记录。使⽤完 ThreadLocal⽅法后 最好⼿动调⽤remove()⽅法

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
 
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

弱引⽤介绍:
如果⼀个对象只具有弱引⽤,那就类似于可有可⽆的⽣活⽤品。弱引⽤与软引⽤的区别在于:只具有弱引⽤的对象拥有更短暂的⽣命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,⼀旦发现了只具有弱引⽤的对象,不管当前内存空间⾜够与否,都会回收它的内存。不过,由于垃圾回收器是⼀个优先级很低的线程, 因此不⼀定会很快发现那些只具有弱引⽤的对象。

弱引⽤可以和⼀个引⽤队列(ReferenceQueue)联合使⽤,如果弱引⽤所引⽤的对象被垃圾回收,Java 虚拟机就会把这个弱引⽤加⼊到与之关联的引⽤队列中

线程池

线程池提供了⼀种限制和管理资源(包括执⾏⼀个任务)。每个线程池还维护⼀些基本统计信
息,例如已完成任务的数量。

  • 线程池的好处
    • 降低资源消耗。通过重复利⽤已创建的线程降低线程创建和销毁造成的消耗。
    • 提⾼响应速度。当任务到达时,任务可以不需要的等到线程创建就能⽴即执⾏。
    • 提⾼线程的可管理性。线程是稀缺资源,如果⽆限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使⽤线程池可以进⾏统⼀的分配,调优和监控。

实现 Runnable 接⼝和 Callable 接⼝的区别

Runnable⾃ Java 1.0 以来⼀直存在,但Callable仅在 Java 1.5 中引⼊,⽬的就是为了来处
理Runnable不⽀持的⽤例。Runnable 接⼝不会返回结果或抛出检查异常,但是Callable 接⼝
可以。所以,如果任务不需要返回结果或抛出异常推荐使⽤ Runnable 接⼝,这样代码看起来会
更加简洁。

⼯具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。
Executors.callable(Runnable task)
Executors.callable(Runnable task,Object resule)

执⾏ execute()⽅法和 submit()⽅法的区别是什么呢?

  • execute()⽅法⽤于提交不需要返回值的任务,所以⽆法判断任务是否被线程池执⾏成功与否

  • submit()⽅法⽤于提交需要返回值的任务。线程池会返回⼀个Future类型的对象,通过这个 Future 对象可以判断任务是否执⾏成功,并且可以通过 Future 的 get()⽅法来获取返回值,get()⽅法会阻塞当前线程直到任务完成,⽽使⽤ get(long timeout,TimeUnit unit)⽅法则会阻塞当前线程⼀段时间后⽴即返回,这时候有可能任务没有执⾏完。

如何创建线程池

《阿⾥巴巴 Java 开发⼿册》中强制线程池不允许使⽤ Executors 去创建,⽽是通过ThreadPoolExecutor 的⽅式,这样的处理⽅式让写的同学更加明确线程池的运⾏规则,规避资源
耗尽的⻛险

Executors 返回线程池对象的弊端如下:

  • FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列⻓度为 Integer.MAX_VALUE ,可能堆积⼤量的请求,从⽽导致 OOM。
  • CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为
    Integer.MAX_VALUE ,可能会创建⼤量线程,从⽽导致 OOM。

方式一:

⽅式⼆:通过 Executor 框架的⼯具类 Executors 来实现

我们可以创建三种类型的 ThreadPoolExecutor:

  • FixedThreadPool :该⽅法返回⼀个固定线程数量的线程池。该线程池中的线程数量始终不变。当有⼀个新的任务提交时,线程池中若有空闲线程,则⽴即执⾏。若没有,则新的任务会被暂存在⼀个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
  • SingleThreadExecutor:⽅法返回⼀个只有⼀个线程的线程池。若多余⼀个任务被提交到该线程池,任务会被保存在⼀个任务队列中,待线程空闲,按先⼊先出的顺序执⾏队列中的任务。
  • CachedThreadPool:该⽅法返回⼀个可根据实际情况调整线程数量的线程池。线程池的
    线程数量不确定,但若有空闲线程可以复⽤,则会优先使⽤可复⽤的线程。若所有线程均在⼯作,⼜有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执⾏完毕后,将返回线程池进⾏复⽤。

ThreadPoolExecutor 类分析

  • ThreadPoolExecutor 类中提供的四个构造⽅法。我们来看最⻓的那个,其余三个都是在这个构造⽅法的基础上产⽣(其他⼏个构造⽅法说⽩点都是给定某些默认参数的构造⽅法⽐如默认制定拒绝策略是什么)

ThreadPoolExecutor构造函数重要参数分析

ThreadPoolExecutor 3 个最重要的参数

  • corePoolSize : 核⼼线程数线程数定义了最⼩可以同时运⾏的线程数量。
  • maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运⾏的线程数
    量变为最⼤线程数。

介绍⼀下 Atomic 原⼦类

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

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

JUC 包中的原⼦类是哪 4 类?


AQS了解么?

  • AQS 的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下⾯。

  • AQS 是⼀个⽤来构建锁和同步器的框架,使⽤ AQS 能简单且⾼效地构造出应⽤⼴泛的⼤量的同
    步器,⽐如我们提到的 ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等皆是基于 AQS 的。当然,我们⾃⼰也能利⽤ AQS ⾮常轻松容易地构造出符合我们⾃⼰需求的同步器。

posted @ 2021-07-03 16:23  BigMonster85  阅读(94)  评论(0)    收藏  举报