Java线程池工作原理

Java线程池工作原理

Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池,所以我们就要认识并弄懂线程池,以便于更好为业务场景服务。
(异步与并发)

一、线程池的好处

在开发过程中,合理地使用线程池大致有3个好处:

  • 第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

但是,要做到合理利用线程池,必须对其实现原理了如指掌。

二、线程池工作流程

1)当提交一个新任务到线程池时,线程池判断corePoolSize线程池是否都在执行任务,如果有空闲线程,则创建一个新的工作线程来执行任务,直到当前线程数等于corePoolSize;

2)如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;

3)如果阻塞队列满了,那就创建新的线程执行当前任务,直到线程池中的线程数达到maxPoolSize,这时再有任务来,由饱和策略来处理提交的任务。

三、线程池参数

下面是ThreadPoolExecutor类的构造方法传参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public ThreadPoolExecutor(
int corePoolSize, #核心线程数
int maximumPoolSize, #最大线程数
long keepAliveTime, #达到最大线程数数时候,线程池的工作线程空闲后,保持存活的时间
TimeUnit unit, #keepAliveTime单位
BlockingQueue<Runnable> workQueue #阻塞队列
RejectedExecutionHandler handler #饱和策略

) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}

new ThreadPoolExecutor(6 ,12, 5L, TimeUnit.SECONDS,new LinkedBlockingQueue<Runnable>(10),new ThreadPoolExecutor.CallerRunsPolicy());

比如corePoolSize为6,maximumPoolSize为12,keepAliveTime为5秒,队列长度为10;提交任务数达到核心线程数6时候,新来的任务就会被放入LinkedBlockingQueue阻塞队列。
当队列任务数达到10个时候,就会创建新线程执行任务,直到达到maximumPoolSize数量12。如果还有新来的任务,由策略来处理提交的任务;如果没有,线程池空闲时候,超过5秒,创建的maximumPoolSize,就会被销毁。

四、阻塞队列

阻塞队列BlockingQueue接口,从jdk1.5开始,有四个实现类,jdk8亦是如此

  • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。

  • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue,静态工厂方法Executors.newFixedThreadPool()使用了这个队列。

  • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个列。

  • PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

五、饱和策略

当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。在JDK 1.5中Java线程池框架提供了以下4种策略。

  • AbortPolicy:直接抛出异常。
  • CallerRunsPolicy:只用调用者所在线程来运行任务。
  • DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
  • DiscardPolicy:不处理,丢弃掉。

当然,也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化存储不能处理的任务。

六、向线程池提交任务

可以使用两个方法向线程池提交任务,分别为execute()和submit()方法

  • 1、execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功
  • 2、submit()方法用于提交需要返回值的任务。

线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit),在指定的时间内会等待任务执行,超时则抛出超时异常,等待时候会阻塞当前线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package com.mine.test;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class ThreadPoolTest {
public static void main(String[] args) {

// 新建线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(6, // 核心线程数
12, // 最大线程数
5L, // KeepAlive Time long
TimeUnit.SECONDS, // TimeOut
new LinkedBlockingQueue<Runnable>(10), // 阻塞队列
new ThreadPoolExecutor.CallerRunsPolicy()// 饱和策略
);

// 向线程池提交任务
// 1、execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功

threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println(
"执行当前线程体,线程名: " + Thread.currentThread().getName() + "当前:" + System.currentTimeMillis());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});

Future<?> future = threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("执行当前线程体,线程名: " + Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});

/**
* 线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,
* get()方法会阻塞当前线程直到任务完成, 而使用get(long timeout,TimeUnit
* unit),在指定的时间内会等待任务执行,超时则抛出超时异常,等待时候会阻塞当前线程
*/
try {
// 阻塞当前线程,直到任务完成
Object obj = future.get();
// 当前线程等待执行结果的返回值,延迟2s
Object obj2 = future.get(10, TimeUnit.MINUTES);

} catch (InterruptedException e) {
// 处理中断异常
// TODO: handle exception
} catch (ExecutionException e) {
// 处理执行异常
// TODO Auto-generated catch block
e.printStackTrace();
} catch (TimeoutException e) {
// 处理超时异常
e.printStackTrace();
} finally {// 关闭线程池
threadPoolExecutor.shutdown();
}
threadPoolExecutor.shutdown();
}
}

七、关闭线程池

ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow(),它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程

shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务。

shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。

只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务
都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。

因此,判断线程池所有线程是否执行完成,可以这样写:

1
2
3
4
5
6
7
while(true){//死循环
if(threadPool.isTerminated()) {
//执行自己的操作
break;//true停止
}
Thread.sleep(500);//休眠500继续循环
}

shutdown,只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程,等待执行任务的线程完成。

shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。

八、线程池状态

线程池有五种运行状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1RUNNING

