java线程运行机制

java虚拟机中,执行程序是由线程完成的,每个线程都有独立的程序计数器(PC或rip
)和栈

栈中有三个区,局部变量区,操作数栈和栈数据区。

操作数栈是线程的工作区,用来存放运算过程产生的临时变量(怎么有种寄存器的感觉)。

栈数据区为线程执行指令提供相关信息,例如定位到堆区和方法区的特定数据(类成员变量),正常退出方法等(存放地址信息)。

每次java虚拟机启动一个虚拟机进程时,虚拟机都会创建一个主线程。该线程从main开始执行。

例如在一个类中定义了一个实例变量a,然后再main函数中调用了这个类的成员函数A(),这个函数会让成员变量加一。首先进入这个方法,然后发现a++;就去栈数据区取出a的地址,之后定位到堆区取出a进行操作。

  • 方法区存放了字节码
  • 堆区存放了线程所需要长时间保存的数据(类)
  • 栈区存放了临时数据,栈帧和跳转到其他区域地址。

线程的创建和启动

我们现在免费有了一个主线程,如果我们还想要创建其他线程的话,有两种方式。

  • 扩展java.lang.Thread类
  • 实现Runnable接口

要注意一点,创建了线程并启动之后会有自己的栈区,堆区,方法区,也就是说可以把它看成一个新的程序,里面的变量都是最原始的,不要认为主线程中操作的变量还有用。但是有些时候也可以使用主线程的对象

一个线程只可以被启动一次

扩展java.lang.Thread类

thread是线程,它的最主要的两个方法是:

  • run(): 包含线程运行时执行的代码(相当于main方法)
  • start(): 用于启动线程,不需要覆盖

start方法用来确定什么时候开始执行,可以在别的main函数中执行,例如:

public class Machine extends Thread
{
public void run()
{
for(int i=0; i<50; i++)
{
System.out.println(i);
}
}

public static void main(String[] args)
{
Machine machine = new Machine();
machine.start(); //启动
}
}

注意,调用了start并不是指立刻执行这个线程,而是让这个线程进入就绪状态,如果想让这个线程执行可以使用yield。

实现Runnable接口

java不允许一个类继承多个类,所以一旦继承了Thread类,那么就不能继承其他类。所以说接口这时就体现出优越性了。定义如下:

public void run();

例:

public class Machine implements Runnable
{
public void run()
{
for(int i=0; i<10; i++)
{
System.out.println(i);
}
}
public static void main(String[] args)
[
Machine machine = new Machine();
Thread t1 = new thread(machine);
t1.start();
}
}

Thread构造方法中有Runnable接口的,这个时候Thread就掌管了run方法,只要使用start就可以启动。

主线程和用户自定义线程并发运行

并发运行指的是一个线程没有结束另一个线程开始执行,上面举的例子都是并发运行。

Thread中的currentThread()静态方法返回当前线程的引用,getname()返回当前线程的名字,main方法名字是main,用户创建的线程根据顺序从Thread-0,Thread-1一直往后排,可以用setName()设置名字。

例:

String name = Thread.currentThread().getName();
相当于
Thread thread = Thread.currentThread();
String name = thread.getName();

为了让每个线程轮流获得cpu,可以使用sleep(time)放弃cpu并睡眠若干时间。

多个线程共享一个对象的实例变量

例:

  • 方法内部局部变量不共享

这是因为这些数据都是动态在栈中分配的,每个线程都有自己的堆栈。

  • 成员变量,如果是指向同一个对象就共享
package file2;
public class Analy {
public static void main(String[] args) {
Num i=new Num(0); //新建对象,准备传递给线程
new OwnThread(i).start(); //新建线程,并启动
new OwnThread(i).start(); //新建线程,并启动
System.out.println("主线程中i的值变为了:"+i.i); //获取目前对象i的数值
}
}

class OwnThread extends Thread
{
Num id; //申明对象,默认null,就是没有指向任何实体
int sno; //申明int变量。因为系统默认初始化为0,所以应该是定义一个int变量
OwnThread(Num id)
{
this.id=id;
}

public void run()
{
for(int i=0;i<5;i++)
{
synchronized(this)
{
sno=id.i; //保存id.i的数值,到线程私有变量sno
id.i++;
try {
Thread.sleep(1);
}
catch (InterruptedException e) {}
}
System.out.println(this.getName()+","+sno);
}
}
}

