Java多线程编程

一、基本概念梳理

1.1 线程生命周期:

Java中线程的状态分为6

1. 初始(NEW)新创建了一个线程对象,但还没有调用start()方法。

2. 运行(RUNNABLE)Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。

线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。

3.阻塞(BLOCKED)表示线程阻塞于锁。

4.等待(WAITING)进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

5.超时等待(TIMED_WAITING)该状态不同于WAITING,它可以在指定的时间后自行返回。

6. 终止(TERMINATED)表示该线程已经执行完毕。

       直接使用thread执行run方法会咋样呢?因为run方法是thread里面的一个普通的方法,所以我们直接调用run方法,这个时候它是会运行在我们的主线程中的。
       调start方法,正常的话,是将该线程加入线程组,最后尝试调用start0方法,而start0方法是私有的native方法(Native Method是一个java调用非java代码的接口)。

3.阻塞(BLOCKED):表示线程阻塞于锁。
这个状态,一般是线程等待获取一个锁,来继续执行下一步的操作,比较经典的就是synchronized关键字,这个关键字修饰的代码块或者方法,均需要获取到对应的锁,在未获取之前,其线程的状态就一直未BLOCKED,如果线程长时间处于这种状态下,我们就是当心看是否出现死锁的问题了。
 
4.等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
一个线程会进入这个状态,一定是执行了如下的一些代码,例如
Object.wait()
Thread.join()
LockSupport.park()
当一个线程执行了Object.wait()的时候,它一定在等待另一个线程执行Object.notify()或者Object.notifyAll()。
或者一个线程thread,其在主线程中被执行了thread.join()的时候,主线程即会等待该线程执行完成。当一个线程执行了LockSupport.park()的时候,其在等待执行LockSupport.unpark(thread)。当该线程处于这种等待的时候,其状态即为WAITING。需要关注的是,这边的等待是没有时间限制的,当发现有这种状态的线程的时候,若其长时间处于这种状态,也需要关注下程序内部有无逻辑异常。
 
5.超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
这个状态和WAITING状态的区别就是,这个状态的等待是有一定时效的,即可以理解为WAITING状态等待的时间是永久的,即必须等到某个条件符合才能继续往下走,否则线程不会被唤醒。但是TIMED_WAITING,等待一段时间之后,会唤醒线程去重新获取锁。
Thread.sleep(long)
Object.wait(long)
Thread.join(long)
LockSupport.parkNanos()
LockSupport.parkUntil()

1.2 Java虚拟机(JVM)简介

https://www.cnblogs.com/hexinwei1/p/9406239.html

1java代码编译执行过程:

        1. 源码编译:通过Java源码编译器将Java代码编译成JVM字节码(.class文件)

        2. 加载:通过ClassLoader及其子类来完成JVM的类加载

        3. 执行:字节码被装入内存,进入JVM虚拟机,被解释器解释执行

2JVM在它的生存周期中有一个明确的任务,那就是运行Java程序,因此当Java程序启动的时候,就产生JVM的一个实例;当程序运行结束的时候,该实例也跟着消失了。

1.3 JVM运行时内存划分

https://blog.csdn.net/weixin_42762133/article/details/95735737

https://blog.csdn.net/qq_31615049/article/details/81611918

1)堆  Java Heap

         所有对象实例以及数组都要在堆上分配,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java 虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JIT 编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。

  堆是Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。堆中不存放基本类型和对象引用,只存放对象本身

2)方法区

方法区(Method Area) Java一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息常量静态变量即时编译器编译后的代码等数据。

1. 静态变量static修饰的变量,类的所有实例都共享,我们只需知道,在方法区有个静态区,静态区专门存放静态变量和静态块。

2. 信息:类的版本、字段、方法、接口、构造函数等描述信息;

3. 常量池:类常量池、运行时常量池、字符串常量池

       在JDK1.7以前HotSpot虚拟机使用永久代(永久代是一片连续的堆空间)来实现方法区,永久代的大小在启动JVM时可以设置一个固定值-XX:MaxPermSize),不可变;

       在JDK1.7中 存储在永久代的部分数据就已经转移到Java Heap或者Native memory,永久代仍存在于JDK 1.7中,并没有完全移除。

JDK1.8中进行了较大改动:

        1. 除了永久代(PermGen),替换为元空间(Metaspace)。除JVM占用的内存,剩余的内存空间都可以被元空间(metaspace使用;

        2. 常量池、字符串常量池和类静态变量存在 Java 堆中;

        3. 运行时常量池存在本地内存的元空间Metaspace)中

3)程序计数器

         每个线程都有一个程序计算器,就是一个指针,指向方法区中的方法字节码(下一个将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。

4)本地方法栈

         Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies一个Native Method就是一个java调用非java代码的接口)。

  本地方法栈与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务。

5)栈 JVM Stack

         Java虚拟机栈(Java Virtual Machine Stacks) 也是线程私有的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表,操作栈动态链接方法出口等信息一个方法被调用直至执行完成的过程对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

         局部变量表存放了编译期可知的各种基本数据类型(Boolean, byte , char, short, int, float , long , double),对象引用(reference类型不等同于对象本身, 根据不同的虚拟机实现可能是一个指向对象起始地址的引用指针可能指向一个代表对象的句柄或者其他与此对象相关的位置)returnAddress类型(指向了一条字节码指令的地址)

线程共享所属进程的内存是堆内存和方法区内存,栈内存不共享,每个线程有自己的栈

1.4 变量的存储区域

成员变量:

1、成员变量定义在类中,在整个类中都可以被访问。

2、成员变量随着类对象的建立而建立,随着对象的消失而消失,存在于对象所在的堆内存中。

3、成员变量有默认初始化值

局部变量:存在栈内存中,作用的范围结束,变量空间会自动释放。局部变量没有默认初始化值

1.5 主内存和工作内存

 https://blog.csdn.net/y874961524/article/details/61617778

         Java内存模型(JMM)规定了所有的共享的变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory)。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),线程私有内存中存储了该线程以读/写共享变量的副本线程私有内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化

         线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成

1.6 线程安全与非线程安全

         当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为(这个类的结果行为都是我们设想的正确行为),那么就称这个类是线程安全的。

         非线程安全主要是指多个线程对同一个对象中的同一个实例变量进行操作时会出现值被更改、值不同步的情况,进而影响程序的执行流程。

          线程安全就是以获得的实例变量的值是经过同步处理的,不会出现脏读(取到的数据其实是被更改过的)的现象。通俗点说,就是线程访问时不产生资源冲突。

         线程不安全示例AtomicIntegerTest.java

二、多线程的使用

2.1 线程实现的三种方式:

1继承Thread类,重写run方法

        Thread本质上也是实现了Runnable接口的一个实例,它代表一个线程的实例,并且启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法

       这种方式实现多线程很简单,通过自己的类直接extend Thread并重写run()方法,就可以启动新线程并执行自己定义的run()方法。

2)实现Runnable接口,重写run方法

如果自己的类已经extends另一个类,就无法直接extends Thread,此时,必须实现一个Runnable接口。

为了启动自己RunnableThread 需要首先实例化一个Thread,并传入自己RunnableThread实例

RunnableThread runnableThread = new RunnableThread();

new Thread(runnableThread).start();

3)实现有返回结果的多线程

实现Callable接口,重写call()方法,有返回

1.创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。

2.创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。(FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了FutureRunnable接口。)

3.使用FutureTask对象作为Thread对象的target创建并启动新线程。

4.调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

