八股文--->并发编程

目录

一:并发编程的优点

二:并发编程的缺点

三:并发编程三要素是什么?

四:在 Java 程序中怎么保证多线程的运行安全?

四(1)voliate为什么不能保证原子性

五:并行和并发有什么区别?

六:什么是JMM模型

七:什么是多线程,多线程的优劣?

八:什么是进程和线程

九:进程和线程的区别

十:什么是上下文切换 

十一:用户线程和非守护线程

十二:形成死锁的四个必要条件 

十三:如何避免线程死锁 

十四:创建线程的四种方式 

十五:继承Thread和实现Runnable区别

十六:实现Runnable和Callable的区别

十七:线程的Run()和Start()有什么区别

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

十九:什么是 Callable 和 Future?

二十:sleep() 和 wait() 有什么区别?

二十一:说说线程的生命周期和五种基本状态

二十二: Java 中用到的线程调度算法是什么? 

二十三:什么是线程调度器和时间片

二十四:请说出线程同步与线程调度的相关方法

二十五:你是如何调用wait()方法的,在if块还是循环,为什么?

二十六:为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在Object 类里?而sleep定义在Thread类里面?

二十七:为什么 wait(),notify(),notifyAll()必须在同步方法或者同步块中被调用?

二十八:notify()和notifyAll()有什么区别

二十九:sleep()和yield()的区别

三十:如何停止一个正在运行的线程

三十一:Java 中 interrupted 和 isInterrupted 方法的区别?

三十二:同步方法和同步块哪个更好

三十三:Java 线程数过多会造成什么异常?

三十四:线程类的构造方法、静态块是被哪个线程调用的

三十五:为什么代码会重排序?

三十六:synchronized 的作用?

三十七:说说自己是怎么使用 synchronized 关键字,在项目中用到了吗

三十八:说一下 synchronized 底层实现原理?

(1)monitorenter   monitorexit

(2)为什么会有两个monitorexit呢?

(3)synchronized可重入的原理

三十九:Synchronized优化---锁粗化,锁消除,锁升级

四十:什么是自旋

四十一:如果你提交任务时,线程池队列已满,这时会发生什么

四十二:线程池的拒绝策略********************

四十三:了解单例模式吗,来手写一个

懒汉式(线程不安全)

双重检查锁定

饿汉式

四十四:各种锁

(无状态锁,偏向锁、轻量级锁、重量级锁的升级以及区别)

(1)对象头

(2)偏向锁

实现原理

(3)轻量级锁

(4)重量级锁

四十五:锁的升级过程

四十六:哪些是乐观锁,哪些是悲观锁

四十七:什么是CAS机制?

四十五:CAS的缺点

四十六:Java实现CAS的原理 - Unsafe类********

四十七:Semaphore介绍

四十八:Lock与synchronized有以下区别:

四十九:synchronized的不⾜之处

五十:锁的几种分类

(1)可重入锁和非可重入锁

(2)公平锁与非公平锁

(3)读写锁和排它锁

五十二 原子操作-AtomicInteger类源码简析

五十三:谈谈对AQS(AbstractQueuedSynchronizer)理解

(3)获取资源acquire,addWriter,acquireQueued

(4)释放资源release

五十四:reetrankLoc的实现

五十五:线程池的基本知识

(1)什么是线程池

(2)为什么要使用线程池

五十六:线程池的原理---ThreadPoolExecutor

(1)ThreadPoolExecutor提供的构造方法

 五十七:**ThreadPoolExecutor的策略

五十八:线程池主要的任务处理流程(源码)

为什么要二次检查线程池的状态?

五十九:*****ThreadPoolExcuter是如何做到线程复用的 

addWork

 Worker类源码

 我们再看看 runWorker 的逻辑:

 getTask源码

六十:四种常见的线程池

(1)newCachedThreadPool

(2)newFixedThreadPool 

(3)newSingleThreadExecutor 

(4)newScheduledThreadPool 

六十一:JAVA定时任务--ScheduledThreadPoolExecutor

(1)优缺点

(2)使用案例


一:并发编程的优点

  • 充分利用多核CPU的计算能力:通过并发编程的形式可以将多核CPU 的计算能力发挥到极致,性能得到提升
  • 方便进行业务拆分,提升应用性能现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分

二:并发编程的缺点

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

三:并发编程三要素是什么?

  1. 原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
  2. 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。 (synchronized,volatile)
  3. 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)

四:在 Java 程序中怎么保证多线程的运行安全?

-----------------------------出现线程安全问题的原因 -------------------------------------------
线程切换带来的原子性问题
缓存导致的可见性问题  
编译优化带来的有序性问题
---------------------------------------解决方法 -------------------------------------------
synchronized、LOCK,可以解决原子性问题
synchronized、volatile、LOCK,可以解决可见性问题
volatile可以解决有序性问题

四(1)voliate为什么不能保证原子性

        可见性与Java的内存模型有关,模型采用缓存与主存的方式对变量进行操作,也就是说,每个线程都有自己的缓存空间,对变量的操作都是在缓存中进行的,之后再将修改后的值返回到主存中,这就带来了问题,有可能一个线程在将共享变量修改后,还没有来的及将缓存中的变量返回给主存中,另外一个线程就对共享变量进行修改,那么这个线程拿到的值是主存中未被修改的值,这就是可见性的问题。

        Volatile 是轻量级的 synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。

        volatile很好的保证了变量的可见性,变量经过volatile修饰后,对此变量进行写操作时,汇编指令中会有一个LOCK前缀指令,这个不需要过多了解,但是加了这个指令后,会引发两件事情:

(1)将当前处理器缓存行的数据写回到系统内存
(2)这个写回内存的操作会使得在其他处理器缓存了该内存地址无效

什么意思呢?意思就是说当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值,这就保证了可见性。
 

问题来了,既然它可以保证修改的值立即能更新到主存,其他线程也会捕捉到被修改后的值,那么为什么不能保证原子性呢?
首先需要了解的是,Java中只有对基本类型变量的赋值和读取是原子操作,如i = 1的赋值操作,但是像j = i或者i++这样的操作都不是原子操作,因为他们都进行了多次原子操作,比如先读取i的值,再将i的值赋值给j,两个原子操作加起来就不是原子操作了。

所以,如果一个变量被volatile修饰了,那么肯定可以保证每次读取这个变量值的时候得到的值是最新的,但是一旦需要对变量进行自增这样的非原子操作,就不会保证这个变量的原子性了。

i++操作,线程A 和线程 B 都执行完了加操作,都还没刷新到主存,此时其中一个线程执行了写入操作,强制刷新主存,对于另一个线程来说,自身的缓存变得无效,需要和主存一致,但自身的加法操作已经执行过了,而volatile 无法像 MVCC 一样保证加操作再执行一遍。这样就出现了漏算结果的操作。

四(2)volatile禁止指令重排序的原理

首先我们就需要了解4个内存屏障,他们分别是

(1)LoadLoad:禁止下面的所有的普通读操作喝上面的voliate读重排序

(2)LoadStore:禁止下面的所有普通写操作喝上面的voliate读重排序

(3)StroeStore:禁止上面的所有普通写操作和下面的voliate写重排序

(4)StroeLoad:禁止下面的voliate写操作和下面可能有的voliate读写操作重排序

 

 

 

 

五:并行和并发有什么区别?

  • 并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行
  • 并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行
  • 串行:有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题。

做一个形象的比喻:
并发 = 两个队列和一台咖啡机。
并行 = 两个队列和两台咖啡机。
串行 = 一个队列和一台咖啡机。

六:什么是JMM模型

Java内存模型(Java Memory Model简称JMM)是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段 和构成数组对象的元素)的访问方式。

JVM运行程序的实体是线程,而每个线程创建时 JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据;

而Java 内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问, 但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自 己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝。

前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必 须通过主内存来完成。 

七:什么是多线程,多线程的优劣?

多线程是指一个程序含有多个执行流,即一个程序可以同时运行多个线程来执行多个不同的任务

多线程的好处

可以提高CPU的利用率;在多线程环境中,一个线程必须等待时,CPU可以运行其他的线程而不是等待,这样就大大提高了程序的运行效率,

多线程的劣势:
(1)线程也是程序,所以线程也需要占用内容,线程越多占用的内存也就越多
(2) 多线程需要协调和管理,所以需要 CPU 时间跟踪线程;
(3) 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问

八:什么是进程和线程

进程:

一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以拥有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。

线程:

进程中的一个执行任务(控制单元) ,负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可以共享数据。

九:进程和线程的区别

根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
资源开销:每个进程都有独立的代码和程序上下文,程序之间的切换会有较大的开销,线程之间的切换,开销比较小。 
内存分配: 同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空 间和资源是相互独立的
执行过程 每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是 线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控 制,两者均可并发执行

 

十:什么是上下文切换 

多线程编程中一般线程的个数都大于 CPU 核心的个数, 而一个 CPU 核心在任意时刻只能被一个
线程使用 ,为了让这些线程都能得到有效执行,CPU 采取的策略是为 每个线程分配时间片并轮转
的形式 。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属
于一次上下文切换。
概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便
下次再切换回这个任务时,可以再加载这个任务的状态。任务 从保存到再加载的过程就是一次上下
文切换

十一:用户线程和非守护线程

在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程) 

用个比较通俗的比如,任何一个守护线程都是整个JVM中所有非守护线程的保姆:

Thread daemonTread = new Thread();
 
  // 设定 daemonThread 为 守护线程,default false(非守护线程)
 daemonThread.setDaemon(true);
 
 // 验证当前线程是否为守护线程,返回 true 则为守护线程
 daemonThread.isDaemon();

只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个

非守护线程结束时,守护线程随着JVM一同结束工作。

如果 JVM 中没有一个正在运行的非守护线程,这个时候,JVM 会退出。换句话说,守护线程拥有

自动结束自己生命周期的特性,而非守护线程不具备这个特点

        Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。如果说不具备该特性,会发生什么呢?

        当 JVM 要退出时,由于垃圾回收线程还在运行着,导致程序无法退出,这就很尴尬了!!!由此可见,守护线程的重要性了。

        通常来说,守护线程经常被用来执行一些后台任务,但是呢,你又希望在程序退出时,或者说 JVM 退出时,线程能够自动关闭,此时,守护线程是你的首选。

User和Daemon两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread已经

全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了。 因为没有了被守护者,

Daemon也就没有工作可做了,也就没有继续运行程序的必要了。

注意事项:
1. setDaemon(true) 必须在 start() 方法前执行,否则会抛出 IllegalThreadStateException 异常
2. 在守护线程中产生的新线程也是守护线程
3. 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑
4. 守护 (Daemon) 线程中不能依靠 finally 块的内容来确保执行关闭或清理资源的逻辑。因为我们
上面也说过了一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作,所以守护
(Daemon) 线程中的 finally 语句块可能无法被执行

十二:形成死锁的四个必要条件 

(1)互斥条件:进程对于所分配的资源具有排他性,即一个资源只能被一个线程占用,直到该资源被释放

(2)请求与保持条件:一个资源因请求被占用资源而发送阻塞时,对已获得的资源保持不放

(3)不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只能自己使用完之后释放资源

(4)循环等待条件:当发生死锁时,所等待的线程必定会形成一个环路,造成永久堵塞。

十三:如何避免线程死锁 

我们只要破坏产生死锁的四个必要条件中的其中一个就可以了

(0)破坏互斥条件
这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)
(1)破坏请求条件
一次性分配所有资源,这样就不会再有请求了
(2)破坏保持条件
只要有一个资源得不到分配,也不给这个进程分配其他的资源
(3)破坏不剥夺条件
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
(4)破坏循环等待条件
靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

十四:创建线程的四种方式 

  • 继承 Thread 类;
package java.lang;
public class Thread implements Runnable {
	// 构造方法
	public Thread(Runnable target);
	public Thread(Runnable target, String name);
	
	public synchronized void start();
}
public class Main {
    public static void main(String[] args) {
        new MyThread().start();
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "\t" +                 
        Thread.currentThread().getId());
    }
}
  • 实现 Runnable 接口;
package java.lang;

@FunctionalInterface
public interface Runnable {
    pubic abstract void run();
}
public class Main {
    public static void main(String[] args) {
    	 // 将Runnable实现类作为Thread的构造参数传递到Thread类中,然后启动Thread类
        MyRunnable runnable = new MyRunnable();
        new Thread(runnable).start();
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "\t" + 
        Thread.currentThread().getId());
    }
}
  • 实现 Callable 接口;重写call()方法,然后包装成java.util.concurrent.FutureTask, 再然后包装成Thread
public class Main {
    public static void main(String[] args) throws Exception {
    	 // 将Callable包装成FutureTask,FutureTask也是一种Runnable
        MyCallable callable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        new Thread(futureTask).start();

        // get方法会阻塞调用的线程
        Integer sum = futureTask.get();
        System.out.println(Thread.currentThread().getName() + Thread.currentThread().getId() + "=" + sum);
    }
}


class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() + "\t" + new Date() + " \tstarting...");

        int sum = 0;
        for (int i = 0; i <= 100000; i++) {
            sum += i;
        }
        Thread.sleep(5000);

        System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() + "\t" + new Date() + " \tover...");
        return sum;
    }
}
  • 使用 Executors 工具类创建线程池
public class Main {
    public static void main(String[] args) throws Exception {
    	Executor executor = Executors.newFixedThreadPool(5);
        executor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread()+"创建线程的第四种方法");
            }
        });
        ((ExecutorService) executor).shutdown();
 
    }
}

十五:继承Thread和实现Runnable区别

可以看到第一种方法和第二种方法这两种方式都是围绕着Thread和Runnable,继承Thread类把

run()写到类中,实现Runnable接口,是把run()方法写到接口中然后再用Thread类来包装, 两种方

式最终都是调用Thread类的start()方法来启动线程的。


两种方式在本质上没有明显的区别,在外观上有很大的区别,第一种方式是继承Thread类,因

Java是单继承,如果一个类继承了Thread类,那么就没办法继承其它的类了,在继承上有一点受

制,有一点不灵活,第二种方式就是为了解决第一种方式的单继承不灵活的问题,所以平常使用就

使用第二种方式


十六:实现Runnable和Callable的区别

Runnable接口定义的run方法,Callable定义的是call方法。

run方法没有返回值,call方法必须有返回值。

run方法无法抛出异常,call方法可以抛出checked exception。

Callable和Runnable都可以应用于executors。而Thread类只支持Runnable.

十七:线程的Run()和Start()有什么区别

每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,run()方法称为线体。
通过调用Thread类的start()方法来启动一个线程。start() 方法用于启动线程,run() 方法用于执行线
程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。
 start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行
完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此
Thread类调用方法run()来完成其运行状态,run()方法运行结束, 此线程终止。然后CPU再调度其
它线程。
run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实
就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下
面的代码,所以执行 路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使start()
方法而不是run()方法。

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

new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪

状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行

run() 方法的内容,这是真正的多线程工作。

而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线
程中执行它,所以这并不是多线程工作。
总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是thread 的一个普通方
法调用,还是在主线程里执行。

十九:什么是 Callable 和 Future?

Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无
法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值
可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。
Future 接口表示异步任务,是一个可能还没有完成的异步任务的结果。所以说Callable用于产生结
果,Future 用于获取结果。

二十:sleep() 和 wait() 有什么区别?

两者都可以暂停线程的执行
  1. 类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
  2. 是否释放锁:sleep() 不释放锁;wait() 释放锁。
  3. 用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
  4. 用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。

二十一:说说线程的生命周期和五种基本状态

  1. 新建(new):新创建了一个线程对象
  2. 可运行(runnable):线程对象创建后,调用start()方法,该线程处于就绪状态,等待被线程调度使用,获取CPU的使用权
  3. 运行(running):可运行状态的线程(runnable)的线程获取了cpu时间片,执行程序代码;
  4. 阻塞(block):处于运行状态的线程因为某种原因,暂时放弃对cpu的使用权,进入阻塞状态,直到其进入到就绪状态,才有机会被cpu调用以进入到运行状态。
  5. 死亡(dead):线程run()或者main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。线程的死亡不可复生。 
阻塞的情况分三种:
(一). 等待阻塞: 运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)
中,使本线程进入到等待阻塞状态;
(二). 同步阻塞: 线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),,则JVM会把
该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;
(三). 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当
sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。

二十二: Java 中用到的线程调度算法是什么? 

计算机通常只有一个 CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU 的使用权
才能执行指令。所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得 CPU 的使用
权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待 CPU,JAVA 虚拟机
的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配 CPU 的使用权。
有两种调度模型:分时调度模型和抢占式调度模型。
分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU 的时
间片这个也比较好理解。
Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行
池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运
行,直至它不得不放弃 CPU

二十三:什么是线程调度器和时间片

线程调度器是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。 时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配CPU 时间可以基于线程优先级或者线程等待的时间。 线程调度并不受到 Java 虚拟机控制,所以由应用程序来控制它是更好的选择

(也就是说不要让你的程序依赖于线程的优先级)

二十四:请说出线程同步与线程调度的相关方法

(1)wait():使线程处于一个等待(阻塞)状态,并且释放所持有的对象的锁

(2)sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常

(3)notify():唤醒一个处于等待的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待的线程,而是由JVM确定唤醒哪一个线程,并且与优先级无关

(4)notifyAll():唤醒所有处于等待的线程,该方法并不是将对象的锁给所有线程,而是让他们竞争,只有获得锁的线程才可以进入就绪状态;

二十五:你是如何调用wait()方法的,在if块还是循环,为什么?

因为正在等待的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足情况的条件下退出;

wait() 方法应该在循环调用,因为当线程获取到 CPU 开始执行的时候,其他条件可能还没有满足,足,所以在处理前,循环检测条件是否满足会更好。

二十六:为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在Object 类里?而sleep定义在Thread类里面?

JAVA提供的锁是对象级的而不是线程级的,每个对象都有个锁,而线程是可以获得这个对象的。因此线程需要等待某些锁,那么只要调用对象中的wait()方法便可以了。而wait()方法如果定义在Thread类中的话,那么线程正在等待的是哪个锁就不明确了。这也就是说wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中是因为锁是属于对象的原因。

sleep的作用是:让线程在预期的时间内执行,其他时候不要来占用CPU资源。从上面的话术中,便可以理解为sleep是属于线程级别的,它是为了让线程在限定的时间后去执行。而且sleep方法是不会去释放锁的

二十七:为什么 wait(),notify(),notifyAll()必须在同步方法或者同步块中被调用?

当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这
个对象锁并进入等待状态直到其他线程调用这个对象上的notify()方法。同样的,当一个线程需要调
用对象的 notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象
锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在
同步方法或者同步块中被调用

二十八:notify()和notifyAll()有什么区别