class Num //定义一个类
{
int i;
Num(int i)
{
this.i=i;
}
}

程序中主函数定义了Num对象的实例i,定义线程是传递到了Thread0和Thread1这样三个变量就共享了一个Num对象的实例。而线程Thread0和线程Thread1又有自己的私有变量sno,可以用来保存某一时刻的共享变量的数值。

(1)Java中判断对象是否为同一个对象使用地址判断的。地址相同就是同一个对象,上面的三个就是同一个对象。

(2)如果把上面的例子中共享的对象实例用基本数据类型替换是不行的。因为基本数据类型程序会自动的用默认值初始化,也就是申明和定义时一起的。此时在main函数中定义线程,传递的基本数据类型参数,只能是初始化线程中的另一个对象,而不是同一个对象。

也就是说,只有类才可以共享,并且用同一个实例启动多个线程的时候这个实例是共享的,并且也只有这个实例时共享的,如果在线程中创建的新实例也不是共享的。

这里来自这篇博客

线程状态转换

新建状态

也就是只有new没有start状态。

就绪状态

调用了start方法后,就进入了就绪状态。虚拟机会为他创建栈和rip,这个线程在运行池中等待cpu使用权。

运行状态

就是得到了cpu使用权,如果是多个cpu,那么可以同时运行多个线程。

阻塞状态

阻塞状态优先级比就绪状态低,阻塞状态过去后首先进入就绪状态然后进入运行状态。

  • 如果调用了某个对象的wait()方法,那么会进入这个对象等待池中。
  • 如果试图获得某个对象的同步锁(后面说)但是被其他线程使用时,会把这个线程放到这个对象的锁池中。
  • 调用sleep。

    死亡状态

    当退出了run后,进入死亡状态。前面已经提到过一个线程不能呗start两次。可以用Thread的isAlive()方法判断这个线程是否活着。如果处于新建状态时,也是false。

    线程调度

线程调度就是确定cpu的使用权在那个线程手上,cpu通常只有一个。有两种调度模型:分时调度和抢占型调度。

分时型调度就是让每个线程都执行一定时间。

抢占型是java虚拟机采用的方式。它是将所有线程规定一个优先级,优先让优先级高的线程运行,如果优先级都相同,那么随机选取一个线程。一旦抢到cpu,那么它将会一直运行直到被迫停止运行。

被迫停止运行的可能:

  • 虚拟机让当前线程放弃cpu。
  • 当前线程进入阻塞状态
  • 线程运行结束

不同操作系统对抢占型的实现不同,有的是会一直让这个线程运行,有的是一段时间后停止运行。

调整优先级

可以使用Thread的setPriority(int)和getPriority()设置优先级,可以使用currentThread().setPriority(?)设置等级。

三个静态常量:

  • MAX_PRIORITY 10级,最高等级
  • MIN_PRIORITY 1,最低等级
  • NORM_PRIORITY 5, 默认等级

如果将主线程优先级更改然后再创建其他线程,那么其他线程优先级也会更改。被创建者的默认优先级和创建者优先级保持相同。

注意,上面三个静态常量在不同操作系统中是不同的,例如windows只有七个优先级,所以最好只使用上面三种优先级。

Thread.sleep()线程睡眠

如果使用sleep方法,那么他会放弃cpu进入阻塞状态。sleep后面的数据是以毫秒做单位。完成之后也不是立刻重新开始执行,而是先进入就绪状态,如果没人和它抢cpu就开始执行。

例如: sleep(100);表示阻塞100毫秒

可以使用sleep让两个线程比较同步的执行。例如每个线程都会调用sleep(100),那么第一个线程调用睡眠后第二个线程开始执行,如果执行时间小于100ms那么第二个线程执行完后第一个线程仍在睡眠,如果大于100ms第一个线程会立刻开始执行。因为它已经睡了100ms进入就绪状态了(同样也是只有两个线程的情况)。

可以使用sleep.interrupt()中断睡眠

如果线程在睡眠时被中断,那么就会抛出InterruptedException,程序跳到异常处理代码块。

Thread.yield()线程让步

执行yield后,如果有相同或者更高优先级的线程在就绪状态,那么将会把当前线程放入运行池中并让优先级高的线程执行。

join()等待其他线程结束

