- 程序(program):一段静态的文本代码。
- 进程(process):当程序加载进内存运行起来后,就成了进程。是一个动态的过程:有其自身的生命周期。
- 线程(thread):一个进程里面的一条或者几条执行路径。
- 若一个进程同一时间并行执行多个代码,就是支持多线程的。
- 但是由于线程共享JVM中的方法区和堆,多个线程操作共享的系统资源可能会有冲突,带来安全隐患。
| 每个线程独享一份 | 每个线程共享的 |
|---|---|
| 虚拟机栈 (VM Stack) | 堆 (Heap) |
| 程序计数器 (Program Counter Register) | 方法区 (Method Area) |
| 并行 | 并发 |
|---|---|
| 多个CPU同时执行多个任务。比如:多个人同时做不同的事。 | 一个CPU( 采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。 |
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
- 需要 一些后台运行的程序时。
( 1 1 1) 创建一个继承于Thread类的子类;
( 2 2 2) 重写Thread类中的run()方法;
( 3 3 3) 创建Thread类的子类的对象;
( 4 4 4) 通过对象调用start()方法开始执行此多线程任务。
- 其中,start() 方法的作用有:启动当前线程和调用当前线程的run() 方法。
例:
public class CreateThreadTest extends Thread{
@Override
public void run() {
for (int i=0;i<20;i++){
if (i%2==0){
System.out.println(i);
}
}
}
public static void main(String[] args) {
CreateThreadTest thread1 = new CreateThreadTest();
thread1.start();
//以下是主线程输出奇数
for (int i=0;i<20;i++){
if (i%2!=0){
System.out.println(i+"*******main********");
}
}
}
}
输出:
1*******main******** 3*******main******** 5*******main******** 7*******main******** 0 2 4 6 8 10 12 14 16 18 9*******main******** 11*******main******** 13*******main******** 15*******main******** 17*******main******** 19*******main********
从结果可以看到:
- thread1 线程中的输出偶数的线程和main() 函数主线程中的输出奇数的线程,是同时进行的。
- 两个线程的输出由于是同时进行的,因此输出结果会互相穿插。
-
不能通过调用 thread1.run(); 的方式来启动该线程。否则仍然是在主线程 (main) 中按顺序执行罢了。并没有开辟新的线程去执行。
-
想要再启动一个线程,不能再写一次 thread1.start(); 。否则在main 线程中出现 2 2 2 次 thread1.start(); 会报illegalThreadStateException 异常。正确的方法是:再new一个线程对象。
CreateThreadTest thread1 = new CreateThreadTest(); thread1.start(); //正确的再次启动线程方法 CreateThreadTest thread2 = new CreateThreadTest(); thread2.start();
题目:
练习:创建两个分线程,其中一个线程遍历100以内的偶数,另一个线程遍历100以内的奇数。
我的首次答案:
public class ThreadExer1 {
public static void main(String[] args) {
Thread1 t1 = new Thread1();
t1.start();
Thread2 t2 = new Thread2();
t2.start();
}
}
class Thread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
class Thread2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
输出结果:
Thread-0:0 Thread-0:2 Thread-0:4 Thread-0:6 Thread-0:8 Thread-0:10 Thread-0:12 Thread-0:14 Thread-0:16 Thread-0:18 Thread-0:20 Thread-0:22 Thread-0:24 Thread-0:26 Thread-0:28 Thread-0:30 Thread-0:32 Thread-0:34 Thread-0:36 Thread-0:38 Thread-0:40 Thread-0:42 Thread-0:44 Thread-0:46 Thread-0:48 Thread-0:50 Thread-0:52 Thread-0:54 Thread-0:56 Thread-0:58 Thread-0:60 Thread-0:62 Thread-0:64 Thread-0:66 Thread-0:68 Thread-0:70 Thread-0:72 Thread-0:74 Thread-1:1 Thread-1:3 Thread-1:5 Thread-1:7 Thread-1:9 Thread-1:11 Thread-1:13 Thread-1:15 Thread-1:17 Thread-1:19 Thread-0:76 Thread-0:78 Thread-0:80 Thread-0:82 Thread-0:84 Thread-0:86 Thread-0:88 Thread-0:90 Thread-0:92 Thread-0:94 Thread-0:96 Thread-0:98 Thread-1:21 Thread-1:23 Thread-1:25 Thread-1:27 Thread-1:29 Thread-1:31 Thread-1:33 Thread-1:35 Thread-1:37 Thread-1:39 Thread-1:41 Thread-1:43 Thread-1:45 Thread-1:47 Thread-1:49 Thread-1:51 Thread-1:53 Thread-1:55 Thread-1:57 Thread-1:59 Thread-1:61 Thread-1:63 Thread-1:65 Thread-1:67 Thread-1:69 Thread-1:71 Thread-1:73 Thread-1:75 Thread-1:77 Thread-1:79 Thread-1:81 Thread-1:83 Thread-1:85 Thread-1:87 Thread-1:89 Thread-1:91 Thread-1:93 Thread-1:95 Thread-1:97 Thread-1:99
练习的体会:
-
输出的线程会互相交错出现,说明2条线程确实是并行运行的。
-
可以用
System.out.println(Thread.currentThread().getName());
来查看当前执行的线程是哪一个。
-
当需要执行多个不同功能的线程时,需要创建不同的子类来实现。
- 上述练习创建的线程对象,都是只用一次。因此可以创建匿名子类的匿名对象来启动线程。
优化后的代码:
//线程1-遍历100以内的偶数
new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
//线程2-遍历100以内的奇数
new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
10.2.4 线程的常用方法
| 线程方法 | 作用 |
|---|---|
| void start() | 启动线程,并执行对象的 run()方法 |
| run() | 线程在被调度时执行的操作 |
| String getName() | 返回线程的名称 |
| void setName() | 设置该线程名称 |
| static Thread currentThread() | 返回当前线程。在 Thread 子类中就是this ,通常用于主线程和 Runnable实现类 |
| static void yield() | 线程让步:暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程;若队列中没有同优先级的线程,忽略此方法 |
| join() | 当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止。低优先级的线程也可以获得执行。 |
| static void sleep(long millis):(指定时间:毫秒) | ① 令当前活动线程在指定时间段内放弃对 CPU 控制,使其他线程有机会被执行,时间到后重新排队。② 抛出 InterruptedException 异常。 |
| stop() | 强制线程生命期结束,不推荐使用! |
| boolean isAlive() | 返回 boolean ,判断线程是否还活着 |
使用例子:
( 1 1 1) 设置线程名称
- 次线程可通过 setName() 设置名字。
- 主线程可通过 currentThread().setName() 设置名字。
public class ThreadMethods {
public static void main(String[] args) {
ThreadName threadName = new ThreadName();
threadName.setName("求偶数的线程");
threadName.start();
//主线程设置名字操作
Thread.currentThread().setName("主线程");
for (int i = 0; i < 20; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
//展示如何给线程取名的操作
class ThreadName extends Thread{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
输出:
主线程:1 主线程:3 主线程:5 求偶数的线程:0 求偶数的线程:2 求偶数的线程:4 求偶数的线程:6 求偶数的线程:8 主线程:7 主线程:9 主线程:11 主线程:13 主线程:15 主线程:17 主线程:19 求偶数的线程:10 求偶数的线程:12 求偶数的线程:14 求偶数的线程:16 求偶数的线程:18
( 2 2 2) yield线程让步
-
放弃当前的线程。然后有两种可能:
① 被放弃的线程被别的线程抢到执行;
② 又被自己抢到执行机会。这样就会看起来没什么区别。
-
例子:当 i i i 能被4整除时,调用 yield 。
public class ThreadMethods {
public static void main(String[] args) {
ThreadName threadName = new ThreadName();
threadName.setName("求偶数的线程");
threadName.start();
//主线程设置名字操作
Thread.currentThread().setName("主线程");
for (int i = 0; i < 20; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
//展示如何给线程取名的操作
class ThreadName extends Thread {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
if (i % 4 == 0) {
Thread.yield();
}
}
}
}
输出:
主线程:1 主线程:3 主线程:5 主线程:7 主线程:9 求偶数的线程:0 主线程:11 求偶数的线程:2 求偶数的线程:4 主线程:13 主线程:15 主线程:17 主线程:19 求偶数的线程:6 求偶数的线程:8 求偶数的线程:10 求偶数的线程:12 求偶数的线程:14 求偶数的线程:16 求偶数的线程:18
- 可以看到,当 求偶数的线程:4 时,线程放弃执行机会,后面就被主线程抢到。直到主线程输出完所有20以内的奇数为止。
( 3 3 3) join操作:打断当前线程的执行,插入其他线程
-
join() 操作就是一个插队操作。在当前线程中插入别的线程的 join() 方法后,当前线程就中断,去执行另一个线程直到执行完毕再恢复原来的线程。
-
join() 方法要处理异常。
-
下面的例子中,当主线程的 i i i 执行到 5 5 5 时,主线程被中断,插入了另一个线程的 threadName.join() 方法。只有当 treadName 线程执行完毕,才能恢复主线程的执行。
public class ThreadMethods {
public static void main(String[] args) {
ThreadName threadName = new ThreadName();
threadName.setName("求偶数的线程");
threadName.start();
//主线程设置名字操作
Thread.currentThread().setName("主线程");
for (int i = 0; i < 20; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
//join()方法的测试
if (i == 5) {
try {
threadName.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class ThreadName extends Thread {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
输出:
主线程:1 主线程:3 主线程:5 求偶数的线程:0 求偶数的线程:2 求偶数的线程:4 求偶数的线程:6 求偶数的线程:8 求偶数的线程:10 求偶数的线程:12 求偶数的线程:14 求偶数的线程:16 求偶数的线程:18 主线程:7 主线程:9 主线程:11 主线程:13 主线程:15 主线程:17 主线程:19
( 4 4 4) sleep(long millis):让线程阻塞 millis 的时间
- sleep(long millis) 需要异常处理。
- 下面的例子是 threadName 线程每隔 1000 1000 1000 毫秒 ( 1 1 1 秒) 才输出一个偶数。可以看到有倒计时的效果。
public class ThreadMethods {
public static void main(String[] args) {
ThreadName threadName = new ThreadName();
threadName.setName("求偶数的线程");
threadName.start();
//主线程设置名字操作
Thread.currentThread().setName("主线程");
for (int i = 0; i < 20; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
//join()方法的测试
if (i == 5) {
try {
threadName.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
//展示如何给线程取名的操作
class ThreadName extends Thread {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
//sleep(long millis)的测试
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
( 5 5 5) isAlive():让线程阻塞 millis 的时间
- 线程在 run() 方法执行完之后就死亡了。可以使用 isAlive() 方法判断该线程是否还存活。
public class ThreadMethods {
public static void main(String[] args) {
ThreadName threadName = new ThreadName();
threadName.setName("求偶数的线程");
threadName.start();
//主线程设置名字操作
Thread.currentThread().setName("主线程");
for (int i = 0; i < 20; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
//join()方法的测试
if (i == 5) {
try {
threadName.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//isAlive()方法测试
System.out.println("线程threadName是否存活:" + threadName.isAlive());
}
}
//展示如何给线程取名的操作
class ThreadName extends Thread {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
//sleep(long millis)的测试
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
输出:
主线程:1 主线程:3 主线程:5 求偶数的线程:0 求偶数的线程:2 求偶数的线程:4 求偶数的线程:6 求偶数的线程:8 求偶数的线程:10 求偶数的线程:12 求偶数的线程:14 求偶数的线程:16 求偶数的线程:18 主线程:7 主线程:9 主线程:11 主线程:13 主线程:15 主线程:17 主线程:19 线程threadName是否存活:false10.2.5 线程的优先级
-
线程优先级分为 1 1 1 ~ 10 10 10 级。其中 10 10 10 级为最高优先级。
-
Thread 类中默认设置了三档优先级属性:
优先级 属性 最大优先级 MAX_PRIORITY:10 默认优先级 NORM_PRIORITY:5 最小优先级 MIN _PRIORITY:1 - 如果没有明确声明,一般线程优先级默认设置为 5 5 5 。
-
关于优先级的方法:
方法名 作用 getPriority() 获取当前线程的优先级 setPriority(Thread.MAX_PRIORITY) 设置当前线程的优先级
说明:
- 至少要在 start() 前,设置优先级。
- 线程被设置为最大优先级时,并不意味着该线程一定能执行完,才去执行低优先级的线程。只是有很大概率能被 CPU 优先执行。
例子:
public class ThreadMethods {
public static void main(String[] args) {
ThreadName threadName = new ThreadName();
threadName.setName("求偶数的线程");
//设置线程优先级为最大
threadName.setPriority(Thread.MAX_PRIORITY);
threadName.start();
//主线程设置名字操作
Thread.currentThread().setName("主线程");
//设置主线程优先级为最小
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
for (int i = 0; i < 20; i++) {
if (i % 2 != 0) {
System.out.println("优先级:" + Thread.currentThread().getPriority() +
"|" + Thread.currentThread().getName() + ":" + i);
}
}
//isAlive()方法测试
System.out.println("线程threadName是否存活:" + threadName.isAlive());
}
}
//线程ThreadName
class ThreadName extends Thread {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
System.out.println("优先级:" + getPriority() +
"|" + Thread.currentThread().getName() + ":" + i);
}
}
}
}
输出:
优先级:1|主线程:1 优先级:10|求偶数的线程:0 优先级:10|求偶数的线程:2 优先级:10|求偶数的线程:4 优先级:10|求偶数的线程:6 优先级:10|求偶数的线程:8 优先级:10|求偶数的线程:10 优先级:1|主线程:3 优先级:10|求偶数的线程:12 优先级:10|求偶数的线程:14 优先级:10|求偶数的线程:16 优先级:10|求偶数的线程:18 优先级:1|主线程:5 优先级:1|主线程:7 优先级:1|主线程:9 优先级:1|主线程:11 优先级:1|主线程:13 优先级:1|主线程:15 优先级:1|主线程:17 优先级:1|主线程:19 线程threadName是否存活:false
- 可以看到优先级为 1 的主线程,居然在第一行被优先执行了,说明优先级高的并不一定先被执行。但从整体来说,求偶数的线程 被优先执行的概率确实是比较大的。
题目
创建三个窗口卖票,总票数为100张。
首先创建一个卖票的窗口类Window:
- 其中,ticket 变量必须设置为 static 的,所有的线程实例都共享这个静态变量。否则会出现每个线程各自卖 100 100 100 张票。
class Window extends Thread {
private static int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(getName() + ":卖票,票号为:" +
ticket);
ticket--;
} else {
System.out.println("票已售罄");
break;
}
}
}
}
然后,在主线程中创建3个窗口实例。开始卖票:
public class WindowTest {
public static void main(String[] args) {
Window w1 = new Window();
w1.setName("窗口1");
Window w2 = new Window();
w2.setName("窗口2");
Window w3 = new Window();
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
}
}
输出:
- 可以看到,3个窗口实例线程都卖出了票号为 100 100 100 的票。这显然是不符合现实逻辑的。这就引出了线程安全问题,引出了下面第二种创建线程的方式。
( 1 1 1) 创建一个实现了Runnable接口的类;
( 2 2 2) 实现类去实现Runnable中的抽象方法:run();
( 3 3 3) 创建实现类的对象;
( 4 4 4) 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象;
( 5 5 5) 通过Thread类的对象调用start()。
例:
//1.创建一个实现了Runnable接口的类
class CreateTread implements Runnable {
private int ticket = 10;//注意这里没加static
//2.实现类去实现Runnable中的抽象方法:run()
@Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() +
":卖票,票号为:" + ticket);
ticket--;
} else {
System.out.println("票已售罄");
break;
}
}
}
}
public class ThreadCreate2 {
public static void main(String[] args) {
//3.创建实现类的对象
CreateTread c = new CreateTread();
//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread t1 = new Thread(c);
t1.setName("窗口1");
//5.通过Thread类的对象调用start()
t1.start();
//启动新线程
Thread t2 = new Thread(c);
t2.setName("窗口2");
t2.start();
}
}
输出:
窗口1:卖票,票号为:10 窗口1:卖票,票号为:9 窗口1:卖票,票号为:8 窗口1:卖票,票号为:7 窗口1:卖票,票号为:6 窗口1:卖票,票号为:5 窗口1:卖票,票号为:4 窗口1:卖票,票号为:3 窗口1:卖票,票号为:2 窗口1:卖票,票号为:1 票已售罄 窗口2:卖票,票号为:10 票已售罄
- 可以看到,虽然 ticket 没有加 static 修饰,但2个线程是共用这10张票的。因为线程 t1 和 t2 都共用 c 这一个对象创建线程。
- 可以看到还是存在 10 10 10 号票被2个窗口都卖出去的情况。并没有解决实际问题。
-
开发中,优先选择:实现Runnable接口的方式。
- 原因1:实现的方式没有类的单继承性的局限性。而继承的方式就不能继承其他父类了。
- 原因2:实现的方式更适合来处理多个线程有共享数据的情况。而继承的方式处理多个线程共享数据时需要把此数据定义为 static 的。
-
联系:Thread 类实际上也实现了 Runnable 接口。
public class Thread implements Runnable { …… } -
相同点:两种方式都需要重写 run() ,将线程要执行的逻辑声明在 run() 中。
| 线程状态 | 描述 |
|---|---|
| 新建 | 当一个 Thread 类或其子类的对象被声明并创建时,新生的线程对象处于新建状态 |
| 就绪 | 处于 新建 状态的线程被 start() 后,将进入线程队列等待 CPU 时间片。此时它已具备了运行的条件 ,只是没分配到 CPU 资源。 |
| 执行 | 当就绪的线程被调度并获得 CPU 资源时,便进入运行状态, run() 方法定义了线程的操作和功能 |
| 阻塞 | 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态 |
| 死亡 | 线程完成了它的全部工作、或线程被提前强制性地中止、或出现异常导致结束 |
- 死亡 是线程的最终状态。任何线程必然走向死亡。 阻塞不是线程的最终状态,而是中间的、暂时的一种状态。
-
以上方卖票窗口的代码为例,会出现重票、甚至错票的问题。
- 重票:
-
错票:
问题出现的原因
- 当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
如何解决?
- 当一个线程a在操作 ticket 的时候,其他线程不能参与进来。直到线程a操作完 ticket 时,其他线程才可以开始操作 ticket 。这种情况即使线程a出现了阻塞,也不能被改变。
- 相当于上厕所时,给厕所门锁上。只有当自己方便完之后,门外的人才能进来。
在Java中,通过 线程同步机制 来解决线程安全问题。
格式:
- 在需要对共享数据进行操作的部分,用下面的语句包起来:
- 被 synchronized () {} 包裹的代码块,只允许一个线程进来。该线程执行过程中,其他所有线程都必须在代码块外等待。直到该线程执行完毕,其他线程再抢夺进入该代码块的执行权。
synchronized (同步监视器) {
需要被同步的代码;
}
说明:
-
操作共享数据的代码,即为需要被同步的代码。 -->不能包含代码多了,也不能包含代码少了。
-
共享数据:多个线程共同操作的变量。比如:ticket 就是共享数据。
-
同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
-
要求:多个线程必须要共用同一把锁。
-
补充:在实现 Runnable 接口创建多线程的方式中,我们可以考虑使用 this 充当同步监视器。注意继承 Thread 方式创建的线程则不能用 this 。因为继承方式每一个线程都是不同的对象。
同步代码块在继承法 Thread 创建的线程中的使用
-
在继承 Thread 方式创建的线程时,第一种方法是同步监视器必须要创建新的 static 对象。这样才能保证每个线程实例对象都共用同一个同步监视器。
static Object obj = new Object();
-
在继承 Thread 方式创建的线程的第二种同步监视器方式是 线程类名.class 。
synchronized (线程类名.class) { }
例子:
- 在 实现 Runnable 接口创建多线程的方式中采用 synchronized () {} 同步代码块:
class CreateTread implements Runnable {
private int ticket = 100;
Object obj = new Object();//创建了一个对象用作同步监视器
//2.实现类去实现Runnable中的抽象方法:run()
@Override
public void run() {
while (true) {
//线程同步方式一:同步代码块synchronized () {}
synchronized (obj) {
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
":卖票,票号为:" + ticket);
ticket--;
} else {
System.out.println("票已售罄");
break;
}
}
}
}
}
public class ThreadCreate2 {
public static void main(String[] args) {
//3.创建实现类的对象
CreateTread c = new CreateTread();
//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread t1 = new Thread(c);
t1.setName("窗口1");
//5.通过Thread类的对象调用start()
t1.start();
//启动新线程
Thread t2 = new Thread(c);
t2.setName("窗口2");
t2.start();
Thread t3 = new Thread(c);
t3.setName("窗口3");
t3.start();
}
}
输出:
窗口1:卖票,票号为:50 窗口1:卖票,票号为:49 窗口1:卖票,票号为:48 窗口1:卖票,票号为:47 窗口1:卖票,票号为:46 窗口1:卖票,票号为:45 窗口1:卖票,票号为:44 窗口1:卖票,票号为:43 窗口1:卖票,票号为:42 窗口1:卖票,票号为:41 窗口1:卖票,票号为:40 窗口1:卖票,票号为:39 窗口1:卖票,票号为:38 窗口1:卖票,票号为:37 窗口1:卖票,票号为:36 窗口1:卖票,票号为:35 窗口1:卖票,票号为:34 窗口1:卖票,票号为:33 窗口1:卖票,票号为:32 窗口1:卖票,票号为:31 窗口1:卖票,票号为:30 窗口3:卖票,票号为:29 窗口3:卖票,票号为:28 窗口3:卖票,票号为:27 窗口3:卖票,票号为:26 窗口3:卖票,票号为:25 窗口3:卖票,票号为:24 窗口3:卖票,票号为:23 窗口3:卖票,票号为:22 窗口3:卖票,票号为:21 窗口3:卖票,票号为:20 窗口3:卖票,票号为:19 窗口3:卖票,票号为:18 窗口3:卖票,票号为:17 窗口3:卖票,票号为:16 窗口3:卖票,票号为:15 窗口3:卖票,票号为:14 窗口3:卖票,票号为:13 窗口3:卖票,票号为:12 窗口3:卖票,票号为:11 窗口3:卖票,票号为:10 窗口3:卖票,票号为:9 窗口3:卖票,票号为:8 窗口3:卖票,票号为:7 窗口3:卖票,票号为:6 窗口3:卖票,票号为:5 窗口3:卖票,票号为:4 窗口3:卖票,票号为:3 窗口3:卖票,票号为:2 窗口2:卖票,票号为:1 票已售罄 票已售罄 票已售罄
-
可以看到,重票和错票就已经没有再发生了。
-
好处:同步的方式,解决了线程的安全问题。
-
坏处:操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。
-
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
-
同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
-
Runnable 接口线程此时的同步方法是非静态的,同步监视器默认为:this 。
-
继承 Thread 线程的同步方法必须是静态的,同步监视器默认为:线程类名.class 。
-
格式:
private synchronized void show() {//同步监视器默认设置为:this 需要被同步的代码; }
例子:
//1.创建一个实现了Runnable接口的类
class Window2 implements Runnable {
private int ticket = 100;//注意这里没加static
//2.实现类去实现Runnable中的抽象方法:run()
@Override
public void run() {
while (true) {
show();
if (ticket <= 0) {
System.out.println("票已售罄");
break;
}
}
}
//同步方法
private synchronized void show() {//同步监视器默认设置为:this
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
":卖票,票号为:" + ticket);
ticket--;
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
//3.创建实现类的对象
Window2 w = new Window2();
//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread t1 = new Thread(w);
t1.setName("窗口1");
//5.通过Thread类的对象调用start()
t1.start();
//启动新线程
Thread t2 = new Thread(w);
t2.setName("窗口2");
t2.start();
Thread t3 = new Thread(w);
t3.setName("窗口3");
t3.start();
}
}
2. 同步方法处理 继承 Thread 创建的线程
-
根据同步监视器必须只有一个,因此继承 Thread 线程的同步方法必须是静态的,此时默认的同步监视器是 线程类名.class 。才能保证下面3个线程实例对象共用同一个静态同步方法。
-
例子:
class Window3 extends Thread { private static int ticket = 100; @Override public void run() { while (true) { show(); if (ticket <= 0) { System.out.println("票已售罄"); break; } } } //静态同步方法 private static synchronized void show() {//同步监视器默认设置为:Window3.class if (ticket > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket); ticket--; } } } //主线程 public class WindowTest3 { public static void main(String[] args) { Window3 w1 = new Window3(); w1.setName("窗口1"); Window3 w2 = new Window3(); w2.setName("窗口2"); Window3 w3 = new Window3(); w3.setName("窗口3"); w1.start(); w2.start(); w3.start(); } }
-
在单例模式中,分为饿汉式和懒汉式。懒汉式是到了调用的时候才新建对象,这样会存在线程安全问题。
-
要求:使用同步机制将单例模式中的懒汉式改写为线程安全的。
用同步代码块:
//懒汉的单例模式
class Bank {
private Bank() {
}
private static Bank bank = null;
public static Bank getInstance() {
synchronized (Bank.class) {
if (bank == null) {
bank = new Bank();
}
return bank;
}
}
}
用同步方法:
//懒汉的单例模式
class Bank {
private Bank() {
}
private static Bank bank = null;
public static synchronized Bank getInstance() {//此时默认同步监视器是Bank.class
if (bank == null) {
bank = new Bank();
}
return bank;
}
}
注意:
以上两种效率都不高。并不是说线程同步导致的效率低,而是:假如很多个线程同时进来 getInstance() 方法。第一个抢到执行权的线程 new 了一个 Bank 的实例对象,并返回了 bank 实例。后面的所有线程进来不用再 new 一个 Bank 的实例对象了。因此已经不存在线程安全问题,但是却还都要一个一个地排队 return bank 。这样效率显然很低,后面进来的线程不需要进入同步代码块了。
优化代码:
//懒汉的单例模式
class Bank {
private Bank() {
}
private static Bank bank = null;
public static Bank getInstance() {
//方式一:效率稍差
// synchronized (Bank.class) {
// if (bank == null) {
// bank = new Bank();
// }
// return bank;
// }
//方式二:效率更高
if (bank==null){
synchronized (Bank.class){
if (bank==null){
bank=new Bank();
}
}
}
return bank;
}
}
10.4.5 死锁的问题
10.4.6 Lock锁解决线程安全问题
- Lock锁是 JDK 5.0 之后新增的特性。在实际开发中,优先使用 Lock锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性 (提供更多子类) 。
-
Lock锁是一个接口,需要通过 ReentrantLock 实现类去实例化对象使用。
-
实现Lock接口的类:ReentrantLock ,要声明成 private 和final的:
private final ReentrantLock lock = new ReentrantLock();
-
通过 lock 对象调用 lock() 方法。此后代码只允许单线程进入,要用try-finally包起来,确保在 finally 中使用 unlock() 方法解锁:
//上锁,此后代码只允许单线程进入 lock.lock(); try { 需要同步的代码; } finally { //解锁,此后代码可以恢复为多线程操作 lock.unlock(); }
class Window4 implements Runnable {
private int ticket = 100;//注意这里没加static
//实现Lock接口的类:ReentrantLock,要声明成private和final的
private final ReentrantLock lock = new ReentrantLock();
//2.实现类去实现Runnable中的抽象方法:run()
@Override
public void run() {
while (true) {
//上锁,此后代码只允许单线程进入,要用try-finally包起来确保解锁
lock.lock();
try {
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
":卖票,票号为:" + ticket);
ticket--;
} else {
System.out.println("票已售罄");
break;
}
} finally {
//解锁,此后代码可以恢复为多线程操作
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
//3.创建实现类的对象
Window4 w = new Window4();
//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread t1 = new Thread(w);
t1.setName("窗口1");
//5.通过Thread类的对象调用start()
t1.start();
//启动新线程
Thread t2 = new Thread(w);
t2.setName("窗口2");
t2.start();
Thread t3 = new Thread(w);
t3.setName("窗口3");
t3.start();
}
}
3. synchronized 与 Lock锁的对比
-
相同:二者都可以解决线程安全问题。
-
不同:
-
synchronized 机制在执行完相应的同步代码以后,自动地释放同步监视器。
-
Lock 需要手动的启动同步 lock(),同时结束同步也需要手动地实现unlock() 。
-
Lock 只有代码块锁,synchronized 有代码块锁和方法锁。
-
使用 Lock锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性 (提供更多子类) 。
-
Lock --> synchronized同步代码块(已经进入了方法体,分配了相应资源) --> synchronized 同步方法(在方法体之外)
10.4.7 线程同步练习题目:
银行有一个账户。 有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。 问题:该程序是否有线程安全问题?如果有,如何解决?
存在线程安全的版本:
//实现Runnable方式创建多线程
class Account implements Runnable {
//共享的银行账户
private double balance;
@Override
public void run() {
int i = 3;//存3次
while (i > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance += 1000;
System.out.println(Thread.currentThread().getName() +
"存钱,余额为:" + balance);
i--;
}
}
}
//主线程
public class AccountTest {
public static void main(String[] args) {
Account a = new Account();
//两个储户线程
Thread t1 = new Thread(a);
t1.setName("储户1");
Thread t2 = new Thread(a);
t2.setName("储户2");
//开始存钱
t1.start();
t2.start();
}
}
输出:
储户2存钱,余额为:1000.0 储户1存钱,余额为:1000.0 储户1存钱,余额为:3000.0 储户2存钱,余额为:3000.0 储户2存钱,余额为:5000.0 储户1存钱,余额为:4000.0
- 可见,余额应该是 6000.0 6000.0 6000.0 元,但因为线程不同步导致余额为 5000.0 5000.0 5000.0 元。
1.用Lock锁解决线程安全的版本:
//实现Runnable方式创建多线程
class Account implements Runnable {
//共享的银行账户
private double balance;
//Lock锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
int i = 3;//存3次
while (i > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//上锁
lock.lock();
try {
balance += 1000;
System.out.println(Thread.currentThread().getName() +
"存钱,余额为:" + balance);
} finally {
//解锁
lock.unlock();
}
i--;
}
}
}
//主线程
public class AccountTest {
public static void main(String[] args) {
Account a = new Account();
//两个储户线程
Thread t1 = new Thread(a);
t1.setName("储户1");
Thread t2 = new Thread(a);
t2.setName("储户2");
//开始存钱
t1.start();
t2.start();
}
}
输出:
储户2存钱,余额为:1000.0 储户1存钱,余额为:2000.0 储户2存钱,余额为:3000.0 储户1存钱,余额为:4000.0 储户2存钱,余额为:5000.0 储户1存钱,余额为:6000.0
2.用 synchronized 同步方法解决线程安全的版本:
//实现Runnable方式创建多线程
class Account implements Runnable {
//共享的银行账户
private double balance;
@Override
public void run() {
int i = 3;//存3次
while (i > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//调用同步方法
deposit();
i--;
}
}
//同步方法
private synchronized void deposit() {
balance += 1000;
System.out.println(Thread.currentThread().getName() +
"存钱,余额为:" + balance);
}
}
//主线程
public class AccountTest {
public static void main(String[] args) {
Account a = new Account();
//两个储户线程
Thread t1 = new Thread(a);
t1.setName("储户1");
Thread t2 = new Thread(a);
t2.setName("储户2");
//开始存钱
t1.start();
t2.start();
}
}
输出:
储户1存钱,余额为:1000.0 储户2存钱,余额为:2000.0 储户2存钱,余额为:3000.0 储户1存钱,余额为:4000.0 储户1存钱,余额为:5000.0 储户2存钱,余额为:6000.0
3.用 synchronized 同步代码块解决线程安全的版本:
//实现Runnable方式创建多线程
class Account implements Runnable {
//共享的银行账户
private double balance;
@Override
public void run() {
int i = 3;//存3次
while (i > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//同步代码块
synchronized (this) {
balance += 1000;
System.out.println(Thread.currentThread().getName() +
"存钱,余额为:" + balance);
}
i--;
}
}
}
//主线程
public class AccountTest {
public static void main(String[] args) {
Account a = new Account();
//两个储户线程
Thread t1 = new Thread(a);
t1.setName("储户1");
Thread t2 = new Thread(a);
t2.setName("储户2");
//开始存钱
t1.start();
t2.start();
}
}
输出:
储户1存钱,余额为:1000.0 储户2存钱,余额为:2000.0 储户2存钱,余额为:3000.0 储户1存钱,余额为:4000.0 储户2存钱,余额为:5000.0 储户1存钱,余额为:6000.010.5 线程的通信 10.5.1 例题
使用两个线程打印1~20。线程1、线程2交替打印。
解:
class Number implements Runnable {
private int num = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
//唤醒被wait()的线程
notify();
if (num <= 20) {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
"打印:" + num);
num++;
//使得调用如下wait()方法的线程进入阻塞状态
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
public class ThreadCommunication {
public static void main(String[] args) {
Number n = new Number();
Thread t1 = new Thread(n);
t1.setName("线程1");
Thread t2 = new Thread(n);
t2.setName("线程2");
t1.start();
t2.start();
}
}
-
上述代码的执行经过:线程1抢到同步锁进入 synchronized() 代码块中。–>
-
没有线程被 wait() ,因此 notify() 无效。–>
-
线程1输出 number 1 1 1,执行 wait() 被阻塞,同时释放同步锁。–>
-
线程2得到同步锁进入 synchronized() 代码块中。–>
-
线程2执行 notify() ,激活线程1。但此时同步锁在线程2手上,因此线程1只能在 synchronized() 代码块外等待。–>
-
线程2继续输出 number 2 2 2,执行 wait() 被阻塞,同时释放同步锁。–>
-
线程1得到同步锁进入 synchronized() 代码块中。–>
-
线程1执行 notify() ,激活线程2。但此时同步锁在线程1手上,因此线程2只能在 synchronized() 代码块外等待。–>
-
线程1继续输出 number 3 3 3,执行 wait() 被阻塞,同时释放同步锁。–>
……
如此循环交替输出。
输出:
线程1打印:1 线程2打印:2 线程1打印:3 线程2打印:4 线程1打印:5 线程2打印:6 线程1打印:7 线程2打印:8 线程1打印:9 线程2打印:10 线程1打印:11 线程2打印:12 线程1打印:13 线程2打印:14 线程1打印:15 线程2打印:16 线程1打印:17 线程2打印:18 线程1打印:19 线程2打印:2010.5.2 通信涉及的三个方法
| 通信方法 | 作用 |
|---|---|
| wait() | 一旦执行此方法,该线程就进入阻塞状态。并释放同步监视器的锁。 |
| notify() | 一旦执行此方法,就会唤醒被 wait() 的一个线程。如果有多个线程被 wait() ,那么唤醒优先级高的。如果都为默认优先级,则随机唤醒一个。 |
| notifyAll() | 一旦执行此方法,就会唤醒所有被 wait() 的线程。 |
- wait() ,notify() ,notifyAll() 三个方法必须使用在同步代码块或同步方法中。
- wait() ,notify() ,notifyAll() 三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现IllegalMonitorStateException 异常。
- wait() ,notify() ,notifyAll() 三个方法是定义在java.lang.Object类中。而不是 Thread 类中。
相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
不同点:
1)两个方法声明的位置不同:Thread 类中声明 sleep() , Object 类中声明 wait() 。
2)调用的要求不同:sleep() 可以在任何需要的场景下调用。 wait() 必须使用在同步代码块或同步方法中。
3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep() 不会释放锁,wait() 会释放锁。
10.5.5 经典例题:生产者/消费者问题题目:
难点
-
生产者端和消费者端的线程之间如何通信?
答:让生产者端和消费者端的构造器都与店员 (Clerk) 关联起来。
class Producer implements Runnable { private Clerk clerk; //把生产者端和中间端店员关联起来 public Producer(Clerk clerk) { this.clerk = clerk; } ...... } -
消费者线程一开始就因为产品为0而被 wait() ,notify() 应该加在哪儿呢?
答:只要生产了1件商品,就可以通知消费者线程开始消费了。相应的,如果店员满了20件商品被 wait() ,只要消费了1件商品,就可以通知生产线程开始生产了。
products++; notify(); System.out.println(Thread.currentThread().getName() + "生产,产品数:" + products);
整体代码:
//共享对象:店员
class Clerk {
//固定的产品数量-20个
protected int products;
//生产
public synchronized void produce() {//此时同步监视器为Clerk的对象
//超过20个等一下
if (products >= 20) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
products++;
notify();
System.out.println(Thread.currentThread().getName() +
"生产,产品数:" + products);
}
}
//消费
public synchronized void consume() {//此时同步监视器为Clerk的对象,与生产端一样
//没有商品了等一下
if (products <= 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
products--;
notify();
System.out.println(Thread.currentThread().getName() +
"消费,产品数:" + products);
}
}
}
//生产者
class Producer implements Runnable {
private Clerk clerk;
//把生产者端和中间端店员关联起来
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
//不断增加产品
while (true) {
//设置一下生产速度,暂定1秒生产1个
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produce();
}
}
}
//消费者
class Customer implements Runnable {
private Clerk clerk;
public Customer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
//不断消耗产品
while (true) {
//设置一下消费速度,暂定1秒消耗1个
try {
Thread.sleep(700);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consume();
}
}
}
//主线程
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer p = new Producer(clerk);
Customer c = new Customer(clerk);
//生产者端
Thread p1 = new Thread(p);
p1.setName("生产者1");
Thread p2 = new Thread(p);
p2.setName("生产者2");
Thread p3 = new Thread(p);
p3.setName("生产者3");
//消费者端
Thread c1 = new Thread(c);
c1.setName("消费者1");
//多线程启动
p1.start();
p2.start();
p3.start();
c1.start();
}
}
练习的体会:
- Clerk 类,既没有继承 Thread 类,也没有实现 Runnable 接口。因此没有线程的 run() 方法,也创建不了多线程。但是,其被实现了 Runnable 接口的生产者 Producer 类和消费者 Customer 类各自关联到自己的构造器当中了,且调用了Clerk 类中的同步方法 生产produce() 和 消费 consume() 。因此,Clerk 类中可以定义 synchronized 同步方法生产produce() 和 消费 consume() 。并在其中调用了线程通信的 wait() 和 notify() 方法。
- 两个不同的类:实现了 Runnable 接口的生产者 Producer 类和消费者 Customer 类,可以互相通知唤醒 notify()对方的线程。因为生产者 Producer 类和消费者 Customer 类都调用了Clerk 类中的同步方法 生产produce() 和 消费 consume() ,且这两个同步方法拥有相同的同步监视器:Clerk 类的对象 this 。
- 创建一个 Callable 的实现类;
- 实现 call() 方法,将此线程需要执行的操作声明在call()中。
- 创建一个 Callable 的实现类的对象;
- 将此Callable 接口实现类的对象作为传递到 FutureTask 构造器中,创建 FutureTask 的对象;
- 将 FutureTask 的对象作为参数传递到 Thread 类的构造器中,创建 Thread 对象,并调用 start() ;
- (可选) 获取 Callable 中 call 方法的返回值。
- 相比 run() 方法,call() 方法可以有返回值。
- call() 方法可以抛出异常。
- 支持泛型的返回值 (后面介绍什么是泛型)。
- 需要借助 FutureTask 类,比如获取返回结果。
- Future 接口:
- 可以对具体Runnable 、Callable 任务的执行结果进行取消、查询是否完成、获取结果等。
- FutureTask 类是Future 接口唯一的实现类。
- FutureTask 类同时实现了Runnable 和 Future 接口。它既可以作为 Runnable 被线程执行,又可以作为Future 得到Callable 的返回值。
- 输出20内的偶数,并返回这些偶数的和:
//1.创建一个Callable的实现类
class NumThread implements Callable {
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
System.out.println(i);
sum += i;
}
}
return sum;//包装类Integer自动装箱
}
}
public class ThreadNew {
public static void main(String[] args) {
//3.创建callable实现类的对象
NumThread numThread = new NumThread();
//4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(numThread);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
Thread t1 = new Thread(futureTask);
t1.start();
try {
//6.获取Callable中call方法的返回值
Object sum = futureTask.get();
System.out.println("sum = " + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
10.6.2 创建方式四:线程池
- 开发中真正用的都是线程池。
- 背景: 经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
- 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
- 好处:
- 提高响应速度 (减少创建新线程的时间);
- 降低资源消耗 (重复利用线程池中的线程,不需要每次都创建)
- 便于线程管理:
- corePoolSize:核心池大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
- ……
-
JDK5.0起提供了线程池相关API:ExecutorService 和 Executors 。
-
ExecutorService :真正的线程池接口。常见实现类 ThreadPoolExecutor
-
void executor(Runnable command)
执行任务/命令,没有返回值,一般用来执行 Runnable 。
-
Future submit(Callable task) 执行任务,有返回值,一般用来执行 Callable 。
-
void shutdown()
关闭线程池。
-
-
Executors :工具类、线程池的工厂类,用于创建并返回不同类型的线程池。
-
Executors.newCachedThreadPool()
创建一个可根据需要,创建新线程的线程池。
-
Executors.newFixedThreadPool(n)
创建一个可重用固定线程数的线程池。
-
Executors.newSingleThreadExecutor()
创建一个只有一个线程的线程池。
-
Exrcutors.newScheduledThreadPool(n)
创建一个线程池,它可以安排在给定延迟后运行命令或者定期地执行。
-
- 提供指定线程数量的线程池:
ExecutorService service = Executors.newFixedThreadPool(10);
- 执行指定的线程操作,需要提供实现Runnable或者Callable接口实现类的对象:
service.execute(new NumThread1()); Future future = service.submit(new NumThread2());
- 关闭线程池
service.shutdown();3. 线程池使用说明
- 下面代码中,ExecutorService 是接口,因此 service 必定是 ExecutorService 是接口的某个实现类。
ExecutorService service = Executors.newFixedThreadPool(10);
- 通过以下代码获取 service 的类:
System.out.println(service.getClass());
输出:
class java.util.concurrent.ThreadPoolExecutor
可以看到,ThreadPoolExecutor 是 ExecutorService 是接口的实现类:
-
因此,就可以通过强转成 ThreadPoolExecutor 类的对象,来//设置线程池的属性:
//强转 ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
class NumThread1 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() +
":" + i);
}
}
}
}
//创建一个Callable的实现类
class NumThread2 implements Callable {
//实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 0; i < 20; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() +
":" + i);
sum += i;
}
}
return sum;//包装类Integer自动装箱
}
}
public class ThreadPool {
public static void main(String[] args) {
//1.提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//2.执行指定的线程操作,需要提供实现Runnable或者Callable接口实现类的对象
service.execute(new NumThread1());//执行Runnable接口实现类对象的线程
Future future = service.submit(new NumThread2());//执行Callable接口实现类对象的线程
try {
//获取Callable中call方法的返回值
Object sum = future.get();
System.out.println("sum = " + sum);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
//3.关闭线程池
service.shutdown();
}
}
输出:
pool-1-thread-1:0 pool-1-thread-1:2 pool-1-thread-1:4 pool-1-thread-1:6 pool-1-thread-1:8 pool-1-thread-1:10 pool-1-thread-1:12 pool-1-thread-1:14 pool-1-thread-1:16 pool-1-thread-1:18 pool-1-thread-2:1 pool-1-thread-2:3 pool-1-thread-2:5 pool-1-thread-2:7 pool-1-thread-2:9 pool-1-thread-2:11 pool-1-thread-2:13 pool-1-thread-2:15 pool-1-thread-2:17 pool-1-thread-2:19 sum = 100