(1) 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
(2) 状态切换:线程池的初始化状态是RUNNING。线程池被一旦被创建,
就处于RUNNING状态,且线程池中的任务数为0

2SHUTDOWN

(1) 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
(2) 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN

3STOP

(1) 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
(2) 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP

4TIDYING

(1) 状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为
TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。
terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,
进行相应的处理;可以通过重载terminated()函数来实现。
(2) 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务
也为空时,就会由 SHUTDOWN->TIDYING。当线程池在STOP状态下,线程池中执行的任务
为空时,就会由STOP -> TIDYING

5TERMINATED

(1) 状态说明:线程池彻底终止,就变成TERMINATED状态。
(2) 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,
就会由 TIDYING -> TERMINATED

Java对象的大小

Java对象的大小

基本数据的类型的大小是固定的,这里就不多说了。对于非基本类型的Java对象,其大小就值得商榷。

在Java中,一个空Object对象的大小是8byte,这个大小只是保存堆中一个没有任何属性的对象的大小。看下面语句:

基本类型包括:byte, short, int, long, char, float, double, Boolean

引用类型包括:类类型,接口类型和数组。

1
Object ob = new Object();

这样在程序中完成了一个Java对象的生命,但是它所占的空间为:4byte+8byte。4byte是因为在程序中,创建一个对象,Java栈中保存引用的所需要的空间。而那8byte则是Java堆中对象的信息。因为所有的Java非基本类型的对象都需要默认继承Object对象,因此不论什么样的Java对象,其大小都必须是大于8byte。

1
2
3
4
5
6
7
8
9
10
11
Class NewObject {

int count;

boolean flag;

Object ob;

}

其大小为:空对象大小(8byte)+int大小(4byte)+Boolean大小(1byte)+空Object引用的大小(4byte)=17byte。但是因为Java在对对象内存分配时都是以8的整数倍来分,因此大于17byte的最接近8的整数倍的是24,因此此对象的大小为24byte

这里需要注意一下基本类型的包装类型的大小。因为这种包装类型已经成为对象了,因此需要把他们作为对象来看待。包装类型的大小至少是12byte(声明一个空Object至少需要的空间),而且12byte没有包含任何有效信息,同时,因为Java对象大小是8的整数倍,因此一个基本类型包装类的大小至少是16byte。这个内存占用是很恐怖的,它是使用基本类型的N倍(N>2),有些类型的内存占用更是夸张(随便想下就知道了)。因此,可能的话应尽量少使用包装类。在JDK5.0以后,因为加入了自动类型装换,因此,Java虚拟机会在存储方面进行相应的优化。

引用类型

引用类型

对象引用类型分为强引用、软引用、弱引用和虚引用。

强引用: 就是我们一般声明对象是时虚拟机生成的引用,强引用环境下,垃圾回收时需要严格判断当前对象是否被强引用,如果被强引用,则不会被垃圾回收

软引用: 软引用一般被做为缓存来使用。与强引用的区别是,软引用在垃圾回收时,虚拟机会根据当前系统的剩余内存来决定是否对软引用进行回收。如果剩余内存比较紧张,则虚拟机会回收软引用所引用的空间;如果剩余内存相对富裕,则不会进行回收。换句话说,虚拟机在发生OutOfMemory时,肯定是没有软引用存在的。

弱引用: 弱引用与软引用类似,都是作为缓存来使用。但与软引用不同,弱引用在进行垃圾回收时,是一定会被回收掉的,因此其生命周期只存在于一个垃圾回收周期内。

强引用不用说,我们系统一般在使用时都是用的强引用。而“软引用”和“弱引用”比较少见。他们一般被作为缓存使用,而且一般是在内存大小比较受限的情况下做为缓存。因为如果内存足够大的话,可以直接使用强引用作为缓存即可,同时可控性更高。因而,他们常见的是被使用在桌面应用系统的缓存。

并发编程

并发编程

JVM内存模型(JMM)
1.Java当中的线程通讯和消息传递
2.什么是重排序和顺序一致性?Happens-Before?As-If-Serial?

Synchronized的概念和分析
1.同步,重量级锁以及Sychronized的原理分析
2.自旋锁,偏向锁,轻量级锁,重量级锁的概念、使用以及如何优化

Volatile和DCL的知识
1.Volatile的使用场景和Volatile实现机制、内存语义、内存模型
2.DCL的单例模式,什么是DCL?如何来解决DCL的问题

并发基础之AQS的深度分析
1.AnstractAueuedSynchronizer同步器的概念、CLH同步队列是什么?
2.同步状态的获取和释放、线程阻塞和唤醒