如果线程调用了对象的wait()方法,那么线程便会处于该对象的等待池中,等待线程池中的对象不会去竞争对象的锁。

notify()只会唤醒一个线程,notifyAll()会唤醒所有的线程

notiayAll()调用后,会将所有线程由等待池移动到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功就会继续锁被释放,继续参与竞争;

notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。

二十九:sleep()和yield()的区别

sleep与yield都属于暂停线程。都是静态方法,直接写在线程体中。

sleep()可以理解为“抱着资源睡觉”,由原来的运行状态进入阻塞状态,当时间到达,再由阻塞状态回到就绪状态,等待CPU的调度。

yield()直接由运行状态跳回就绪状态,表示退让线程,让出CPU,让CPU调度器重新调度。礼让可能成功,也可能不成功,也就是说,回到调度器和其他线程进行公平竞争。

三十:如何停止一个正在运行的线程

在java中有以下3种方法可以终止正在运行的线程:
1. 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
2. 使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废
的方法。
3. 使用interrupt方法中断线程.

三十一:Java 中 interrupted 和 isInterrupted 方法的区别?

interrupt:interrupt方法是用于中断线程的,调用该方法的线程的状态将被置为"中断"状态。注意:线程中断仅仅是设置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出InterruptedException的方法,比如这里的sleep,以及Object.wait等方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。
interrupted:是静态方法,查看当前中断信号是true还是false并且清除中断信号。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了。
isInterrupted:查看当前中断信号是true还是false,只是查看,不会清除中断状态

静态方法interrupted将会清除中断状态(传入的参数ClearInterrupted为true),而实例方法isInterrupted则不会(传入的参数ClearInterrupted为false)。

三十二:同步方法和同步块哪个更好

同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象
同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则: 同步的范围越小越好

三十三:Java 线程数过多会造成什么异常?

(1)线程的生命周期开销非常高
(2)消耗过多的 CPU
资源如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争CPU资源时还将产生其他性能的开销。
(3)降低稳定性JVM
在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError 异常

三十四:线程类的构造方法、静态块是被哪个线程调用的

这是一个非常刁钻和狡猾的问题。
请记住:线程类的构造方法、静态块是被 new 这个线程类所在的线程所调用的,而 run 方法里面的代码才是被线程自身所调用的。
如果说上面的说法让你感到困惑,那么我举个例子,假设 Thread2 中 new了Thread1,main 函数中 new Thread2,那么:
(1)Thread2 的构造方法、静态块是 main 线程调用的,Thread2 的 run()方法是Thread2 自己调用的
(2)Thread1 的构造方法、静态块是 Thread2 调用的,Thread1 的 run()方法
是Thread1 自己调用的

三十五:为什么代码会重排序?

在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是 不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:
(1)在单线程环境下不能改变程序运行的结果;
(2)存在数据依赖关系的不允许重排序;
需要注意的是:重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义。

三十六:synchronized 的作用?

在 Java 中,synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行。synchronized 可以修饰类、方法、变量。

另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时 需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间 ,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。
庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化, 如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

三十七:说说自己是怎么使用 synchronized 关键字,在项目中用到了吗