ExecutorServiceCallableFuture实际上都是属于Executor框架中的功能类。Executor框架实现的就是线程池的功能。

线程创建示例ThreadCreate.java

什么是Native方法

https://www.jianshu.com/p/22517a150fe5

简单地讲,一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。这个特征并非java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern "C"告知C++编译器去调用一个C的函数。

创建线程的三种方式的对比

1、采用实现RunnableCallable接口的方式创建多线程时

优势是

         线程类只是实现了Runnable接口或Callable接口,还可以继承其他类

         在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

劣势是

         编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。

2、使用继承Thread类的方式创建多线程时

优势是:编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程

劣势是: 线程类已经继承了Thread类,所以不能再继承其他父类。

3RunnableCallable区别

(1) Callable规定重写的方法是call()Runnable规定重写的方法是run()

(2) Callable的任务执行后可返回值,而Runnable的任务是不能返回值的

(3) call方法可以抛出异常,run方法不可以

(4) 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

2.2 Thread的几种常用方法

start();  //启动线程

getId();  //获得线程ID

getName();  //获得线程名字

getPriority();  //获得优先级(1~1010个等级,最小为1,最大为10,正常为5

isAlive();  //判断线程是否活动

isDaemon();  //判断是否守护线程

getState();  //获得线程状态

sleep(long mill);  //让当前正在执行的线程休眠

join();  //等待线程结束

yield();  //放弃cpu使用权利

interrupt();  //中断线程

currentThread();  //获得正在执行的线程对象

yield()放弃当前的CPU资源,将它让给其他任务去占用CPU执行时间。但放弃的时间不确定,有可能刚刚放弃,马上又获得了CPU时间片

interrupt()中断仅仅是在线程对象做一个标记而已,称为中断标志。中断标志默认为false,在线程 t 调用自己的 t.interrupt() 方法后,此线程中断标志就变成true。但是,中断标志为true实际上不会对正常运行的线程产生影响,因为正常运行的线程不会自己去检查自己的中断标志。

       只有那些被阻塞的线程才会不停的检查自己的中断标志,这个阻塞包括因 waitjoinyield、而进入阻塞的线程,这些被阻塞的线程如果检查到自己的中断标志为true,就会抛出InterruptException异常。

活动状态:线程已经启动且尚未终止,线程处于正在运行或准备开始运行的状态,就认为线程是存活的。

RUNNABLE  BLOCKED  WAITING  TIMED_WAITING

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

当前JVM实例中最后一个用户线程结束时,守护线程就会随着JVM一同结束工作。Daemon的作用是为其他线程的运行提供便利服务

demo示例ThreadMethod.java

Java中有两类线程:User Thread(用户线程)Daemon Thread(守护线程)
• 
当前JVM实例中最后一个用户线程结束时,守护线程就会随着JVM一同结束工作。Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器)
•  JVM
实例的诞生:当启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点。

任何线程都可以是“守护线程
Daemon”或“用户线程User”。他们在几乎每个方面都是相同的,唯一的区别是判断虚拟机何时离开:
用户线程:
Java虚拟机在它所有用户线程离开后自动离开。
守护线程:守护线程则是用来服务用户线程的,如果没有其他用户线程在运行,那么就没有可服务对象,也就没有理由继续下去。会随着
JVM一同结束。
注意:
1thread.setDaemon(true)必须在thread.start()之前设置,否则会抛出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
2)在Daemon线程中产生的新线程也是Daemon的。
3)不要认为所有的任务都可以分配给Daemon来进行服务,比如读写操作或者计算逻辑。
因为你不可能知道在所有的
User完成之前,Daemon是否已经完成了预期的服务任务。一旦User退出了,可能大量数据还没有来得及读入或写出,计算任务也可能多次运行结果不一样。这对程序是毁灭性的。造成这个结果理由已经说过了:一旦所有User Thread离开了,虚拟机也就退出运行了。

4)使用循环控制守护线程里的run方法(run方法结束,线程也就结束了),不要让守护线程提前于服务的用户线程消亡。
代码的逻辑里如果让守护线程提前于用户线程消亡的情况下,守护线程并不会主动延长生命和用户线程一起消亡。但是,代码的逻辑让守护线程延迟于用户线程消亡的情况下,守护线程会提前和用户线程一起消亡。

2.3 线程同步方法一:关键字sychronized

sychronized 一般用来修饰一个方法或者一个代码块。利用sychronized实现同步的基础:java中每一个对象都可以作为锁。具体表现为以下3中形式

1.对于普通方法的同步,锁是当前实例对象

         sychronized修饰普通方法的时候,锁是当前实例对象,每个实例对象一把锁,同一时刻只能一个线程获得锁。不同实例对象获取的锁不一样,不需等待其他实例对象锁的释放。

        当两个并发线程(thread1thread2)访问同一个对象(test1)中的synchronized代码时在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。

         Thread1thread2是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象

2.对于静态方法的同步,锁是当前类的Class对象。

        sychronized修饰静态方法的时候,锁是当前类的Class对象,每个类对应一把此类的所有对象用的是同一把锁多线程不同实例对象同一时刻也只能是一个获得锁,其他必须等待锁的释放。

3.对于同步方法块,锁是sychronized括号里配置的对象。

         sychronizedobject{ // 代码}

         object为普通变量的时候,每个实例对象一把锁,同一时刻只能一个线程获得锁。不同实例对象获取的锁不一样,不需等待其他实例对象锁的释放。

         object为静态变量的时候,锁是当前类的Class对象,每个类对应一把锁,多线程不同实例对象同一时刻也只能是一个获得锁,其他必须等待锁的释放。

         object为类Class的时候,锁是当前类的Class对象,每个类对应一把锁,多线程不同实例对象同一时刻也只能是一个获得锁,其他必须等待锁的释放。

sychronized关键字总结:

        A. 无论synchronized关键字是修饰在方法上还是代码块,如果它作用的对象是非静态的,则它取得的锁是对象,每个实例对象一把锁;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。

        B. 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。

sychronized修饰方法的注意事项:

1、接口方法时不能使用synchronized关键字;

2、构造方法不能使用synchronized关键字,但可以使用synchronized代码块进行同步;

(解释:由于锁即对象,构造函数用于创建对象,无对象何来锁,锁的安全性也不用顾及);

3synchronized关键字无法继承;

         虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。

如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的。

类方法同步的解决方案:

1)子类方法也加上synchronized 关键字

2)子类方法中调用父类同步的方法,例如:使用 super.xxxMethod()调用父类方法

demo示例见ThreadSychronized.java

并发之原子性、可见性、有序性:

https://www.cnblogs.com/guanghe/p/9206635.html

原子性:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

         在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。原子性是拒绝多线程交叉操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。例如 a=1是原子性操作,但是a++a +=1就不是原子性操作。

可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

有序性:即程序执行的顺序按照代码的先后顺序执行

         Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

synchronized通过锁机制的实现,满足了原子性,可见性和有序性,所以synchronized能够保证线程安全

2.4 线程同步方法二:关键字volatile

线程volatile变量的过程

         1. 改变线程工作内存中volatile变量副本的值

         2. 改变后的副本的值从工作内存刷新到主内存

线程volatile变量的过程

         1. 主内存中读取volatile变量的最新值到线程的工作内存中

         2. 工作内存中读取volatile变量的副本

volatile只能保证可见性和有序性(通过加入内存屏障和禁止重排序优化),不能保证原子性操作,也就不能保证线程安全。

为何volatile不能保证原子性操作:

private volatile int i = 0;  i++;