Lock和并发常用工具类
1.Java当中的Lock、ReenrantLock、ReentrantReadWriteLock、Condition
2.Java当中的并发工具类CyclicBarrier、CountdownLatch、Semphore
3.Java当中的并发集合类ConcurrentHashMap、ConcurrentLinkedQueue…

原子操作常用知识讲解
1.基本类型的原子操作比如经典的AtomicBoolean、AtomicIngter、AtomicLong
2.数组类型的原子操作代表的几个类AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
3.引用类型的原子操作的典型AtomicReference、AtomicReferenceFieldUpdater…
4.CAS概念知识、COmpare And Swap以及缺陷

线程池和并发并行
1.Excutor、ThreadPoolExcutor、Callable & Future、ScheduledExcutorService
2.ThreadLocal、Fork & Join?什么是并行?线程池如何保证核心线程不被销毁?

对象实例与对象引用有何不同

对象实例与对象引用有何不同

new运算符,new创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。
一个对象引用可以指向0个或1个对象(一根绳子可以不系气球,也可以系一个气球);
一个对象可以有n个引用指向它(可以用n条绳子系住一个气球)。

1)分配地址
2)创建对象实例(堆内存)
3)对象引用指向对象实例 (栈内存)

栈代表了处理逻辑,而堆代表了数据。

栈是运行时的单位,而堆是存储式的单位。

栈解决程序运行的问题,解决程序如何运行的问题,如何处理数据。

堆解决数据存储问题,数据存哪,怎么存。

定时任务

定时任务

Java实现定时任务的三种方法:

  • 普通thread实现
  • TimerTask实现
  • ScheduledExecutorService实现

普通thread

这是最常见的,创建一个thread,然后让它在while循环里一直运行着,通过sleep方法来达到定时任务的效果。这样可以快速简单的实现,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ScheduleTask {
public static void main(String[] args) {
// run in a second 定时任务
final long timeInterval = 1000;
Runnable runnable = new Runnable() {
public void run() {
while (true) {
// ------- code for task to run
System.out.println("Hello !!");
// ------- ends here
try {
Thread.sleep(timeInterval);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread thread = new Thread(runnable);
thread.start();
}
}

Timer和TimerTask

上面的实现是非常快速简便的,但它也缺少一些功能。
用Timer和TimerTask的话与上述方法相比有如下好处:

  • 当启动和去取消任务时可以控制
  • 第一次执行任务时可以指定你想要的delay时间
  • 在实现时,Timer类可以调度任务,TimerTask则是通过在run()方法里实现具体任务。
  • Timer实例可以调度多任务,它是线程安全的。

当Timer的构造器被调用时,它创建了一个线程,这个线程可以用来调度任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.Timer;
import java.util.TimerTask;

public class TimerTaskTest {

public static void main(String[] args) {

TimerTask task = new TimerTask() {

@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("Hello World!");
}

};

Timer timer = new Timer();
long delay = 0;
long inteval = 1 * 1000;

timer.scheduleAtFixedRate(task, delay, inteval);
}

}

ScheduledExecutorService

是从Java SE 5的java.util.concurrent里,做为并发工具类被引进的,这是最理想的定时任务实现方式。
相比于上两个方法,它有以下好处:

  • 相比于Timer的单线程,它是通过线程池的方式来执行任务的
  • 可以很灵活的去设定第一次执行任务delay时间
  • 提供了良好的约定,以便设定执行的时间间隔
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledExecutorServiceTest {
public static void main(String[] args) {

Runnable runnable = new Runnable() {
public void run() {
// task to run goes here
System.out.println("Hello !!");
}
};

ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
service.scheduleAtFixedRate(runnable, 0, 1, TimeUnit.SECONDS);
}
}

接口和抽象类的区别

接口和抽象类的区别

接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。

接口中除了static、final变量,不能有其他变量,而抽象类中则不一定。

一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过extends关键字扩展多个接口。

接口方法默认修饰符是public,抽象方法可以有public、protected和default这些修饰符(抽象方法就是为了被重写所以不能使用private关键字修饰!)。

从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。

Thread sleep() 和 wait()

Thread sleep() 和 wait()

sleep()方法是Thread类里面的,主要的意义就是让当前线程停止执行,让出CPU给其他的线程,但是不会释放对象锁资源以及监控的状态,当指定的时间到了之后又会自动恢复运行状态。

wait()方法是Object类里面的,主要的意义就是让线程放弃当前的对象的锁,进入等待此对象的等待锁定池,只有针对此对象调动notify方法后本线程才能够进入对象锁定池准备获取对象锁进入运行状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

public class ThreadSleepWait {
/** java中的sleep()和wait()的区别 */
public static void main(String[] args) {
new Thread(new Thread1()).start();
try {
System.out.println("主线程Sleep");
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}
new Thread(new Thread2()).start();
}

private static class Thread1 implements Runnable{
@Override
public void run(){
synchronized (ThreadSleepWait.class) {
System.out.println("启动线程" + Thread.currentThread().getName());
System.out.println("线程等待中...");
try {
//调用wait()方法,线程会放弃对象锁,进入等待此对象的等待锁定池
ThreadSleepWait.class.wait();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("线程1继续");
System.out.println("线程1结束");
}
}
}

private static class Thread2 implements Runnable{
@Override
public void run(){
synchronized (ThreadSleepWait.class) {
System.out.println("启动线程" + Thread.currentThread().getName());
System.out.println("线程等待中...");
//只有针对此锁对象调用notify后
//本线程才进入对象锁定池准备获取对象锁进入运行状态。
ThreadSleepWait.class.notify();

//区别
//如果我们把代码:ThreadSleepWait.class.notify给注释掉,
//ThreadSleepWait.class调用了wait()方法但是没有调用notify()方法,
//则线程1永远处于挂起状态。

try {
//Sleep方法导致了程序暂停执行指定的时间,让出cpu该其他线程,
//但是他的监控状态依然保持者,当指定的时间过又会自动恢复运行状态。
//在调用sleep方法的过程中,线程不会释放对象锁。
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("线程2继续");
System.out.println("线程2结束");
}
}
}
}

运行效果:

1
2
3
4
5
6
7
8
9
主线程Sleep
启动线程Thread-0
线程等待中...
启动线程Thread-1
线程等待中...
线程2继续
线程2结束
线程1继续
线程1结束

如果注释掉代码,之后调用

1
ThreadSleepWait.class.notify();

运行效果:

1
2
3
4
5
6
7
主线程Sleep
启动线程Thread-0
线程等待中...
启动线程Thread-1
线程等待中...
线程2继续
线程2结束

Runnable接口程实现多线程

Runnable接口程实现多线程

写一个类实现Runnable接口,实现run方法。用new Thread(Runnableclass).start()方法来启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class RunnableTask {

public static void main(String[] args) {
RunnableTask rt = new RunnableTask();
Runner1 runner1 = rt.new Runner1();
Runner2 runner2 = rt.new Runner2();

Thread thread1 = new Thread(runner1);
Thread thread2 = new Thread(runner2);
thread1.start();
thread2.start();
//thread1.run();
//thread2.run();
}


class Runner1 implements Runnable {
// 实现了Runnable接口,jdk就知道这个类是一个线程
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("进入Runner1运行状态——————————" + i);
}
}
}

class Runner2 implements Runnable {
// 实现了Runnable接口,jdk就知道这个类是一个线程
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("进入Runner2运行状态==========" + i);
}
}
}
}

Thread实现多线程一

Thread实现多线程一

Thread类实现了Runnable接口。

以下是关系到线程运行状态的几个方法:

1)start方法

