多线程
|字数总计:6.4k|阅读时长:25分钟|阅读量:|
基本概念
程序、进程、线程
程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
进程(process)是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。——生命周期
- 如:运行中的QQ,运行中的MP3播放器
- 程序是静态的,进程是动态的
- 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。
- 若一个进程同一时间并行执行多个线程,就是支持多线程的
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
- 一个进程中的多个线程共享相同的内存单元/内存地址空间–>它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。
单核CPU和多核CPU的理解
- 单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过,那么CPU就好比收费人员。如果有某个人不想交钱,那么收费人员可以把他“挂起”(晾着他,等他想通了,准备好了钱,再去收费)。但是因为CPU时间单元特别短,因此感觉不出来。
- 如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)
- 一个Java应用程序java.exe,其实至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
并行与并发
并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。
使用多线程的优点
背景:以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短,为何仍需多线程呢?
多线程程序的优点:
提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
提高计算机系统CPU的利用率。
改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。
何时需要多线程
线程的创建和启动
Thread类
API中创建线程的两种方式
JDK1.5之前创建新执行线程有两种方法:继承Thread类的方式和实现Runnable接口的方式
方式一:继承Thread类
1)定义子类继承Thread类。
2)子类中重写Thread类中的run方法。
3)创建Thread子类对象,即创建了线程对象。
4)调用线程对象start方法:启动线程,调用run方法。
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
| package com.nanzx.java;
class MyThread extends Thread { @Override public void run() { for (int i = 0; i < 100; i++) { if (i % 2 == 0) { System.out.println(Thread.currentThread().getName() + ":" + i); } } } }
public class ThreadTest { public static void main(String[] args) { MyThread t1 = new MyThread(); t1.start(); new Thread(() -> { for (int i = 0; i < 100; i++) { if (i % 2 == 0) { System.out.println(Thread.currentThread().getName() + ":" + i); } } }).start(); } }
|
方式二:实现Runnable接口
定义子类,实现Runnable接口。
子类中重写Runnable接口中的run方法。
通过Thread类含参构造器创建线程对象。
将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| package com.nanzx.java;
class MyThread implements Runnable { @Override public void run() { for (int i = 0; i < 100; i++) { if (i % 2 == 0) { System.out.println(Thread.currentThread().getName() + ":" + i); } } } }
public class ThreadTest { public static void main(String[] args) { MyThread t1 = new MyThread(); Thread thread = new Thread(t1); thread.start(); } }
|
注意点:
如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
想要启动多线程,必须调用start方法。
一个线程对象只能调用一次start()方法启动,如果重复调用,则将抛出异常“IllegalThreadStateException”。
继承方式和实现方式的联系与区别
public class Thread extends Object implements Runnable
区别:
实现Runnable接口方式的好处:
- 避免了单继承的局限性。
- 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。而继承Thread需将属性定义为static才能共享。
Thread类的有关方法
void start(): 启动线程,并执行对象的run()方法
run(): 线程在被调度时执行的操作
String getName(): 返回线程的名称
void setName(String name):设置该线程名称
static Thread currentThread(): 返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类
static void yield(): 线程让步。暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程。若队列中没有同优先级的线程,忽略此方法。
join(): 当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止。低优先级的线程也可以获得执行。
static void sleep(long millis): (指定时间:毫秒) 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。需抛出InterruptedException异常。
stop(): 强制线程生命期结束,不推荐使用
boolean isAlive(): 返回boolean,判断线程是否还活着
线程的优先级
线程的调度
调度策略:
- 时间片:
- 抢占式: 高优先级的线程抢占CPU
Java的调度方法:
线程的生命周期
JDK中用Thread.State类定义了线程的几种状态,要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
1 2 3 4 5 6 7 8 9 10 11 12 13
| public enum State { NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED; }
|
线程的同步
问题的提出
例题:
模拟火车站售票程序,开启三个窗口售票
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
| package com.nanzx.java;
class Window implements Runnable {
private int ticket = 100;
@Override public void run() { while (true) { if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket); ticket--; } else { break; } } } }
class ThreadTest { public static void main(String[] args) { Window w = new Window();
Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w);
t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3");
t1.start(); t2.start(); t3.start(); } }
|
运行结果出现异常(例如):
窗口1:卖票,票号为:100
窗口2:卖票,票号为:100
窗口3:卖票,票号为:100
窗口1:卖票,票号为:-1
1.多线程出现了安全问题
2. 问题的原因:
当多条语句在操作共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行,导致共享数据的错误。
3. 解决办法:
当一个线程a在操作共享数据的时候,其他线程不能参与进来。直到线程a操作完共享数据后,其他线程才可以开始操作共享数据。这种情况即使线程a出现了阻塞,也不能被改变。
Synchronized的使用方法
Java对于多线程的安全问题提供了专业的解决方式:同步机制
1. 同步代码块:
synchronized (同步监视器){
// 需要被同步的代码;
}
说明:
- 操作共享数据的代码,即为需要被同步的代码。 –>不能包含代码多了,也不能包含代码少了。
共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
同步监视器,俗称:锁。**任何一个类的对象,都可以充当锁。**
要求:多个线程必须要**共用同一把锁**。
在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。
2.synchronized还可以放在方法声明中,表示整个方法为同步方法。
public synchronized void show (String name){
// 需要被同步的代码;
}
关于同步方法的总结:
- 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
- 非静态的同步方法,同步监视器是:this
**静态的同步方法,同步监视器是:当前类本身**
使用同步代码块且实现Runnable
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
| package com.nanzx.java;
class Window1 implements Runnable{
private int ticket = 100; Object obj = new Object();
@Override public void run() { while(true){ synchronized (this){ if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket); ticket--; } else { break; } } } } }
public class ThreadTest1 { public static void main(String[] args) { Window1 w = new Window1();
Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w);
t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3");
t1.start(); t2.start(); t3.start(); } }
|
使用同步代码块且继承Thread
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
| package com.nanzx.java;
class Window2 extends Thread{
private static int ticket = 100; private static Object obj = new Object();
@Override public void run() { while(true){ synchronized (Window2.class){ if(ticket > 0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getName() + ":卖票,票号为:" + ticket); ticket--; }else{ break; } } } } }
public class ThreadTest2 { public static void main(String[] args) { Window2 t1 = new Window2(); Window2 t2 = new Window2(); Window2 t3 = new Window2();
t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3");
t1.start(); t2.start(); t3.start(); } }
|
使用同步方法且实现Runnable
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
| package com.nanzx.java;
class Window3 implements Runnable {
private int ticket = 100;
@Override public void run() { while (true) { show(); } }
private synchronized void show(){ if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket); ticket--; } } }
public class ThreadTest3 { public static void main(String[] args) { Window3 w = new Window3();
Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w);
t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3");
t1.start(); t2.start(); t3.start(); } }
|
使用同步方法且继承Thread
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
| package com.nanzx.java;
class Window4 extends Thread {
private static int ticket = 100;
@Override public void run() { while (true) { show(); } }
private static synchronized void show() { if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket); ticket--; } } }
public class ThreadTest4 { public static void main(String[] args) { Window4 t1 = new Window4(); Window4 t2 = new Window4(); Window4 t3 = new Window4();
t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3");
t1.start(); t2.start(); t3.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
| package com.nanzx.java;
public class BankTest { }
class Bank {
private Bank() { }
private static Bank instance = null;
public static Bank getInstance() {
if (instance == null) { synchronized (Bank.class) { if (instance == null) {
instance = new Bank(); } } } return instance; } }
|
线程的死锁问题
死锁
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
解决方法
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
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
| package com.nanzx.java;
public class DeadLock {
public static void main(String[] args) { StringBuffer s1 = new StringBuffer(); StringBuffer s2 = new StringBuffer();
new Thread() { @Override public void run() { synchronized (s1) { s1.append("a"); s2.append("1"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s2) { s1.append("b"); s2.append("2"); System.out.println(s1); System.out.println(s2); } } } }.start();
new Thread(new Runnable() { @Override public void run() { synchronized (s2) { s1.append("c"); s2.append("3"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s1) { s1.append("d"); s2.append("4"); System.out.println(s1); System.out.println(s2); } } } }).start(); } }
|
Lock(锁)
从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
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
| package com.nanzx.java;
import java.util.concurrent.locks.ReentrantLock;
class Window implements Runnable{
private int ticket = 100; private ReentrantLock lock = new ReentrantLock(); @Override public void run() { while(true){ try{ lock.lock();
if(ticket > 0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket); ticket--; }else{ break; } }finally { lock.unlock(); } } } }
public class LockTest { public static void main(String[] args) { Window w = new Window();
Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w);
t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3");
t1.start(); t2.start(); t3.start(); } }
|
synchronized 与 Lock 的对比
Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
Lock只有代码块锁,synchronized有代码块锁和方法锁
使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)。
1 2
| 优先使用顺序: Lock -> 同步代码块(已经进入了方法体,分配了相应资源) -> 同步方法(在方法体之外)
|
线程的通信
wait(),notify(),notifyAll()
使用两个线程打印 1-100。线程1, 线程2交替打印
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
| package com.nanzx.java;
class Number implements Runnable { private int number = 1; private Object obj = new Object();
@Override public void run() { while (true) { synchronized (obj) { obj.notify(); if (number <= 100) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":" + number); number++; try { obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } else { break; } } } } }
public class CommunicationTest { public static void main(String[] args) { Number number = new Number(); Thread t1 = new Thread(number); Thread t2 = new Thread(number);
t1.setName("线程1"); t2.setName("线程2");
t1.start(); t2.start(); } }
|
- wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
- notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
- notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
- wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
- wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现IllegalMonitorStateException异常
- wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。
面试题:sleep() 和 wait()的异同
经典例题:生产者/消费者问题
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
这里可能出现两个问题:
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 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
| package com.nanzx.java;
class Clerk {
private int productCount = 0;
public synchronized void produceProduct() { if (productCount < 20) { productCount++; System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个产品"); notify(); } else { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } }
}
public synchronized void consumeProduct() { if (productCount > 0) { System.out.println(Thread.currentThread().getName() + ":开始消费第" + productCount + "个产品"); productCount--; notify(); } else { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } }
} }
class Producer extends Thread {
private Clerk clerk;
public Producer(Clerk clerk) { this.clerk = clerk; }
@Override public void run() { System.out.println(getName() + ":开始生产产品....."); while (true) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } clerk.produceProduct(); }
} }
class Consumer extends Thread { private Clerk clerk;
public Consumer(Clerk clerk) { this.clerk = clerk; }
@Override public void run() { System.out.println(getName() + ":开始消费产品....."); while (true) { try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } clerk.consumeProduct(); } } }
public class ProductTest {
public static void main(String[] args) { Clerk clerk = new Clerk();
Producer p1 = new Producer(clerk); p1.setName("生产者1");
Consumer c1 = new Consumer(clerk); c1.setName("消费者1"); Consumer c2 = new Consumer(clerk); c2.setName("消费者2");
p1.start(); c1.start(); c2.start(); } }
|
JDK5.0新增线程创建方式
新增方式一:实现Callable接口
与使用Runnable接口相比, Callable接口功能更强大些:
Future接口
可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
FutrueTask是Futrue接口的唯一的实现类
FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
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
| package com.nanzx.java;
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask;
class NumThread implements Callable<Integer> { @Override public Integer call() throws Exception { int sum = 0; for (int i = 0; i <= 100; i++) { System.out.println(i); sum = sum + i; } return sum; } }
public class ThreadNew { public static void main(String[] args) { NumThread numThread = new NumThread(); FutureTask<Integer> task = new FutureTask<>(numThread); new Thread(task).start(); try { Integer sum = task.get(); System.out.println(sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
|
新增方式二:使用线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
好处:
JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
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
| package com.nanzx.java;
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor;
class NumberThread implements Runnable { @Override public void run() { for (int i = 0; i <= 100; i++) { if (i % 2 == 0) { System.out.println(Thread.currentThread().getName() + ": " + i); } } } }
class NumberThread1 implements Runnable { @Override public void run() { for (int i = 0; i <= 100; i++) { if (i % 2 != 0) { System.out.println(Thread.currentThread().getName() + ": " + i); } } } }
public class ThreadPool { public static void main(String[] args) { ExecutorService service = Executors.newFixedThreadPool(10); ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
service.execute(new NumberThread()); service.execute(new NumberThread1());
service.shutdown(); } }
|