i++操作可以被拆分为三步

      1、线程读取i

      2i进行自增计算

      3、刷新回i

        某线程使用该变量时,重新从主存内读取该变量的值,为3,然后对其进行+1操作,此时该线程i变量的副本值为4

        此时该线程的时间片时间到了,等该线程再次获得时间片的时候,主存内a的值已经是另外的值,如5,但是该线程并不知道,该线程继续完成其未完成的工作,将线程内i副本的值4写入主存。

        这时,主存i值就是4了。这样,之前修改i值为5的操作就相当于没有发生了i值出现了意料之外的结果。

总结:

1.volatile本身并不处理数据的原子性,而是强制对数据的读写及时影响到主内存。

2.volatile主要使用的场合是在多个线程中可以感知实例变量被更改了,并且可以获得最新的值使用,也就是用多线程读取共享变量时可以获得最新值使用

3.volatile最适用一个线程写,多个线程读的场合(即对于一写多读,是可以解决变量同步问题)如果有多个线程并发写操作,仍然需要使用锁或者线程安全的容器或者原子变量来代替

demo示例FlagThread.java

volatile如何实现内存可见性、有序性:

深入来说:通过加入内存屏障和禁止重排序优化来实现的。

volatile变量执行写操作时,会在写操作后加入一条store屏障指令

volatile变量执行读操作时,会在读操作前加入一条load屏障指令

2.5 线程同步方法三:使用ReentrantLock

ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法具有相同的基本行为和语义,并且扩展了其能力。

synchronized锁和ReentrantLock锁的异同:

1synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。

2synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。

3synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以响应中断

4ReentrantLock还可以实现公平锁机制。(公平锁:在锁上等待时间最长的线程将获得锁的使用权,通俗的理解就是谁排队时间最长谁先执行获取锁。)

关于Lock对象和synchronized关键字的选择:

1)最好两个都不用,使用一种java.util.concurrent包提供的机制,能够帮助用户处理所有与锁相关的代码。

2)如果synchronized关键字能满足用户的需求,就synchronized因为它能简化代码 。

3)如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁。

总而言之,ReentrantLock是一种比较“高级”的锁,比较适合“高级”地去使用

demo示例见ThreadReentrantLock.java

读写锁ReentrantReadWriteLock

         ReentrantReadWriteLockLock的另一种实现方式ReentrantLock是一个排他锁,同一时间只允许一个线程访问,而ReentrantReadWriteLock允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。(读读共享、写写互斥、读写互斥、写读互斥)

        线程进入读锁的前提条件:没有其他线程的写锁没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。

        线程进入写锁的前提条件:没有其他线程的读锁;没有其他线程的写锁。

2.6 使用线程局部变量ThreadLocal

        ThreadLocal是除了加锁这种同步方式之外的一种规避多线程访问出现线程不安全的方法,创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。

       ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

ThreadLocalsynchronized的比较:

        synchronized通过对象的锁机制保证同一时间只有一个线程访问变量,这时该变量是多个线程共享的。 synchronized用于线程间的数据共享

        ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了ThreadLocal则用于线程间的数据隔离。

         概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

ThreadLocal类提供了四个方法

        get():返回此线程局部变量的当前线程副本中的值

        set(T value):将此线程局部变量的当前线程副本中的值设置为指定值

        remove():移除此线程局部变量当前线程的值。

        initialValue():返回此线程局部变量的当前线程的“初始值”,默认返回null,供子类重写

可以get方法实现源码,当mapnull时,会调用initialValue方法赋初值。比如创建ThreadLocal变量后就立即使用 get() 方法访问变量的时候返回的就是初值。如果线程先于 get 方法调用 set(T) 方法,则不会在线程中再调用 initialValue 方法。

ThreadLocal应用场景:

        1、服务器(例如tomcat)处理请求的时候,会从线程池中取一条出来进行处理请求,如果想把每个请求的用户信息保存到一个静态变量里以便在处理请求过程中随时获取到用户信息。这时候可以建一个拦截器,请求到来时,把用户信息存到一个静态ThreadLocal变量中,那么在请求处理过程中可以随时从静态ThreadLocal变量获取用户信息。

        2Spring的事务实现也借助了ThreadLocal类。Spring会从数据库连接池中获得一个connection,然会把connection放进ThreadLocal中,也就和线程绑定了,事务需要提交或者回滚,只要从ThreadLocal中拿到connection进行操作

demo示例见ThreadLocalTest.java

2.7 线程间通信:等待/通知机制 wait/notifynotifyAll

方法wait()的作用:

         使当前执行代码的线程进行等待,该方法用来将当前线程置入“预执行队列”中,并且在wait()所在的代码行处停止执行,直到接到通知或被中断为止调用wait()之前,线程必须获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法。在执行wait()方法后,当前线程释放锁。wait()返回前,线程与其他线程竞争重新获得锁。如果调用wait()方法时没有持有适当的锁,则抛出IllegalMonitorStateException异常。

方法notify()的作用:

          要在同步方法或同步块中调用,即在调用前,线程也必须获得该对象的对象级别锁。如调用notify()时没有持有适当的锁,也会抛出IllegalMonitorStateException。该方法用来通知那些可能等待该对象的对象锁的其他线程,如果有多个线程等待,则由线程规划器随机挑选出其中一个呈wait状态的线程,对其发出通知notify,并使它等待获取该对象的对象锁。

          需要说明的是,在执行notify()方法后,当前线程不会马上释放该对象锁,呈wait状态的线程也并不能马上获取该对象锁,到等到执行notify()方法的线程将程序执行完,也就是退出synchronized代码块后,当前线程才会释放锁,而呈wait状态所在的线程才可以获取该对象锁。

         当第一个获得了该对象锁的wait线程运行完毕以后,它会释放掉该对象锁,此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,还会继续阻塞在wait状态,直到这个对象发出一个notifynotifyAll

总结:

         wait()方法可以使调用该方法的线程释放共享资源的锁,然后从运行状态退出,进入等待队列,直到被再次唤醒。

  notify()方法可以随机唤醒等待队列中等待同一共享资源的“一个”线程,并使该线程退出等待队列,进入可运行状态,也就是notify()方法仅随机通知“一个”线程。

  notifyAll()方法可以使所有正在等待队列中等待统一共享资源的“全部”线程从等待状态退出,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能是随机执行,因为这要取决于JVM虚拟机的实现

        带一个参数的wait(long)方法的功能是等待某一时间内是否有线程对锁进行唤醒,如果超过这个时间则自动唤醒

        wait()方法可以被interrupt 打断并抛出InterruptedException。遇到异常导致呈wait状态的线程终止,锁也会被释放。

demoTestWaitNotify.java

2.7 线程间通信:join方法

1、在很多情况下,主线程创建并启动子线程,如果子线程中要进行大量的耗时计算,主线程往往将早于子线程结束之前结束。这时,如果主线程想等待子线程执行完成之后再结束,比如子线程处理一个数据,主线程要取得这个数据中的值,就要用到 join() 方法了。

2join() 的作用是等待线程销毁,而使当前线程进行无限期的阻塞,等待 join() 的线程销毁后再继续执行当前线程的代码join方法可以使得线程之间的并行执行变为串行执行。在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行。

3、同样的,join() 方法可以被 interrupt() 方法打断并抛出 InterruptedException 异常(是调用thread.join()的线程被中断才会进入异常,比如a线程调用b.join(),线程a在等待线程b结束,此时调用a. interrupt(); 线程a中就会抛出异常,不再等待了,直接进入异常处理代码)