synchronized关键字最主要的三种使用方式:
(1)修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
(2)修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员(static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的 锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例 对象锁
(3)修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!

三十八:说一下 synchronized 底层实现原理?

(1)monitorenter   monitorexit

synchronized是Java中的一个关键字,在使用的过程中并没有看到显示的加锁和解锁过程。因此有必要通过javap命令,查看相应的字节码文件。

通过JDK 反汇编指令 javap -c -v SynchronizedDemo

可以看出在执行同步代码块之前之后都有一个monitor字样,其中前面的是monitorenter,后面的是离开monitorexit,不难想象一个线程也执行同步代码块,首先要获取锁,而获取锁的过程就是monitorenter ,在执行完代码块之后,要释放锁,释放锁就是执行monitorexit指令。

(2)为什么会有两个monitorexit呢?

这个主要是防止在同步代码块中线程因异常退出,而锁没有得到释放,这必然会造成死锁(等待的线程永远获取不到锁)。因此最后一个monitorexit是保证在异常情况下,锁也可以得到释放,避免死锁。 仅有ACC_SYNCHRONIZED这么一个标志,该标记表明线程进入该方法时,需要 monitorenter,退出该方法时需要monitorexit。

(3)synchronized可重入的原理

重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁

三十九:Synchronized优化---锁粗化,锁消除,锁升级

锁粗化
互斥的临界区范围应该尽可能小,这样做的目的是为了使同步的操作数量尽可能缩小,缩短阻塞时间,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。

但是加锁解锁也需要消耗资源,如果存在一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,锁粗化就是将「多个连续的加锁、解锁操作连接在一起」,扩展成一个范围更大的锁,避免频繁的加锁解锁操作
 

锁消除
Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,经过逃逸分析(对象在函数中被使用,也可能被外部函数所引用,称为函数逃逸),去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的时间消耗。

代码中使用Object作为锁,但是Object对象的生命周期只在incrFour()函数中,并不会被其他线程所访问到,所以在J I T编译阶段就会被优化掉(此处的Object属于没有逃逸的对象)。
 

四十:什么是自旋

很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然 synchronized 里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized 的边界做忙循环,这就是自旋。如果做了多次循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。

四十一:如果你提交任务时,线程池队列已满,这时会发生什么

(1)如果使用的是无界队列LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务

(2)如果使用的是有界队列例如ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据 maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy

四十二:线程池的拒绝策略********************

(1)CallerRunsPolicy

当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。

(2)AbortPolicy

丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。

(3)DiscardPolicy

直接丢弃,啥也不说

(4)DiscardOldestPolicy

要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入

四十三:了解单例模式吗,来手写一个

懒汉式(线程不安全)

public class Singleton{
    private Singleton(){}

    private static Singleton single=null;

    public static Singleton getInstance(){
        if(single==null){
            single=new Singleton();
        }
        return single;
    }


}

双重检查锁定

public class Singleton{

    private SingleTon(){}

    private static Singleton single=null;

    private static Singleton getInstance(){
        if(singleton==null){
            synchronized(Singleton.class){
                if(singleton==null){
                    singleton=new SingleTon();
                }
            }
        }
        return single;
    }
}

        第一次校验:也就是第一个if(singleton==null),这个是为了代码提高代码执行效率,由于单例模式只要一次创建实例即可,所以当创建了一个实例之后,再次调用getInstance方法就不必要进入同步代码块,不用竞争锁。直接返回前面创建的实例即可。

        第二次校验:也就是第二个if(singleton==null),这个校验是防止二次创建实例,假如有一种情况,当singleton还未被创建时,线程t1调用getInstance方法,由于第一次判断singleton==null,此时线程t1准备继续执行,但是由于资源被线程t2抢占了,此时t2页调用getInstance方法,同样的,由于singleton并没有实例化,t2同样可以通过第一个if,然后继续往下执行,同步代码块,第二个if也通过,然后t2线程创建了一个实例singleton。此时t2线程完成任务,资源又回到t1线程,t1此时也进入同步代码块,如果没有这个第二个if,那么,t1就也会创建一个singleton实例,那么,就会出现创建多个实例的情况,但是加上第二个if,就可以完全避免这个多线程导致多次创建实例的问题。

饿汉式

饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的。

public clsss Singleton(){

    private Singleton();

    private static final Singleton single=new Singleton();

    public static Singleton getInstance(){
        return single;   
    }

}

四十四:各种锁

(无状态锁,偏向锁、轻量级锁、重量级锁的升级以及区别)

⽆锁就是没有对资源进⾏锁定,任何线程都可以尝试去修改它,⽆锁在这⾥不再细 讲。

首先通过一个小例子来解释一下三种锁的区别:

假如家里只有一个碗,当我自己在家时,没有人会和我争碗,这时即为偏向锁状态

当我和女朋友都在家吃饭时,如果女朋友不是很饿,则她会等我吃完再用我的碗去吃饭,这就是轻量级锁状态

当我和女朋友都很饿的时候,这时候就会去争抢这唯一的一个碗(贫穷的我)吃饭,这就是重量级锁状态
 

⼏种锁会随着竞争情况逐渐升级,锁的升级很容易发⽣,但是锁降级发⽣的条件会⽐较苛刻,锁降
级发⽣在Stop The World 期间,当 JVM 进⼊安全点的时候,会检查是否有闲置的锁,然后进⾏降
级。



(1)对象头

(2)偏向锁

Hotspot 的作者经过以往的研究发现⼤多数情况下锁不仅不存在多线程竞争,⽽且总是由同⼀线程
多次获得,于是引⼊了偏向锁。 偏向锁会偏向于第⼀个访问锁的线程,如果在接下来的运⾏过程
中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。也就是说,偏向锁
在资源⽆竞争情况下消除了同步语句,连CAS 操作都不做了,提⾼了程序的运⾏性能。

⼤⽩话就是对锁置个变量,如果发现为 true ,代表资源⽆竞争,则⽆需再⾛各种加锁/ 解锁流程。如果为 false ,代表存在其他线程竞争资源,那么就会⾛后⾯的流程。

实现原理

⼀个线程在第⼀次进⼊同步块时,会在对象头和栈帧中的锁记录⾥存储锁的偏向的线程ID 。当下次
该线程进⼊这个同步块时,会去检查锁的 Mark Word ⾥⾯是不是放的⾃⼰的线程ID 如果是,表明
该线程已经获得了锁,以后该线程在进⼊和退出同步块时不需要花费CAS操作来加锁和解锁 ;如
果不是,就代表有另⼀个线程来竞争这个偏向锁 。这个时候会尝试使⽤CAS 来替换 Mark Word ⾥⾯
的线程 ID 为新线程的 ID
这个时候要分两种情况:
  1. 成功,表示之前的线程不存在了, Mark Word⾥⾯的线程ID为新线程的ID,锁不会升级,仍然为偏向锁;
  2. 失败,表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为0,并设置锁标志位为00,升级为轻量级锁,会按照轻量级锁的⽅式进⾏竞争锁。

(3)轻量级锁

多个线程在不同时段获取同⼀把锁 ,即不存在锁竞争的情况,也就没有线程阻塞。 针对这种情
况,JVM采⽤轻量级锁来避免线程的阻塞与唤醒
JVM 会为每个线程在当前线程的栈帧中创建⽤于存储锁记录的空间,我们称为Displaced Mark
Word。如果⼀个线程获得锁的时候发现是轻量级锁,会把锁的Mark Word复制到⾃⼰的 Displaced
Mark Word ⾥⾯。 然后线程尝试⽤CAS 将锁的 Mark Word 替换为指向锁记录的指针。如果成功,当
前线程获得锁,如果失败,表示Mark Word 已经被替换成了其他线程的锁记录,说明在与其它线程
竞争锁,当前线程就尝试使⽤⾃旋来获取锁。
⾃旋:不断尝试去获取锁,⼀般⽤循环来实现。
⾃旋是需要消耗 CPU 的,如果⼀直获取不到锁的话,那该线程就⼀直处在⾃旋状态,⽩⽩浪费
CPU 资源。解决这个问题最简单的办法就是指定⾃旋的次数,例如让其循环10 次,如果还没获取
到锁就进⼊阻塞状态。 但是JDK 采⽤了更聪明的⽅式 —— 适应性⾃旋 ,简单来说就是线程如果⾃
旋成功了,则下次⾃旋的次数会更多,如果⾃旋失败了,则⾃旋的次数就会减少。⾃旋也不是⼀直
进⾏下去的,如果⾃旋到⼀定程度(和JVM、操作系统相关),依然没有获取到锁,称为⾃旋失
败,那么这个线程会阻塞。同时这个锁就会升级成重量级锁。

(4)重量级锁

重量级锁依赖于操作系统的互斥量( mutex ) 实现的,⽽操作系统中线程间状态的转换需要相对⽐
较⻓的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗CPU。
前⾯说到,每⼀个对象都可以当做⼀个锁,当多个线程同时请求某个对象锁时,对象锁会设置⼏种
状态⽤来区分请求的线程:
Contention List :所有请求锁的线程将被⾸先放置到该竞争队列
Entry List Contention List 中那些有资格成为候选⼈的线程被移到 Entry List
Wait Set :那些调⽤ wait ⽅法被阻塞的线程被放置到 Wait Set
OnDeck :任何时刻最多只能有⼀个线程正在竞争锁,该线程称为 OnDeck
Owner :获得锁的线程称为 Owner
!Owner :释放锁的线程
当⼀个线程尝试获得锁时,如果该锁已经被占⽤,则会将该线程封装成⼀个 ObjectWaiter 对象插
⼊到 Contention List 的队列的队⾸,然后调⽤ park 函数挂起当前线程。
当线程释放锁时,会从 Contention List EntryList 中挑选⼀个线程唤醒,被选中的线程叫做 Heir
presumptive 即假定继承⼈,假定继承⼈被唤醒后会尝试获得锁,但 synchronized 是⾮公平的,
所以假定继承⼈不⼀定能获得锁。这是因为对于重量级锁,线程先⾃旋尝试获得锁,这样做的⽬的
是为了减少执⾏操作系统同步操作带来的开销。如果⾃旋不成功再进⼊等待队列。这对那些已经在
等待队列中的线程来说,稍微显得不公平,还有⼀个不公平的地⽅是⾃旋线程可能会抢占了Ready
线程的锁。
线程获得锁后调⽤ Object.wait ⽅法,则会将线程加⼊到 WaitSet 中,当被 Object.notify 唤醒后,会
将线程从 WaitSet 移动到 Contention List EntryList 中去。需要注意的是,当调⽤⼀个锁对象的 wait
notify ⽅法时,如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。

四十五:锁的升级过程

  1. 检测Mark Word里面是不是当前线程的ID,如果是则表示当前线程处于偏向锁;
  2. 如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1;
  3. 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁;
  4. 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁;
  5. 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁;
  6. 如果自旋成功则依然处于轻量级状态;
  7. 如果自旋失败,则升级为重量级锁;

四十六:哪些是乐观锁,哪些是悲观锁

悲观锁:(synchronized,ReentrantLock
悲观锁就是我们常说的锁。对于悲观锁来说,它总是认为每次访问共享资源时会发⽣冲突,所以必
须对每次数据操作加上锁,以保证临界区的程序同⼀时间只能有⼀个线程在执⾏。
乐观锁:(CAS)
乐观锁⼜称为 ⽆锁 ,顾名思义,它是乐观派。乐观锁总是假设对共享资源的访问没有冲突,线程
可以不停地执⾏,⽆需加锁也⽆需等待。⽽⼀旦多个线程发⽣冲突,乐观锁通常是使⽤⼀种称为
CAS 的技术来保证线程执⾏的安全性。由于⽆锁操作中没有锁的存在,因此不可能出现死锁的情
况,也就是说乐观锁天⽣免疫死锁。 乐观锁多⽤于“读多写少 的环境,避免频繁加锁影响性能;⽽
悲观锁多⽤于 写多读少“ 的环境,避免频繁失败和重试影响性能。

四十七:什么是CAS机制?

CAS 的全称是:⽐较并交换( Compare And Swap )。在 CAS 中,有这样三个值:
V :要更新的变量 (var)
E :预期值 (expected)
N :新值 (new)
⽐较并交换的过程如下:
判断 V 是否等于 E ,如果等于,将 V 的值设置为 N ;如果不等,说明已经有其它线程 更新了V ,则当
前线程放弃更新,什么都不做。

我们先看一段代码:

启动两个线程,每个线程中让静态变量count循环累加100次。

 最终输出的count结果一定是200吗?因为这段代码是非线程安全的,所以最终的自增结果很可能会小于200。我们再加上synchronized同步锁,再来看一下。

加了同步锁之后,count自增的操作变成了原子性操作,所以最终输出一定是count=200,代码实现了线程安全。虽然synchronized确保了线程安全,但是在某些情况下,这并不是一个最有的选择。

关键在于性能问题。

 synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。

尽管JAVA 1.6为synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过过度,但是在最终转变为重量级锁之后,性能仍然比较低。所以面对这种情况,我们就可以使用java中的“原子操作类”。

所谓原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。如AtomicBoolean,AtomicUInteger,AtomicLong。它们分别用于Boolean,Integer,Long类型的原子性操作。

现在我们尝试使用AtomicInteger类:

使用AtomicInteger之后,最终的输出结果同样可以保证是200。并且在某些情况下,代码的性能会比synchronized更好。

而Atomic操作类的底层正是用到了“CAS机制”。

CAS是英文单词Compare and Swap的缩写,翻译过来就是比较并替换。

我们看一个例子:

1. 在内存地址V当中,存储着值为10的变量。

2. 此时线程1想把变量的值增加1.对线程1来说,旧的预期值A=10,要修改的新值B=11.

3. 在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。

4. 线程1开始提交更新,首先进行A和地址V的实际值比较,发现A不等于V的实际值,提交失败。

5. 线程1 重新获取内存地址V的当前值,并重新计算想要修改的值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。

6. 这一次比较幸运,没有其他线程改变地址V的值。线程1进行比较,发现A和地址V的实际值是相等的。

7. 线程1进行交换,把地址V的值替换为B,也就是12.

从思想上来说,synchronized属于悲观锁,悲观的认为程序中的并发情况严重,所以严防死守,CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去重试更新。

在java中除了上面提到的Atomic系列类,以及Lock系列类夺得底层实现,甚至在JAVA1.6以上版本,synchronized转变为重量级锁之前,也会采用CAS机制。

四十五:CAS的缺点

1) CPU开销过大

在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。

2) 不能保证代码块的原子性

CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。

3) ABA问题

这是CAS机制最大的问题所在。(后面有介绍)

我们下面来介绍一下两个问题:

1. JAVA中CAS的底层实现

2. CAS的ABA问题和解决办法。

我们看一下AtomicInteger当中常用的自增方法incrementAndGet:

public final int incrementAndGet() {

    for (;;) {

        int current = get();

        int next = current + 1;

        if (compareAndSet(current, next))

            return next;

    }

}

private volatile int value; 

public final int get() {

    return value;

}

这段代码是一个无限循环,也就是CAS的自旋,循环体中做了三件事:

1. 获取当前值

2. 当前值+1,计算出目标值

3. 进行CAS操作,如果成功则跳出循环,如果失败则重复上述步骤

这里需要注意的重点是get方法,这个方法的作用是获取变量的当前值。

如何保证获取的当前值是内存中的最新值?很简单,用volatile关键字来保证(保证线程间的可见性)。我们接下来看一下compareAndSet方法的实现:

compareAndSet方法的实现很简单,只有一行代码。这里涉及到两个重要的对象,一个是unsafe,一个是valueOffset。