当前线程可以调用另一个线程的join()方法,直到另一个线程结束这个线程才会又开始执行(进入就绪状态)。

如果加了参数,那么类似于sleep()将会休眠若干时间,不同的是join是给指定的线程执行。

后台线程

例如垃圾回收线程,只有当前台所有线程都停止之后,后台线程才会结束生命周期。

主线程默认是前台线程,前台线程创建的线程默认也是前台线程。

可以调用Thread的setDaemon(true)方法,把线程设置成后台线程。可以用isDaemon()判断是否是后台线程。

只有在线程启动前(使用start())之前才可以设置成后台。并且后台创建的线程还是后台线程。

定时器

java.util包中提供了定时器Timer,TimerTask类表示定时器执行的一项任务。例;

package usetimer;
improt java.util.Timer;
import java.util.TimerTask;
public class Machine extends Thread
{
private int a;

public void start()
{
super.start();//想自定义start必须要先用父类的start
Timer timer = new Timer(ture);//把Timer关联的线程设为后台线程
TimerTask task = new TimerTask()
{
public void run()
{
reset();
}
};//匿名类,便于设置定时任务
timer.schedule(task, 10, 50);//设置定时任务
}
public void reset()
{
a = 0;
}
public void run()
{
for(int i=0; i<1000; i++)
{
System.out.println(getName()+":"+a++);
}
yield();
}
public static void main(String[] args)throws Exception
{
Machine machine = new Machine();
machine.start();
}
}

TimerTask类是一个抽象类,它实现了RUnnable接口。start()中匿名类继承了TimerTask类。

TImer(boolean isDaemon)可以把相关联的线程设置成后台线程。如果是true就是后台

schedule(TimerTask task, long delay, long period)用来设置定时任务。所以前面的匿名类就是用来启动定时任务的。delay是推迟多少毫秒之后执行,period是每次执行任务的间隔。其中delay只在第一次运行时有效。

还可以不要period参数,表示只执行一次。例如,timer.schedule(task, 10);

同一个定时器可以执行多个任务。

线程同步

有时候线程的抢占可能导致问题。例:

public class Machine implements Runnable
{
private int a = 1;//共享
public void run()
{
for(int i=1; i<=100; i++)
{
a+=i;
Thread.yield();//让给其他线程执行
a-=i;
System.out.println(a);
}
}

public static void main(String[] args)
{
Machine machine = new Mahcine();
Thread t1 = new Thread(machine);
Thread t2 = new Thread(machine);
t1.start();
t2.start();
}
}

上面这个程序本来是要一直输出零的,但是因为让给了t2执行,所以会输出1 2 2 3 …。这样就和原来的逻辑不符。

逻辑紧密相关的一组操作叫做原子操作,为了防止原子操作被打断,就提出了线程同步思想。

线程同步只有在两个线程使用同一变量时才会使用。如上面都使用了machine的a,另外一种情况是都使用了全局变量。

synchronized同步代码块

例:

public String pop()
{
synchronized(this)
{
String goods = buffer[point];
...
}
...
}

这样就设置了this对象的锁。

  • 如果这个锁被其他线程占用,那么就会把该线程加入锁池中,进入阻塞状态
  • 如果没有线程占用,那么他就会占用并执行代码块。

也可以这样写;

public synchronized String pop()
{
...
}

注意,在同步代码块中同样也可以使用sleep和yield,同样也是把cpu给其他线程。只是如果其他线程正好碰到了同步代码块那么又要将控制权归还,

synchronized不会被继承,也就是说哪怕父类中写了synchronized如果子类没写也不是同步的。

同步代码块中应包含尽量少的操作,因为操作多了可能一线程要在这里工作很久,其他线程就会都进入锁池中无法工作,这样会给一些需要即时反应的线程带来麻烦

释放对象的锁

释放锁的情况:

  • 执行完同步代码块,会释放锁
  • 线程异常终止时
  • 执行了锁所属对象的wait()方法,这个线程会释放锁,并进入等待吃

但是使用sleep或yield只会放弃cpu,并不会释放锁。还有suspend方法

死锁

死锁指的是a在等b锁释放,b在等a锁释放,这样就永运无法释放,最为关键的是虚拟机并不会检查这类问题,所以只有程序员自己注意。

线程通信