4join() 方法是 wait/notify 范式的简洁应用。当子线程调用 join() 时,主线程执行 wait() 方法进入等待,释放锁。子线程得到锁,执行结束后调用 notifyAll() 方法,释放锁。主线程接着执行。

5join方法中如果传入参数,则表示这样的意思:如果A线程中调用B线程的join(10),则表示A线程会等待B线程执行10毫秒,10毫秒过后,AB线程并行执行。需要注意的是,jdk规定,join(0)的意思不是A线程等待B线程0毫秒,而是A线程等待B线程无限时间,直到B线程执行完毕,即join(0)等价于join()(其实join()中调用的是join(0))

demoTestJoin.java

2.8 死锁以及死锁的排查

1、什么是死锁?

        死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待进程(线程)称为死锁进程(线程)

2、死锁的根本原因:

1)是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖的闭环

2)默认的锁申请操作是阻塞的。

3、如何避免死锁?

最好的是从源头控制问题,而不是后期遇到问题再去填坑。

1)避免在对象的同步方法中调用其它对象的同步方法,那么就可以避免死锁产生的可能性

两个锁的申请就没有发生交叉,避免了死锁的可能性,这是理想情况,因为锁没有发生交叉。

2)按顺序加锁:必须存在锁交叉的时候,在一个类中有多个方法需要同时获得两个对象上的锁,那么这些方法就必须以相同的顺序获得锁

3)获取锁时限:每个获取锁的时候加上时限,如果超时就放弃获取锁之类的。

4)在使用阻塞等待获取锁的方式中,必须在try代码块之外,并且在加锁方法与try代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally中无法解锁。

阿里开发规约,里面有对避免死锁的说明,具体如下

强制对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会 造成死锁。 说明:线程一需要对表 ABC 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 ABC,否则可能出现死锁。

4、死锁的排查(jstackjcmd

jdk带的工具包中提供的命令

1)查看当前机器上所有的 jvm 进程信息

jcmd   导出堆、查看Java进程、导出线程信息、执行GC、还可以进行采样分析(jmc 工具的飞行记录器)。

jcmd -l

jps   列出系统中所有的 Java 应用程序

2)使用jstack 或者 jcmd:打印指定Java进程的线程堆栈跟踪信息

      使用jstack -l pid或者jcmd pid Thread.print可以查看当前应用的进程信息,如果有死锁也会分析出来。

Linux系统中的定位:

        通过top命令定位到cpu占用率较高的线程之后,继续使用jstack pid命令查看当前java进程的堆栈状态:

1通过top命令查看各个进程的cpu使用情况,默认按cpu使用率排序

2、占用了较多的cpu资源Java进程的pid

3、通过top -Hp pid可以查看该进程下各个线程的cpu使用情况。

4、查看占了较多的cpu资源线程的pid利用jstack命令继续查看该线程当前的堆栈状态

死锁示例SynchronizedDeadLock.java

concurrent发包

3.1 java.util.concurrent并发包介绍

concurrent 并发包里有很多内容,可查看下面链接了解详细内容。

https://blog.csdn.net/lfj19941225/article/details/88549965

这里主要介绍以下几个常用类:

atomic  原子操作类

BlockingQueue  阻塞队列

ConcurrentMap  并发 Map

locks  LockReentrantLockReentrantReadWriteLock

Executor框架  ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务。

3.2 原子操作类

1. 原子操作类介绍

          在并发编程中很容易出现并发安全的问题,有一个很简单的例子就是多线程更新变量i=1,比如多个线程执行i++操作,就有可能获取不到正确的值,而这个问题,最常用的方法是通过Synchronized进行控制来达到线程安全的目的。但是由于synchronized是采用的悲观锁策略并不是特别高效的一种解决方案

         实际上J.U.C下的atomic包提供了一系列的操作简单,性能高效,并能保证线程安全的类去更新基本类型变量,数组元素,引用类型以及更新对象中的字段类型。atomic包下的这些类都是采用的是乐观锁策略去原子更新数据,在java中则是使用CAS操作具体实现。

         悲观锁:顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。

        乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁适用于多读的应用类型,这样可以提高吞吐量。

2. CAS操作

https://blog.csdn.net/qq_32998153/article/details/79529704

          使用锁时,线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。而CAS操作是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。那么,如果出现冲突了怎么办?无锁操作是使用CAS(compare and swap)又叫做比较并替换鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。

          CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:V 内存地址存放的实际值;O 预期的值(旧值);N 更新的新值。当VO相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。反之,VO不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试(重新获取主内存变量最新值,重新执行对变量的操作,重新尝试更新)。

CAS的缺点:

1CPU开销过大:

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

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

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

3ABA问题:

       这CAS机制最大的问题所在。

3. 原子类的使用

atomic包提供原子更新基本类型的工具类,主要有这些:

1AtomicBoolean:以原子更新的方式更新boolean

2. AtomicInteger:以原子更新的方式更新Integer;

3. AtomicLong:以原子更新的方式更新Long

几个类的用法基本一致,这里以AtomicInteger为例总结常用的方法:

1. addAndGet(int delta) :以原子方式将输入的数值与实例中原本的值相加,并返回最后的结果;

2. incrementAndGet() :以原子的方式将实例中的原值进行加1操作,并返回最终相加后的结果;

3. getAndSet(int newValue):将实例中的值更新为新值,并返回旧值;

4. getAndIncrement():以原子的方式将实例中的原值加1,返回的是自增前的旧值;

使用示例AtomicIntegerTest.java

3.3 ConcurrentHashMap

         ConcurrentHashMapJDK1.5开始随java.util.concurrent包一起引入JDK中,主要为了解决HashMap线程不安全和Hashtable效率不高的问题HashMap在多线程编程中是线程不安全的,而Hashtable由于使用了synchronized修饰方法而导致执行效率不高synchronized关键字加锁是对整个对象进行加锁,也就是说在进行put等修改Hash表的操作时,锁住了整个Hash,从而使得其表现的效率低下;

        ConcurrentHashMap 能够提供比 HashTable 更好的并发性能。在你从中读取对象的时候 ConcurrentHashMap 并不会把整个 Map 锁住。此外,在你向其中写入对象的时候,ConcurrentHashMap 也不会锁住整个 Map

        JDK1.7之前的ConcurrentHashMap使用分段锁机制实现即将整个Hash表划分为多个分段。使用ReetrantLock对每个分段进行加锁),JDK1.8则使用数组+链表+红黑树数据结构和CAS原子操作实现ConcurrentHashMap

使用示例:ConcurrentHashMapTest.java

Java 8系列之重新认识HashMap

https://cloud.tencent.com/developer/article/1343130

3.4 BlockingQueue 阻塞队列

         阻塞队列,顾名思义,首先它是一个队列,通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出;当队列满时,插入阻塞;当队列为空时,删除(取出)阻塞。多线程环境中,通过队列可以很容易实现数据共享,比如经典的“生产者”和“消费者”模型中,

         一个线程将会持续生产新对象并将其插入到队列之中,直到队列达到它所能容纳的临界点。也就是说,它是有限的。如果该阻塞队列到达了其临界点,负责生产的线程将会在往里边插入新对象时发生阻塞。它会一直处于阻塞之中,直到负责消费的线程从队列中拿走一个对象

         负责消费的线程将会一直从该阻塞队列中拿出对象。如果消费线程尝试去从一个空的队列中提取对象的话,这个消费线程将会处于阻塞之中,直到一个生产线程把一个对象丢进队列

         在concurrent包发布以前,在多线程环境下,我们都必须去自己控制这些细节,尤其还要兼顾效率和线程安全。这也是我们在多线程环境下,为什么需要BlockingQueue的原因。作为BlockingQueue的使用者,我们再也不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了。