start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。

2)run方法

run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。

3)sleep方法

sleep方法有两个重载版本:

1
2
sleep(long millis)     //参数为毫秒
sleep(long millis,int nanoseconds) //第一参数为毫秒,第二个参数为纳秒

sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。

但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import java.util.Date;

public class MutipleThread {

private int i = 10;
private Object object = new Object(); //锁


public class ProcessThread extends Thread{
@Override
public void run() {
System.out.println("开始线程" + new Date());
//会上锁
synchronized (object) {
i++;
System.out.println("i:" + i);
try {
System.out.println("线程" + Thread.currentThread().getName() + "进入睡眠状态");
Thread.currentThread().sleep(10000);
} catch (InterruptedException e) {
// TODO: handle exception
}
System.out.println("线程"+Thread.currentThread().getName()+"睡眠结束");
i++;
System.out.println("i:" + i);
}
}
}

public static void main(String[] args) {
// TODO Auto-generated method stub
MutipleThread mThread = new MutipleThread();
ProcessThread pThread = mThread.new ProcessThread();
ProcessThread pThread2 = mThread.new ProcessThread();
pThread.start();
pThread2.start();
}
}

执行结果 :

1
2
3
4
5
6
7
8
9
10
开始线程Thu Jun 27 09:43:40 CST 2019
i:11
线程Thread-0进入睡眠状态
开始线程Thu Jun 27 09:43:40 CST 2019
线程Thread-0睡眠结束
i:12
i:13
线程Thread-1进入睡眠状态
线程Thread-1睡眠结束
i:14

从上面输出结果可以看出,当Thread-0进入睡眠状态之后,Thread-1并没有去执行具体的任务。只有当Thread-0执行完之后,此时Thread-0释放了对象锁,Thread-1才开始执行。

注意,如果调用了sleep方法,必须捕获InterruptedException异常或者将该异常向上层抛出。当线程睡眠时间满后,不一定会立即得到执行,因为此时可能CPU正在执行其他的任务。所以说调用sleep方法相当于让线程进入阻塞状态。