线程通信就是一个线程告诉另外一个线程某个信息,通过配合完成某件事。

java.lang.Object类中有两个用于线程通信的方法:

  • wait() 释放对象的锁,然后把该线程放入等待池中,等待其他线程把它唤醒
  • notify() 唤醒在等待池中的线程。随机选取等待池中的线程,并加入锁池中。

进入等待池后,锁和cpu全部放弃。如果使用notify也只是进入锁池,还要和其他线程争夺锁。

notifyAll() 唤醒所有在等待池中的线程。

注意:wait()方法必须放在一个循环中。因为在调用notify后并不是立刻可以得到执行,而是先要获得该对象的锁和cpu执行权限才可以运行。这个时候可能其他线程又将状态改变了,这时又要重新运行。

要注意的是想要唤醒等待池中的线程首先自己要掌握这个锁,也就是说必须在同步代码块中写notify,不然会报IllegalMonitorStateException错误。

public synchronized String pop()
{
this.notifyAll();

if(point == -1)
{
System.out.ptintln(Thread.currentThread().getName()+":wait");
try
{
this.wait();
}
catch(InterruptedException e)
{
throw new RuntimeException(e);
}
...
}
}

上面例子中,首先point是-1就要让一个线程处理,现在point是-1,然后一个线程调用wait()进入等待池,之后另一个线程操作了这个方法唤醒了该线程,但是这个时候线程并没有执行,而是先让其他的线程执行了一会才开始执行。但是其他线程执行过程中又把point变成-1了,这个时候处理线程已经结束,就会出现问题。

此外,wait必须写在synchronized中,不然运行时会报错。

中断阻塞

当线程A处于阻塞状态的时候,B调用A的interrupt()方法,那么A会发送一个InterruptedExecption。

实际上interrupt方法如果处理处于阻塞状态的线程(如wait,sleep,join等)才会抛出异常,实际上它是把一个中断线程的标志位设为true,因为处于阻塞状态,所以抛出异常,仅此而已。决定是否退出线程还是由我们自己决定的,如果我们在catch块中没有退出,那么还是会继续运行。

如果interrput一个正在处于运行的线程,那么只会把标志位设置成true,不会做其他事。

可以通过isinterrupt来看标志位是否变成true

例:

public class Machine extends Thread
{
private int a = 0;
private Timer timer = new Timer(true);
public synchronized void reset()
{
a = 0;
}
public void run()
{
final Thread thread = Thread.currentThread();
TimerTask timerTask = new TimerTask()
{
publc void run()
{
System.out.println(thread.getName()+"has waited for 3s");
thread.interrupt();
}
};

while(true)
{
synchronized(this)
{
timer.schedule(timerTask, 3000);
try
{
this.wait();
}
catch(InterruptException e)
{
System.out.println(thread.getName);
return;
}
}
a++;
System.out.println("a="+a);
}
}
public static void main(String[] args)
{
Machine machine = new Machine();
machine.start();
}
}

这段程序作用是如果a>3,那么就把线程放入等待池,如果等待时间超过三秒,那么就抛出InterruptException信号从而中断线程。

线程控制

  • start() 启动线程
  • suspend() 使线程暂停
  • resume() 使暂停的线程恢复运行
  • stop() 终止线程

但是其实后面三种已经被废弃了,但是可以通过编程实现同样的功能

用编程方式控制线程

可以设置一个标志变量来表示现在的状态,假设标志变量有三个值。

  • SUSP, 暂停状态
  • STOP, 终止状态
  • RUN, 运行状态

例:

public class Control extends Thread
{
public static final int SUSP = 1;//设置静态变量的话所有类都可以看到
public static final int STOP = 2;
public static final int RUN = 0;

public int state = RUN;

public synchronized void setState(int state)
{
this.state = state;
if(state == RUN)
{
notify();
}
}
public synchronized boolean checkState()
{
while(state == SUSP)
{
try
{
System.out.println(Thread.currentThread().getName()+"wait");
wait();
}
catch(InterruptException e)
{
throw new RuntimeException(e.getMessage());
}
}
if(state == STOP)
{
return false;
}
return true;
}
}

通过上面的两个方法,就可以对线程进行控制,但注意这种控制不是实时的,也就是说即使执行了setState也不会立刻进入暂停状态,而是machine先获得cpu,开始执行checkState方法时才会进入暂停状态。