BlockingQueue 是个接口,常用的实现有:

ArrayBlockingQueue:基于数组实现的阻塞队列,有界。
LinkedBlockingQueue基于链表实现的阻塞队列,可以当做无界队列,也可以当做有界队列来使用

       如果没有指定其容量大小,会默认一个类似无限大小的容量(Integer.MAX_VALUEint型的 0x7fffffff
PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列

       默认情况下元素采用自然顺序升序排序,也可以通过构造函数来指定比较器Comparator来对元素进行排序。
DelayQueue:延迟阻塞队列、底层是基于优先级队列PriorityQueue来实现的、无界。

      只有在延迟期满时才能从中提取元素应用场景:店铺取票后五分钟内没有出票,即收回所取的票。
SynchronousQueue:同步队列。没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列。

      直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。每个插入操作必须等待另一个操作执行相应的删除操作线程,反之亦然。同步队列没有任何内部容量,甚至没有一个容量。

常用的队列主要有以下两种:

先进先出(FIFO):先插入的队列的元素也最先出队列,类似于排队的功能。从某种程度上来说这种队列也体现了一种公平性。

后进先出(LIFO):后插入队列的元素最先出队列,这种队列优先处理最近发生的事件。

DelayQueue是一个无界阻塞队列,用于放置实现了Delayed接口的对象,只有在延迟期满时才能从中提取元素。该队列时有序的,即队列的头部是延迟期满后保存时间最长的Delayed 元素。注意:不能将null元素放置到这种队列中。

PriorityBlockingQueue是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序升序排序,当然我们也可以通过构造函数来指定Comparator来对元素进行排序。需要注意的是PriorityBlockingQueue不能保证同优先级元素的顺序。

BlockingQueue的方法

BlockingQueue 具有 4 组不同的方法用于插入、移除以及对队列中的元素进行检查。如果请求的操作不能得到立即执行的话,每个方法的表现也不同。这些方法如下:

组不同的行为方式解释:

抛异常:如果尝试的操作无法立即执行,抛一个异常。

特定值:如果尝试的操作无法立即执行,返回一个特定的值(true / false)

阻塞:如果尝试的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。

超时:如果尝试的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功(true / false)

无法向一个 BlockingQueue 中插入 null。如果你试图插入 nullBlockingQueue 将会抛出一个 NullPointerException

add: 内部实际上获取的offer方法,当Queue已经满了时,抛出一个异常。不会阻塞。

offer:Queue已经满了时,返回false。不会阻塞。

put:Queue已经满了时,会进入等待,只要不被中断,就会插入数据到队列中。会阻塞,可以响应中断

取出方法中 removeadd相互对应。也就是说,调用remove方法时,假如对列为空,则抛出一场polloffer相互对应。takeput相互对应

element() peek()都是用来返回队列的头元素,不删除

在队列元素为空的情况下,element() 方法会抛出NoSuchElementException异常,peek() 方法只会返回 null

使用示例见BlockingQueueTest.java

ArrayBlockingQueue  个由数组结构组成的有界阻塞队列:

          基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,其内部没有实现读写分离(都是共用同一个锁对象)长度是需要定义的,按照先进先出(FIFO)的原则对元素进行排序,是有界队列(bounded)。

LinkedBlockingQueue  一个由链表结构组成的有界阻塞队列:

          是一个用链表实现的有界阻塞队列。此队列的默认和最大长度为Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序

          需要注意的是:如果没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUEint型的0x7fffffff),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。

          LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。但在线程数量很大时其性能的可预见性低于ArrayBlockingQueue.

二者不同点:

1、锁机制不同

  LinkedBlockingQueue中的锁是分离的,生产者的锁PutLock,消费者的锁takeLock

  而ArrayBlockingQueue生产者和消费者使用的是同一把锁

 Doug Lea之所以没这样去做,也许是因为ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。

2、底层实现机制也不同

  LinkedBlockingQueue内部维护的是一个链表结构。在生产和消费的时候,需要创建Node对象进行插入或移除,大批量数据的系统中,其对于GC的压力会比较大。

  而ArrayBlockingQueue内部维护了一个数组。在生产和消费的时候,是直接将枚举对象插入或移除的,不会产生或销毁任何额外的对象实例

3、构造时候的区别

  LinkedBlockingQueue有默认的容量大小为:Integer.MAX_VALUE,当然也可以传入指定的容量大小。当添加速度大于移除速度时,大小为默认的MAX_VALUE情况下,可能会造成内存溢出等问题。

         ArrayBlockingQueue在初始化的时候,必须传入一个容量大小的值。

个人感觉大多数场景适合使用LinkedBlockingQueue。优势:读写分离;需要的时候才会创建一个Node节点。

3.5 Executor框架

         在Java中使用线程来异步执行任务。Java线程的创建与销毁需要一定的开销,如果我们为每一个任务创建一个新线程来执行,这些线程的创建与销毁将消耗大量的计算资源

        单个的线程既是工作单元也是执行机制,JDK1.5开始,为了把工作单元与执行机制分离开,Executor框架诞生了,他是一个用于统一创建与运行的接口。Executor框架实现的就是线程池的功能线程池就是线程的集合,线程池集中管理线程,以实现线程的重用,降低资源消耗,提高响应速度等。       

         在Executor框架中工作单元与执行机制分离开来。RunnableCallable是工作单元(也就是俗称的任务),而执行机制由Executor来提供。这样一来Executor可以理解为基于生产者消费者模式的,提交任务的操作相当于生成者,执行任务的线程相当于消费者。

Executor框架包括三大部分

1任务。也就是工作单元,包括被执行任务需要实现的接口:Runnable接口或者Callable接口;

2任务的执行。也就是把任务分派给多个线程的执行机制,包括Executor接口及继承自Executor接口的ExecutorService接口。

3异步计算的结果。包括Future接口及实现了Future接口的FutureTask类。

Executor框架的成员及其关系可以右边关系图表示

使用步骤示例:

1)创建Runnable并重写run()方法或者Callable对象并重写call()方法

2)创建Executor接口的实现类ThreadPoolExecutor类或者ScheduledThreadPoolExecutor类的对象,然后调用其execute()方法或者submit()方法把工作任务添加到线程中,如果有返回值则返回Future对象。其中Callable对象有返回值,因此使用submit()方法;

        Runnable可以使用execute()方法,此外还可以使用submit()方法。只要使用callableRunnable task)或者callable(Runnable task,  Object result)方法把Runnable对象包装起来就可以,使用callableRunnable task)方法返回的null,使用callable(Runnable task,  Object result)方法返回result

        ThreadPoolExecutor tpe = new ThreadPoolExecutor(5, 10, 100, MILLISECONDS, new ArrayBlockingQueue<Runnable>(5));

        Future<String> future = tpe.submit(new callableTest());

3)调用Future对象的get()方法后的返回值,或者调用Future对象的cancel()方法取消当前线程的执行。最后关闭线程

try {

System.out.println(future.get());

} catch (Exception e) {

e.printStackTrace();

} finally {

tpe.shutdown();

}

demo示例见CallableDemo.java

Executor框架内容

