线程-四

[TOC]

定时器

timer的使用

timer类的主要作用就是设置计划任务,但封装任务的类却是TimerTask类,

schedule(TimerTask task,Date time)

该方法的作用是在指定日期执行一次某一任务。
代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ThreadTimer {
private static Timer timer = new Timer();
public static void main(String[] args) throws ParseException {

timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("运行中。。。"+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
}, new Date(System.currentTimeMillis()+1000));
}

}

上述代码是在一秒之后打印当前时间。
虽然指定了执行时间,都是如果多个任务执行的话会先执行完前面的任务才会执行后面的任务。

schedule(TimerTask task,Date time,long period)

该方法时指定日期之后按照间隔周期无限的执行某一任务。

1
2
3
4
5
6
 timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("运行中。。。"+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
}, new Date(System.currentTimeMillis()+1000),1000);
1
2
3
4
5
6
7
运行中。。。2019-09-03 17:37:36
运行中。。。2019-09-03 17:37:36
运行中。。。2019-09-03 17:37:37
运行中。。。2019-09-03 17:37:38
运行中。。。2019-09-03 17:37:39
运行中。。。2019-09-03 17:37:40
运行中。。。2019-09-03 17:37:41

如果传入的时间早于当前时间,则立即执行任务。

schedule(TimerTask task,long delay)

延时delay之后执行一次任务

schedule(TimerTask task,long delay,long period)

延时delay之后执行任务,且每隔period循环一次。

scheduleAtFixedRate

该方法与schedule区别的只有延时,
使用schedule方法:如果执行任务的时间没有被延时,那么下次任务的执行时间参考上一次任务的开始时间来计算。
此方法则参考上一次任务结束的时间。
有延时则没有区别。

TimerTask的cancel

TimerTask的cancel方法时将自身从任务队列中移除

Timer的cancel

和Timertask的cancel不同,Timer的cancel的作用是将全部任务清空。

单例模式与多线程

在多线程的世界里,很多看起来很正常的代码可能会出现不一样的效果。比如单例模式。如果在设计不佳的情况下,就有可能在多线程环境下出现问题。
饿汉模式/立即加载
饿汉模式是在方法调用前就已经创建了实例。

1
2
3
4
5
6
7
public class MyObject{
private static MyObject obj=new MyObject();
private MyObject(){}
public static MyObject getInstance(){
return obj;
}
}

懒汉模式/延迟加载
懒汉模式即在使用的时候加载

1
2
3
4
5
6
7
8
9
10
class MyObject{
private static MyObject obj;
private MyObject(){}
public static MyObject getInstance(){
if (obj!=null){
return obj;
}
return obj=new MyObject();
}
}

但是,这两种模式在多线程下都是线程不安全的。即有可能创建多个实例。
解决方案

synchronized同步方法

在getInstance方法中加入synchronized即可,但是这种方法效率比较低下。

同步代码块

在方法中使用synchronized包裹代码块,这种方案跟同步方法差不多。而且效率也比较低。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class SingleModeService {
private static SingleModeService service;

private SingleModeService() {
}

public static SingleModeService getInstance() {
if (service == null) {
synchronized (SingleModeService.class) {
service = new SingleModeService();
}
}
return service;
}
}

将判断是否为空放到同步代码外,这样效率会提高一些。

双重检验锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class SingleModeService {
private static SingleModeService service;

private SingleModeService() {
}

public static SingleModeService getInstance() {
if (service == null) {
synchronized (SingleModeService.class) {
if(service==null){
service = new SingleModeService();
}
}
}
return service;
}
}

使用双重检验锁的原因还是因为前面的同步代码块其实并不安全。主要是因为判空是在同步代码块外面,所以有可能刚刚判断为空,另外一条线程立马就创建了一个实例,这时候已经过了判断空值的行。所以在进入到同步代码块之前还是有可能已经不为空了。

静态内部类

1
2
3
4
5
6
7
8
9
10
public class SingleMode {

private static class SingleModeHandler{
private static final SingleMode single=new SingleMode();
}
private SingleMode(){}
public static SingleMode getInstance(){
return SingleModeHandler.single;
}
}

静态内部类可以解决线程安全的问题。

使用static代码块

静态代码块中的代码在使用类的时候就已经执行了。所以可以使用这个特性来实现单例设计模式。

1
2
3
4
5
6
7
8
9
10
11
public class SingleMode {

private static SingleMode single;
static {
single=new SingleMode();
}
private SingleMode(){}
public static SingleMode getInstance(){
return single;
}
}

使用枚举实现单例

在《Java编程思想》中推荐使用的方式。这也是由于枚举的特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SingleEnum {
public static void main(String[] args) {

for (int i = 0; i < 10; i++) {
Thread thread = new Thread( () -> {
System.out.println( ServiceEnum.instance.hashCode());
});
thread.start();
}
}
}

enum ServiceEnum {
instance;
private SingleEnum singleEnum;
private ServiceEnum(){
singleEnum=new SingleEnum();
}
}

输出的结果也是正确的。