线程组

ThreadGroup类表示线程组,他可以对一组线程集中管理。用户创建的线程都属于某个线程组。

指定线程组: Thread(ThreadGroup group, String name)

如果线程A创建线程B且创建时没有指定线程组,那么会自动加入A的线程组中。一旦线程加入线程组,就不能退出

用户创建的线程组都有父线程组,默认情况下,如果A创建了一个新线程组,那么A所在的线程组就是父亲线程组。

指定父亲线程组: ThreadGroup(ThreadGroup parent, String name)

可以使用activeCount()返回当前活着的线程,enumerate(Thread[] tarray)把或者的线程复制到tarray中。

处理线程未捕获的异常

如果线程没有捕获异常,那么虚拟机会找UncaughtExceptionHandler实例(这东西是个接口)。并且调用它的uncaughtException(Thread t, Throwable e)方法

设置异常处理类:

setDefaultUncaughtExcpetionHandler(Thread.UncaughtExceptionHandler eh)
setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)

第一个是设置默认异常处理器(静态方法),第二个是设置当前异常处理器(实例方法)。

ThreadGroup线程组实现了这个接口。每次出现未捕获异常时,先找当前线程的异常处理器,如果没找到就用线程组的异常处理器。

并且线程组的异常处理器还不是直接调用。如果这个线程有父线程,那么就调用父线程的。如果没有父线程,那么如果自己实现了这个接口,那么就用。如果没有,那么就打印调用堆栈的异常信息。

ThreadLocal类

这个类用来存放线程的局部变量。这些局部变量是每个线程独有的,不会共享。

主要有三个方法:

public T get(): 返回当前线程局部变量 protected T initialValue(): 返回局部变量初始值 public void set(T value): 设置局部变量

其中initialValue()只会在第一次调用get()或set()时才会被使用,并且只会执行一次。

concurrent并发包

在编写多线程程序时,既要考虑并发,又要防止死锁,还要考虑性能,难度很大。为了降低难度,增加了java.util.concurrent包。下面是包含的类和接口

Lock外部锁

这个主要用于线程同步。这是由类提供的锁,区别于对象的锁(可以叫内部锁)。他有几个方法。

  • lock() 获得当前线程的锁,如果被占用,那么进入阻塞状态。这和内部锁是一样的
  • tryLock() 试图获得当前线程的锁(看看现在锁是不是有人用),如果被占用,就返回false,否则返回true。
  • tryLock(long time, TimeUnit unit),如果超过了设置时间没有获得锁,放回false。例如 tryLock(50L, TimeUnit.SECONDS)表示时间限制50s
  • unlock() 释放线程锁占用的锁。

    Lock接口有一个实现类ReentrantLock,构造方法ReentrantLock(boolean fair).fair如果是真,那么会采用公平策略。公平策略是指让阻塞时间长的更有可能获得锁。这是以性能作为代价的。

    例:

    public class Sample
    {
    private final ReentrantLock lock = new ReentrantLock();//创建

    public void method()
    {
    lock.lock();
    try
    {
    //
    }
    catch
    {
    ...
    }
    finally
    {
    lock.unlock();//释放锁
    }
    }
    }

    外部锁可以用来弥补内部所的一些不足,我们可以直接用lock,unlock而不使用synchronized。

    Condition 线程通信接口

    java.lang.concurrent.locks.Conditon用于线程通信。Lock接口的newCondition()方法返回Condition的实例。

    方法:

  • await() 和wait()相似

  • await(long time, TimeUnit unit):和上面类似,如果超过时间线程没有被唤醒,返回false。
  • signal(): 和notify()类似。
  • signalAll():和notifyAll()类似

    Callable和Feture

    这两个是用来做异步计算的。Runnable接口的返回值是void,如果其他线程需要这个线程的返回值怎么办呢?这个可以通过共享变量来实现,但是共享变量需要共享类,这又可能导致问题。所以这两个接口就是解决这类麻烦。

    Callable接口:和Runnable接口类似,Runnable中的run()相当于Callable中的call()。但是call可以有泛型的返回值。此外,这个不能作为Thread类的参数。

    Future接口: 保存运算结果,以下参数

  • get():返回异步运算的结果。如果结果没有出来,当前线程就会被阻塞直到运算结束。
  • get(long timeout, TimeUnit unit): 和第一个类似。只是如果超出时间还没有得到结果就会抛出TimeoutException。
  • cancel(boolean mayInterrupt): 取消该运算,如果运算没有开始,就立刻取消。如果已经开始,如果mayInterrrupt为true,那么也取消。
  • isCancelled():判断运算时候被取消
  • isDone():判断运算是否已经完成。