1Executor接口和ExecutorService接口

         Executor:一个接口,其定义了一个接收Runnable对象的方法executor,其方法签名为executor(Runnable command),该方法接收一个Runable实例,它用来执行一个任务,任务即为一个实现了Runnable接口的类。

         ExecutorService:是一个比Executor使用更广泛的子类接口,其提供了生命周期管理的方法,返回 Future 对象,以及可跟踪一个或多个异步任务执行状况返回Future的方法;可以调用ExecutorServiceshutdown()方法来平滑地关闭 ExecutorService,调用该方法后,将导致ExecutorService停止接受任何新的任务且等待已经提交的任务执行完成(已经提交的任务会分两类:一类是已经在执行的,另一类是还没有开始执行的),当所有已经提交的任务执行完毕后将会关闭ExecutorService。因此我们一般用该接口来实现和管理多线程。

        通过 ExecutorService.submit() 方法返回的 Future 对象,可以调用isDone()方法查询Future是否已经完成。当任务完成时,它具有一个结果,你可以调用get()方法来获取该结果。你也可以不用isDone()进行检查就直接调用get()获取结果,在这种情况下,get()将阻塞,直至结果准备就绪,还可以取消任务的执行。Future 提供了 cancel() 方法用来试图取消任务的执行。

简单工厂模式以及其他设计模式

https://www.jianshu.com/p/e55fbddc071c

2Executors类:主要用于提供线程池相关的操作

Executors类,提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。

newSingleThreadExecutor()创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

newFixedThreadPool(int Threads) 创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

newCachedThreadPool()创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

newScheduledThreadPool (int corePoolSize) :创建一个支持定时及周期性的任务执行的线程池。

虽然Executors利用工厂模式向我们提供了4种线程池实现方式,但是并不推荐使用。

1newFixedThreadPool newSingleThreadExecutor

传入的最后一个参数阻塞队列 ”workQueue,默认的长度是INTEGER.MAX_VALUE,而它们允许的最大线程数量又是有限的,所以当请求线程的任务过多线程不够用时,它们会在队列中等待,又因为队列的长度特别长,所以可能会堆积大量的请求,从而导致内存不足(Out Of Memory)

2newCachedThreadPool newScheduledThreadPool

它们的阻塞队列长度有限,但是传入的第二个参数maximumPoolSize Integer.MAX_VALUE,这就意味着当请求线程的任务过多线程不够而且队列也满了的时候,线程池就会创建新的线程,因为它允许的最大线程数量是相当大的,所以可能会创建大量线程,导致OOM

里的 Java开发手册,上面有线程池的一个建议

强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

3ExecutorExecutorServiceExecutors

这三者均是 Executor 框架中的一部分总结一下这三者间的区别:

1Executor ExecutorService 这两个接口主要的区别是:ExecutorService 接口继承了 Executor 接口, Executor 的子接口。

2Executor ExecutorService 第二个区别是:Executor 接口定义了 execute()方法用来接收一个Runnable接口的对象,而 ExecutorService 接口中的 submit()方法可以接受RunnableCallable接口的对象。

3Executor ExecutorService 接口第三个区别是 Executor 中的 execute() 方法不返回任何结果,ExecutorService 中的 submit()方法可以通过一个 Future 对象返回运算结果。

4Executor ExecutorService 接口第四个区别是除了允许客户端提交一个任务ExecutorService 还提供用来控制线程池的方法。比如:调用 shutDown() 方法终止线程池。

5Executors 类提供工厂方法用来创建不同类型的线程池

4、使用Executor直接new Thread()的优点:

 Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处。

    第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

    第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

    第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源, 还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

线程-- ThreadPoolExecutor

         线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。

        ThreadPoolExecutorExecutor接口的一个重要的实现类,是线程池的具体实现,用来执行被提交的任务。

ThreadPoolExecutor定义了很多构造函数,以其中一个为例:

public ThreadPoolExecutor(int corePoolSize,

      int maximumPoolSize,

      long keepAliveTime,

      TimeUnit unit,

      BlockingQueue<Runnable> workQueue,

      ThreadFactory threadFactory,

      RejectedExecutionHandler handler)

1corePoolSize:必需参数,线程池中所保存的核心线程数,包括空闲线程。在创建了线程池之后,默认情况下线程池中没有任何线程提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池核心线程数时就不再创建。

2maximumPoolSize:必需参数,线程池中允许的最大线程数,这个参数表示了线程池中最多能创建的线程数量,当任务数量比corePoolSize大时,任务添加到workQueue,当workQueue满了,将继续创建线程以处理任务,maximumPoolSize表示的就是wordQueue满了,线程池中最多可以创建的线程数量

3keepAliveTime必需参数,线程池中的空闲线程所能持续的最长时间。当线程池中的线程数大于corePoolSize时,keepAliveTime为多余的空闲线程等待新任务的最长时间,超过这个时间后多余的线程将被终止

4unit:必需参数, keepAliveTime持续时间单位。天、小时、分钟、秒、毫秒、微秒、纳秒。

5workQueue:必需参数,任务执行前保存等待执行的任务的队列

6threadFactory必需参数不设置此参数会采用内置默认参数defaultThreadFactory()方法创建属于同一个ThreadGroup对象的基本线程对象)。也可以自己定义线程工厂,重新实现ThreadFactorynewThread Runnable)方法,设置属性.

7handler必需参数不设置此参数会采用内置默认参数,设置饱和策略,当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。

RejectedExecutionHandler 的实现类在ThreadPoolExecutor中有四个静态内部类,这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。共有4策略:包括AbortPolicy(丢弃任务并抛出RejectedExecutionException异常)、 DiscardPolicy(丢弃任务,但是不抛出异常) DiscardOldestPolicy(丢弃执行队列中最老的任务,尝试为当前提交的任务腾出位置) CallerRunsPolicy调用者所在线程来运行任务)。可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。

当提交一个任务给线程池之后,线程池的处理流程

1、如果线程池中的线程数量少于corePoolSize,即使线程池中有空闲线程,也会创建一个新的线程来执行新添加的任务;

2、如果线程池中的线程数量大于等于corePoolSize(核心线程池里的线程都在执行任务),但缓冲队列workQueue未满,则将新添加的任务放到workQueue中,按照FIFO的原则依次等待执行(线程池中有线程空闲出来后依次将缓冲队列中的任务交付给空闲的线程执行);

3、如果线程池中的线程数量大于等于corePoolSize,且缓冲队列workQueue已满,但线程池中的线程数量小于maximumPoolSize,则会创建新的线程来处理被添加的任务;

4、如果线程池中的线程数量等于了maximumPoolSize,有4种处理方式。

总结起来,也即是说

         有新的任务要处理时,先看线程池中的线程数量是否大于corePoolSize,再看缓冲队列workQueue是否满,最后看线程池中的线程数量是否大于maximumPoolSize

         另外,当线程池中的线程数量大于corePoolSize时,如果里面有线程的空闲时间超过了keepAliveTime,就将其移除线程池,这样,可以动态地调整线程池中线程的数量。

线程-- ThreadPoolExecutor

这些参数中,比较容易引起问题的有corePoolSize, maximumPoolSize, workQueue以及handler

1corePoolSizemaximumPoolSize设置不当会影响效率,甚至耗尽线程;

2workQueue设置不当容易导致OutOfMemory(内存不足);

3handler设置不当会导致提交任务时抛出异常。

workQueue如果使用无界队列会带来如下影响:

1、当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中的线程数不会超过corePoolSize

2、由于1,使用无界队列时maximumPoolSize将是一个无效参数。

3、由于12,使用无界队列时keepAliveTime将是一个无效参数。

线程池使用注意点:

        避免使用无界队列,不要使用Executors.newXXXThreadPool()快捷方法创建线程池,因为这种方式会使用无界的任务队列,为避免OOM,我们应该使用ThreadPoolExecutor的构造方法手动指定队列的最大长度。

