目次
2. マルチスレッド化の 2 番目の実装: Runnable インターフェイス
3. マルチスレッドの 3 番目の実装: Callable インターフェイスと Future インターフェイス
7. プロデューサとコンシューマ (ウェイクアップ メカニズムを待機)
1. マルチスレッドとは?
- マルチスレッドを使用すると、プログラムに複数のことを同時に実行させることができます。
1. スレッドとプロセス
- スレッドは、オペレーティング システムで操作をスケジュールできる最小単位です。これはプロセスに含まれており、プロセス内の実際の作業単位です。
- 簡単な理解:互いに独立しており、アプリケーションの着用者で同時に実行できる機能。
-
プロセスは、プログラムの基本的な実行エンティティです。
2. 並行性と並列性
- 並行性:同時に、1 つの CPUで複数の命令が交互に実行されます。
- 並列性:同時に、複数の命令が複数の CPUで同時に実行されます。
第二に、マルチスレッドの実装
- Threadクラスを継承して実装
- Runnable インターフェースの実装による実装
- CallableインターフェースとFutureインターフェースを使って実現する
1. マルチスレッドの最初の実装: Thread クラス
まず、 API ヘルプ ドキュメントを参照して、Thread クラスとは何かを調べてみましょう。
マルチスレッドの最初の実装:
- Thread クラスを継承する クラスを手動で定義します。
- 内部のrunメソッドをオーバーライドします。
- サブクラス オブジェクトを作成し、スレッドを開始します。
public class ThreadDemo01 {
public static void main(String[] args) {
// 多线程第一种实现方式:
// 1.自己定义一个类继承Thread类
// 2.重写里面run方法
// 3.创建子类对象,并启动线程
myThread t1 = new myThread();
myThread t2 = new myThread();
// t1.run() 只是单纯调用一个方法
t1.setName("线程1");
t1.setName("线程2");
t1.start();
t2.start();
}
}
myThread.java
public class myThread extends Thread {
@Override
public void run() {
//书写线程执行代码
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "HelloWrold");
}
}
}
2. マルチスレッド化の 2 番目の実装: Runnable インターフェイス
まず、 API ヘルプ ドキュメント を参照して、Runnable インターフェースとは 何かを調べてみましょう。
マルチスレッドの 2 番目の実装:
- Runnable インターフェイスを実装するクラスを手動で定義します。
- 内のrun メソッドをオーバーライドします。
- 独自のクラスのオブジェクトを作成します。
- Thread クラスのオブジェクトを 作成し、スレッドを開始します。
public class ThreadDemo2 {
public static void main(String[] args) {
// 多线程第二种实现方式:
// 1.自己定义一个类去实现Runnable接口
// 2.重写里面的run方法
// 3.创建自己的类的对象
// 4.创建一个Thread类的对象,并开启线程
// 创建MyRun的任务对象
MyRun mr = new MyRun();
// 创建线程对象
// 将任务mr传递给线程
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
//MyRun.java
public class MyRun implements Runnable {
@Override
public void run() {
// 书写线程执行代码
for (int i = 0; i < 100; i++) {
//获取当前线程对象
Thread thread = Thread .currentThread()
System.out.println(thread.getName() + "HelloWrold");
}
}
}
3. マルチスレッドの 3 番目の実装: Callable インターフェイスと Future インターフェイス
マルチスレッドの 3 番目の実装:
- Callable インターフェイスを実装するクラス MyCallable を作成します。
- 内のcall メソッドをオーバーライドします。(戻り値はマルチスレッド操作の結果を表します)
- MyCallable のオブジェクトを作成します。(複数のスレッドで実行されるタスクを表します)
- FutureTask オブジェクトを作成します。(マルチスレッドの運用結果を管理する役割)
- Thread クラスのオブジェクトを作成し、スレッドを開始します。(スレッドを表す)
特徴:マルチスレッド操作の結果を取得できます。
public class ThreadDemo3 {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 多线程第三种实现方式:
// 特点: 可以获取到多线程运行的结果
// 1.创建一个类Mycallable实现Callable接口
// 2.重写里面的call方法(返回值表示多线程运行结果)
// 3.创建MyCallable的对象(表示多线程要执行的任务)
// 4.创建FutureTask的对象(作用管理多线程运行的结果)
// 5.创建Thread类的对象,并启动(表示线程)
//创建MyCallable对象
MyCallable mc = new MyCallable();
//创建FuturaTask对象
FutureTask<Integer> ft = new FutureTask<>(mc);
//创建线程对象
Thread t1 = new Thread();
t1.start();
//获取线程运行结果
Integer result = ft.get();
System.out.println(result);
}
//MyCallable.java
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 求1 ~ 100 和
int sum = 0;
for (int i = 0; i < 100; i++) {
sum = sum + i;
}
return sum;
}
}
4. マルチスレッドの 3 つの実装の比較
アドバンテージ | 欠点 | |
Thread クラスを継承する | 比較的単純になり、Thread クラスのメソッドを直接使用できます | 拡張性が低く、他のクラスを継承できなくなった |
Runnable を実装する | 拡張性が高く、このインターフェースを実装しながら、他のクラスを継承することもできます | プログラミングは比較的複雑で、Thread クラスのメソッドを直接使用することはできません |
Callable インターフェースを実装する |
3. マルチスレッドでよく使われるメンバーメソッド
メソッド名 | 例証する |
文字列getName ( ) | このスレッドの名前を返します |
void setName (文字列名) | スレッドの名前を設定します (コンストラクターでも名前を設定できます) |
静的スレッドcurrentThread ( ) | 現在のスレッドのオブジェクトを取得する |
静的ボイドスリープ(長時間) | ミリ秒単位で指定された時間、スレッドをスリープ状態にします |
setPriority (int newPriority ) | スレッドの優先度を設定する |
最終的な int getPriority ( ) | スレッドの優先度を取得する |
final void setDaemon ( boolean on ) | デーモンスレッドとして設定 |
public static void yield ( ) | 譲りスレ・丁寧スレ |
public static void join ( ) | スレッドの挿入/キュー スレッド |
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
// 1.getName 返回此线程的名称
myThread t1 = new myThread();
// 如果我们没有给线程命名,线程默认名字
// 格式:Thread-X(X序号,从0开始)
t1.start();
// 2.currentThread 获取当前线程的对象 (静态方法)
Thread t = Thread.currentThread();
// 哪条线程执行到这个方法,此时获取的就是哪条线程的对象
System.out.println(t.getName());
// 3.sleep 让线程休眠指定的时间
// 方法参数:表示睡眠时间,单位好眠
// 当时间到了之后,线程就会自动的醒来,继续执行下面的其他代码
System.out.println("1111");
Thread.sleep(5000);
System.out.println("2222");
}
}
スレッドの優先度:
- プリエンプティブ スケジューリング: 各スレッドを実行する CPU のタイミングと実行時間は不確実です。
- 非プリエンプティブ スケジューリング: すべてのスレッドが順番に実行され、実行時間はほぼ同じです。
public class ThreadDemo {
public static void main(String[] args) {
//创建线程要执行的参数对象
MyRunnable mr = new MyRunnable();
//创建线程对象
Thread t1 = new Thread(mr,"飞机");
Thread t2 = new Thread(mr,"坦克");
//优先级默认 : 5
System.out.println(t1.getPriority());
System.out.println(t2.getPriority());
System.out.println(Thread.currentThread().getPriority());
//细节:当其他的非守护线程执行完毕之后,守护线程将会陆续结束。
// 把第二个线程设置为守护线程
t2.setDaemon(true);
}
}
第四に、スレッドのライフサイクル
質問: sleep メソッドはスレッドをスリープ状態にします.スリープ時間が経過したら、次のコードはすぐに実行されますか?
- 答え:いいえ。sleep メソッドが終了すると、ready 状態になり、CPU の実行権を獲得してから次のコードが実行されます。
スレッドの 6 つの状態:
新しい状態 (新しい) | スレッド オブジェクトを作成する |
準備完了状態 (RUNNABLE) | 開始方法 |
ブロック状態 (BLOCKED ) | ロック オブジェクトを取得できません |
待機状態 ( WAITING ) | 待機方法 |
待機時間 (TIMED_WAITING) | 睡眠方法 |
終了状態 ( TERMINATED ) | すべてのコードの実行 |
五、スレッドセーフの問題
小さな演習でスレッド セーフについて学びます。
要件:
現在国内の大ヒット作を上映中の映画館で合計100枚のチケットが販売されており、3つの窓口でチケットを販売している映画館をシミュレートしてチケットを販売するプログラムを設計してください
public class ThreadDemo {
public static void main(String[] args) {
//创建线程对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
//线程命名
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
//开启线程
t1.start();
t2.start();
t3.start();
}
}
//MyThread.java
public class MyThread extends Thread {
// 表示这个类的对象都共享一个ticket对象
static int ticket = 0;
@Override
public void run() {
// 书写线程执行代码
while (true) {
if (ticket < 100) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票");
} else {
break;
}
}
}
}
同じチケットを販売するか、チケットの数を超える 3 つの窓口があることがわかりました。
では、どうすれば修正できるでしょうか。
同期:
(1) リエントラント
同期ロック オブジェクトには、スレッドがロックを取得した回数を記録するカウンター (再帰変数) があります。同期は
再入可能ロックであり、各ロック オブジェクトには、スレッドが取得した回数を記録するカウンターがあります。同期コードブロックが実行されると、カウンタ数が 0 になるまでカウンタ数が -1 になり、ロックが解除されます。(2) 無停電
1 つのスレッドがロックを取得した後、別のスレッドがロックを取得するには、ブロック状態または待機状態である必要があります。最初のスレッドがロックを解放しない場合、2 番目のスレッドは常にブロックまたは待機状態になり、中断することはできません。
1. 同期コードブロック
- 共有データを操作するコードをロックダウンする
- 特徴1:ロックはデフォルトで開いており、スレッドが入ると自動的にロックが閉じます。
- 特徴2:中のコードが全て実行され、糸が出て、自動的に錠前が開く。
public class MyThread extends Thread {
// 表示这个类的对象都共享一个ticket对象
static int ticket = 0;
// 锁对象一定唯一
static Object obj = new Object();
@Override
public void run() {
// 书写线程执行代码
while (true) {
//锁对象是任意的
synchronized(obj) {
if (ticket < 100) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票");
} else {
break;
}
}
}
}
}
2. 同期方法
- メソッドに synchronized キーワードを追加するだけです。
- 特徴1:同期方法は、メソッド内のすべてのコードをロックすることです
- 特徴2:ロックオブジェクトを単独で指定することはできません。
public class MyRunnable implements Runnable {
int ticket = 0;
@Override
public void run() {
// 1.循环
while (true) {
// 2.同步代码块(同步方法)
if (method()) {
break;
}
}
}
//this
public synchronized boolean method() {
// 3.判断共享数据是否到了末尾 如果到了末尾
if (ticket == 100) {
return true;
// 4.判断共享数据是否到了末尾 如果没到末尾
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
ticket++;
System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票");
}
return false;
}
}
3.ロックロック
同期されたコードブロックと同期されたメソッドのロックオブジェクトの問題は理解できますが、
しかし、どこにロックが追加され、どこでロックが解除されたかを直接見ることはできませんでした。
ロックをロックおよび解除する方法をより明確に表現するために、JDK5 は新しいロック オブジェクト Lock later を提供します。
ロックの実装は、同期されたメソッドとステートメントを使用して取得できるよりも広い範囲のロック操作を提供します。
Lock は、ロックを取得および解放するメソッドを提供します。
メンバーメソッド | 例証する |
ボイドロック( ) | ロックを取得します |
ボイドアンロック( ) | ロック解除 |
Lock は直接インスタンス化できないインターフェイスであり、その実装クラス ReentrantLock がここでインスタンス化されます。
施工方法 | 例証する |
リエントラントロック ( ) | ReentrantLock のインスタンスを作成する |
public class MyRunnable implements Runnable {
int ticket = 0;
// 多个对象共享同一个锁
static Lock lock = new ReentrantLock();
@Override
public void run() {
// 1.循环
while (true) {
// 2.同步代码块(同步方法)
// synchronized (MyThread.class) {
lock.lock();
try {
// 3.判断共享数据是否到了末尾 如果到了末尾
if (ticket == 100) {
break;
// 4.判断共享数据是否到了末尾 如果没到末尾
} else {
Thread.sleep(100);
ticket++;
System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
六、デッドロック
- いわゆるデッドロックとは、動作中に複数のプロセスがリソースを競合してデッドロック状態になることを指し、プロセスがこのデッドロック状態になると、プロセスは外力なしでは前に進むことができなくなります。
- デッドロックは誤りであり、将来のプログラミング プロセスではロックのネストを避ける必要があります。
//MyThread.java
public class MyThread extends Thread {
static Object objA = new Object();
static Object objB = new Object();
@Override
public void run() {
// 1.循环
while (true) {
if ("线程A".equals(getName())) {
synchronized (objA) {
System.out.println("线程A拿到了A锁,准备拿B锁");
synchronized (objB) {
System.out.println("线程A拿到了B锁,顺利执行完一轮");
}
}
} else if ("线程B".equals(getName())) {
if ("线程B".equals(getName())) {
synchronized (objB) {
System.out.println("线程B拿到了B锁,准备拿A锁");
synchronized (objA) {
System.out.println("线程B拿到了A锁,顺利执行完一轮");
}
}
}
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.setName("线程A");
t2.setName("线程B");
t1.start();
t2.start();
}
}
実行結果: (スタック)
7. プロデューサとコンシューマ (ウェイクアップ メカニズムを待機)
プロデューサー コンシューマー モデルは、非常に古典的なマルチスレッド コラボレーション モデルです。
一般的な方法:
メンバーメソッド | 例証する |
無効待機( ) | 現在のスレッドは、別のスレッドによって起動されるまで待機します |
無効通知( ) | シングルスレッドを起こす |
void notifyAll ( ) | すべてのスレッドを起こす |
例:
1. 消費者コードの実装
//Desk.java
public class Desk {
// 作用: 控制生产者和消费者的执行
//判断桌子上是否有面条: 0:没有 ; 1:有
public static int foodFlag = 0;
//定义总个数
public static int count = 10;
//锁对象
public static Object lock = new Object();
}
//Foodie.java
public class Foodie extends Thread {
@Override
public void run() {
// 1.循环
while (true) {
// 同步代码块
synchronized (Desk.lock) {
if (Desk.count == 0) {
break;
} else {
// 先判断桌子上是否有面条
if (Desk.foodFlag == 0) {
// 没有:等待
try {
Desk.lock.wait(); // 让当前线程与锁进行绑定
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
// 把吃的总数- 1
Desk.count--;
// 有: 开吃
System.out.println("吃货在吃面条,还能再吃" + Desk.count + "碗");
// 吃完之后:唤醒厨师继续做
Desk.lock.notifyAll();
// 修改桌子的状态
Desk.foodFlag = 0;
}
}
}
}
}
}
2.プロデューサーコードの実装
public class ThreadDemo {
public static void main(String[] args) {
// 创建线程对象
Cook c = new Cook();
Foodie f = new Foodie();
// 线程命名
c.setName("厨师");
f.setName("吃货");
//开启线程
c.start();
f.start();
}
}
//Cook.java
public class Cook extends Thread{
@Override
public void run() {
// 1.循环
while (true) {
// 同步代码块
synchronized (Desk.lock) {
if (Desk.count == 0) {
break;
} else {
// 判断桌子上是否有食物
if (Desk.foodFlag == 1) {
// 如果有:就等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
// 没有: 就制作食物
System.out.println("厨师做了一碗面条");
// 修改桌子上的食物状态
Desk.foodFlag = 1;
// 等待的消费者开吃
Desk.lock.notifyAll();
}
}
}
}
}
}
八、スレッドプール
以前にマルチスレッドを作成することの欠点:
欠点 1:スレッドを使用する場合は、スレッドを作成する必要があります | デメリット2:使い切ると糸が消える |
したがって、スレッドプールを導入します。
スレッド プールは、実際にはマルチスレッド処理の一種であり、処理中にタスクをキューに追加し、スレッドの作成後にこれらのタスクを自動的に開始することができます。
- 空のプールを作成します。
- タスクをサブミットすると、プールは新しいスレッド オブジェクトを作成します。タスクが実行された後、スレッドはプールに戻されます。次回タスクをサブミットするときに、新しいスレッドを作成する必要はなく、既存のスレッドは直接再利用できます。
- ただし、プールにアイドル状態のスレッドがなく、タスクの送信時に新しいスレッドを作成できない場合、タスクはキューに入れられます。
1.スレッドプールメソッドの実装
Executors:スレッド プール ツール クラスは、メソッドを呼び出すことによって、さまざまな種類のスレッド プール オブジェクトを返します。
メソッド名 | 例証する |
public static ExecutorService newCachedThreadPool ( ) | 無制限のスレッド プールを作成する |
public static ExecutorService newFixedThreadPool ( int nThreads ) | 创建有上限的线程池 |
public class MyThreadPoolDemo {
public static void main(String[] args) {
// 1.获取线程池对象
ExecutorService pool1 = Executors.newCachedThreadPool();
ExecutorService pool2 = Executors.newFixedThreadPool(3);
// 2.提交任务
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
// pool2只能看到3个线程
pool2.submit(new MyRunnable());
pool2.submit(new MyRunnable());
pool2.submit(new MyRunnable());
pool2.submit(new MyRunnable());
// 3.销毁任务
pool1.shutdown();
}
}
//MyRunnable.java
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " - - " + i);
}
}
}
2.自定义线程池
任务拒绝策略 | 说明 |
ThreadPoolExecutor.AbortPolicy | 默认策略:丢弃任务并抛出RejectedExecutionException异常 |
ThreadPoolExecutor.DiscardPolicy | 丢弃任务,但是不抛出异常这是不推荐的做法 |
ThreadPoolExecutor.DiscardoldestPolicy | 抛弃队列中等待最久的任务然后把当前任务加入队列中 |
ThreadPoolExecutor.callerRunsPolicy | 调用任务的run()方法绕过线程池直接执行 |
- 核心元素一:核心线程的数量(不能小于0)
- 核心元素二:线程池中最大线程的数量(最大数量>=核心线程数量)
- 核心元素三:空闲时间(值)(不能小于0)
- 核心元素四:空闲时间(单位)(用TimeUnit指定)
- 核心元素五:堵塞队列(不能为null)
- 核心元素六:创建线程的方式(不能为null)
- 核心元素七:要执行的任务过多时的解决方案(不能为null)
public class MyThreadPoolDemo {
public static void main(String[] args) {
ThreadPoolExecutor pool1 = new ThreadPoolExecutor(3, // 核心线程数量,不能小于0
6, // 最大线程数,不能小于0,最大数量 >= 核心线程数量
60, // 空间线程最大存活时间
TimeUnit.SECONDS, // 时间单位
new LinkedBlockingQueue<>(3), // 任务队列
Executors.defaultThreadFactory(), // 创建线程工厂
new ThreadPoolExecutor.AbortPolicy() // 任务拒绝策略
);
}
}
不断的提交任务,会有以下三个临界点:
- 当核心线程满时,再提交任务就会排队。
- 当核心线程满,队伍满时,会创建临时线程。
- 当核心线程满,队伍满,临时线程满时,会触发任务拒绝策略。
3.最大并行数
CPU密集型运算 (读取文件操作比较少) |
|
I/O密集型运算 (读取文件操作比较多) |
public class MyThreadPoolDemo {
public static void main(String[] args) {
//向Java虚拟机返回可用处理器的数目
int count = Runtime.getRuntime().availableProcessors();
System.out.println(count); //12
}
}
所以线程池多大合适呢?
示例:(4线8核通过计算公式)
九、综合练习
1. 抢红包
抢红包也用到了多线程。
假设:100块,分成了3个包,现在有5个人去抢。其中,红包是共享数据。
5个人是5条线程。
打印结果如下:
XXX抢到了XXX元XXX抢到了XXX元
XXX抢到了XXX元
XXX没抢到
XXX没抢到
public class MyThread extends Thread {
// 总金额
static BigDecimal money = BigDecimal.valueOf(100.0);
// 个数
static int count = 6;
// 最小抽奖金额
static final BigDecimal MIN = BigDecimal.valueOf(0.01);
@Override
public void run() {
synchronized (MyThread.class) {
if (count == 0) {
System.out.println(getName() + "没有抢到红包");
}else {
//中奖金额
BigDecimal prize;
if (count == 1) {
prize = count;
}else {
//获取抽奖范围
double bounds = money.subtract(BigDecimal.valueOf(count -1).multiply(MIN).doubleValue());
Random r = new Random();
//抽奖金额
prize = BigDecimal.valueOf(r.nextDouble()bounds);
}
//设置抽中红包,小数点保留两位,四舍五入
prize = prize.setScale(2,RoundingMode.HALF_UP);
//在总金额中去掉对应的钱
money = money.subtract(prize);
//红包少了一个
count--;
//输出红包信息
System.out.println(getName() + "抽中了" + prize + "元");
}
}
}
}
//Test.java
public class Test {
public static void main(String[] args) {
// 创建线程对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
MyThread t4 = new MyThread();
MyThread t5 = new MyThread();
MyThread t6 = new MyThread();
// 线程命名
t1.setName("张三");
t2.setName("李四");
t3.setName("王五");
t4.setName("赵六");
t5.setName("钱七");
t6.setName("孙八");
// 线程启动
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
t6.start();
}
}
2. 抽奖
有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池中的奖项为{10,5,20,50,100,200,500,800,2,80,300,700};
创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1”,“抽奖箱2”随机从抽奖池中获取奖项元素并打印在控制台上,格式如下:
每次抽出一个奖项就打印一个(随机)
抽奖箱1又产生了一个10元大奖抽奖箱1又产生了一个100元大奖
抽奖箱1又产生了一个200元大奖
抽奖箱1又产生了一个800元大奖
抽奖箱2又产生了一个700元大奖
...
public class MyThread extends Thread {
ArrayList<Integer> list;
public MyThread(ArrayList<Integer> list) {
this.list = list;
}
@Override
public void run() {
while (true) {
synchronized (MyThread.class) {
if (list.size() == 0) {
break;
} else {
// 继续抽奖
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
Collections.shuffle(list);
int prize = list.remove(0);
System.out.println(getName() + "又产生了一个" + prize + "元大奖");
}
}
}
}
}
//Test.java
public class Test {
public static void main(String[] args) {
//创建奖池
ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list, 10,5,20,50,100,200,500,800,2,80,300,700);
// 创建线程对象
MyThread t1 = new MyThread(list);
MyThread t2 = new MyThread(list);
// 线程命名
t1.setName("抽奖箱1");
t2.setName("抽奖箱2");
// 线程启动
t1.start();
t2.start();
}
}