什么是unsafe呢?Java语言不像C,C++那样可以直接访问底层操作系统,但是JVM为我们提供了一个后门,这个后门就是unsafe。unsafe为我们提供了硬件级别的原子操作。

至于valueOffset对象,是通过unsafe.objectFiledOffset方法得到,所代表的是AtomicInteger对象value成员变量在内存中的偏移量。我们可以简单的把valueOffset理解为value变量的内存地址。

我们上面说过,CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

而unsafe的compareAndSwapInt方法的参数包括了这三个基本元素:valueOffset参数代表了V,expect参数代表了A,update参数代表了B。

正是unsafe的compareAndSwapInt方法保证了Compare和Swap操作之间的原子性操作。

我们现在来说什么是ABA问题。假设内存中有一个值为A的变量,存储在地址V中。

此时有三个线程想使用CAS的方式更新这个变量的值,每个线程的执行时间有略微偏差。线程1和线程2已经获取当前值,线程3还未获取当前值。

接下来,线程1先一步执行成功,把当前值成功从A更新为B;同时线程2因为某种原因被阻塞住,没有做更新操作;线程3在线程1更新之后,获取了当前值B。

在之后,线程2仍然处于阻塞状态,线程3继续执行,成功把当前值从B更新成了A。

最后,线程2终于恢复了运行状态,由于阻塞之前已经获得了“当前值A”,并且经过compare检测,内存地址V中的实际值也是A,所以成功把变量值A更新成了B。

看起来这个例子没啥问题,但如果结合实际,就可以发现它的问题所在。

我们假设一个提款机的例子。假设有一个遵循CAS原理的提款机,小灰有100元存款,要用这个提款机来提款50元。

由于提款机硬件出了点问题,小灰的提款操作被同时提交了两次,开启了两个线程,两个线程都是获取当前值100元,要更新成50元。

理想情况下,应该一个线程更新成功,一个线程更新失败,小灰的存款值被扣一次。

线程1首先执行成功,把余额从100改成50.线程2因为某种原因阻塞。这时,小灰的妈妈刚好给小灰汇款50元。

线程2仍然是阻塞状态,线程3执行成功,把余额从50改成了100。

线程2恢复运行,由于阻塞之前获得了“当前值”100,并且经过compare检测,此时存款实际值也是100,所以会成功把变量值100更新成50。

原本线程2应当提交失败,小灰的正确余额应该保持100元,结果由于ABA问题提交成功了。

怎么解决呢?加个版本号就可以了。

真正要做到严谨的CAS机制,我们在compare阶段不仅要比较期望值A和地址V中的实际值,还要比较变量的版本号是否一致。

我们仍然以刚才的例子来说明,假设地址V中存储着变量值A,当前版本号是01。线程1获取了当前值A和版本号01,想要更新为B,但是被阻塞了。

这时候,内存地址V中变量发生了多次改变,版本号提升为03,但是变量值仍然是A。

随后线程1恢复运行,进行compare操作。经过比较,线程1所获得的值和地址的实际值都是A,但是版本号不相等,所以这一次更新失败。

在Java中,AtomicStampedReference类就实现了用版本号作比较额CAS机制。

1. java语言CAS底层如何实现?

利用unsafe提供的原子性操作方法。

2.什么事ABA问题?怎么解决?

当一个值从A变成B,又更新回A,普通CAS机制会误判通过检测。

利用版本号比较可以有效解决ABA问题。

四十六:Java实现CAS的原理 - Unsafe类********

前⾯提到,CAS是⼀种原⼦操作。那么Java是怎样来使⽤CAS的呢?我们知道,在Java中,如果⼀个方法是native的,那Java就不负责具体实现它,而是交给底层的JVM使⽤c或者c++去实现。 在Java中,有⼀个 Unsafe 类,它在 sun.misc 包中。它⾥⾯是⼀些 native ⽅法,其中就有⼏个关于CAS的:
boolean compareAndSwapObject (Object o, long offset,Object expected, Object x) ;
boolean compareAndSwapInt (Object o, long offset, int expected, int x) ;
boolean compareAndSwapLong (Object o, long offset, long expected, long x) ;
当然,他们都是 public native 的。
Unsafe 中对 CAS 的实现是 C++ 写的,它的具体实现和操作系统、 CPU 都有关系。 Linux的 X86 下主要是通过 cmpxchgl 这个指令在 CPU 级完成 CAS 操作的,但在多处理器情况下必须使⽤ lock 指令加锁来完成。
当然, Unsafe 类⾥⾯还有其它⽅法⽤于不同的⽤途。⽐如⽀持线程挂起和恢复的 park unpark  ,LockSupport 类底层就是调⽤了这两个⽅法。

四十七:Semaphore介绍

Semaphore有一个构造函数,可以传入一个 int 型整数 n,表示某段代码最多只有 n 个线程可以访问,如果超出了 n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果 Semaphore 构造函数中传入的 int 型整数 n=1,相当于变成了一个 synchronized 了。
Semaphore(信号量)-允许多个线程同时访问: synchronized 和ReentrantLock 都是一次只允许一
个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源
// 默认情况下使⽤⾮公平
public Semaphore(int permits) {
     sync = new NonfairSync(permits);
}