合理配置线程池:

CPU密集:

        CPU密集的意思是该任务需要大量的运算(如对视频进行高清解码等,全靠CPU的运算能力),而没有阻塞,CPU一直全速运行。CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程,该任务都不可能得到加速,因为CPU总的运算能力就那些。

IO密集:

         IO密集型,即该任务需要大量的IO,即大量的阻塞(CPU等待硬盘、内存、网络等读写操作)。在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即时在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。

要想合理的配置线程池的大小,首先得分析任务的特性,可以从以下几个角度分析

1.  任务的性质:CPU密集型任务、IO密集型任务、混合型任务

2.  任务的优先级:高、中、低

3.  任务的执行时间:长、中、短

4.  任务的依赖性:是否依赖其他系统资源,如数据库连接等。

性质不同的任务可以交给不同规模的线程池执行。

对于不同性质的任务来说,CPU密集型任务应配置尽可能小的线程,如配置CPU个数+1的线程IO密集型任务应配置尽可能多的线程,因为IO操作不占用CPU,不要让CPU闲下来,应加大线程数量,如配置两倍CPU个数+1对于混合型的任务,如果可以拆分,拆分成IO密集型和CPU密集型分别处理,前提是两者运行的时间是差不多的,如果处理时间相差很大,则没必要拆分了。

若任务对其他系统资源有依赖,如某个任务依赖数据库的连接返回的结果,这时候等待的时间越长,则CPU空闲的时间越长,那么线程数量应设置得越大,才能更好的利用CPU

可以总结为:

线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。

CPU密集型时,任务可以少配置线程数,大概和机器CPU数相当,这样可以使得每个线程都在执行任务

IO密集型时,大部分线程都阻塞,故需要多配置线程数,2*CPU核数。

获取CPU的核数

private static final int corePoolSize = Runtime.getRuntime().availableProcessors();

监控线程池运行状态  

使用线程池,则有必要对线程池进行监控,方便在出现问题时,如线程池阻塞,无法提交新任务等。就可以根据线程池的使用状况快速定位问题

可以通过线程池执行类ThreadPoolExecutor提供的方法获取到相关的属性去监控线程池的当前状态

getTaskCount()线程池需要执行的任务数量(排队任务数+活动线程数+已执行完成的任务数)。

getQueue().size():当前排队的任务数。

getCompletedTaskCount():线程池在运行过程中已完成的任务数量,小于或等于taskCount

getActiveCount():线程池中活动的线程数(正在执行任务)。

getLargestPoolSize()线程池里曾经创建过的最大线程数量,通过可以判断线程池是否曾经满过。如该数值等于线程池的最大大小,则表示线程池曾经满过;

getPoolSize():线程池当前的线程数量;

         还可以通过扩展线程池进行监控。可以通过继承线程池来自定义线程池,重写线程池的 beforeExecuteafterExecuteterminated方法,也可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控。例如监控任务的平均执行时间、最大执行时间和最小执行时间等。 这几个方法在线程池里是空方法。

demo示例见ThreadPoolTest.java

ScheduledThreadPoolExecutor

         ScheduledThreadPoolExecutor类继承了ThreadPoolExecutor并实现了ScheduledExecutorService接口。主要用于在给定的延迟后执行任务或者定期执行任务。作用类似于java.util包下的Timer定时器类,但是比Timer功能更强大、更灵活,因为Timer只能控制单个线程延迟或定期执行,而ScheduledThreadPoolExecutor对应的是多个线程的后台线程。

        调度线程池主要用于定时器或者延迟一定时间在执行任务时候使用。内部使用优化的DelayQueueDelayedWorkQueue)来实现,由于使用队列来实现定时器,有出入队调整堆等操作,所以定时并不是非常非常精确。

demo示例见ScheduledThreadPoolTest.java

Future接口

       JDK5新增了Future接口,用于描述一个异步计算的结果。Future是一个接口,他提供给了我们方法来检测当前的任务是否已经结束,还可以等待任务结束并且拿到一个结果,通过调用Futureget()方法可以当任务结束后返回一个结果值,如果工作没有结束,则会阻塞当前线程,直到任务执行完毕。

       可以通过调用cancel()方法来试图停止一个任务,如果停止成功,则cancel ()方法会返回true;如果任务已经完成或者已经停止了或者这个任务无法停止,则cancel()返回一个falseisDone()isCancelled()方法可以判断当前工作是否完成和是否取消。

       Future通常和线程池搭配使用,用来获取线程池返回执行后的返回值。我们假设通过Executors工厂方法构建一个线程池es es要执行某个任务有两种方式,一种是执行 es.execute(runnable) 这种情况是没有返回值的; 另外一种情况是执行 es.submit(runnale)或者 es.submit(callable) ,这种情况会返回一个Future的对象,然后调用Futureget()来获取返回值。

也就是说Future提供了三种功能

1)判断任务是否完成;

2)能够中断任务;

3)能够获取任务执行结果。

FutureTask

        因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了FutureTask类。FutureTask类实现了RunnableFuture接口。而RunnableFuture继承了Runnable接口和Future接口。

        FutureTask实现了Runnable,因此它既可以传递给Thread对象执行,也可以提交给ExecuteService来执行。

        FutureTask实现了Futrue可以直接通过get()函数获取执行结果,该函数会阻塞,直到结果返回。

        FutureTask是为了弥补Thread的不足而设计的,它可以让程序员准确地知道线程什么时候执行完成并获得到线程执行完成后返回的结果(如果有需要)。

FutureTask是一种可以取消的异步的计算任务。它的计算是通过Callable实现的,它等价于可以携带结果的Runnable,并且有三个状态:等待、运行和完成。完成包括所有计算以任意的方式结束,包括正常结束、取消和异常

demo示例见CallableDemo.java

方法解析:

V get() :获取异步执行的结果,如果没有结果可用,此方法会阻塞直到异步计算完成。

V get(Long timeout , TimeUnit unit) :获取异步执行结果,如果没有结果可用,此方法会阻塞,但是会有时间限制,如果阻塞时间超过设定的timeout时间,该方法将抛出异常。

boolean isDone() :如果任务执行结束,无论是正常结束或是中途取消还是发生异常,都返回true

boolean isCancelled() :任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true

boolean cancel(boolean mayInterruptRunning) :如果任务还没开始,执行cancel(...)方法将返回false;如果任务已经启动,执行cancel(true)方法将以中断执行此任务线程的方式来试图停止任务,如果停止成功,返回true;当任务已经启动,执行cancel(false)方法将不会对正在执行的任务线程产生影响(让线程正常执行到完成),此时返回false;当任务已经完成,执行cancel(...)方法将返回falsemayInterruptRunning参数表示是否中断执行中的线程。

Future模式之CompletableFuture – JDK8

Future 接口的局限性

        虽然 Future 以及相关使用方法提供了异步执行任务的能力,但是对于结果的获取却是很不方便,只能通过阻塞get()或者轮询isDone()方式得到任务的结果。阻塞的方式显然和我们的异步编程的初衷相违背,轮询的方式又会耗费无谓的 CPU 资源,而且也不能及时地得到计算结果而且很难直接表述多个Future 结果之间的依赖性。

实际开发中,经常需要达成以下目的:

1、将两个异步计算合并为一个——这两个异步计算之间相互独立,同时第二个又依赖于第一个的结果

2、等待 Future 集合中的所有任务都完成

