1.进程与线程
1.进程是什么?
进程是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间。这也就意味着,每个进程都有自己的堆内存栈内存,互不相通。
2.线程是什么?
线程是进程中的一个执行路径,同一个进程中的不同线程共享同一个内存空间,线程之间可以自由切换,并发执行,一个进程最少有一个线程。线程实际上是在进程基础上的进一步划分,一个进程启动后,里面的若干执行路径又可以划分成若干个线程。
3.进程调度
分时调度:所有的线程轮流使用CPU的使用权,CPU将某一段时间平均分成若干份分配给每一个线程使用。
抢占式调度:CPU会优先让优先级高的线程使用CPU,如果线程的优先级相同,会随机选择一个线程。Java使用的就是抢占式调度方式。另外,CPU使用抢占式调度模式在多个线程间进行着高速的切换.对于CPU的一个核心而言,某个时刻只能执行一个线程,而CPU在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。但事实上,多线程并不会提高程序的运行速度,但能够提高CPU的使用率。
4.同步与异步
同步: 排队执行,效率低但安全。
异步:同时执行,效率高但数据很不安全。
5.并发与执行
并发:指两个或多个事情在同一时间段发生。
并行:指两个或多个事情在同一时刻发生。(也就是同时发生)
2.继承Thread实现增加线程
继承Thread类可以用于增加一个线程,使用方法如下:
首先我们需要创建一个类,这个类需要继承Thread方法,并重写run()方法,run()方法的内容为这个线程执行的任务。
public class MyThread extends Thread{
public void run(){
for (int i=0;i<5;i++){
System.out.println("子线程"+i);
}
}
}
然后我们需要在测试类中创建一个MyThred对象来新增加一个线程,并通过 start() 方法来开启这个线程。
public class test{
public static void main(String[] args) {
MyThread m = new MyThread();
m.start();
for (int i=0;i<5;i++){
System.out.println("主线程"+i);
}
}
}
上例代码的运行结果如下:
主线程0
子线程0
主线程1
子线程1
主线程2
主线程3
子线程2
主线程4
子线程3
子线程4
线程m被称为子线程,子线程开启后,会与main线程并发执行,二者会抢占执行权,交叉执行。我们分析上述结果可以发现上述结果并不规律,这就说明了两个线程是交叉的。
main方法的执行流程如下:
每一个线程都有独立的栈空间,共享同一块堆内存。
3.实现Runnable接口
Runnable接口中只有一个run()抽象方法,我们需要在实体类中继承Runnable接口并重写run()方法。run()方法的内容就是一个要执行的任务。使用方法如下:
首先我们需要新建一个实体类,实现Runnable接口,并重写run()方法
public class MyRunnable implements Runnable{
@Override
public void run(){
for (int i=0;i<5;i++){
System.out.println("子线程" + i);
}
}
}
在测试类中使用MyRunnable类
我们可以将这个过程理解为,先创建一个任务对象,也就是MyRunnable对象,然后创建一个 线程对象,也就是Thread对象,并给这个对象分配任务,再使用 start()方法开启线程。
public class test{
public static void main(String[] args) {
//1. 创建一个任务对象
MyRunnable r = new MyRunnable();
//2. 创建一个线程,并给这个线程分配任务
Thread t = new Thread(r);
//3. 开启线程
t.start();
for (int i =0;i<5;i++){
System.out.println("主线程" + i);
}
}
}
与继承Thread实现多线程相比的优势:
- 通过创建任务,然后给线程分配的方式实现多线程,更适合多个线程同时执行相同任务的情况。
- 可以避免单继承带来的局限性。
- 任务与线程本身是分离的,提高了程序的健壮性。
- 后续学习的线程池技术,接收Runnable类型的任务,不接收Thread类型的线程。
4.设置和获取线程名称
这一块我们用到三个方法:
- public static native Thread currentThread():获得当前线程
- public final String getName():获得线程名称
- public final synchronized void setName(String name):设置线程名称。
废话不多说,直接上代码
public MyRunnable implenments Runnabel{
public void run(){
for(int i=0;i<5;i++){
System.out.println("当前正在执行的线程:" + Thread.current.getName());
}
}
}
public test{
public static void main(String[] args){
MyRunnable r = new MyRunnable();
Thread t = new Thread(r);
t.setName("t");//设置t线程名称为t
t.start();
for (int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+"正在执行");
}
}
}
5.线程的休眠
public static native void sleep(long millis):让线程休眠指定的时间
下面的代码作用是让控制台每隔1s输出一次i的值
public static void main(String[] args) throws InterruptedException {
for (int i=0;i<5;i++){
System.out.println(i);
Thread.sleep(1000);
}
}
6.线程阻塞
所谓线程堵塞指的是一切耗费时间的操作,比如读取文件,等待用户输入等等,文件不读取结束,用户不输入,线程就会一直不动,就叫做线程堵塞,或者叫做耗时操作。
7.线程中断
如果线程长时间占用资源,阻碍其他线程的进行,这时候就需要强制中断线程,释放所占用的资源。线程只能由线程自身结束,我们只能给线程打上中断标记,如果线程检查到了中断标记,会抛出InterputException异常,我们需要在处理异常的catch块中释放资源,结束线程。
直接上代码演示
public static void main(String[] args) throws InterruptedException {
myRunnable r = new myRunnable();
Thread t = new Thread(r);
t.setName("t");
t.start();
for (int i=0;i<5;i++){
System.out.println(i);
Thread.sleep(1000);
}
//当mian线程任务结束后给t线程打上中断标记。
t.interrupt();
}
static class myRunnable implements Runnable{
public void run() {
for (int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("检测到中断标记了,我得结束自己了。");
return ;//直接return,结束任务。
}
}
}
}
8.守护线程
线程的分类
用户线程:当一个进程不存在活着的用户线程时,进程结束。
守护线程:守护用户线程,当最后一个用户线程死亡后,守护线程自动死亡。
守护线程设置的方法很简单,将setDaemon的参数设置为true即可。下面是代码示例
public class MyRunnable implements Runnable{
@Override
public void run(){
for (int i=0;i<10;i++){
String name = Thread.currentThread().getName();
System.out.println(Thread.currentThread().getName()+":" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
}
}
public static void main(String[] args) {
MyRunnable r = new MyRunnable();
Thread t = new Thread(r);
t.setDaemon(true);//将线程t设置为守护线程。
t.start();
for (int i=1;i<=5;i++){
System.out.println(Thread.currentThread().getName()+":" + i);
}
//main线程是唯一一个用户线程,当main线程结束之后,守护线程t也会自动结束。
}
上面代码的输出结果如下:
main:1
main:2
Thread-0:0
main:3
main:4
main:5
正常来说子线程会输出10次,但现在只输出了1次就结束了,是因为所有的用户线程(在本例中只有main主线程)都结束了,守护线程会自杀。
9.线程安全问题
多个线程同时使用一个资源时,会出现线程安全问题。
举个卖票的例子,假如说有三个终端同时再售票,来一个极限情况,这时候只剩一张票了。此时三个终端同时发生售票操作,我们假设终端的售票操作有三个过程:1.判断票的库存是否为0,不为0的话继续执行后面的操作。2.打印票据信息。3.出票并将票据库存-1。此时库存只剩一张票,三个终端先后接收到了售票指令,我们将售票操作的时间无限放大。此时终端A先判断票数不为0,进入第2阶段的操作,终端B和终端C同样判断票数不为0,也进入了第2阶段。终端A速度稍微快了一点,先执行了阶段3,此时票的库存变为了0,但是B和C还在售票操作中,最终出现了库存为0时库存依然-1的操作,导致库存数变成了-1,-2,也没有成功出票。
我们将上述过程反应在代码中
public class Demo2 {
public static void main(String[] args) {
MyRunnable run = new MyRunnable();
Thread A = new Thread(run);
Thread B = new Thread(run);
Thread C = new Thread(run);
A.setName("A");
B.setName("B");
C.setName("C");
A.start();
B.start();
C.start();
}
static class MyRunnable implements Runnable {
private int COUNT = 10;//总票数
@Override
public void run() {
while (COUNT > 0) {
System.out.println(Thread.currentThread().getName() + "开始执行出票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
COUNT--;
System.out.println(Thread.currentThread().getName() +"出票完毕!余票:" + COUNT + "张");
}
}
}
}
输出结果如下:
C开始执行出票
B开始执行出票
A开始执行出票
C出票完毕!余票:9张
B出票完毕!余票:8张
B开始执行出票
A出票完毕!余票:8张
C开始执行出票
A开始执行出票
B出票完毕!余票:7张
B开始执行出票
C出票完毕!余票:5张
A出票完毕!余票:5张
C开始执行出票
A开始执行出票
B出票完毕!余票:4张
B开始执行出票
C出票完毕!余票:3张
C开始执行出票
A出票完毕!余票:2张
A开始执行出票
B出票完毕!余票:1张
B开始执行出票
C出票完毕!余票:0张
A出票完毕!余票:-1张
B出票完毕!余票:-2张
很明显,出现问题了,正常售票是不可能出现余票为负数的情况,但现在出现了,这就是线程安全问题。
出现这种问题的原因就在于多个线程同时操作同一个资源,反应在这个售票问题中就是在一个终端准备售票的过程中,其他终端也进入了售票阶段。当处于极限情况,也就是只剩一张票的时候,会出现这种情况,一个终端已经进入售票,但还未减少票的库存,此时其他终端也可能进入卖票,但此时只剩下1张票,又减少了两张票,所以会出现负数的情况。
10.线程安全1:同步代码块
格式:
synchronized(锁对象){
//同步代码块
}
锁对象可以是任何对象,但必须保证传入的所有进程传入的锁对象都是同一个,如此才能起到锁的作用。
当一个线程进入同步代码块时,锁对象会被打上一个锁标记,当其他线程运行到同步代码块时,会检查锁对象是否有锁标记,如果有,就会停在同步代码块之前,等待上一个进入同步代码块的进程执行完毕才会开始抢占时间片,获得执行权,一旦抢到时间片进入同步代码块,该进程又会给锁对象打上标记。也就是synchronized()能让进程排队执行同步代码块的内容。
下面我们用synchronized来解决售票问题。
public class Demo2 {
public static void main(String[] args) {
MyRunnable run = new MyRunnable();
Thread A = new Thread(run);
Thread B = new Thread(run);
Thread C = new Thread(run);
A.setName("A");
B.setName("B");
C.setName("C");
A.start();
B.start();
C.start();
}
static class MyRunnable implements Runnable {
private int COUNT = 10;//总票数
private Object o = new Object();
@Override
public void run() {
while (true) {
//一个线程进入同步代码块,其余线程只能在这里等待。
synchronized(o){
if (COUNT>0){
System.out.println(Thread.currentThread().getName() + "开始执行出票");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
COUNT--;
System.out.println(Thread.currentThread().getName() +"出票完毕!余票:" + COUNT + "张");
}
}
}
}
}
}
运行代码,结果如下:
A开始执行出票
A出票完毕!余票:9张
A开始执行出票
A出票完毕!余票:8张
A开始执行出票
A出票完毕!余票:7张
A开始执行出票
A出票完毕!余票:6张
C开始执行出票
C出票完毕!余票:5张
C开始执行出票
C出票完毕!余票:4张
C开始执行出票
C出票完毕!余票:3张
C开始执行出票
C出票完毕!余票:2张
C开始执行出票
C出票完毕!余票:1张
C开始执行出票
C出票完毕!余票:0张
卖票很正常,同步代码快能完美解决不同步卖票问题。
11.线程安全2:同步方法
我们可以将10中的同步代码块封装成一个方法,然后用synchronized修饰这个方法,也能实现同步代码块的作用。
代码示例如下
public class Demo1 {
public static void main(String[] args) {
Ticket run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
private static int COUNT = 10;
@Override
public void run() {
while (true){
boolean flag = sale();
if (!sale()){
break;
}
}
}
public synchronized boolean sale(){
if (COUNT > 0){
System.out.println(Thread.currentThread().getName() + "正在出票");
COUNT--;
System.out.println("出票成功,余票:" + COUNT);
return true;
}
return false;
}
}
}
这里有一个问题,就是同步方法的锁对象是谁?答案是调用这个同步方法的对象本身,也就是this。在上述代码中也就是Ticket对象,run,我们可以重写toString()方法然后在sale()方法中输出一下。如果synchronized锁的是静态方法,那锁对象就是Tick.class,也就是Tick对应的字节码对象。
另外,如果同步代码块的锁对象也是this,那当一个线程执行同步代码块 中的内容时,其他线程也无法执行同步方法,因为二者用的是同一个锁对象,当锁对象被打上标记的时候同步代码块与同步方法都不能被其他线程使用。
12.线程安全3:显示锁
synchronized锁被称为隐式锁,Lock锁被称为显示锁。
Lock的用法很简单,创建一个对象,然后在同步代码块前调用一下lock()方法,在同步代码块之后调用一下unlock()方法即可。
创建Lock对象:
Lock lock = new ReentrantLock();
代码示例:
static class MyRunnable implements Runnable{
private int COUNT = 10;//总票数
private Lock l = new ReentrantLock();
@Override
public void run() {
while (COUNT>0){
l.lock();
if (COUNT > 0){
System.out.println(Thread.currentThread().getName() + "开始执行出票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
COUNT--;
System.out.println(Thread.currentThread().getName() + "出票完毕!余票:" + COUNT + "张");
}else {
break;
}
l.unlock();
}
13.公平锁
所谓公平锁就是指,在等待执行被锁上的代码块时,等锁解开后,按照线程先来后到的顺序执行。举个例子就是线程A,B,C依次运行到某块被锁的代码,等代码解锁后,线程A先执行这块代码,然后是B,最后是C,先来后到。
公平锁的用法也很简单,在创建Lock对象的时候添加参数true即可
公平锁的创建:
Lock lock = new ReentrantLock(true);
14.生产者消费者问题
生产者消费者问题泛指一类问题,消费者想要消费产品只有等待生产者生产出来之后才能进行消费。举个例子,一家饭馆有一个厨师和一个服务员,只有一个盘子。服务员想要上菜,只能等厨师把菜炒出来才行;厨师想要炒菜,必须等盘子空了也就是服务员上菜之后等待顾客用餐完毕再拿回来才能继续炒菜。下面使用代码演示这个问题。
public class Main {
public static void main(String[] args) {
test.Food f = new test.Food();
new test.Cook(f).start();
new test.Waiter(f).start();
}
static class Cook extends Thread {
private test.Food f = new test.Food();
public Cook(test.Food f) {
this.f = f;
}
public synchronized void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
f.setNameAndTaste("A", "A");
System.out.println("厨师做的菜名:A,味道A");
} else {
f.setNameAndTaste("B", "B");
System.out.println("厨师做的菜名:B,味道B");
}
}
}
}
static class Waiter extends Thread {
private test.Food f = new test.Food();
public Waiter(test.Food f) {
this.f = f;
}
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.get();
}
}
}
static class Food {
private String name;
private String taste;
//这个方法由厨师调用
public void setNameAndTaste(String name, String taste) {
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
}
//这个方法由服务员调用
public void get(){
System.out.println("服务员端走的菜名为:" + this.name + ",味道为:" + this.taste);
}
}
}
为了解决上述问题,我们先分析一下,首先我们的目的是让服务员和厨师配合工作,厨师做完一道菜,服务员才会端走,而且端走的菜的味道与菜名是对应的。这时候我们可以在Food类中做手脚。
首先,通过我们的目的可以发现,厨师和服务员必须分开工作,不能同时进行,因为只有一个盘子。厨师和服务员的工作反应到Food类中对应的就是setNameAndTaste()方法与get()方法,这两个方法是是厨师和服务员两个线程分别调用的,为了达到我们的目的,必须让两者交替进行。所以,我们可以使用一个布尔变量flag来标志是哪个线程在运行,厨师线程运行的时候,服务员线程进入阻塞状态,服务员进程运行的时候,厨师线程需要进入阻塞状态。
将进程阻塞使用的是Object类中的wait()方法,唤醒阻塞进程用的是Object类中的notifyAll()方法,这两个方法都需要在同步线程块中使用(原因可以参考为什么wait()、notify()方法需要和synchronized一起使用)。通过线程通信我们就能实现生产者消费者正常运行。
完整代码如下:
public static void main(String[] args) {
test.Food f = new test.Food();
new test.Cook(f).start();
new test.Waiter(f).start();
}
static class Cook extends Thread {
private test.Food f = new test.Food();
public Cook(test.Food f) {
this.f = f;
}
public synchronized void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
f.setNameAndTaste("A", "A");
System.out.println("厨师做的菜名:A,味道A");
} else {
f.setNameAndTaste("B", "B");
System.out.println("厨师做的菜名:B,味道B");
}
}
}
}
static class Waiter extends Thread {
private test.Food f = new test.Food();
public Waiter(test.Food f) {
this.f = f;
}
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.get();
}
}
}
static class Food {
private String name;
private String taste;
private boolean flag = true;
//这个方法由厨师调用
public synchronized void setNameAndTaste(String name, String taste) {
if (flag){
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
}
flag = false;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//这个方法由服务员调用
public synchronized void get(){
if (!flag){
System.out.println("服务员端走的菜名为:" + this.name + ",味道为:" + this.taste);
flag = true;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
现在厨师与服务员就能正常配合工作了。
15.线程的六种状态
- NEW:尚未启动的线程处于此状态。
- RUNNABLE:执行中的线程状态。
- BLOCKED:被阻塞的多功能带监视器锁定的线程处于此状态。
- WAITING:无限期等待另一个线程执行特定操作的状态。
- TIMED_WAITING:计时器等待状态,也就是线程会等待另一个线程执行特定操作指定的时间,如果已经等待了指定的时间则不会再等待。
- TERMINATED:已经退出的线程的状态。
16.Callable实现多线程
使用Callable接口也能实现多线程,但它与Runnable接口不一样的地方在于Callable实现的子线程能够返回结果给主线程main线程。
Callable是一个接口,使用的时候比Runnable稍微麻烦一点,首先我们来看一下它的源码:
public interface Callable<V> {
V call() throws Exception;
}
我们可以看到,实现Callable接口的时候需要指定一个泛型,这个泛型也是该线程返回的结果类型。
Callable接口实现多线程的步骤是这样的:首先创建一个实现Callable接口的实体类,并且在这个实体类中重写call()方法,call方法的内容就是线程的执行内容。创建之后new一个对象,将其作为new FutureTask()的参数,创建一个FutureTask对象。我们可以将这个对象理解为任务对象。将这个对象传入new Thread()中,创建一个新线程。
代码示例如下:
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable callable = new MyCallable();
FutureTask<Integer> task = new FutureTask<>(callable);
new Thread(task).start();
for (int i = 0;i<5;i++){
System.out.println(i);
}
}
static class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
for (int i=0;i<5;i++){
System.out.println(i);
}
return 100;
}
}
如果我们想要将子线程的结果返回给主线程,需要调用FutureTask中的get()方法。此时主线程需要等待子线程结束返回结果之后才能继续向下运行。
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable callable = new MyCallable();
FutureTask<Integer> task = new FutureTask<>(callable);
new Thread(task).start();
//调用task方法
Integer num = task.get();
//当子线程执行完毕返回结果之后主线程才会继续向下运行。
System.out.println("num = " + num);
for (int i = 0;i<5;i++){
System.out.println(i);
}
}
static class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
Thread.sleep(1000);
for (int i=0;i<5;i++){
System.out.println(i);
}
return 100;
}
}
17.线程池概述
线程池,顾名思义,是一个容纳多个线程的容器。新的线程执行任务时,需要四步,创建线程,创建任务,执行任务,关闭线程。其中最耗费时间的是创建线程与关闭线程。为了提高代码执行效率,出现了线程池的概念。线程池中是一个线程数组,里面的线程有两个状态,闲与忙。与线程池一起的还有一个任务池的概念。任务池是一个容纳待执行任务的容器。
18.缓存线程池
缓存线程池的长度没有限制,任务加入后,执行流程:
- 判断是否有空闲线程
- 如果有,则用空闲线程执行任务;
- 如果没有,创建新的线程放入线程池然后执行任务。
使用示例
首先我们需要获取一个线程池
ExecutoService service = Executors.newCathchedThreadPool();
然后我们指挥线程池执行Runnable任务
service.execute(run);
这里可以演示一下线程池的特性,如果有空闲线程存在就会用空闲线程执行任务。
public static void main(String[] args) {
//获取线程池
ExecutorService service = Executors.newCachedThreadPool();
//向线程池中加入任务。
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "锄禾日当午");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "锄禾日当午");
}
});
//我们让主线程休眠1秒钟,这样下一个Runnbale任务就能用线程池中的空闲线程执行了。
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "锄禾日当午");
}
});
}
19.定长线程池
定长线程池中的线程数量是在获取线程池的时候指定的固定数量,不能变化,执行任务的流程如下:
- 判断是否有空闲线程
- 有空闲线程,将任务分配给空闲线程
- 没有空闲线程,等待线程空闲下来,然后将任务分配给空闲线程。
示例代码:
获取定长线程池,并指定线程数量为2
ExecutorService service = Executors.newFixedThreadPool(2);
定长线程池的使用与缓存线程池一样,不过定长线程池不会扩展,线程数量不够的时候只能等待线程空闲下来。
public static void main(String[] args) {
Run run = new Run();
//获取定长线程池,并制定线程数量为2
ExecutorService service = Executors.newFixedThreadPool(2);
//给线程池中的线程指派任务
service.execute(run);
service.execute(run);
service.execute(run);
}
static class Run implements Runnable{
public void run(){
System.out.println(Thread.currentThread().getName() + "锄禾日当午~");
//这里让线程休眠2s是为了能够更清楚的观察到定长线程池的特性。在打印完信息之后休眠2s才会空闲下来,执行下一个任务。
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
20.单线程池
单线程线程池其实就是定长线程池数量为1的情况,只是获得线程池的方法稍微有一点不同。
获取单线程线程池
ExecutorService service = Executors.newSingleThreadPool();
21.周期性定长线程池
周期性定长线程池的获取方式跟前面的线程池有点不同,获取方式如下
获取指定数量为1的周期性定长线程池
ScheduleExecutorService service = new ScheduleThreadPoolExecutor(1);
它有两种执行方式,定时执行任务和定时循环执行任务,前者是定时执行一次任务,后者是设定时间执行第一次任务,然后按照指定的时间间隔循环执行任务。
定时执行任务:
service.schedule(任务,定时时长,时间单位)
定时5s后执行任务
service.schedule(run,5,TimeUnit.SECONDS);
定时循环执行任务
service.schedule(任务,定时时长,循环间隔时长,时间单位);
定时5s后开始执行任务,循环间隔为1s
service.schedule(run,5,1,TimeUnit.SECONDS);