参考资料:《Java线程》、《JDK API 1.6 中文版》
学习视频:b站《尚硅谷Java入门视频教程》(主讲:宋红康老师)
第 9 章 Java マルチスレッド化
1. 基本的な考え方
- スレッドとは、プログラム内で実行されるスレッドです。Java 仮想マシンを使用すると、アプリケーションは複数の実行スレッドを同時に実行できます。
- 各スレッドには優先順位があり、優先順位の高いスレッドの実行が優先順位の低いスレッドよりも優先されます。各スレッドはデーモンとしてマークされる場合とマークされない場合があります。スレッド内で実行されているコードが新しい Thread オブジェクトを作成すると、新しいスレッドの初期優先順位は作成スレッドの優先順位に設定され、作成スレッドがデーモン スレッドである場合に限り、新しいスレッドはデーモンになります。
- Java 仮想マシンが起動すると、通常は 1 つの非デーモン スレッドが存在します (通常、これは指定されたクラスの main メソッドを呼び出します)。Java 仮想マシンは、次のいずれかの状況が発生するまでスレッドの実行を続けます。
✒️ Runtime クラスの exit メソッドが呼び出され、セキュリティ マネージャーによって終了操作の実行が許可されます。
✒️ デーモン スレッドではないすべてのスレッドは、run メソッドへの呼び出しから戻るか、run メソッドの外に伝播する例外をスローすることによって、実行を停止しました。 - 新しい実行スレッドを作成するには 2 つの方法があります。1 つの方法は、クラスを Thread のサブクラスとして宣言することです。サブクラスは Thread クラスの run メソッドをオーバーライドする必要があります。もう 1 つの方法は、Runnable インターフェイスを実装するクラスを宣言することです。次に、クラスは run メソッドを実装します。このクラスのインスタンスを割り当て、スレッドの作成時にパラメータとして渡して開始することができます。
—— 摘自官方文档JDK API 1.6 中文版
1. プログラム、プロセス、スレッド
- プログラム: 特定のタスクまたは機能を完了するために記述された一連の命令、つまり静的なコード/静的オブジェクト
- プロセス: プログラムの実行結果は、ライフサイクルを持つ動的なプロセスであり、システム リソース割り当ての単位として機能します。システムは、実行中に異なるメモリを割り当てます。
- スレッド: プロセスをさらに改良したもので、プログラム内の実行パスです。
✒️Java プログラムには少なくとも 3 つのスレッドがあります: main() メイン スレッド、gc() ガベージ コレクション スレッド、例外処理スレッド ✒️Java スレッドは 2 つのカテゴリに分類できます。1 つはデーモン スレッド、もう 1 つはユーザー スレッドです
( Java ガベージ コレクションは典型的なデーモン スレッドです)
✒️setDaemon(true) はユーザー スレッドをデーモン スレッドに変えることができます。JVM 内にすべてのデーモン スレッドがある場合、JVM は終了します
2. 並列処理と同時実行性
- 並列処理: 複数の CPU が複数のタスクを同時に実行する
- 同時実行性: 単一の CPU が複数のタスクを実行します (シリアル ポート送信と似ていますが、実際には同時に実行されません - タイム スライス)
2. スレッドのスケジューリング
关于线程调度的具体实现和原理,详见《Java线程》第六章
1. スレッドのスケジューリング戦略とスケジューリング方法
- タイム スライス + プリエンプティブ: **優先度の高いスレッドが CPU を占有します
✒️タイム スライス戦略: 同じ優先度のスレッドが先进先出
キューを形成します
✒️プリエンプティブ戦略: 優先度の高いスレッドが优先
CPU を占有します
PS: 優先度の高いスレッドが最初に CPU の実行権を奪いますが、これはプリエンプションの可能性が高いだけであり、優先度の高いスレッドが優先度の低いスレッドよりも先に実行しなければならないという意味ではありません。
2. スレッドの優先順位
(1) スレッドの優先度
Threadクラスで定義される定数 | 優先度 |
---|---|
MIN_PRIORITY | 1 |
NORM_PRIORITY (デフォルトの優先度) | 5 |
MAX_PRIORITY | 10 |
(2) スレッド優先度の取得と設定
- setPriority(int newPriority): スレッドの優先順位を newPriority に設定します。
- getPriority(): スレッドの現在の優先度を取得します。
3. スレッドの作成と使用
1. 方法 1: Thread クラスから継承する
(1) 利用手順
- Threadクラスを継承するサブクラスを作成する
- Thread クラスの run() をオーバーライドする
- Thread クラスのサブクラス オブジェクトをインスタンス化します。
- このオブジェクトに対して start() を呼び出してスレッドを開始します。
package com.javaThread.java;
/* 线程1遍历100内偶数 */
class subThread1 extends Thread{
/* 1、创建子类继承于Thread */
@Override
/* 2、重写run方法 */
/* 3、到main里实例化线程1对象 */
/* 4、到main里用线程1对象调用start方法 */
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2 == 0) System.out.println(this.getName()+":"+i);
}
}
}
/* 线程2遍历100内奇数 */
class subThread2 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2 != 0) System.out.println(this.getName()+":"+i);
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) throws InterruptedException {
subThread1 t1 = new subThread1();
subThread2 t2 = new subThread2();
t1.start();
t2.start();
while (true) {
System.out.println("Hello world");
Thread.sleep(1000);
}
}
}
(2) 匿名サブクラスの書き方
package com.javaThread.java;
public class ThreadDemo2 {
public static void main(String[] args) {
/* 创建Thread类的匿名子类 */
new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2 == 0) System.out.println(this.getName()+":"+i);
}
}
}.start();
/* 创建Thread类的匿名子类 */
new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2 != 0) System.out.println(this.getName()+":"+i);
}
}
}.start();
}
}
(3) Threadクラスの共通メソッド
- start(): 現在のスレッドを開始し、現在のスレッドの run() メソッドを呼び出します。
- run(): スレッドの作成によって実行される操作は、このメソッドで宣言されます (Linux マルチスレッドの pthread_create() に渡されるポインタ関数と同様)
- currentThread(): 静的メソッド、現在のスレッドを返します。
- getName(): 現在のスレッド名を取得します。
- setName(): 現在のスレッド名を設定します。
- yield(): CPU の実行権を解放し、再スケジュールします (解放後、他のスレッドによってプリエンプトされる可能性があります。または、単独でプリエンプトされる可能性があります)
- join(): ブロックし、他のスレッドの実行が完了するまで待機し、ブロックを終了します。
- sleep(long millis): スレッドはブロックされ、一定時間 (ミリ秒レベル) 待機します。
- isAlive(): スレッドが生きているか死んでいるかを判断し、スレッドのステータス true/false を返します。
- その他のメソッド (非推奨 - 廃止): stop() を使用して強制的に終了します。
(4) 典型的な例: マルチウィンドウ チケット発行 v1.0
/* 三个窗口(线程)同时卖票,一共100张票 */
class Windows extends Thread{
public Windows(String threadName){
super(threadName);
}
private static int tickets = 100;
@Override
public void run() {
while (tickets > 0){
System.out.println(this.getName()+" > NO."+tickets+" has been sold");
tickets --;
}
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
Windows w1 = new SellTickets("windows_1");
Windows w2 = new SellTickets("windows_2");
Windows w3 = new SellTickets("windows_3");
w1.start();
w2.start();
w3.start();
}
}
- BUG レコード: v1.0 にはスレッド セーフティの問題があります (重複または間違った投票)
2. 方法 2: Runnable インターフェイスを実装する
- 注: 公式 API ドキュメントによると、スレッドを作成するには 2 つの方法があります。1 つは Thread クラスから継承する方法、もう 1 つは Runnable インターフェイスを実装する方法です。実際には、JDK 5.0 以降、Callable インターフェースを実装する方法とスレッド プールを使用する方法という 2 つの方法が追加されました。(スレッドの立て方は全部で4通りあります)
(1) 利用手順
- Runnableインターフェイスを実装するクラス(実装)を作成します。
- Runnable に抽象メソッド run() を実装する
- クラスを実装するオブジェクトを作成する
- このオブジェクトを Thread クラスのコンストラクターに渡し、Thread オブジェクトを作成します
- Thread オブジェクトを通じて start() メソッドを呼び出す
/* 创建线程方式二:implements Runnable的方式*/
/* 1.创建一个实现Runnable接口的类(implements) */
class Thread1 implements Runnable{
/* 2.实现Runnable中的抽象方法run() */
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2 == 0) System.out.println(i);
}
}
}
public class ThreadDemo4 {
public static void main(String[] args) {
/* 3.创建实现类的对象 */
Thread1 t1 = new Thread1();
/* 4.将此对象传到Thread类的构造器中并创建Thread对象 */
Thread thread1 = new Thread(t1);
//Thread thread1 = new Thread(new Thread1()); //3+4
/* 5.通过Thread对象去调用start()方法 */
thread1.start();
}
}
(2) 典型的な例: マルチウィンドウ チケット発行 v1.1
/* 实现Runnable接口的方式创建的线程 */
class Windows implements Runnable{
private int tickets = 100; //和v1.0不同,这里不用static
@Override
public void run() {
while(tickets > 0){
System.out.println(Thread.currentThread().getName()+"> NO."+tickets+" has been sold");
tickets --;
}
}
}
public class ThreadDemo5 {
public static void main(String[] args) {
Windows windows = new Windows();
/* 一个windows可传到三个Thread类的构造器中 */
Thread t1 = new Thread(windows);
Thread t2 = new Thread(windows);
Thread t3 = new Thread(windows);
/* 共用一个windows对象,也就共享100张票 */
t1.setName("window_1");
t2.setName("window_2");
t3.setName("window_3");
t1.start();
t2.start();
t3.start();
}
}
/* 同样这里也有线程安全的问题,出现了重票的错票的问题,待解决 */
3. スレッドを作成する 2 つの方法の比較
- 開発中は、次の理由から、Runnable インターフェイスの実装によるスレッドの作成が優先されます。 ✒️実装方法にはクラスの単一継承の制限
がない✒️実装方法は、データを共有するマルチスレッド操作の処理により適しています。 - 接続: 実際には、Runnable インターフェイスも Thread クラスに実装されています。
- 類似点: どちらのメソッドも run() メソッドを書き直し、 run() でスレッド実行のロジックを宣言する必要があります。
4. スレッドのライフサイクル
- このうち、suspend() とresume() はデッドロックを引き起こす可能性があるため、廃止予定です。スレッドが一時停止中にロックを保持している場合、スレッドが再起動されるまで、どのスレッドもリソースにアクセスできません。このスレッドを再起動する別のスレッドが再開を呼び出す前にモニターがロックされている場合、デッドロックが発生します。このタイプのデッドロックは通常、「フリーズした」プロセスとして現れます。
5. スレッドの同期
1. マルチウィンドウチケット販売のバグ
- 重複チケットと間違ったチケットの発生: スレッドがチケットを操作しているとき、中間時間差 (非常に小さい可能性がありますが無視できない) やスレッドのステータス変化などの問題により、操作の前に他のスレッドもチケットを操作します。完成されました。
(PS:例如当某一个线程拿到tickets=100,还未到tickets--就有其他线程参与进来,这时也拿到了tickets=100。最终出现票号为99的重票;同样的当某一个线程操作完tickets--到等于0,还未结束循环时,另一个线程参与进来,拿到tickets=0就会出现票号为-1的错票)
- Java では、スレッド セーフティの問題は、次の 2 つの同期方法によって解決されます。
2. 方法 1: 同期されたコード ブロック
synchronized(同步监视器){
//需要被同步的代码
…… …… ……
}
- 共有データを操作するコードが同期が必要なコードです。
- 共有データ: 複数のスレッドが一緒に動作する変数。例: 窓口販売のチケット
- 同步监视器(锁):任何一个类的对象都可以充当同步监视器。要求多个线程必须共用同一把锁
✒️实现Runnable接口的方式创建线程,可以考虑用this充当同步监视器
✒️继承Thread类的方式创建线程,可以考虑用当前类作同步监视器,总之需要保证锁是唯一的 - 局限性:同步虽然解决的线程安全问题,但是在操作同步代码时只能有一个线程参与,其余等待,相当于此过程是单线程的。
3、方式二:同步方法
- 同步方法仍然涉及到同步监视器,只是不需要显式的声明
- 静态同步方法,同步监视器是:this
- 非静态同步方法,同步监视器是:当前类
4、线程同步解决窗口售票的BUG
(1)经典例子:多窗口售票v2.0.0
/* 实现Runnable接口的方式创建的线程 */
/* 同步代码块解决线程安全问题 */
class Windows implements Runnable{
private int tickets = 100;
Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
//可传this,此时的this是windows的唯一对象
/* 为了出现重票和错票的概率变大,这里加入sleep() */
try{
Thread.sleep(50);
}catch (InterruptedException e) {
e.printStackTrace();
}
if(tickets > 0) {
System.out.println(Thread.currentThread().getName() + "> NO." + tickets + " has been sold");
tickets --;
}else{
break;
}
}
}
}
}
public class ThreadDemo5 {
public static void main(String[] args) {
Windows windows = new Windows();
Thread t1 = new Thread(windows);
Thread t2 = new Thread(windows);
Thread t3 = new Thread(windows);
t1.setName("window_1");
t2.setName("window_2");
t3.setName("window_3");
t1.start();
t2.start();
t3.start();
}
}
(2)经典例子:多窗口售票v2.0.1
/* 继承Thread类的方式创建的线程 */
/* 同步方法解决线程安全问题 */
class Windows extends Thread {
public Windows(String threadName) {
super(threadName);
}
private static int tickets = 100;
private static boolean flag = true;//标志位,控制循环
@Override
public void run() {
while(flag) {
sellTickets(); //调用同步方法
}
}
/* 同步方法实现,注意是静态的同步方法 */
private static synchronized void sellTickets() {
/* sleep提高错票重票的概率 */
try{
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + " > NO." + tickets + " has been sold");
tickets--;
}else{
flag = false;
}
}
}
public class ThreadDemo6 {
public static void main(String[] args) {
Windows w1 = new Windows("windows_1");
Windows w2 = new Windows("windows_2");
Windows w3 = new Windows("windows_3");
w1.start();
w2.start();
w3.start();
}
}
5、线程死锁问题
- 死锁:不同的线程分别占用对方的同步资源,都在等待对方释放自己所需的同步资源,造成了线程的死锁。
✒️出现死锁后不会报错和抛异常,所有线程处在阻塞状态,无法继续。
✒️避免嵌套同步、尽量减少同步资源的定义以避免出现死锁
/* 死锁的简单例子 */
public class ThreadDemo6 {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
/* 线程1 - 匿名类*/
new Thread(){
@Override
public void run() {
synchronized (s1){
s1.append("a");
s2.append("1");
/* sleep增加死锁概率 */
try{
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2){
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
/* 当线程1先执行,拿到s1阻塞,此时线程2可能刚好拿了s2也阻塞,
* 接着就出现线程1拿着s1等s2,线程2拿着s2等s1 */
/* 线程2 */
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2){
s1.append("c");
s2.append("3");
/* sleep增加死锁概率 */
try{
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (s1){
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}
6、方式三:Lock锁(JDK 5.0新增)
- 问:synchronized和Lock的异同?
答:虽然synchronized和Lock都能解决线程安全的问题,但是使用synchronized的方式在执行完相应的同步代码后,即使发生异常,也会自动释放占用的锁(同步监视器)。而用Lock的方式则需要手动的启动(lock( ) -上锁),手动的结束同步(unlock( ) - 解锁),在发生异常时若没有unlock( ),很有可能导致死锁,所以unlock( )需要写在finally中以保证异常情况下锁也能够被释放;且synchronized是非公平锁,Lock可以选择公平或非公平锁。(默认非公平) - 用法见下例代码:
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class Window implements Runnable{ private int tickets = 100; /* 实例化ReentrantLock对象 */ ReentrantLock l = new ReentrantLock(); @Override public void run() { while(true){ /* 给同步代码上锁 */ l.lock(); try{ if(tickets > 0){ try{ Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+">\tNO."+tickets+" has been sold"); tickets --; }else break; }finally { /* 手动解锁 */ l.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("window 1");t1.start(); t2.setName("window 2");t2.start(); t3.setName("window 3");t3.start(); } }
7、编程练习
银行有一个账户,有两个储户分别向同一个账户存3000元,每次存1000,存5次。每次存完打印账户余额。
问题:该程序是否有安全问题,如果有,如何解决?
【提示】
1,明确哪些代码是多线程运行代码,须写入run()方法
2,明确什么是共享数据。
3,明确多线程运行代码中哪些语句是操作共享数据的。
【拓展问题】能否实现储户交替存钱的操作?
import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;
class Account{
//账号
private double balance = 0; //当前余额
/* 存钱并显示当前余额的方法,操作共享数据需要同步 */
public synchronized void saveMoney(double money){
if(money < 0) return;
balance = balance + money;
String customerName = Thread.currentThread().getName();
System.out.println(customerName+"> "+"Deposit "+money+"¥ succeed.Your current balance is "+balance+"¥");
}
}
class Customer extends Thread{
//客户——线程
private Account acc;
private ReentrantLock lock = new ReentrantLock();
/* 提供构造器传入账户,可实现多客户共用一个账户 */
public Customer(Account account) {
this.acc = account;
}
/* 重写run方法,根据题目 */
@Override
public void run() {
for(int i=0;i<5;i++){
/* 提高出现存钱问题概率,加入睡眠 */
try{
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
acc.saveMoney(1000);
}
}
}
public class AccountTest{
public static void main(String[] args) {
Account acc = new Account();
Customer c1 = new Customer(acc);
Customer c2 = new Customer(acc);
Customer c3 = new Customer(acc);
c1.setName("Customer 1");
c2.setName("Customer 2");
c1.start();
c2.start();
}
}
六、线程间通信
- wait( ):调用此方法当前线程进入阻塞状态,并释放同步监视器
- notify( )、notifyAll( ):调用此方法会唤醒正在处于wait的线程,多个线程则唤醒优先级高的线程
(PS:以上只能在同步代码块或者同步方法中使用-sychronized,且其调用者必须是同步监视器,否则会出现IllegalMonitorStateException) - 【拓展问题】实现两个储户交替存钱的操作?
import java.util.concurrent.locks.ReentrantLock;
class Account {
//账号
public double balance = 0; //当前余额
/* 存钱并显示当前余额的方法,操作共享数据需要同步 */
static ReentrantLock l = new ReentrantLock();
public synchronized void saveMoney(double money) {
if (money < 0) {
System.out.println("error");
return;
}
else {
notify();
balance = balance + money;
String customerName = Thread.currentThread().getName();
System.out.println(customerName + "> " + "Deposit " + money + "$ succeed.Your current balance is " + balance + "$");
try {
wait(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Customer extends Thread {
//客户——线程
private Account acc;
/* 提供构造器传入账户,可实现多客户共用一个账户 */
public Customer(Account account) {
this.acc = account;
}
/* 重写run方法,根据题目 */
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
acc.saveMoney(1000);
}
}
}
public class AccountTest {
public static void main(String[] args) {
Account acc = new Account();
Customer c1 = new Customer(acc);
Customer c2 = new Customer(acc);
c1.setName("Customer 1");
c2.setName("Customer 2");
c1.start();
c2.start();
}
}
七、创建线程另外两种方式(JDK 5.0新增)
1、实现Callable接口
- 与Runnable相比,Callable的功能更加强大:
✒️可以有返回值
✒️可以抛出异常
✒️支持泛型
✒️可借助FutureTask类,比如获取返回结果 - 使用步骤:
✒️创建一个实现Callable的实现类
✒️重写call方法,将线程执行的操作声明在call( )中
✒️new一个Callable接口实现类的对象传到FutureTask构造器中
✒️new一个FutureTask对象传到Thread类的构造器中
✒️使用Thread类对象调用start( )启动线程 - 下面使用实现Callable接口的方式创建线程,依然是上面"储户存钱"的问题
package com.ThreadDemos.java;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
class Account{
public double balance = 0;
}
/* 1.创建一个实现Callable的实现类 */
class Customer implements Callable {
static Account acc = new Account();
/* 2.重写call方法,将线程执行的操作声明在call()中 */
@Override
public Object call() throws Exception {
String threadName = Thread.currentThread().getName();
for (int i = 0; i < 3; i++) {
synchronized (acc) {
Thread.sleep(1000);
acc.balance += 1000;
System.out.println(threadName + "转入金额:"+1000+"$");
}
}
return acc.balance;
}
}
public class CallableTest {
public static void main(String[] args) {
Customer customer1 = new Customer();
Customer customer2 = new Customer();
/* 3.new一个Callable接口实现类的对象传到FutureTask构造器中 */
FutureTask futureTask1 = new FutureTask(customer1);
FutureTask futureTask2 = new FutureTask(customer2);
/* 4.new一个FutureTask对象传到Thread类的构造器中 */
Thread t1 = new Thread(futureTask1);
Thread t2 = new Thread(futureTask2);
/* 5.使用Thread类对象调用start()启动线程 */
t1.setName("客户1");
t2.setName("客户2");
t1.start();
t2.start();
try {
if((double)futureTask1.get() > (double)futureTask2.get()){
System.out.println("当前账户余额是:"+futureTask1.get()+"$");
}else {
System.out.println("当前账户余额是:"+futureTask2.get()+"$");
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
2、使用线程池创建
- 概述:提前创建好多个线程组成线程池,使用时从线程池中直接获取,使用完重新放回池中。可以避免频繁创建、销毁的操作,实现重复利用;对于创建和销毁、使用线程量大的资源(如:高并发线程),能提高响应速度、降低资源销毁。
- 优点:①减少了创建线程的时间,提高响应速度;②重复利用线程池中的线程,降低了资源消耗;③便于线程管理
- 使用步骤:
✒️提供指定线程数量的线程池
✒️提供实现Runnable接口或者Callable接口实现类对象
✒️关闭线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
public class ThreadPollTest {
public static void main(String[] args) {
/* 1.提供指定线程数量的线程池 */
ExecutorService service = Executors.newFixedThreadPool(10);
/* 2.设置线程池的属性[可选] */
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) service;
threadPoolExecutor.setCorePoolSize(10); //设置核心池大小
threadPoolExecutor.setMaximumPoolSize(10); //设置最大线程数
/* 3.提供实现Runnable接口或者Callable接口实现类对象 */
service.submit(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2 == 0) System.out.print(i+" ");
}
}
});
/* 4.关闭线程池 */
service.shutdown();
}
}