3、仅等待 Future集合中最快结束的任务完成(有可能因为它们试图通过不同的方式计算同一个值),并返回它的结果

4、通过编程方式完成一个Future任务的执行(即以手工设定异步操作结果的方式)

5、应对 Future 的完成事件(即当 Future 的完成事件发生时会收到通知,并能使用 Future 计算的结果进行下一步的操作,不只是简单地阻塞等待操作的结果

CompletableFuture

         CompletableFuture Java 8 新增加的API,该类实现了FutureCompletionStage两个接口,提供了非常强大的Future的扩展功能(包含约50个方法),Java拥有了完整的非阻塞编程模型。

        阻塞指线程处理异步任务时,当异步任务获取到数据时使用回调函数处理数据,而不是CPU空闲等待数据返回后再处理。

         CompletableFuture函数风格的异步和事件驱动编程模型,它不会造成堵塞。CompletableFuture背后依靠的是fork/join框架来启动新的线程实现异步与并发。当然,我们也能通过指定线程池来做这些事情。

什么是函数式编程:

      函数式编程中的函数指的并不是编程语言中的函数(或方法),它指的是数学意义上的函数,即映射关系(如:y = f(x)),就是 y x 的对应关系。

      数学上对于函数的定义是这样的:“给定一个数集 A,假设其中的元素为 x。现对 A 中的元素 x 施加对应法则 f,记作 f(x),得到另一数集 B。假设 B 中的元素为 y。”

      所以当我们在讨论“函数式”时,我们其实是在说“像数学函数那样,接收一个或多个输入,生成一个或多个结果,并且没有副作用”

回调函数:

       回调函数比较通用的解释是,它是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外一方调用的,用于对该事件或条件进行响应。

回调函数的机制:

1定义一个回调函数;

2提供函数实现的一方在初始化时候,将回调函数的函数指针注册给调用者;

3当特定的事件或条件发生的时候,调用者使用函数指针调用回调函数对事件进行处理。

什么是Fork/Join框架

https://www.jianshu.com/p/42e9cd16f705

       Fork/Join框架是一组允许程序员利用多核处理器支持的并行执行的API。它使用了“分而治之”策略:把非常大的问题分成更小的部分,反过来,小部分又可以进一步分成更小的部分,递归地直到一个部分可以直接解决。这被叫做“fork”

       然后所有部件在多个处理核心上并行执行。每个部分的结果被“join”在一起以产生最终结果。因此,框架的名称是“Fork/Join”

       Fork/Join框架在JDk7中被加入,并在JDK8中进行了改进。它用了Java语言中的几个新特性,包括并行的Stream API和排序。

Fork/Join框架简化了并行程序的原因有:

      它简化了线程的创建,在框架中线程是自动被创建和管理。

      它自动使用多个处理器,因此程序可以扩展到使用可用处理器。

      由于支持真正的并行执行,Fork/Join框架可以显著减少计算时间,并提高解决图像处理、视频处理、大数据处理等非常大问题的性能。

关于Fork/Join框架的一个有趣的地方是:它使用工作窃取算法来平衡线程之间的负载:如果一个工作线程没有事情要做,它可以从其他仍然忙碌的线程窃取任务。

CompletableFuture的使用

1、主动完成计算

CompletableFuture类实现了CompletionStageFuture接口,所以你还是可以像以前一样通过阻塞或者轮询的方式获得结果,尽管这种方式不推荐使用。

public T   get()  //该方法为阻塞方法,会等待计算结果完成

public T   get(long timeout, TimeUnit unit)  //有时间限制的阻塞方法

public T   getNow(T valueIfAbsent)  //立即获取方法结果,如果没有计算结束则返回传的值

public T   join()  //get() 方法类似也是主动阻塞线程,等待计算结果。和get() 方法对抛出的异常的处理有细微的差别

 2、执行异步任务

创建一个异步任务

•public static <U> CompletableFuture<U> completedFuture(U value)

创建一个有初始值的CompletableFuture

•public static CompletableFuture<Void> runAsync(Runnable runnable)

•public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)

•public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)

•public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

1)以上四个方法中,以 Async 结尾并且没有 Executor 参数的,会默认使用 ForkJoinPool.commonPool() 作为它的线程池执行异步代码。

2)以run开头的,因为以 Runable 类型为参数所以没有返回值,CompletableFuture的计算结果为空。

3supplyAsync方法以Supplier<U>函数式接口类型为参数,CompletableFuture的计算结果类型为U。因为方法的参数类型都是函数式接口,所以可以使用lambda表达式实现异步任务。

demo示例见CompletableFutureDemo.java; Disposition040107.java

3、当计算结果完成时对结果进行处理

CompletableFuture的计算结果完成,或者抛出异常的时候,我们可以执行特定的Action

public CompletableFuture<T>   whenComplete(BiConsumer<? super T,? super Throwable> action)

public CompletableFuture<T>   whenCompleteAsync(BiConsumer<? super T,? super Throwable> action)

public CompletableFuture<T>   whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor)

public CompletableFuture<T>       exceptionally(Function<Throwable,? extends T> fn)

1)参数类型为 BiConsumer<? super T, ? super Throwable> 会获取上一步计算的计算结果和异常信息。

2)不Async结尾的方法由原来的线程计算,以Async结尾的方法由默认的线程池ForkJoinPool.commonPool()或者指定的线程池executor运行 whenComplete这几个方法都会返回CompletableFuture,当Action执行完毕后它的结果返回原始的CompletableFuture的计算结果或者返回异常。

3exceptionally方法用来处理异常的情况原始的CompletableFuture抛出异常的时候,就会触发这个CompletableFuture计算。

4、对计算结果进行转换

public <U> CompletableFuture<U>   thenApply(Function<? super T,? extends U> fn)

public <U> CompletableFuture<U>   thenApplyAsync(Function<? super T,? extends U> fn)

public <U> CompletableFuture<U>   thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)

1)这一组函数的功能是当原来的CompletableFuture计算完后,将结果传递给函数fn,将fn的结果作为新的CompletableFuture计算结果。因此它的功能相当于将CompletableFuture<T>转换成CompletableFuture<U>

2)需要注意的是,这些转换并不是马上执行的,也不会阻塞,而是在前一个stage完成后继续执行。

5、合并多个任务的结果 allOf anyOf

public static CompletableFuture<Void>   allOf(CompletableFuture<?>... cfs)

把所有方法都执行完才往下执行,无CompletableFuture的计算结果。

public static CompletableFuture<Object>   anyOf(CompletableFuture<?>... cfs)

任何一个方法执行完都往下执行,返回的是其中任意一CompletableFuture,所以这里没有明确的返回类型,统一使用Object接受。

Lambda 表达式

        Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。使用 Lambda 表达式可以使代码变的更加简洁紧凑

语法

lambda 表达式的语法格式如下:

(parameters) -> expression  

(parameters) ->{ statements; }

以下lambda表达式的重要特征:

可选类型声明: 不需要声明参数类型,编译器可以统一识别参数值。
可选的参数圆括号: 一个参数无需定义圆括号,但多个参数需要定义圆括号。
可选的大括号: 如果主体包含了一个语句,就不需要使用大括号。

可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。

Lambda 表达式简单实例:

1. 不需要参数,返回值为

() -> 5 

2. 接收一个参数(数字类型),返回2倍的值 

x -> 2 * x 

3. 接受2个参数(数字),并返回他们的差值 

(x, y) -> x – y 

4. 接收2int整数,返回他们的和 

(int x, int y) -> x + y 

5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void) 

(String s) -> System.out.print(s)

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值