关于进程线程的概念在此不进行阐述, 自行补操作系统相关知识.
内存模型
模型
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
- 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
- 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。
下面通过示意图来说明这两个步骤:
-
如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。
-
从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。
内存交互操作
关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
- unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
- write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。也就是read和load之间,store和write之间是可以插入其他指令的,如对主内存中的变量a、b进行访问时,可能的顺序是read a,read b,load b,load a。Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:
- 不允许read和load、store和write操作之一单独出现
- 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
- 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
- 一个变量在同一时刻只允许一条线程对其进行lock操作,lock和unlock必须成对出现
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
- 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
- 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。
多线程的创建和启动
线程创建
- 继承
Thread
类
public class ThreadTest {
public static void main(String[] args) {
//实例化一个自定义线程并启动
//注意:如果直接调用这个对象的run方法,这时底层资源并没有完成资源的创建和请求分配,仅仅是简单的对象调用。
new MyThread().start();
//注意,此时线程处于就绪状态, 需要操作系统调度后才可以真正运行
//主线程中打印输出
for (int i = 0; i < 10; i++) {
//获取当前线程的名称
System.out.println(Thread.currentThread().getName() + " --> " + i);
}
}
}
//创建自定义线程,继承Thread父类
class MyThread extends Thread {
@Override
//覆写父类的run()方法,从而实现自己的业务逻辑
public void run() {
for (int i = 0; i < 10; i++) {
//Thread.currentThread().getName() 获取当前线程的名称
System.out.println(Thread.currentThread().getName() + " --> " + i);
}
}
}
- 实现
Runnable
接口
public class ThreadTest {
public static void main(String[] args) {
//实例化一个自定义线程并启动
//注意,此时线程处于就绪状态
MyThread myThread = new MyThread();
new Thread(myThread).start();
//主线程中打印输出
for (int i = 0; i < 10; i++) {
//获取当前线程的名称
System.out.println(Thread.currentThread().getName() + " --> " + i);
}
}
}
//创建自定义线程,实现Runnable接口
class MyThread implements Runnable {
@Override
//实现接口的run()方法,从而实现自己的业务逻辑
public void run() {
for (int i = 0; i < 10; i++) {
//Thread.currentThread().getName() 获取当前线程的名称
System.out.println(Thread.currentThread().getName() + " --> " + i);
}
}
}
区别:
- 在继承
Thread
中可以直接使用this
来调用thread
的方法。比如this.getName()
获取线程名字,而继承Runnable
只能先获取当前的进程对象,Thread.currentThread().getName()
。 - 如果线程类只是实现了
Runnable
接口,那么该类还可以继承其他类。但是继承了Thread
就不能继承其它类了。 - 实现
Runnable
。可以多个线程共享一个Runnable target
对象的资源,所以非常适合多个相同线程来处理同一份资源。
推荐实现Runnable
接口。
用户线程和守护线程
java
中的线程主要分为:用户线程和守护线程:
-
一般
java
代码默认创建的线程为用户线程。 -
守护线程是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程。
java
中的守护线程,在其运行之前将Thread
实例设置了setDaemon(true)
该方法必须在启动线程前调用。 该方法首先调用该线程的 checkAccess
方法,且不带任何参数。这可能抛出 SecurityException
(在当前线程中)
//守护线程设置
thread.setDaemon(true);
// 准备就绪 等待运行
thread.start();
区别:
-
当进程中没有活动的用户线程时,守护线程也不会存在,
jvm
会直接退出 -
守护线程的优先级一般都比较低
Thread常用方法
-
sleep()
如果我们需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用
Thread
的sleep
方法。静态方法, sleep过程中不释放锁, 仍然持有锁, 其他进程无法访问加锁的对象.public class TestThreadSleep implements Runnable{ public static void main(String[] args) { TestThreadSleep runnable = new TestThreadSleep(); Thread thread = new Thread(runnable); thread.start(); } @Override public void run() { System.out.println("i am sleep for a while!"); try { Date currentTime = new Date(); long startTime = currentTime.getTime(); Thread.sleep(4000); currentTime = new Date(); long endTime = currentTime.getTime(); System.out.println("休眠时间为:"+(endTime-startTime)+"ms"); } catch (InterruptedException e) { e.printStackTrace(); } } }
-
yield()
yield()
方法和sleep()
方法有点相似,它也是Thread
类提供的一个静态的方法,它也可以让当前正在执行的线程暂停,让出cpu资源给其他的线程。但是和sleep()
方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。yield()
方法只是让当前线程暂停一下,重新进入就绪的线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()
方法之后,线程调度器又将其调度出来重新进入到运行状态执行。实际上,当某个线程调用了
yield()
方法暂停之后,优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程更有可能获得执行的机会,当然,只是有可能,因为我们不可能精确的干涉cpu调度线程。class Test1 { public static void main(String[] args) throws InterruptedException { new MyThread("低级", 1).start(); new MyThread("中级", 5).start(); new MyThread("高级", 10).start(); } } class MyThread extends Thread { public MyThread(String name, int pro) { super(name);// 设置线程的名称 this.setPriority(pro);// 设置优先级 } @Override public void run() { for (int i = 0; i < 30; i++) { System.out.println(this.getName() + "线程第" + i + "次执行!"); if (i % 5 == 0) Thread.yield(); } } }
-
join()
等待T线程终止,应用场景是当一个线程必须等待另一个线程执行完毕才能执行时,
Thread
类提供了join
方法来完成这个功能,注意,它不是静态方法。
它有3个重载的方法:void join()
: 当前线程等该加入该线程后面,等待该线程终止。void join(long millis)
: 当前线程等待该线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度void join(long millis,int nanos)
: 等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
public class Test1 { public static void main(String[] args) throws InterruptedException { MyThread thread = new MyThread(); thread.start(); thread.join(1);//将主线程加入到子线程后面,不过如果子线程在1毫秒时间内没执行完,则主线程便不再等待它执行完,进入就绪状态,等待cpu调度 for (int i = 0; i < 30; i++) { System.out.println(Thread.currentThread().getName() + "线程第" + i + "次执行!"); } } } class MyThread extends Thread { @Override public void run() { for (int i = 0; i < 1000; i++) { System.out.println(this.getName() + "线程第" + i + "次执行!"); } } }
线程的优先级
眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。
每个线程默认的优先级都与创建它的父线程具有相同的优先级,在默认情况下,main
线程具有普通优先级。
Thread
类提供了setPriority(int newPriority)
和getPriority()
方法来设置和返回一个指定线程的优先级,其中setPriority
方法的参数是一个整数,范围是1~10
public class Test1 {
public static void main(String[] args) throws InterruptedException {
new MyThread("高级", 10).start();
new MyThread("低级", 1).start();
}
}
class MyThread extends Thread {
public MyThread(String name, int pro) {
super(name);//设置线程的名称
setPriority(pro);//设置线程的优先级
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(this.getName() + "线程第" + i + "次执行!");
}
}
}
结束一个线程
Thread.stop()
、Thread.suspend
、Thread.resume
、Runtime.runFinalizersOnExit
这些终止线程运行的方法已经被废弃了,使用它们是极端不安全的!想要安全有效的结束一个线程,可以使用interrupt
结束一个线程。
public class Test1 {
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
thread.start();
}
}
class MyThread extends Thread {
int i = 1;
@Override
public void run() {
while (true) {
System.out.println(i);
System.out.println(this.isInterrupted());
try {
System.out.println("我马上去sleep了");
Thread.sleep(2000);
this.interrupt();//结束线程
} catch (InterruptedException e) {
System.out.println("异常捕获了" + this.isInterrupted());
return;
}
i++;
}
}
}
线程的等待与唤醒
-
wait()
等待对象的同步锁, 需要获得该对象的同步锁才可以调用这个方法, 否则编译可以通过,但运行时会收到一个异常:
IllegalMonitorStateException
。调用任意对象的
wait()
方法导致该线程阻塞,该线程不可继续执行, 让出CPU,并且该对象上的锁被释放。 -
notify()
唤醒在等待该对象同步锁的线程(只唤醒一个,如果有多个在等待),注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
调用任意对象的
notify()
方法则导致因调用该对象的wait()
方法而阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。 -
notifyAll()
唤醒所有等待的线程,注意唤醒的是
notify
之前wait
的线程,对于notify
之后的wait
线程是没有效果的。
其他方法
isAlive()
: 判断一个线程是否存活。activeCount()
: 程序中活跃的线程数。enumerate()
: 枚举程序中的线程。currentThread()
: 得到当前线程。setName()
: 为线程设置一个名称。
线程的生命周期
图片有点问题, 获取和失去处理器资源箭头反了, 大家应该能看出来.
- 新建状态
用new
关键字和Thread
类或其子类建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start
方法进入就绪状态(runnable
)。
注意:不能对已经启动的线程再次调用start()
方法,否则会出现java.lang.IllegalThreadStateException异常。
- 就绪状态
处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为cpu的调度不一定是按照先进先出的顺序来调度的),等待系统为其分配CPU。等待状态并不是执行状态,当系统选定一个等待执行的Thread
对象后,它就会从等待执行状态进入执行状态,系统挑选的动作称之为“cpu调度”。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。
如果希望子线程调用start()方法后立即执行,可以使用Thread.sleep()方式使主线程睡眠一伙儿,转去执行子线程。
- 运行状态
处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
处于就绪状态的线程,如果获得了cpu的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。如果该线程失去了cpu资源,就会又从运行状态变为就绪状态。重新等待系统分配资源。也可以对在运行状态的线程调用yield()
方法,它就会让出cpu资源,再次变为就绪状态。
当发生如下情况是,线程会从运行状态变为阻塞状态:
-
线程调用
sleep
方法主动放弃所占用的系统资源 -
线程调用一个阻塞式I/O方法,在该方法返回之前,该线程被阻塞
-
线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有
-
线程在等待某个通知(
notify
) -
程序调用了线程的
suspend
方法将线程挂起。不过该方法容易导致死锁,所以程序应该尽量避免使用该方法。 -
当线程的
run()
方法执行完,或者被强制性地终止,例如出现异常,或者调用了stop()
,desyory()
方法等等,就会从运行状态转变为死亡状态。
- 阻塞状态
处于运行状态的线程在某些情况下,如执行了sleep
方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。
在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。
- 死亡状态
当线程的run()方法执行完,或者被强制性地终止,就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()
方法,会抛出java.lang.IllegalThreadStateException异常。
线程同步
同步的原因:
-
多线程同时操作同一变量的时候会产生脏数据,也就是不是预期的结果。
常见的售票问题, 存钱问题.
public class SellTicket { public static void main(String[] args) { SaleTicket st = new SaleTicket(); //创建四个线程买票 Thread t1 = new Thread(st, "一号窗口"); Thread t2 = new Thread(st, "二号窗口"); Thread t3 = new Thread(st, "三号窗口"); Thread t4 = new Thread(st, "四号窗口 "); t1.start(); t2.start(); t3.start(); t4.start(); } } class SaleTicket implements Runnable { private int tickets = 100; public void run() { //当票量大于0时,继续买票 while (tickets > 0) { System.out.println(Thread.currentThread().getName() + "卖出 第 " + (tickets--) + "张票"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }
-
多线程可能造成死锁,导致程序崩溃。
由于两个或多个线程都互相占用资源, 并且都在等待其他进程释放资源.
public class DeadLock { //水壶 private Object object1 = new Object(); //水杯 private Object object2 = new Object(); public static void main(String[] args) { new DeadLock().test(); } private void test() { // TODO Auto-generated method stub //people 1 Thread th1 = new Thread(new Dead(0), "小明"); //people 2 Thread th2 = new Thread(new Dead(1), "小华"); th1.start(); th2.start(); try { Thread.sleep(6000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } //死锁类 class Dead implements Runnable { private int tag = 0; public Dead(int _tag) { tag = _tag; // TODO Auto-generated constructor stub } @Override public void run() { // TODO Auto-generated method stub if (tag == 0) { //尝试着拿水壶 synchronized (object1) { System.out.println(Thread.currentThread().getName() + "拿到了水壶"); try { Thread.sleep(3000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } //尝试着去拿水杯 System.out.println(Thread.currentThread().getName() + "尝试着拿水杯"); synchronized (object2) { System.out.println(Thread.currentThread().getName() + "也拿到了水杯"); } } } else { //尝试着拿水杯 synchronized (object2) { System.out.println(Thread.currentThread().getName() + "拿到了水杯"); try { Thread.sleep(3000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } //尝试着去拿水壶 System.out.println(Thread.currentThread().getName() + "尝试着拿水壶"); synchronized (object1) { System.out.println(Thread.currentThread().getName() + "也拿到了水壶"); } } } } } }
同步的方法
Java 语言提供了两个关键字:synchronized
和volatile
,一个对象ReetrantLock
来实现线程的同步。
synchronized
synchronized
是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
- 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
- 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
- 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
- 修改一个类,其作用的范围是
synchronized
后面括号括起来的部分,作用主的对象是这个类的所有对象。
/*给某个对象加锁*/
//object必须为要同步的对象,也就是可能会出现问题的对象
synchronized (object){
//业务逻辑
}
/*修饰方法*/
public synchronized void method ()
{
// todo
}
/*修饰静态方法*/
public synchronized static void method () {
// todo
}
/*修饰类*/
class ClassName {
public void method() {
synchronized (ClassName.class) {
// todo
}
}
}
注意:
- 无论
synchronized
关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized
作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。 - 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
- 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
volatile
-
它是一个类型修饰符,线程在获取用
volatile
声明的变量时,直接从主存读取,修改后直接写入主存。 -
volatile
关键字为域变量的访问提供了一种免锁机制, 使用volatile
修饰域相当于告诉JVM该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值。 -
volatile
不会提供任何原子操作,它也不能用来修饰final
类型的变量。class Bank { //需要同步的变量加上volatile private volatile int account = 100; public int getAccount() { return account; } //这里不再需要synchronized public void save(int money) { account += money; } }
Volatile
变量的同步性较差(但有时它更简单并且开销更低),而且其使用也更容易出错,所以尽量使用synchronized
同步.
ReetrantLock
java.util.concurrent.lock
中的 Lock
框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock
的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。 ReentrantLock
类实现了 Lock
,它拥有与 synchronized
相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)
ReetrantLock
是在代码的任何地方都可以获得锁、释放锁、但是为了安全推荐在finally
块中释放锁。
Lock lock = new ReentrantLock();
lock.lock();
try {
// update object state
} finally {
lock.unlock();
}
线程池
作用
- 在java中,如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,服务器在创建和销毁线程上花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,服务器应用程序需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务,这就是“池化资源”技术产生的原因。
- 线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。
四种线程池
new SingleThreadExecutor
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
new FixedThreadPool
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
new CachedThreadPool
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
new ScheduledThreadPool
创建一个执行延时、周期行执行任务的线程池。
ExecutorService
表述了异步执行的机制,并且可以让任务在后台执行。
ExecutorService
对象一般使用流程:
Executors
创建一个线程池- 调用
submit
添加线程任务(参数是一个实现Runnable
接口的对象). (周期线程:schedule、scheduleAtFixedRate、scheduleWithFixedDelay) - 调用
shutdown
等任务完成后关闭线程池。
//单线程池
ExecutorService executorService= Executors.newSingleThreadExecutor();
executorService.submit(new SayHello());
executorService.submit(new SayHello());
executorService.submit(new SayHello());
//固定大小的线程池
ExecutorService executorServiceFixed=Executors.newFixedThreadPool(4);
executorServiceFixed.submit(new SayHello());
executorServiceFixed.submit(new SayHello());
executorServiceFixed.submit(new SayHello());
executorService.shutdown();
//可变的(缓存)线程池
ExecutorService executorServiceCache=Executors.newCachedThreadPool();
executorServiceCache.submit(new SayHello());
executorServiceCache.submit(new SayHello());
executorServiceCache.submit(new SayHello());
executorServiceCache.submit(new SayHello());
//周期性线程池
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
//延迟的调用
scheduledExecutorService.schedule(new SayHello(), 2, TimeUnit.SECONDS);
System.out.println(new Date());
//固定延时的线程池。
//long initialDelay 初始化的延迟时间:提交任务X(TimeUnit)后开始执行此任务。
//long delay 周期性的延迟时间:执行完成一个任务后的X(TimeUnit)后再执行下一个任务,时间间隔是以完成任务时的时间点。
//本次任务完成的时间点+delay(TimeUnit)=下次任务开始的时间。
scheduledExecutorService.scheduleWithFixedDelay(new SayHello(), 2, 4, TimeUnit.SECONDS);
System.out.println(new Date());
//固定执行间隔的线程池
//long delay 周期性的执行时间:开始执行一个任务后的X(TimeUnit)后再执行下一个任务,时间间隔是以开始任务时的时间点。
//本次任务开始的时间点+delay(TimeUnit)=下次任务开始的时间。但是:如果delay小于执行任务的执行时间的时候,第二个任务会在第一个任务完成后开始。
scheduledExecutorService.scheduleAtFixedRate(new SayHello(), 3, 4, TimeUnit.SECONDS);
System.out.println(new Date());
推荐阅读:
[1] https://blog.csdn.net/u011204847/article/details/51137062
[2] https://juejin.cn/post/6844903943546339336
[3] https://www.jianshu.com/p/6f7e32a7c07d
[4] https://www.cnblogs.com/snow-flower/p/6114765.html
[5] https://www.cnblogs.com/wxd0108/p/5479442.html