线程的start方法和run方法的区别

创建线程的方式有多种,不管使用继承Thread的方式还是实现Runnable接口的方式,都需要重写run方法。

  • 如果调用run方法(同步的,还是当前线程)。
  • 调用start就是我们刚刚启动的那个线程(异步的)。启动了线程, 由 Jvm 调用 run 方法

启动一个线程是调用 start() 方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由 JVM 调度并执行。这并不意味着线程就会立即运行。

如何实现处理多线程的返回值

  1. 主线程等待法
  2. 使用Thread中的join()方法(可以阻塞当前线程以等待子线程处理完毕)
  3. 通过Callable接口实现:通过FutureTask或者线程池配合Future获取

创建线程的方式

  1. 继承Thread (Thread是实现了Runnable接口的类,使得run支持多线程。)
  2. 覆写Runnable接口 (单一继承原则,推荐使用Runnable接口)

a.实现Runnable接口避免多继承局限
b.实现Runnable()可以更好的体现共享的概念

  1. 覆写Callable接口 (call()方法,有返回值)
  2. 通过线程池启动

终止线程的方式

  1. 使用退出标志,使线程正常退出,也就是当 run() 方法完成后线程中止。
  2. 使用 stop() 方法强行终止线程,但是不推荐使用这个方法,该方法已被弃用。

调用 stop() 方法会立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 语句中的,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭。
调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。

  1. 使用 interrupt 方法中断线程。

线程中断并不会立即终止线程,而是通知目标线程,有人希望你终止。至于目标线程收到通知后会如何处理,则完全由目标线程自行决定。

线程的状态

Java线程主要分为以下六个状态:新建态(new),运行态(Runnable),无限期等待(Waiting),限期等待(TimeWaiting),阻塞态(Blocked),结束(Terminated)

  1. 新建(new)
    新建态是线程处于已被创建但没有被启动的状态,在该状态下的线程只是被创建出来了,但并没有开始执行其内部逻辑。
  2. 运行(Runnable)
    运行态分为Ready和Running,当线程调用start方法后,并不会立即执行,而是去争夺CPU,当线程没有开始执行时,其状态就是Ready,而当线程获取CPU时间片后,从Ready态转为Running态
  3. 等待(Waiting)
    处于等待状态的线程不会自动苏醒,而只有等待被其它线程唤醒,在等待状态中该线程不会被CPU分配时间,将一直被阻塞。以下操作会造成线程的等待:

没有设置timeout参数的Object.wait()方法。
没有设置timeout参数的Thread.join()方法。
LockSupport.park()方法(实际上park方法并不是LockSupport提供的,而是在Unsafe中
锁:https://juejin.im/post/5d8da403f265da5b5d203bf4

  1. 限期等待(TimeWaiting)
    处于限期等待的线程,CPU同样不会分配时间片,但存在于限期等待的线程无需被其它线程显式唤醒,而是在等待时间结束后,系统自动唤醒。以下操作会造成线程限时等待:

Thread.sleep()方法。
设置了timeout参数的Object.wait()方法。
设置了timeout参数的Thread.join()方法。
LockSupport.parkNanos()方法。
LockSupport.parkUntil()方法。

  1. 阻塞(Blocked)
    当多个线程进入同一块共享区域时,例如Synchronized块、ReentrantLock控制的区域等,会去整夺锁,成功获取锁的线程继续往下执行,而没有获取锁的线程将进入阻塞状态,等待获取锁。
  2. 结束(Terminated)
    已终止线程的线程状态,线程已结束执行。
    丙-多线程 - 图1

Synchronized底层实现

  • 有序性 (as-if-serial 单线程情况下程序的结果是正确)
  • 可见性 (JMM)
  • 原子性 (同一时间只有一个线程能拿到锁)
  • 可重入性 (锁对象的时候有个计数器,清0释放锁)
  • 不可中断性 (一个线程获取锁之后,另外一个线程处于阻塞或者等待不会被中断)

JVM 中,对象在内存中分为三块区域

  • 对象头Mark Word(标记字段):默认存储对象的HashCode,分代年龄和锁标志位信息;Klass Point(类型指针)虚拟机通过这个指针来确定这个对象是哪个类的实例。
  • 实例变量:类的数据信息,父类的信息
  • 填充数据:对象起始地址必须是8字节的整数倍。空对象8个字节。

它如何实现可重入?

同步代码块 - synchronized (new test()) {

  1. 当我们进入一个方法时,执行monitorenter,就会获取当前对象的一个所有权,这个时候monitor进入数为1,当前的这个线程就是这个monitor的owner。
  2. 如果已经是这个monitor的owner了,再次进入会把进入数+1.
  3. 同理,当他执行完monitorexit,对应的进入数就-1,直到为0,才可以被其他线程持有。

同步实例/静态方法 - public synchronized void method() { / synchronized(Synchronized.class){

  • 一旦执行到这个方法,就会先判断是否有标志位ACC_SYNCHRONIZED,ACC_SYNCHRONIZED会去隐式调用刚才的monitorenter,monitorexit。

同步方法默认用this或者当前类class对象作为锁;
同步代码块可以选择以什么来加锁,比同步方法要更细颗粒度,我们可以选择只同步会发生同步问题的部分代码而不是整个方法;

synchronized 对象锁和类锁的区别

  • 如果多线程同时访问同一类的 类锁(synchronized 修饰的静态方法) public synchronized static void xxx以及对象锁(synchronized 修饰的非静态方法) public synchronized void xxx这两个方法执行是异步的,原因:类锁和对象锁是两种不同的锁。
  • 类锁对该类的所有对象都能起作用,而对象锁不能。

对象创建后的内存布局:

  1. markword(加锁核心)是否GC、经历了几次Young GC
  2. Klass pointer 记录指向对象的class文件的指针
  3. Instance data记录对象中变量数据。
  4. Padding 对齐使用。(64位系统,内存必须被8字节整除)

    锁升级过程

丙-多线程 - 图2

  1. 偏向锁。锁争夺也就是对象头指向的Monitor对象的争夺,一旦有线程持有了这个对象,标志位修改为1,就进入偏向模式,同时会把这个线程的ID记录在对象的Mark Word中。这个过程是采用了CAS乐观锁操作的,每次同一线程进入,对标志位+1。

CAS 循环时间长开销大/只能保证一个共享变量的原子操作。
AtomicInteger举例,他的自增函数incrementAndGet()利用了CAS

  1. 轻量级锁。偏向锁关闭,或多个线程竞争偏向锁时-》如果这个对象是无锁的,jvm就会在当前线程的栈帧中建立一个叫锁记录(Lock Record)的空间,存储锁对象的Mark Word 拷贝,然后把Lock Record中的owner指向当前对象。
    JVM会利用CAS尝试把对象原本的Mark Word 更新到Lock Record的指针,成功就说明无其它锁竞争,加锁成功,改变锁标志位,执行相关同步操作。
    如果失败了,就会判断当前对象的Mark Word是否指向了当前线程的栈帧,是则表示当前的线程已经持有了这个对象的锁,否则说明被其他线程持有了,开启自旋锁。
  2. 自旋锁。Linux系统的用户态内核态的切换很耗资源(线程的等待唤起过程)。不断自旋,防止线程被挂起,一旦可以获取资源,就直接尝试成功,直到超出阈值(默认10次)。自旋都失败了,那就升级为重量级的锁,像1.5的一样,等待唤起。

synchronized 锁升级之后,怎么降级

我们可以进行一段时间进行统计,统计并发度已经很低,如果还是重量级锁,则进行锁对象的切换。换一个锁对象,这样又开始偏向锁状态,提升了处理速度;锁对象切换时候,需要注意并发操作;

synchronized java规定锁升级之后,则无法锁降级;

为什么要有偏向锁

  • syn锁80%的情况只有1个线程去拿【例如:sout、StringBuffer】,只有一个线程访问共享资源才会有偏向锁。
  • 刚开始执行代码一定有很多线程抢锁,所以有个4s的等待时间才会开启偏向锁。【偏向锁也可以被禁用】

    啥时候轻量级锁变重量级锁

  • 10次自旋

  • 等待CPU调度的线程数超过CPU核数的一半

    重量级锁重在哪-操作系统控制

    JVM将任何与线程有关的操作交给操作系统。而在操作系统中,执行要先入队,启动线程要消耗资源。

javap反解析class文件。能发现1个monitorenter和2个monitorexit,syn是JVM层面的锁,如果异常了jvm自己会给我们释放,这就是多出来的monitorexit。

JDK的优化

锁消除

丙-多线程 - 图3

锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。

锁粗化

丙-多线程 - 图4
如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展 (粗化)到整个操作序列的外部。

用synchronized还是Lock

  • synchronized是关键字,是JVM层面的底层啥都帮我们做了,而Lock是一个接口,是JDK层面的有丰富的API。
  • synchronized会自动释放锁,而Lock必须手动释放锁。
  • synchronized是不可中断的,Lock可以中断也可以不中断。
  • 通过Lock可以知道线程有没有拿到锁,而synchronized不能。
  • synchronized能锁住方法和代码块,而Lock只能锁住代码块。
  • Lock可以使用读锁提高多线程读效率。
  • synchronized是非公平锁,ReentrantLock可以控制是否是公平锁

ThreadLocal

价值:对象统一设置初始值。每个线程对这个变量的修改相互独立。【线程内,跨类跨方法传递数据】

  • 子线程中的traceid为null,需要用InheritableThreadLocal解决父子线程共享线程变量问题。
  • simpledateFormat内部有一个Calendar对象,日期转字符串中多线程共享会产生错误。使用ThreadLocal让每个线程单独拥有这个对象。

副作用:

  • 脏数据:线程池会重用Thead,与之绑定的ThreadLocal变量重用。若没有remove()且没有set新值,会有脏数据
  • 内存泄漏:源码提示中,会提示使用static关键字修饰ThreadLocal,所以触发弱引用机制回收Entry的value就不现实了。每次用完threadLocal记得remove。

    一个对象的所有线程会共享它的全局变量,所以这些变量不是线程安全的,我们可以使用同步技术。但是当我们不想使用同步的时候,我们可以选择 ThreadLocal 变量。

线程私有变量,多线程不互相影响。(采用了空间换时间的设计思想,主要用来实现在多线程环境下的线程安全和保存线程上下文中的变量)。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本。
内部实现是其内部类名叫ThreadLocalMap的成员变量threadLocals,key为本身,value为实际存值的变量副本。它并未实现Map接口,而且他的Entry是继承WeakReference(弱引用)的,也没有看到HashMap中的next,所以不存在链表了。

ThreadLocal的静态内部类ThreadLocalMap为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标就是value存储的对应位置。

丙-多线程 - 图5
ThreadLocalMap在存储的时候会给每一个ThreadLocal对象一个threadLocalHashCode,在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置。
ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。

ThreadLocal的设计本身就是为了能够在当前线程中有属于自己的变量,并不是为了解决并发或者共享变量的问题。

ThreadLocal 内存泄漏问题

在ThreadLocal中,进行get,set操作的时候会清除Map里所有key为null的value。

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行(可能是作为线程池中的一员),那么这个Entry对象中的value就得不到回收,发生内存泄露。ThreadLocal被回收,key的值变成null,导致整个value再也无法被访问到;
解决办法:在使用结束时,调用ThreadLocal.remove将对应的值全部置空;

为什么要将Entry中的key设为弱引用?

如果key 使用强引用,引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

设置为弱引用的key能预防大多数内存泄漏的情况。
如果key为弱引用,引用的ThreadLocal的对象被回收时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

ThreadLocal 应用

ThreadLocal使用场景为 用来解决数据库连接、Session管理(需要传递一个全局变量的情况)
Spring事务隔离级别采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接。
ThreadLocal包装SimpleDataFormat解决线程安全问题:SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。

多线程情况下不能用ThreadLocal

TransmittableThreadLocal 保证在线程池中也能从用户上下文中获取正确的信息。线程池中线程会无限复用,普通的ThreadLocal无法获取。
请求到达拦截器会被统一处理赋值到ThreadLocal,使用AOP可以让其他方式调用对应方法前设置ThreadLocal。

线程同步的方法

  • wait():使一个线程处于等待状态,并且释放所持有的对象的 lock。
  • sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException 异常。
  • notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且不是按优先级。
  • notityAll():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争

AQS ( 底层使用了模板方法模式,继承AbstractQueuedSynchronizer并重写指定的方法)

当有自定义同步器接入时,只需重写API层所需要的部分方法即可,不需要关注底层具体的实现流程。

如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH(虚拟的双向队列)锁实现的(不存在队列实例,仅存在结点之间的关联关系),即将暂时获取不到锁的线程加入到队列中。

  1. // java.util.concurrent.locks.AbstractQueuedSynchronizer
  2. private volatile int state;

AQS使用一个int成员变量state来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。

我们可以通过修改State字段表示的同步状态来实现多线程的独占模式和共享模式(加锁过程)。

AQS 对资源的共享方式:

  • Exclusive(独占tryAcquire-tryRelease):只有一个线程能执行,如ReentrantLock。
  • Share(共享tryAcquireShared-tryReleaseShared):多个线程可同时执行,如Semaphore(允许多个线程同时访问)、CountDownLatch(用来协调多个线程之间的同步)、 CyclicBarrier(让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门 // 它可以多次使用)、ReadWriteLock 。
    ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。

AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

Lock

ReentrantReadWriteLock (实现了ReadWriteLock接口)

也是基于AQS的,读写锁把state高16为记为读状态,低16位记为写状态,就分开了。
readLock()和writeLock()用来获取读锁和写锁。
如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

1.将state域按位分成两部分,高位部分表示读锁,低位表示写锁,由于写锁只有一个,所以写锁的重入计数也解决了,这也会导致写锁可重入的次数减小(前面提到的2^16-1)。
2.读锁是共享锁,可以同时有多个,那么只靠一个state来计算锁的重入次数是不行的。ReentrantReadWriteLock是通过一个HoldCounter的类来实现的,这个类中有一个count计数器,同时该类通过ThreadLocal关键字被修饰为线程私有变量,那么每个线程都保留一份对读锁的重入次数

ReentrantLock (唯一实现了Lock接口)

ReentrantLock在内部使用了内部类Sync来管理锁,内部类Sync继承了AQS,分为公平锁FairSync和非公平锁NonfairSync。
- FairSync: 会判断当前是否有等待队列,如果有则将自己加到等待队列尾;
- NonfairSync: 直接使用CAS去进行锁的占用
- AQS: 队列同步器。AQS 有一个 state 标记位,(CAS修改state,volatile的int类型)值为1 时表示有线程占用,这时会做一个判断(看当前持有锁的线程是不是自己,如果是自己,那么将state的值加1就可以了,表示重入返回即可。)其他线程需要进入到同步队列等待,同步队列是一个双向链表(线程ID和当前请求的线程ID一样就可重入)。当获得锁的线程需要等待某个条件时,会进入 condition等待队列,等待队列可以有多个。当 condition 条件满足时,线程会从等待队列重新进入同步队列进行获取锁的竞争。*(ArrayBlockingQueue和LinkedBlockingQueue都是基于这个实现的)
(1):先通过CAS尝试获取锁, 如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起;
(2):当锁被释放之后, 排在队首的线程会被唤醒CAS再次尝试获取锁,
(3):如果是非公平锁, 同时还有另一个线程进来尝试获取可能会让这个线程抢到锁;
(4):如果是公平锁, 会排到队尾,由队首的线程获取到锁。

StampedLock (不可重入锁)

ReadWriteLock读的过程中不允许写,这是一种悲观的读锁。Java 8引入了新的读写锁StampedLock,读的过程中也允许获取写锁后写入。StampedLock提供了乐观读锁,通过tryOptimisticRead()获取一个乐观读锁,并返回版本号。

独占锁与共享锁
(1):ReentrantLock为独占锁(悲观加锁策略)
(2):ReentrantReadWriteLock中读锁为共享锁 ,Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier也是
(3): JDK1.8 邮戳锁(StampedLock), 不可重入锁

tryLock(可轮询锁/定时锁,可以用它避免死锁) 和 locklockInterruptibly 的区别
(1):tryLock 能获得锁就返回 true,不能就立即返回 false,
(2):tryLock(long timeout,TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁,返回 false
(3):lock 能获得锁就返回 true,不能的话一直等待获得锁
(4):lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程, lock 不会抛出异常,而 lockInterruptibly 会抛出异常。

Volatitle

Java内存模型(JavaMemoryModel)规定 线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量。
可见性的解决方案

  • 加锁 (执行代码,将修改后的副本的值刷新回主内存中,线程释放锁。而获取不到锁的线程会阻塞等待,所以变量的值肯定一直都是最新的。)
  • Volatile修饰共享变量(保证不同线程对共享变量操作的可见性)
  1. 这个变量不会在多个线程中存在复本,直接从内存读取
  2. 这个关键字会禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。

在 Java 中除了 long 和 double 之外的所有基本类型的读和赋值,都是原子性操作。而 64 位的 long 和 double 变量由于会被 JVM 当作两个分离的 32 位来进行操作,所以不具有原子性,会产生字撕裂问题。但是当你定义 long 或 double 变量时,如果使用 volatile 关键字,就会获到(简单的赋值与返回操作的)原子性。

MESI(缓存一致性协议)

为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议MESI(缓存一致性协议)

  1. 嗅探。每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。(对volatile修饰的变量,执行写操作的话,JVM会发送一条lock前缀指令给CPU,CPU在计算完之后会立即将这个值写回主内存。通过lock前缀指令 + MESI缓存一致性协议保证可见性)
  2. 总线风暴。由于Volatile的MESI缓存一致性协议,需要不断的从主内存嗅探和cas不断循环,无效交互会导致总线带宽达到峰值。不要大量使用Volatile
  3. 禁止指令重排序。volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障
  4. as-if-serial,不管怎么重排序,单线程下的执行结果不能被改变。
  5. happens-before对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。
  6. 无法保证原子性。N个线程对同一个变量进行累加也是没办法保证结果是对的,因为读写这个过程并不是原子性的。解决办法:用原子类(AtomicInteger incrementAndGet()方法在一个无限循环体内,不断尝试将一个比当前值大1的新值赋给自己,如果失败则说明在执行”获取-设置”操作的时已经被其它线程修改过了,于是便再次进入循环下一次操作,直到成功为止。CAS),或者加锁。

happens-before规则

jvm会对代码进行编译优化,指令会出现重排序的情况,为了避免编译优化对并发编程安全性的影响,需要happens-before规则定义一些禁止编译优化的场景,保证并发编程的正确性。

  1. 程序的顺序性:一个线程中,按照程序的顺序,前面的操作happens-before后续的任何操作。
  2. volatile规则:对一个volatile变量的写操作,happens-before后续对这个变量的读操作。
  3. 传递性规则:如果A happens-before B,B happens-before C,那么A happens-before C。
  4. 管程中的锁规则:对一个锁的解锁操作,happens-before后续对这个锁的加锁操作。
  5. 线程start()规则:主线程A启动线程B,线程B中可以看到主线程启动B之前的操作。也就是start() happens before 线程B中的操作。
  6. 线程join()规则:主线程A等待子线程B完成,当子线程B执行完毕后,主线程A可以看到线程B的所有操作。也就是说,子线程B中的任意操作,happens-before join()的返回。

volatile 与 synchronized

volatile可以看做是轻量版的synchronized,volatile不保证多线程操作的原子性,但是如果是对一个共享变量进行多个线程的赋值,而没有其他的操作,那么就可以用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile又保证了可见性,所以就可以保证线程安全了。

丙-多线程 - 图6

线程池

  • Executors创建线程池 (阿里巴巴插件提示必须手动创建)
    单一、可变、定长都有一定问题,原因是FixedThreadPool和SingleThreadExecutor底层都是用LinkedBlockingQueue实现的,这个队列最大长度为Integer.MAX_VALUE,容易导致OOM。new LinkedBlockingQueue<>(1024) | 方法名 | 功能 | 适用场景 | | —- | —- | —- | | newFixedThreadPool(int nThreads) | 创建固定大小的线程池 | LinkedBlockingQueue无界阻塞队列。执行长期的任务,性能好很多 | | newSingleThreadExecutor() | 创建只有一个线程的线程池 | 一个任务一个任务执行的场景 | | newScheduledThreadPool () | 创建一个支持定时及周期性任务执行的定长线程池 | new DelayedWorkQueue() 一个按超时时间升序排序的队列 | | newCachedThreadPool() | 创建一个不限线程数上限的线程池,任何提交的任务都将立即执行 | workQueue为SynchronousQueue(同步队列),执行很多短期异步的小程序或者负载较轻的服务器 |

线程池的7个参数

1、corePoolSize线程池的核心线程数 (正式工)
2、maximumPoolSize能容纳的最大线程数 (正式工+临时工)
3、keepAliveTime空闲线程存活时间 (超过这个时间临时工将被解雇)
4、unit 存活的时间单位
5、workQueue 存放提交但未执行任务的阻塞队列 (排期渠道)
6、threadFactory 创建线程的工厂类 (召集线程的渠道)
7、handler 等待队列满后的拒绝策略

可选择的阻塞队列BlockingQueue

  • 无界队列。队列大小无限制,常用的为LinkedBlockingQueue
  • 有界队列。一类是遵循FIFO原则的队列如ArrayBlockingQueue,另一类是优先级队列如PriorityBlockingQueue。
  • 同步移交队列。不希望任务在队列中等待而是希望将任务直接移交给工作线程,可使用SynchronousQueue作为等待队列。只有在使用无界线程池或者有饱和策略时才建议使用该队列。

ArrayBlockingQueue中在入队列和出队列操作过程中,使用的是同一个lock,所以即使在多核CPU的情况下,其读取和操作的都无法做到并行,
而LinkedBlockingQueue读和写有两把锁ReentrantLock takeLock和putLock,它们之间的操作互相不受干扰,因此两种操作可以并行完成,故LinkedBlockingQueue的吞吐量要高于ArrayBlockingQueue。

线程池任务执行流程:

  1. 当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。
  2. 当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行
  3. 当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务
  4. 当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理
  5. 当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程
  6. 当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭

可以向线程池提交的任务有两种:RunnableCallable(JDK1.5时加入的接口,允许有返回值,允许抛出异常。)

提交方式 是否关心返回结果
Future submit(Callable task) 是,Future.get()
方法能够阻塞等待执行结果
void execute(Runnable command)
Future<?> submit(Runnable task) 否,虽然返回Future,但是其get()方法总是返回null

Future接口可以构建异步应用,但依然有其局限性。它很难直接表述多个Future 结果之间的依赖性。
于是Java8带来了CompletableFuture,一个Future的实现类。CompletableFuture完美结合了Java8流的新特性。等到两个线程都计算结束的时候,进行xxx / 当第一个执行结束的时候,就结束,后面任务不再等了

正确使用线程池

  1. 避免使用无界队列。应该使用ThreadPoolExecutor的构造方法手动指定队列的最大长度:
  2. 明确拒绝任务时的行为 | 拒绝策略 | 拒绝行为 | | —- | —- | | AbortPolicy | 抛出RejectedExecutionException | | DiscardPolicy | 什么也不做,直接忽略 | | DiscardOldestPolicy | 丢弃执行队列中最老的任务,尝试为当前提交的任务腾出位置 | | CallerRunsPolicy | 直接由提交任务者执行这个任务 |

  3. 获取处理结果和异常。线程池的处理结果、以及处理过程中的异常都被包装到Future

    提交任务到线程池的方式是: threadPoolExecutor.submit(Runnbale task); ,后面了解到使用 execute() 方式提交任务会把异常日志给打出来,但execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
    submit 提交任务,如果没有获取执行子线程的结果,那么异常就会不被抛出来,即被“吞掉”了。

  1. for (Future<VideoTopVo> future : futures) {
  2. VideoTopVo videoTopVo= new VideoTopVo();
  3. try {
  4. videoTopVo = future.get();
  5. } catch (InterruptedException | ExecutionException e) {
  6. fixPool.shutdown();
  7. log.error("界面初始化返回8个视频流url出现异常:{}",e.getMessage());
  8. }
  9. resultList.add(videoTopVo);
  10. }

这里只能在的 FutureTask 对象的get()中获取异常啦。不然就需要在定义 ThreadFactory 的时候调用setUncaughtExceptionHandler方法,自定义异常处理方法

  1. ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
  2. .setNameFormat("judge-pool-%d")
  3. .setUncaughtExceptionHandler((thread, throwable)-> logger.error("ThreadPool {} got exception", thread,throwable))
  4. .build();

线程池如何维护线程对象? *

  1. private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
  2. final Thread thread;//Worker持有的线程
  3. Runnable firstTask;//初始化的任务,可以为null
  4. }

Worker这个工作线程,实现了Runnable接口,并持有一个线程thread,一个初始化的任务firstTask。
直接通过HashSet持有,Worker对象是在addWorker方法后被新建一个Thread对象运行.Worker对象的run方法中自己会去自旋取阻塞队列中的Runnable任务。我们只维护了Worker对象,启动Worker对象的线程就在Worker对象中持有。

线程池需要管理线程的生命周期,需要在线程长时间不运行的时候进行回收线程池使用一张Hash表去持有线程的引用,这样可以通过添加引用、移除引用这样的操作来控制线程的生命周期。
Worker是通过继承AQS,使用AQS来实现独占锁这个功能。没有使用可重入锁ReentrantLock,而是使用AQS,为的就是实现不可重入的特性去反应线程现在的执行状态
丙-多线程 - 图7

线程池引发的故障到底怎么排查

  • jps -l找到项目的进程号(jps -l
  • jstack dump线程信息,如下会将线程信息dump到一个名为thread.txt的文件中(jstack 16555 > thread.txt

线程池核心线程怎么被保持的,非核心线程怎么回收

丙-多线程 - 图8
ThreadPoolExecutor回收工作线程,一条线程getTask()返回null,就会被回收。

  1. 未调用shutdown() ,RUNNING状态下全部任务执行完成的场景

线程数量大于corePoolSize,线程超时阻塞,超时唤醒后CAS减少工作线程数,如果CAS成功,返回null,线程回收。否则进入下一次循环。当工作者线程数量小于等于corePoolSize,就可以一直阻塞了。

  1. 调用shutdown() ,全部任务执行完成的场景

shutdown() 会向所有线程发出中断信号,这时有两种可能。
2.1)所有线程都在阻塞
中断唤醒,进入循环,都符合第一个if判断条件,都返回null,所有线程回收。
2.2)任务还没有完全执行完
至少会有一条线程被回收。在processWorkerExit(Worker w, boolean completedAbruptly)方法里会调用tryTerminate(),向任意空闲线程发出中断信号。所有被阻塞的线程,最终都会被一个个唤醒,回收。

其他问题

  1. corePoolSize=0会怎么样
    1.6版本之后,提交任务会立即创建一个线程来执行任务;如果提交任务时线程池不为空,则在等待队列中排队,只有队列满了才会创建新线程。
  2. 线程池创建后,会立即创建核心线程吗
    不会。刚创建ThreadPoolExecutor,线程不会立即启动。除非提前调用prestartCoreThread
  3. 核心线程永远不会销毁吗
    从1.6开始,提供方法allowCoreThreaTimeOut,传参为true则允许闲置的核心线程被终止。
    corePoolSize=0:一般情况下只是用一个线程消费任务,并发请求多才用多线程。
  4. 如何保证线程不被销毁
    线程池内部有个内部类worker,它实现了Runnable接口。【线程池中的线程就是worker,等待队列中的元素就是Runnable任务】
    每一个worker创建出来后,会调用自身的run方法(一个while循环,循环不结束,worker不停止)。
  • 没有达到corePoolSize,创建的worker会用workQueue.take()取任务【阻塞接口,取不到就一直阻塞】
  • 超过了corePoolSize,worker会用workQueue.poll(keepAliveTime,TimeUnit)取任务,这个接口只会阻塞keepAliveTime时间,超过这个时间后返回null while循环结束。
  1. 空闲线程过多有什么问题
    线程的内存模型【虚拟机栈+本地方法栈+程序计数器】
  • 局部变量:线程处于阻塞状态,栈帧没有出栈,栈帧中的局部变量表引用的内存均无法被回收。
  • TLAB机制:应用线程数处于高位,新的线程初始化可能因为Eden没有足够的空间分配TLAB而触发YoungGC。
  • ThreadLocal:若使用到ThreadLocal,且其缓存的数据过大又不清理。会有内存占用。
  1. keepAliveTime=0会咋样
    Jdk1.8表示非核心线程执行完立刻终止。
  2. 怎么进行异常处理
  • Execute: 在runnable任务的代码加上try catch处理,还可以自定义线程池,复写afterExecute方法处理异常。
  • Submit(有返回值)【底层依赖Execute】:在主线程中try catch处理。它对Throwable进行了try catch,封装到了outcome属性,底层方法execute的worker拿不到异常。
  1. 线程池需要关闭吗 - 不用,线程池的生命周期跟随服务的生命周期
  2. Shutdown (已添加到线程池的任务执行完关闭)与 shutdownnow(立刻关闭,返回队列中未执行的任务) 区别
  3. Spring中的Executor工具
  • SimpleAsyncTaskExecutor:Spring中使用的@Async注解基于它,默认情况下,它不是线程池,每次都会新开一个线程。
  • Executor接口提供了一个“将来执行命令”的接口,ThreadPoolExecutor才是代表线程池的。
  1. 非核心线程咋样回收的?
    当一个任务来了后,这个任务由Condition里面的等待队列顺序决定线程池哪个线程处理。所以活跃线程数不一定会快速减少。(每隔3s放一个线程任务)
    回收时并不会知道它是核心线程还是非核心,没必要区别。

    sleep与wait区别

  2. sleep属于线程类,wait属于object类;

  3. sleep方法可以在任何地方使用,而wait方法只能在synchronized块或synchronized方法中使用(因为wait方法会释放锁,只有获取锁了才能释放锁);
  4. sleep方法只会让出CPU,不会释放锁。

线程间如何进行通信

线程间通信的模型有两种:共享内存消息传递

  1. 使用 volatile 关键字
  2. 使用Object类的wait() 和 notify() 方法

wait和 notify必须配合synchronized使用,

  1. wait方法释放锁,notify方法不释放锁,notifyAll(唤醒所有线程并根据算法选取其中一个线程获取锁)
  1. // 定义一个锁对象.A发出notify()唤醒通知之后,依然是走完了自己线程的业务之后,线程B才开始执行,这也正好说明了,notify()方法不释放锁,而wait()方法释放锁。
  2. Object lock = new Object();
  3. List<String> list = new ArrayList<>();
  4. // 实现线程A
  5. Thread threadA = new Thread(() -> {
  6. synchronized (lock) {
  7. for (int i = 1; i <= 10; i++) {
  8. list.add("abc");
  9. System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size());
  10. try {
  11. Thread.sleep(500);
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. if (list.size() == 5)
  16. lock.notify();// 唤醒B线程
  17. }
  18. }
  19. });
  20. // 实现线程B
  21. Thread threadB = new Thread(() -> {
  22. while (true) {
  23. synchronized (lock) {
  24. if (list.size() != 5) {
  25. try {
  26. lock.wait();
  27. } catch (InterruptedException e) {
  28. e.printStackTrace();
  29. }
  30. }
  31. System.out.println("线程B收到通知,开始执行自己的业务...");
  32. }
  33. }
  34. });
  35. // 需要先启动线程B
  36. threadB.start();
  37. try {
  38. Thread.sleep(1000);
  39. } catch (InterruptedException e) {
  40. e.printStackTrace();
  41. }
  42. // 再启动线程A
  43. threadA.start();
  1. 使用JUC工具类 CountDownLatch (基于AQS框架,相当于也是维护了一个线程间共享变量state)
  2. 使用 ReentrantLock 结合 Condition

参照Object的wait和notify/notifyAll方法,Condition提供await()
和signal()/signalAll()。Condition能够支持不响应中断,而通过使用Object方式不支持;
Condition能够支持多个等待队列(new 多个Condition对象),而Object方式只能支持一个;
Condition能够支持超时时间的设置,而Object不支持

  1. 基本LockSupport实现线程间的阻塞和唤醒
    LockSupport 是一种非常灵活的实现线程间阻塞和唤醒的工具,使用它不用关注是等待线程先进行还是唤醒线程先运行,但是得知道线程的名字。LockSupport.park(); LockSupport.unpark(threadB);

Java线程之间通信方式

①同步 (加锁)
②while轮询的方式(轮询看满足条件不)
③wait/notify机制
④管道通信

Atomic 原子类

AtomicInteger 类主要利用 CAS (compare and swap) + volatilenative 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。

在高并发情况下,LongAdder(java8新加的累加器)比AtomicLong原子操作效率更高

在高度并发竞争情形下,AtomicLong每次进行add都需要flush和refresh(这一块涉及到java内存模型中的工作内存和主内存的,所有变量操作只能在工作内存中进行,然后写回主内存,其它线程再次读取新值),每次add()都需要同步,在高并发时会有比较多冲突,比较耗时导致效率低;而LongAdder中每个线程会维护自己的一个计数器,在最后执行LongAdder.sum()方法时候才需要同步,把所有计数器全部加起来,不需要flush和refresh操作。

进程切换与线程切换的区别

进程切换与线程切换的一个最主要区别就在于进程切换涉及到虚拟地址空间的切换而线程切换则不会。因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换。

  1. 同进程线程a1切换到线程a2
    保存线程执行现场。
    把指令指针、栈指针这些寄存器的值修改为线程a2的信息。(修改下内存中调度相关的数据结构)
  2. 不同进程线程a1切换到线程b1 (线程切换 + 进程切换)
    CPU保存的页目录地址要切换到进程B.
    进程切换会导致地址空间等进程资源发生变化,导致TLB缓存失效。

从虚拟内存到物理内存,是以页为单位映射的。 页目录-》页表-》物理内存页
【线性地址:页目录中选择一个页表 + 对应页表中锁定物理内存页 + 内存页起始地址偏移值】
os会以链表的形式记录各个进程的控制信息。(win 叫PCB进程控制块,linux 对应task_struct结构体)
CPU会把已经查询的映射关系缓存到TLB中,没有再去查页表。
进程切换时,TLB会失效

上下文切换 - 任务从保存到再加载的过程 【某一时间点CPU寄存器和程序计数器的内容,被称为上下文。】

寄存器是CPU内部的少量速度很快的闪存。
程序计数器是一个专用的寄存器,被用来表示指令序列中CPU正在执行的位置。

CPU通过分配时间片来执行任务,当一个任务的时间片用完,就会切换到另一个任务。在切换之前会保存上一个任务的状态,当下次再切换到该任务,就会加载这个状态。
上下文切换分为两点:

  1. 自发性上下文切换
  1. Thread.sleep()
  2. Object.wait()
  3. Thread.yeild()
  4. Thread.join()
  5. LockSupport.park()
  1. 非自发性上下文切换

切出线程的时间片用完
有一个比切出线程优先级更高的线程需要被运行
虚拟机的垃圾回收动作

上下文切换的开销

上下文切换的开销包括直接开销和间接开销。
直接开销有如下几点:

  • 操作系统保存回复上下文所需的开销
  • 线程调度器调度线程的开销
    间接开销有如下几点:
  • 处理器高速缓存重新加载的开销
  • 上下文切换可能导致整个一级高速缓存中的内容被冲刷,即被写入到下一级高速缓存或主存

互斥锁与自旋锁

重量级锁需要通过操作系统自身的互斥量(mutex lock,也称为互斥锁)来实现,然而这种实现方式需要通过用户态与和核心态的切换来实现,但这个切换的过程会带来很大的性能开销。

申请锁时,从用户态进入内核态,申请到后从内核态返回用户态(两次切换);
没有申请到时阻塞睡眠在内核态。
使用完资源后释放锁,从用户态进入内核态,唤醒阻塞等待锁的进程,返回用户态(又两次切换);被唤醒进程在内核态申请到锁,返回用户态(可能其他申请锁的进程又要阻塞)。
所以,使用一次锁,包括申请,持有到释放,当前进程要进行四次用户态与内核态的切换。同时,其他竞争锁的进程在这个过程中也要进行一次切换。

自旋锁与互斥锁不同的是自旋锁不会引起调用者睡眠。如果自旋锁已经被别的进程保持,调用者就轮询(不断的消耗CPU的时间)是否该自旋锁的保持者已经释放了锁(”自旋”一词就是因此而得名)。

线程的调度是在内核态运行的,而线程中的代码是在用户态运行。

CountDownLatch和CyclicBarrier的区别是什么

  • CountDownLatch一个线程等待其他线程执行到某一个点的时候,在继续执行逻辑(子线程不会被阻塞,会继续执行),只能被使用一次。最常见的就是join形式,主线程等待子线程执行完任务,在用主线程去获取结果的方式(当然不一定),内部是用计数器相减实现的(没错,又特么是AQS),AQS的state承担了计数器的作用,初始化的时候,使用CAS赋值,主线程调用await()则被加入共享线程等待队列里面,子线程调用countDown的时候,使用自旋的方式,减1,直到为0,就触发唤醒。
  • CyclicBarrier回环屏障,主要是等待一组线程到底同一个状态的时候,放闸。CyclicBarrier还可以传递一个Runnable对象,可以到放闸之前,执行这个任务。CyclicBarrier是可循环的,当调用await的时候如果count变成0了则会重置状态,如何重置呢,CyclicBarrier新增了一个字段parties,用来保存初始值,当count变为0的时候,就重新赋值。还有一个不同点,CyclicBarrier不是基于AQS的,而是基于ReentrantLock实现的。存放的等待队列是用了条件变量的方式。
    信号量Semaphore(需要拿到许可才能执行)
    一种固定资源的限制的一种并发工具包,基于AQS实现的,在构造的时候会设置一个值,代表着资源数量。(druid的数据库连接数,就是用这个实现的)
  • acquire() release() 可用于对象池,资源池的构建,比如静态全局对象池,数据库连接池;
  • 可创建计数为1的S,作为互斥锁(二元信号量)

实现生产者消费者模型

  1. 使用synchronize wait+notifyall方法

性能不高原因如下:
1.涉及到同步锁。
2.涉及到线程阻塞状态和可运行状态之间的切换。
3.涉及到线程上下文的切换。

  1. await() / signal()方法 (ReentrantLock和Condition可以实现等待/通知模型)
    JUC包下的锁Lock替代synchronize关键字。await方法代替wait,signalall代替notifyall。
  2. BlockingQueue阻塞队列方法 (在生成对象时指定容量大小,用于阻塞操作的是put()和take()方法。)put()方法:类似于我们上面的生产者线程,容量达到最大时,自动阻塞。
    take()方法:类似于我们上面的消费者线程,容量为0时,自动阻塞。
  1. public class ShareDataV3 {
  2. private static final int MAX_CAPACITY = 10; //阻塞队列容量
  3. private static BlockingQueue<Integer> blockingQueue= new LinkedBlockingDeque<>(MAX_CAPACITY); //阻塞队列
  4. private volatile boolean FLAG = true;
  5. private AtomicInteger atomicInteger = new AtomicInteger();
  6. public void produce() throws InterruptedException {
  7. while (FLAG){
  8. boolean retvalue = blockingQueue.offer(atomicInteger.incrementAndGet(), 2, TimeUnit.SECONDS);
  9. if (retvalue==true){
  10. System.out.println(Thread.currentThread().getName()+"\t 插入队列"+ atomicInteger.get()+"成功"+"资源队列大小= " + blockingQueue.size());
  11. }else {
  12. System.out.println(Thread.currentThread().getName()+"\t 插入队列"+ atomicInteger.get()+"失败"+"资源队列大小= " + blockingQueue.size());
  13. }
  14. TimeUnit.SECONDS.sleep(1);
  15. }
  16. System.out.println(Thread.currentThread().getName()+"FLAG变为flase,生产停止");
  17. }
  18. public void consume() throws InterruptedException {
  19. Integer result = null;
  20. while (true){
  21. result = blockingQueue.poll(2, TimeUnit.SECONDS);
  22. if (null==result){
  23. System.out.println("超过两秒没有取道数据,消费者即将退出");
  24. return;
  25. }
  26. System.out.println(Thread.currentThread().getName()+"\t 消费"+ result+"成功"+"\t\t"+"资源队列大小= " + blockingQueue.size());
  27. Thread.sleep(1500);
  28. }
  29. }
  30. public void stop() {
  31. this.FLAG = false;
  32. }
  33. }
  1. 信号量 Semaphore (计数为0的Semaphore是可以release(就相当于将计数器加1)的,然后就可以acquire(就相当于将计数器减1)(即一开始使线程阻塞从而完成其他执行。))
  1. List<Integer> buffer = new LinkedList<Integer>();
  2. // 互斥量,控制buffer的互斥访问
  3. private Semaphore mutex = new Semaphore(1);
  4. // canProduceCount可以生产的数量(表示缓冲区可用的数量)。 通过生产者调用acquire,减少permit数目
  5. private Semaphore canProduceCount = new Semaphore(10);
  6. // canConsumerCount可以消费的数量。通过生产者调用release,增加permit数目
  7. private Semaphore canConsumerCount = new Semaphore(0);
  8. Random rn = new Random(10);
  9. public void get() throws InterruptedException {
  10. canConsumerCount.acquire();
  11. try {
  12. mutex.acquire();
  13. int val = buffer.remove(0);
  14. System.out
  15. .println(Thread.currentThread().getName() + " 正在消费数据为:" + val + " buffer目前大小为:" + buffer.size());
  16. } finally {
  17. mutex.release();
  18. canProduceCount.release();
  19. }
  20. }
  21. public void put() throws InterruptedException {
  22. // 就相当于将计数器减1
  23. canProduceCount.acquire();
  24. try {
  25. mutex.acquire();
  26. int val = rn.nextInt(10);
  27. buffer.add(val);
  28. System.out
  29. .println(Thread.currentThread().getName() + " 正在生产数据为:" + val + " buffer目前大小为:" + buffer.size());
  30. } finally {
  31. mutex.release();
  32. // 生产者调用release,增加可以消费的数量
  33. canConsumerCount.release();
  34. }
  35. }
  1. 管道 (用于不同线程间直接传送数据,一个线程发送数据到输出管道,另一个线程从输入管道中读数据。)
    inputStream.connect(outputStream)或outputStream.connect(inputStream)作用是使两个Stream之间产生通信链接,这样才可以将数据进行输出与输入。
    这种方式只适用于两个线程之间通信,不适合多个线程之间通信。

协程 一种比线程更加轻量级的存在

一个线程也可以拥有多个协程。协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。协程的开销远远小于线程的开销,但Java的原生语法中并没有实现协程(Kilim框架实现了,其他语言GoLang、Python都有)。

协程和线程的区别在于:线程切换需要陷入内核,然后进行上下文切换,而协程在用户态由协程调度器完成,不需要陷入内核,这代价就小了;另外,协程的切换时间点是由调度器决定的,而不是系统内核决定的,尽管他们切换点都是时间片超过一定阈值,或者进入I/O或睡眠等状态;再次,还有垃圾回收的考虑,因为go实现了垃圾回收,而垃圾回收的必要条件时内存位于一致状态,这就需要暂停所有的线程,如果交给系统去做,那么会暂停所有的线程使其一致,而在go里面调度器知道什么时候内存位于一致状态,那么就没有必要暂停所有运行的协程。

进程切换与线程切换的区别

进程切换涉及到虚拟地址空间的切换而线程切换则不会。因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换。

虚拟内存

虚拟内存是操作系统为每个进程提供的一种抽象,每个进程都有属于自己的、私有的、地址连续的虚拟内存。最终进程的数据及代码必然要放到物理内存上,那么必须有某种机制能记住虚拟地址空间中的某个数据被放到了哪个物理内存地址上,这就是所谓的地址空间映射,也就是虚拟内存地址与物理内存地址的映射关系,那么操作系统是如何记住这种映射关系的呢,答案就是页表,页表中记录了虚拟内存地址到物理内存地址的映射关系。有了页表就可以将虚拟地址转换为物理内存地址了,这种机制就是虚拟内存。
每个进程都有自己的虚拟地址空间,进程内的所有线程共享进程的虚拟地址空间。

用户态内核态

从特权级的调度来理解用户态和内核态。当程序运行在3级特权级上时,就可以称之为运行在用户态,因为这是最低特权级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态;反之,当程序运行在0级特权级上时,就可以称之为运行在内核态。
虽然用户态下和内核态下工作的程序有很多差别,但最重要的差别就在于特权级的不同,即权力的不同。运行在用户态下的程序不能直接访问操作系统内核数据结构和程序。
当我们在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成某些它没有权力和能力完成的工作时就会切换到内核态。

ForkJoin

ForkJoin将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出。

  1. @Override
  2. protected Integer compute() {
  3. int result = 0;
  4. if (last - first <= threshold) {
  5. // 任务足够小则直接计算
  6. for (int i = first; i <= last; i++) {
  7. result += i;
  8. }
  9. } else {
  10. // 拆分成小任务
  11. int middle = first + (last - first) / 2;
  12. ForkJoinExample leftTask = new ForkJoinExample(first, middle);
  13. ForkJoinExample rightTask = new ForkJoinExample(middle + 1, last);
  14. invokeAll(leftTask,rightTask);
  15. result = leftTask.join() + rightTask.join();
  16. }
  17. return result;
  18. }
  19. public static void main(String[] args) {
  20. ForkJoinExample example = new ForkJoinExample(1, 10000);
  21. ForkJoinPool forkJoinPool = new ForkJoinPool();
  22. Future result = forkJoinPool.submit(example);
  23. try {
  24. System.out.println(result.get());
  25. } catch (InterruptedException e) {
  26. e.printStackTrace();
  27. }
  28. }