FutureTask:这是一个适配器,同时实现Runnable和Future接口。还关联了一个Callable实例。FutureTask可以作为Thread类的参数。FutureTask类的构造函数可以带Callable的参数。

Callable myComputation = new ...
FutureTask<Integer> task = new FutureTask<Integer>(myComputation);//Integer是返回值

例如:

public class Machine implements Callable<Integer>
{
public Integer call()
{
int sum = 0;
for(int i=1; i<=100; i++)
{
sum+=a;
try
{
Thread.currentThread().sleep(20);
}
catch(Exception e)
{
e.printStackTrace();
}
}
return sum;
}

public static void main(Stirng[] args)
{
FutureTask<Integer> task = new FutureTask<Integer>(new Machine());
Thread thread = new Thread(task);
threadMachine.start();
System.out.println("从1到100的和"+task.get());//调用get返回值
}
}

通过线程池管理多个线程

Executor表示线程池,execute方法用来执行command的run()中指定的任务,线程会调度空闲的线程来执行该任务。到底什么时候执行,这是由cpu决定的。

  • shutdown(): 预备关闭线程池。如果有任务提交上去,那么要等这些任务执行完后,才会关闭线程池,并且拒绝新任务的入内。
  • shutdownNow(): 终止已经开始的任务,立刻关闭线程池
  • isTermination(): 判断线程池是否关闭,如果关闭返回true
  • awaitTermination(): 等待线程池关闭。

submit(Callable task)和submit(Runnable task)和execute(Runnable command)类似,但是这个支持异步运算。他们都会返回异步运算结果的Future对象。

Excutors中静态方法:

  • newCachedThreadPool(): 创建有缓存的线程池,有任务才创建新线程,空闲的线程停留60s。
  • newFixedThreadPool(int nThreads): 创建有固定数目线程的线程池,空闲线程一直保留
  • newSingleThreadExecution(): 创建只有一个县城的线程池。这个与newFixedThreadPool(1)不同之处在于这个终止就终止了,Fixed的终止了还会创建一个新的。
  • newScheduledThreadPool(int corePoolSize): 线程池会按时间计划创建任务。corePoolsize是线程最小数目。
  • newSingleThreadScheduledExecutor(): 创建只有一个线程的线程池,这个线程池按计划进行任务。
public class Machine implements Runnable
{
private int id;
public Machine(int id)
{
this.id = id;
}
public void run()
{
System.out.println(id);
}

public static void main(String[] args)
{
ExecutorService service = Executors.newFixedThreadPool(2);
for(int i=0; i<5; i++)
{
service.execute(new Machine(i));
}
service.shutdown();
}
}

这里创建了5个线程给线程池,然后线程池中两个空白线程接到任务开始工作,工作完这两个线程后又执行下面两个线程,直到执行完5个线程。然后执行shutdown()关闭线程池。

BlockingQueue 阻塞队列

java.util.concurrent.BlockingQueue接口继承了java.util.Queue接口。BlockingQueue接口为多个线程同时操作一个队列提供了方案。

操作 抛出异常 放回特定值 线程阻塞 超时
添加元素 add(e) offer(e) put(e) offer(e, time, unit)
删除元素 remove() poll() take() poll(time, unit)
读取元素 element() peek()

前面两个是队列的,后面两个是阻塞队列的。这个具体等到队列那里再说。这里先把几个BlockingQueue的实现类列一下:

  • LinkedBlockingQueue类: 默认情况下,LinkedBlockingQueue的容量是没有上限的,也可以指定大小,这是基于链表的队列
  • ArrayBlockingQueue类: ArrayBlockingQueue(int capacity, boolean fair)可以设定容量,并且可以选择是否采用公平策略。这是基于数组的队列。
  • PriorityBlockingQueue:这是优先队列(堆)
  • DelayQueue: 这个队列中存放的是延期元素。这些元素必须实现java.util.concurrent.Delayed接口。只有延期满的元素才可以被取出或者删除。