public Semaphore(int permits, boolean fair) {
     sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
最主要的⽅法是 acquire ⽅法和 release ⽅法。 acquire()⽅法会申请⼀个permit,⽽release⽅法会释
放⼀个permit 。当然,你也可以申请多个 acquire(int permits) 或者释放多个release(int permits)
每次 acquire permits 就会减少⼀个或者多个。如果减少到了 0 ,再有其他线程来acquire,那就要
阻塞这个线程直到有其它线程 release permit 为⽌。
Semaphore 内部有⼀个继承了 AQS 的同步器 Sync ,重写了 tryAcquireShared ⽅法。在这个⽅法
⾥,会去尝试获取资源。如果获取失败(想要的资源数量⼩于⽬前已有的资源数量),就会返回⼀
个负数 (代表尝试获取资源失败)。然后当前线程就会进⼊AQS 的等待队列。

四十八:Lock与synchronized有以下区别:

  1. Lock是一个接口,而synchronized是关键字。
  2. Lock必须手动释放锁,而synchronized会自动释放锁。
  3. Lock可以让等待锁的线程响应中断,而synchronized不会,线程会一直等待下去。
  4. 通过Lock可以知道线程有没有拿到锁,而synchronized不能。
  5. Lock能提高多个线程读操作的效率。
  6. Lock是块范围内的锁,而synchronized能锁住类、方法和代码块。

四十九:synchronized的不⾜之处

我们先来看看 synchronized 有什么不⾜之处。
如果临界区是只读操作,其实可以多线程⼀起执⾏,但使⽤ synchronized 的话,同⼀时间只能有⼀个线程执⾏。
synchronized ⽆法知道线程有没有成功获取到锁
使⽤ synchronized ,如果临界区因为 IO 或者 sleep ⽅法等原因阻塞了,⽽当前线程⼜没有释放锁,就会导致所有线程等待。
⽽这些都是 locks 包下的锁可以解决的。

五十:锁的几种分类

(1)可重入锁和非可重入锁

所谓重⼊锁,顾名思义。就是⽀持重新进⼊的锁,也就是说这个锁⽀持⼀个线程对资源重复加锁。
synchronized 关键字就是使⽤的重⼊锁。⽐如说,你在⼀个 synchronized 实例⽅法⾥⾯调⽤另⼀个
本实例的synchronized 实例⽅法,它可以重新进⼊这个锁,不会出现任何异常。
如果我们⾃⼰在继承 AQS 实现同步器的时候,没有考虑到占有锁的线程再次获取锁
的场景,可能就会导致线程阻塞,那这个就是⼀个 ⾮可重⼊锁 。 ReentrantLock 的中⽂意思就是
可重⼊锁。也说本⽂后续要介绍的重点类。

(2)公平锁与非公平锁

这⾥的 公平 ,其实通俗意义来说就是 先来后到 ,也就是 FIFO 。如果对⼀个锁来说,先对锁获取
请求的线程⼀定会先被满⾜,后对锁获取请求的线程后被满⾜,那这个锁就是公平的。反之,那就
是不公平的。
⼀般情况下,⾮公平锁能提升⼀定的效率。但是⾮公平锁可能会发⽣线程饥饿(有⼀些线程⻓时间
得不到锁)的情况。所以要根据实际的需求来选择⾮公平锁和公平锁。
ReentrantLock ⽀持⾮公平锁和公平锁两种。

(3)读写锁和排它锁

我们前⾯讲到的 synchronized ⽤的锁和 ReentrantLock ,其实都是 排它锁 。也就是说,这些锁在同
⼀时刻只允许⼀个线程进⾏访问。⽽读写锁可以再同⼀时刻允许多个读线程访问。Java 提供了
ReentrantReadWriteLock 类作为读写锁的默认实现,内部维护了两个锁:⼀个读锁,⼀个写锁。
通过分离读锁和写锁,使得在“ 读多写少 的环境下,⼤⼤地提⾼了性能。
注意,即使⽤读写锁,在写线程访问时,所有的读线程和其它写线程均被阻塞。
可⻅,只是 synchronized是远远不能满⾜多样化的业务对锁的要求的。

五十二 原子操作-AtomicInteger类源码简析

上⾯介绍了 Unsafe 类的⼏个⽀持 CAS 的⽅法。那 Java 具体是如何使⽤这⼏个⽅法来实现原⼦操作的呢?
JDK 提供了⼀些⽤于原⼦操作的类,在 java.util.concurrent.atomic 包下⾯。在JDK 11中,有如下 17 个类:

 从名字就可以看得出来这些类⼤概的⽤途:

原⼦更新基本类型
原⼦更新数组
原⼦更新引⽤
原⼦更新字段(属性)
这⾥我们以 AtomicInteger 类的 getAndAdd(int delta) ⽅法为例,来看看 Java 是如何实现原⼦操作的。
先看看这个⽅法的源码:
public final int getAndAdd ( int delta) {
        return U.getAndAddInt( this , VALUE, delta);
}
这⾥的 U 其实就是⼀个 Unsafe 对象:
private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getU
所以其实 AtomicInteger 类的 getAndAdd(int delta) ⽅法是调⽤ Unsafe 类的⽅法来实现的:
@HotSpotIntrinsicCandidate
public final int getAndAddInt (Object o, long offset, int delta) {
        int v;
        do {
                v = getIntVolatile(o, offset);
        } while (!weakCompareAndSetInt(o, offset, v, v + delta));
        return v;
}
我们来⼀步步解析这段源码。⾸先,对象 o this ,也就是⼀个 AtomicInteger 对象。然后 offset 是⼀个常量 VALUE 。这个常量是在 AtomicInteger 类中声明的:
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "va 同样是调⽤的 Unsafe 的⽅法。从⽅法名字上来看,是得到了⼀个对象字段偏移量。
⽤于获取某个字段相对 Java 对象的 起始地址 的偏移量。
⼀个 java 对象可以看成是⼀段内存,各个字段都得按照⼀定的顺序放在这段内存⾥,同时考虑到对⻬要求,可能这些字段不是连续放置的,⽤这个⽅法能准确地告诉你某个字段相对于对象的起始内存地址的字节偏移量,因为是相对偏移量,所以它其实跟某个具体对象⼜没什么太⼤关系,跟class的定义和虚拟机的内存模型的实现细节更相关。
继续看源码。前⾯我们讲到, CAS ⽆锁 的基础,它允许更新失败。所以经常会与while 循环搭配,在失败后不断去重试。这⾥声明了⼀个v ,也就是要返回的值。从 getAndAddInt 来看,它返回的应该是原来的值,⽽新的值的 v + delta 。 这⾥使⽤的是do-while 循环。这种循环不多⻅,它的⽬的是保证循环体内的语句⾄ 少会被执⾏⼀遍。这样才能保证return 的值 v 是我们期望的值。
循环体的条件是⼀个 CAS ⽅法:
public final boolean weakCompareAndSetInt (Object o, long offset, int expected, int x) {
        return compareAndSetInt(o, offset, expected, x);
}
public final native boolean compareAndSetInt (Object o, long offset, int expected, int x) ;
可以看到,最终其实是调⽤的我们之前说到了 CAS native ⽅法。那为什么要经过⼀层weakCompareAndSetInt 呢?从 JDK 源码上看不出来什么。在 JDK 8 及之前的版本,这两个⽅法是⼀样的。
根据本⽂第⼀篇参考⽂章(⽂末链接),它跟 volitile 有关。
简单来说, weakCompareAndSet 操作仅保留了 volatile ⾃身变量的特性,⽽出去了happens-before规则带来的内存语义。也就是说, weakCompareAndSet ⽆法保证处理操作⽬标的volatile 变量外的其他变量的执⾏顺序 ( 编译器和处理器为了优化程序性能⽽对指令序列进⾏重新排序 ) ,同时也⽆法保证这些变量的可⻅性。这在⼀定程度上可以提⾼性能。
再回到循环条件上来,可以看到它是在不断尝试去⽤ CAS 更新。如果更新失败,就继续重试。那为什么要把获取“ 旧值 ”v 的操作放到循环体内呢?其实这也很好理解。前⾯我们说了,CAS 如果旧值 V 不等于预期值 E ,它就会更新失败。说明旧的值发⽣了变化。那我们当然需要返回的是被其他线程改变之后的旧值了,因此放在了do循环体内。

五十三:谈谈对AQS(AbstractQueuedSynchronizer)理解

AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器
(1)AQS 原理概览
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为 有效的工作线
程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占 用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是 用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修 改。
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
状态信息通过protected类型的getState,setState,compareAndSetState进行操作
(2)AQS 对资源的共享方式
AQS定义两种资源共享方式
Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
        公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
        非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
Share (共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某一资源进行读。不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要 实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。

(3)获取资源acquire,addWriter,acquireQueued

获取资源的⼊⼝是 acquire(int arg) ⽅法。 arg 是要获取的资源的个数,在独占模式下始终为1
public final void acquire(int arg) {
     if (!tryAcquire(arg) &&
     acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
     selfInterrupt();
}
⾸先调⽤ tryAcquire(arg) 尝试去获取资源。
如果获取资源失败,就通过 addWaiter(Node.EXCLUSIVE) ⽅法把这个线程插⼊到 等待队列中。其中传⼊的参数代表要插⼊的Node 是独占式的。
这个⽅法的具体实现
private Node addWaiter(Node mode) {
     // ⽣成该线程对应的Node节点
     Node node = new Node(Thread.currentThread(), mode);
     // 将Node插⼊队列中
     Node pred = tail;
     if (pred != null) {
         node.prev = pred;
         // 使⽤CAS尝试,如果成功就返回
         if (compareAndSetTail(pred, node)) {
             pred.next = node;
             return node;
         }
     }
 // 如果等待队列为空或者上述CAS失败,再⾃旋CAS插⼊
     enq(node);
     return node;
}
// ⾃旋CAS插⼊等待队列
private Node enq(final Node node) {
     for (;;) {
         Node t = tail;
         if (t == null) { // Must initialize
             if (compareAndSetHead(new Node()))
             tail = head;
         } else {
             node.prev = t;
             if (compareAndSetTail(t, node)) {
                 t.next = node;
                 return t;
             }
         }
     }
}
上⾯的两个函数⽐较好理解,就是在队列的尾部插⼊新的 Node 节点,但是需要注意的是由于AQS 中会存在多个线程同时争夺资源的情况,因此肯定会出现多个线程同时插⼊节点的操作,在这⾥是通过CAS ⾃旋的⽅式保证了操作的线程安全性
OK ,现在回到最开始的 aquire(int arg) ⽅法。现在通过 addWaiter ⽅法,已经把⼀个 Node放到等待队列尾部了。⽽处于等待队列的结点是从头结点⼀个⼀个去获取资源的。具体的实现我们来看看acquireQueued ⽅法
final boolean acquireQueued(final Node node, int arg) {
     boolean failed = true;
     try {
         boolean interrupted = false;
         // ⾃旋
         for (;;) {
             final Node p = node.predecessor();
             // 如果node的前驱结点p是head,表示node是第⼆个结点,就可以尝试去获取资源了
             if (p == head && tryAcquire(arg)) {
                 // 拿到资源后,将head指向该结点。
                 // 所以head所指的结点,就是当前获取到资源的那个结点或null。
                 setHead(node); 
                 p.next = null; // help GC
                 failed = false;
                 return interrupted;
             }
             // 如果⾃⼰可以休息了,就进⼊waiting状态,直到被unpark()
             if (shouldParkAfterFailedAcquire(p, node) &&
             parkAndCheckInterrupt())
             interrupted = true;
         }
     } finally {
         if (failed)
         cancelAcquire(node);
     }
}

(4)释放资源release

public final boolean release(int arg) {
     if (tryRelease(arg)) {
         Node h = head;
         if (h != null && h.waitStatus != 0)
             unparkSuccessor(h);
         return true;
      }
     return false;
 }

private void unparkSuccessor(Node node) {
     // 如果状态是负数,尝试把它设置为0
     int ws = node.waitStatus;
     if (ws < 0)
         compareAndSetWaitStatus(node, ws, 0);
     // 得到头结点的后继结点head.next
     Node s = node.next;
     // 如果这个后继结点为空或者状态⼤于0
     // 通过前⾯的定义我们知道,⼤于0只有⼀种可能,就是这个结点已被取消
     if (s == null || s.waitStatus > 0) {
         s = null;
         // 等待队列中所有还有⽤的结点,都向前移动
         for (Node t = tail; t != null && t != node; t = t.prev)
             if (t.waitStatus <= 0)
                 s = t;
         }
     // 如果后继结点不为空,
     if (s != null)
     LockSupport.unpark(s.thread);
}

五十四:reetrankLoc的实现

ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。

ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果:

(1)非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取;

(2)公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。

五十五:线程池的基本知识

(1)什么是线程池

线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。

(2)为什么要使用线程池

1. 创建 / 销毁线程需要消耗系统资源,线程池可以复⽤已创建的线程。
2. 控制并发的数量。并发数量过多,可能会导致资源消耗过多,从⽽造成服务器崩溃(主要原因)
3. 可以对线程做统⼀管理。

五十六:线程池的原理---ThreadPoolExecutor

Java 中的线程池顶层接⼝是 Executor 接⼝, ThreadPoolExecutor 是这个接口的实现类。

(1)ThreadPoolExecutor提供的构造方法

// 五个参数的构造函数
public ThreadPoolExecutor ( int corePoolSize,
                                             int maximumPoolSize,
                                             long keepAliveTime,
                                             TimeUnit unit,
                                             BlockingQueue<Runnable> workQueue)
// 六个参数的构造函数 -1
public ThreadPoolExecutor ( int corePoolSize,
                                             int maximumPoolSize,
                                             long keepAliveTime,
                                             TimeUnit unit,
                                             BlockingQueue<Runnable> workQueue,
                                             ThreadFactory threadFactory)
// 六个参数的构造函数 -2
public ThreadPoolExecutor ( int corePoolSize,
                                            int maximumPoolSize,
                                            long keepAliveTime,
                                            TimeUnit unit,
                                            BlockingQueue<Runnable> workQueue,
                                            RejectedExecutionHandler handler)
// 七个参数的构造函数
public ThreadPoolExecutor ( int corePoolSize,
                                            int maximumPoolSize,
                                            long keepAliveTime,
                                            TimeUnit unit,
                                            BlockingQueue<Runnable> workQueue,
                                            ThreadFactory threadFactory,
                                            RejectedExecutionHandler handler)
涉及到 5~7 个参数,我们先看看必须的 5 个参数是什么意思:
int corePoolSize :该线程池中核⼼线程数最⼤值
核⼼线程:线程池中有两类线程,核⼼线程和⾮核⼼线程。核⼼线程默认情况下会⼀直存在于线程池中,即使这个核⼼线程什么都不⼲(铁饭碗),⽽⾮核⼼线程如果⻓时间的闲置,就会被销毁(临时⼯)。
int maximumPoolSize :该线程池中线程总数最⼤值 。
该值等于核⼼线程数量 + ⾮核⼼线程数量。
long keepAliveTime :⾮核⼼线程闲置超时时⻓。
⾮核⼼线程如果处于闲置状态超过该值,就会被销毁。如果设置 allowCoreThreadTimeOut(true),则会也作⽤于核⼼线程。
TimeUnit unit keepAliveTime 的单位。
TimeUnit 是⼀个枚举类型 ,包括以下属性:
NANOSECONDS 1 微毫秒 = 1 微秒 / 1000 MICROSECONDS 1 微秒 = 1毫秒 / 1000 MILLISECONDS 1 毫秒 = 1 /1000 SECONDS : 秒 MINUTES : 分 HOURS : ⼩时
DAYS : 天
BlockingQueue workQueue :阻塞队列,维护着等待执⾏的 Runnable 任务对象。
常⽤的⼏个阻塞队列:
1. LinkedBlockingQueue
链式阻塞队列,底层数据结构是链表,默认⼤⼩是 Integer.MAX_VALUE , 也可以指定⼤⼩。
2. ArrayBlockingQueue
数组阻塞队列,底层数据结构是数组,需要指定队列的⼤⼩。
3. SynchronousQueue
同步队列,内部容量为 0 ,每个 put 操作必须等待⼀个 take 操作,反之亦然。
4. DelayQueue
延迟队列,该队列中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素 。

好了,介绍完 5 个必须的参数之后,还有两个⾮必须的参数。
ThreadFactory threadFactory
创建线程的工厂 ,⽤于批量创建线程,统⼀在创建线程时设置⼀些参数,如是否守护线程、线程的优先级等。如果不指定,会新建⼀个默认的线程工厂。
static class DefaultThreadFactory implements ThreadFactory {
    // 省略属性
    // 构造函数
    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
        Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
        poolNumber.getAndIncrement() +
        "-thread-";
    }
    // 省略
}

RejectedExecutionHandler handler
拒绝处理策略,线程数量⼤于最⼤线程数就会采⽤拒绝处理策略,四种拒绝处理的策略为 :
1. ThreadPoolExecutor.AbortPolicy :默认拒绝处理策略,丢弃任务并抛出RejectedExecutionException 异常。
2. ThreadPoolExecutor.DiscardPolicy :丢弃新来的任务,但是不抛出异常。
3. ThreadPoolExecutor.DiscardOldestPolicy :丢弃队列头部(最旧的)的任务,然后重新尝试执⾏程序(如果再次失败,重复此过程)。
4. ThreadPoolExecutor.CallerRunsPolicy :由调⽤线程处理该任务。

 五十七:**ThreadPoolExecutor的策略

线程池本身有⼀个调度线程,这个线程就是⽤于管理布控整个线程池⾥的各种任务和事务,例如创建线程、销毁线程、任务队列管理、线程队列管理等等。 故线程池也有⾃⼰的状态。 ThreadPoolExecutor 类中定义了⼀个 volatile int 变量runState 来表示线程池的状态 ,分别为 RUNNING、SHURDOWN、STOP、 TIDYING 、TERMINATED
  • 线程池创建后处于 RUNNING 状态。
  • 调⽤ shutdown()方 法后处于 SHUTDOWN 状态,线程池不能接受新的任务,清除⼀些空闲worker, 会等待阻塞队列的任务完成。
  • 调⽤shutdownNow()⽅法后处于 STOP 状态,线程池不能接受新的任务,中断所有线程,阻塞队列中没有被执⾏的任务全部丢弃。此时,poolsize=0, 阻塞队列的size 也为 0。
  • 当所有的任务已终止, ctl 记录的 任务数量 0 ,线程池会变为 TIDYING 状态。接着会执⾏terminated() 函数。
    ThreadPoolExecutor 中有⼀个控制状态的属性叫 ctl ,它是⼀个AtomicInteger类型的变量。
  • 线程池处在 TIDYING 状态时,执⾏完 terminated() ⽅法之后,就会由 TIDYING -> TERMINATED , 线程池被设置为 TERMINATED 状态。
    (线程池彻底终止,就变成TERMINATED状态。)

    五十八:线程池主要的任务处理流程(源码)

    // JDK 1.8 
    public void execute(Runnable command) {
         if (command == null)
             throw new NullPointerException(); 
         int c = ctl.get();
         // 1.当前线程数⼩于corePoolSize,则调⽤addWorker创建核⼼线程执⾏任务
         if (workerCountOf(c) < corePoolSize) {
             if (addWorker(command, true))
                 return;
                 c = ctl.get();
             }
         // 2.如果不⼩于corePoolSize,则将任务添加到workQueue队列。
         if (isRunning(c) && workQueue.offer(command)) {
             int recheck = ctl.get();
             // 2.1 如果isRunning返回false(状态检查),则remove这个任务,然后执⾏拒绝策略。
             if (! isRunning(recheck) && remove(command))
                 reject(command);
             // 2.2 线程池处于running状态,但是没有线程,则创建线程
             else if (workerCountOf(recheck) == 0)
                 addWorker(null, false);
         }
         // 3.如果放⼊workQueue失败,则创建⾮核⼼线程执⾏任务,
         // 如果这时创建⾮核⼼线程失败(当前线程总数不⼩于maximumPoolSize时),就会执⾏拒绝策略。
         else if (!addWorker(command, false))
         reject(command);
    }
    ctl.get() 是获取线程池状态,⽤ int 类型表示。第⼆步中,⼊队前进⾏了⼀次 isRunning 判断,⼊队之后,又进⾏了⼀次 isRunning 判断。

    为什么要二次检查线程池的状态?

    在多线程的环境下,线程池的状态是时刻发⽣变化的。很有可能刚获取线程池状态后线程池状态就改变了。判断是否将 command 加⼊ workqueue 是线程池之前的状态。倘若没有⼆次检查,万⼀线程池处于⾮RUNNING 状态(在多线程环境下很有可能发⽣),那么 command 永远不会执⾏。
    总结⼀下处理流程
    1. 线程总数量 < corePoolSize ,⽆论线程是否空闲,都会新建⼀个核⼼线程执行任务(让核心线程数量快速达到corePoolSize ,在核心线程数量 < corePoolSize时)。注意,这⼀步需要获得全局锁。
    2. 线程总数量 >= corePoolSize 时,新来的线程任务会进⼊任务队列中等待,然后空闲的核心线程会依次去缓存队列中取任务来执⾏(体现了线程复⽤)。
    3. 当缓存队列满了,说明这个时候任务已经多到爆棚,需要⼀些 临时⼯ 来执⾏这些任务了。于是会创建⾮核⼼线程去执⾏这个任务。注意,这⼀步需要获得全局锁。
    4. 缓存队列满了, 且总线程数达到了 maximumPoolSize ,则会采取上⾯提到的拒绝策略进⾏处理。

五十九:*****ThreadPoolExcuter是如何做到线程复用的 

(1)ThreadPoolExcuter在创建线程的时候,会把线程封装成工作线程work

(2)首先会判断核心线程是否创建满了,如果没有创建满,就会继续创建核心线程,任务添加到workQueue队列中

(3)如果核心线程数创建满了,队列也放不下了,就会去创建非核心线程

(4)如果非核心线程都创建满了,就开始拒绝策略.

---------------------------------------------------------------------------------------------------------

此时关注Work类(工人)----addWork方法

(1)首先进行循环,判断线程池的生命状态是不是running状态

(2)如果线程池生命状态Ok,那就new Worker(firstTask),把任务交给worker

但是注意,是把自己丢给线程,而不是把工人丢给线程

(3)然后执行runWork方法--此处的while循环就是线程可以重用的根本原因

while(task!=null || (task=getTask()!=null){}

首先看看自己拿的任务是否为空,如果是,那就从getTask里面去拿

(4)getTask中会判断 timed= allowCoreThreadTimeout  核心线程超过一定时间没工作,要

不要销毁一般设置的大都是false,因为销毁可能带来更大的损失

 Runnable r = timed ? workQueue.poll(xxxx) :workQueue.take();

意思就是当前的线程会不会因为时间而销毁,如果会,走poll(),否则走take()

所以我们的核心线程会一直卡在take(),被阻塞,被挂起,而非核心线程就会走poll()

addWork

我们知道,⼀个线程在创建的时候会指定⼀个线程任务,当执⾏完这个线程任务之后,线程⾃动销毁。但是线程池却可以复⽤线程,即⼀个线程执⾏完线程任务后不销毁,继续执⾏另外的线程任务。那么,线程池如何做到线程复⽤呢?
原来, ThreadPoolExecutor 在创建线程时,会将线程封装成⼯作线程 worker , 并放⼊⼯作线程组中,然后这个worker 反复从阻塞队列中拿任务去执行。
这⾥的 addWorker ⽅法是在上⾯提到的 execute ⽅法⾥⾯调⽤的,先看看上半部分:

 

 Worker类源码

注意,是把worker自己交给了当前线程

this.thread=getThreadFactory.newThread(this);

 我们再看看 runWorker 的逻辑:

 getTask源码

timed= allowCoreThreadTimeout 核心线程超过一定时间没工作,要不要销毁

一般设置的大都是false,因为销毁可能带来更大的损失

 Runnable r = timed ? workQueue.poll(xxxx) :workQueue.take();

意思就是当前的线程会不会因为时间而销毁,如果会,走poll(),否则走take()

所以我们的核心线程会一直卡在take(),被阻塞,被挂起,而非核心线程就会走poll()

六十:四种常见的线程池

(1)newCachedThreadPool

public static ExecutorService newCachedThreadPool() {
     return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                   60L, TimeUnit.SECONDS,
                                   new SynchronousQueue<Runnable>());
}
1. 提交任务进线程池。
2. 因为 corePoolSize 0 的关系,不创建核⼼线程,线程池最⼤为Integer.MAX_VALUE。
3. 尝试将任务添加到 SynchronousQueue 队列。
4. 如果 SynchronousQueue ⼊列成功,等待被当前运⾏的线程空闲后拉取执⾏。如果当前没有空闲线程,那么就创建⼀个⾮核⼼线程,然后从SynchronousQueue拉取任务并在当前线程执⾏。
5. 如果 SynchronousQueue 已有任务在等待,⼊列操作将会阻塞。当需要执⾏很多短时间的任务时,CacheThreadPool 的线程复⽤率⽐较⾼, 会显著的提⾼性能。⽽且线程60s 后会回收,意味着即使没有任务进来,CacheThreadPool并不会占⽤很多资源。

(2)newFixedThreadPool 

public static ExecutorService newFixedThreadPool(int nThreads) {
     return new ThreadPoolExecutor(nThreads, nThreads,
                                   0L, TimeUnit.MILLISECONDS,
                                   new LinkedBlockingQueue<Runnable>());
}
核⼼线程数量和总线程数量相等,都是传⼊的参数 nThreads ,所以只能创建核⼼线程,不能创建⾮核⼼线程。
因为LinkedBlockingQueue 的默认⼤⼩是Integer.MAX_VALUE,故如果核⼼线程空闲,则交给核⼼线程处理;如果核⼼线程不空闲,则⼊列等待,直到核⼼线程空闲。
CachedThreadPool 的区别:
因为 corePoolSize == maximumPoolSize ,所以 FixedThreadPool 只会创建核⼼线程。 ⽽CachedThreadPool 因为 corePoolSize=0 ,所以只会创建⾮核⼼线程。
getTask() ⽅法,如果队列⾥没有任务可取,线程会⼀直阻塞在 LinkedBlockingQueue.take() ,线程不会被回收。 CachedThreadPool 会在60s后收回。
由于线程不会被回收,会⼀直卡在阻塞,所以没有任务的情况下,
FixedThreadPool 占⽤资源更多。 都⼏乎不会触发拒绝策略,但是原理不同FixedThreadPool 是因为阻塞队列可以很⼤(最⼤为Integer 最⼤值),故⼏乎不会触发拒绝策略; CachedThreadPool是因为线程池很⼤(最⼤为 Integer 最⼤值),⼏乎不会导
致线程数量⼤于最⼤线程数,故⼏乎不会触发拒绝策略。

(3)newSingleThreadExecutor 

public static ExecutorService newSingleThreadExecutor() {
     return new FinalizableDelegatedExecutorService
         (new ThreadPoolExecutor(1, 1,
                                 0L, TimeUnit.MILLISECONDS,
                                 new LinkedBlockingQueue<Runnable>()));
}
有且仅有⼀个核⼼线程( corePoolSize == maximumPoolSize=1 ),使⽤了
LinkedBlockingQueue (容量很⼤),所以,不会创建⾮核⼼线程。所有任务按照先来先执⾏的顺序执⾏。如果这个唯⼀的线程不空闲,那么新来的任务会存储在任务队列⾥等待执⾏。

(4)newScheduledThreadPool 

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize
     return new ScheduledThreadPoolExecutor(corePoolSize);
}

//ScheduledThreadPoolExecutor():
public ScheduledThreadPoolExecutor(int corePoolSize) {
     super(corePoolSize, Integer.MAX_VALUE,
           DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
           new DelayedWorkQueue());
}
创建⼀个定⻓线程池,⽀持定时及周期性任务执⾏。
四种常⻅的线程池基本够我们使⽤了,但是《阿⾥把把开发⼿册》不建议我们直接使⽤Executors类中的线程池,⽽是通过 ThreadPoolExecutor 的⽅式,这样的处理⽅式让写的同学需要更加明确线程池的运⾏规则,规避资源耗尽的⻛险。

六十一:JAVA定时任务--ScheduledThreadPoolExecutor

(1)优缺点

这个类有两个用途:
(1)在给定的延迟之后执行任务
(2)周期性重复执行任务
在此之前,本来是用Timer类来完成定时任务的,但是Timer类有缺席
  1. Timer是单线程模式,Timer类不会捕捉TimeTask所抛出的异常,所以一旦出现异常,线程就会终止,其他任务也得不到执行
  2. 如果在执行任务期间某个TimerTask耗时比较久,那么就会影响其他任务的调度
  3. Timer的任务调度是基于绝对时间的,对系统时间敏感

(2)使用案例

假设我有⼀个需求,指定时间给⼤家发送消息。那么我们会将消息(包含发送时间)存储在数据库中,然后想⽤⼀个定时任务,每隔1 秒检查数据库在当前时间有没有需要发送的消息,那这个计划任务怎么写?下⾯是⼀个Demo:
public class ThreadPool {
     private static final ScheduledExecutorService executor = new
         ScheduledThreadPoolExecutor(1, Executors.defaultThreadFactory());
    
     private static SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm

     public static void main(String[] args){
         // 新建⼀个固定延迟时间的计划任务
         executor.scheduleWithFixedDelay(new Runnable() {
             @Override
             public void run() {
                 if (haveMsgAtCurrentTime()) {
                     System.out.println(df.format(new Date()));
                     System.out.println("⼤家注意了,我要发消息了");
                 }
             }
         }, 1, 1, TimeUnit.SECONDS);
     }

     public static boolean haveMsgAtCurrentTime(){
     //查询数据库,有没有当前时间需要发送的消息
     //这⾥省略实现,直接返回true
         return true;
     }
}
2019 - 01 - 23 16 : 16 : 48
⼤家注意了,我要发消息了
2019 - 01 - 23 16 : 16 : 49
⼤家注意了,我要发消息了
2019 - 01 - 23 16 : 16 : 50
⼤家注意了,我要发消息了
2019 - 01 - 23 16 : 16 : 51
⼤家注意了,我要发消息了
2019 - 01 - 23 16 : 16 : 52
⼤家注意了,我要发消息了
2019 - 01 - 23 16 : 16 : 53
⼤家注意了,我要发消息了
2019 - 01 - 23 16 : 16 : 54
⼤家注意了,我要发消息了
2019 - 01 - 23 16 : 16 : 55
⼤家注意了,我要发消息了

public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor
        implements ScheduledExecutorService {
        public ScheduledThreadPoolExecutor ( int corePoolSize,ThreadFactory threadFa
                super (corePoolSize, Integer.MAX_VALUE, 0 , NANOSECONDS,
                        new DelayedWorkQueue(), threadFactory);
        }
        //……
}

ScheduledThreadPoolExecutor 继承了 ThreadPoolExecutor, 实现了ScheduledExecutorService 。 线程池在之前的章节介绍过了,我们先看看 ScheduledExecutorService
public interface ScheduledExecutorService extends ExecutorService {
        public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit u
        public <V> ScheduledFuture<V> schedule (Callable<V> callable, long delay, Tim
        public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                                                        long initialDelay,
                                                                                        long period,
                                                                                        TimeUnit unit);
        public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                                                              long initialDelay,
                                                                                              long delay,
                                                                                              TimeUnit unit);
}
ScheduledExecutorService 实现了 ExecutorService , 并增加若⼲定时相关的接⼝。前两个⽅法⽤于单次调度执⾏任务,区别是有没有返回值。
重点理解⼀下后⾯两个⽅法:
scheduleAtFixedRate
该⽅法在 initialDelay 时⻓后第⼀次执⾏任务,以后每隔 period 时⻓,再次执⾏任务。 注意,period是从任务开始执⾏算起的。开始执⾏任务后,定时器每隔period时⻓检查该任务是否完成,如果完成则再次启动任务,否则等该任务结束后才再次启动任务。
scheduleWithFixDelay
该⽅法在 initialDelay 时⻓后第⼀次执⾏任务, 以后每当任务执⾏完成后,等待 delay 时⻓,再次执⾏任务。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值