本篇讲解java多线程
**程序(program)**是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
**进程(process)**是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。 ——生命周期
- 如:运行中的QQ,运行中的MP3播放器
- 程序是静态的,进程是动态的
- 进程作为资源分配的单位, 系统在运行时会为每个进程分配不同的内存区域
线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。
若一个进程同一时间并行执行多个线程,就是支持多线程的
线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
一个进程中的多个线程共享相同的内存单元/内存地址空间 --> 它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。
main()
主线程, gc()
垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
提高计算机系统CPU的利用率
改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread
类来体现。
Thread类的特性
每个线程都是通过某个特定Thread对象的run()
方法来完成操作的,经常把run()
方法的主体称为线程体
通过该Thread对象的start()
方法来启动这个线程,而非直接调用run()
Thread()
: 创建新的Thread对象
Thread(String threadname)
: 创建线程并指定线程实例名
Thread(Runnable target)
: 指定创建线程的目标对象,它实现了Runnable接口中的run方法
Thread(Runnable target, String name)
: 创建新的Thread对象
创建线程的两种方式
JDK1.5之前创建新执行线程有两种方法:
继承Thread类的方式
定义子类继承Thread类。
子类中重写Thread类中的run方法。
创建Thread子类对象,即创建了线程对象。
调用线程对象start()
方法:启动线程,调用run方法
//1. 创建一个继承于Thread类的子类
class MyThread extends Thread {//2. 重写Thread类的run()@Overridepublic void run() {for (int i = 0; i < 100; i++) {if(i % 2 == 0){System.out.println(Thread.currentThread().getName() + ":" + i);}}}
}//另一个类中
public static void main(String[] args) {//3. 创建Thread类的子类的对象MyThread t1 = new MyThread();//4.通过此对象调用start():①启动当前线程 ② 调用当前线程的run()t1.start();
}
注意:
start()
方法,如果调用的是run()
,并不会开启新的线程,而是当前线程直接执行内部的代码,和之前定义方法然后让对象调用是一样的。start()
的线程去执行。会报IllegalThreadStateException
使用匿名子类简化创建方式:
new Thread(){public void run(){for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName() + ":" + i);}}
}.start();
实现Runnable接口的方式
run()
start()
class MyThreadObj implements Runnable{//1. 创建一个实现了Runnable接口的类@Overridepublic void run() {//2.实现类去实现Runnable中的抽象方法:run()for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName() + ":" + i);}}
}public class ThreadTest {public static void main(String[] args) {//3.创建实现类的对象MyThreadObj myThreadObj = new MyThreadObj();//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象Thread thread = new Thread(myThreadObj);thread.setName("hengxing");thread.start();//5.通过Thread类的对象调用`start()`}
}
比较创建线程的两种方式
开发中:优先选择–>实现Runnable接口的方式
原因:
联系:public class Thread implements Runnable
thread其实也是实现了Runnable接口,实际上和第二种方式没区别
相同点:两种方式都需要重写run()
,将线程要执行的逻辑声明在run()
中。
方法 | 作用 |
---|---|
void start() | 启动线程,并执行对象的run() 方法 |
run() | 线程在被调度时执行的操作 |
String getName() | 返回线程的名称 |
void setName(String name) | 设置该线程名称 |
static Thread currentThread() | 返回当前线程。 在Thread子类中就是this,通常用于主线程和Runnable实现类 |
static void yield() | 线程让步 - 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程 - 若队列中没有同优先级的线程,忽略此方法 |
join() | 当某个程序执行流中调用其他线程的join() 方法时, 调用线程将被阻塞,直到join() 方法加入的 join 线程执行完为止低优先级的线程也可以获得执行 但是要注意:执行前确保线程已被启动。这个方法是等待join的线程完成,但是你如果连线程都没有开始执行,那不就直接结束了吗? |
static void sleep(long millis) | 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。 抛出 InterruptedException 异常 |
boolean isAlive() | 返回boolean ,判断线程是否还活着 |
如何获取和设置当前线程的优先级:
getPriority()
: 获取线程的优先级
setPriority(int p)
: 设置线程的优先级
说明:
高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。
线程创建时继承父线程的优先级
Java中的线程分为两类:一种是守护线程,一种是用户线程
start()
方法前调用thread.setDaemon(true)
可以把一个用户线程变成一个守护线程线程有五种状态:
关于声明周期我们需要关注两个概念:
状态、相应方法
状态a --> 状态b:哪些方法执行了(回调方法)
某个方法主动调用:状态a --> 状态b (例如:wait()
,sleep()
)
阻塞只是临时状态,死亡才是最终状态。程序如果一直卡在阻塞状态,就是一种异常的状态。例如:死锁。
同步是为了解决线程安全问题。先来看一个例子会更好理解:
创建三个窗口卖票,总票数为100张。使用实现Runnable
接口的方式
public class WindowTest1 {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();}
}class window implements Runnable{private int ticket = 100;@Overridepublic void run() {if (ticket <= 0) {break;}try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ":买票票号为;" + ticket--);}
}
我们会发现,有重票的情况出现,这是因为在某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。这就是线程安全问题。
理想状态下,三个线程会同时进入判断语句,均判断票号为0,跳出循环
极端状态下,三个线程均进入阻塞状态,结束阻塞后,都执行后面的买票代码。后两个线程便会输出错票。
所以我们使用同步代码块的方式解决这个问题。
先来介绍同步代码块:
synchronized (同步监视器){//需要被同步的代码
}
操作共享数据的代码,即为需要被同步的代码。 -->不能包含代码多了,也不能包含代码少了。
共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
⭐要求:多个线程必须要共用同一把锁。
在上面的例子中,我们需要在操作共享数据时使用同步锁:
@Override
public void run() {while (true) {synchronized (this) {if (ticket <= 0) {break;}try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ":买票票号为;" + ticket--);}}
}
但如果时使用继承方式实现的线程,由于其生成了多个对象,所以不能使用this作为当前同步的锁,考虑使用window.class
当前类名来作为锁(类在程序中只会加载一次,这个知识会在讲“反射”时提到。)
@Override
public void run() {while (true) {synchronized (window.class) {if (ticket <= 0) {break;}try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ":买票票号为;" + ticket--);}}
}
如果一整个方法都需要同步,那不妨将方法声明为同步方法。
private synchronized void ticket(){if (ticket <= 0) return;try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ":买票票号为;" + ticket--);
}
但如果是在使用继承Thread类方式实现的线程中,你会发现同步锁失效了。这就是我们的另一个知识点:
细心的你一定发现了,同步方法没有要求我们写同步监视器,那,他就不存在了吗?
不是的。它默认使用this
代替。恰巧我们这种方式实现的线程又会生成多个对象,用当前对象肯定不行。
解决方式就是将此同步方法声明为静态的,这时他会使用当前类来代替this
–>window.class
class window extends Thread{private static int ticket = 100;@Overridepublic void run() {while (true) {ticket();}}private static synchronized void ticket(){if (ticket <= 0) {return;}try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ":买票票号为;" + ticket--);}
}
同步的方式,解决了线程的安全问题。—好处
操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。 —局限性
之前我们写懒汉式单例提到,它是线程不安全的。现在进行改进
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;
}
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
如何解决?
一个死锁的实例:
class A {public synchronized void foo(B b) { //同步监视器:A类的对象:aSystem.out.println("当前线程名: " + Thread.currentThread().getName()+ " 进入了A实例的foo方法"); // ①try {Thread.sleep(200);} catch (InterruptedException ex) {ex.printStackTrace();}System.out.println("当前线程名: " + Thread.currentThread().getName()+ " 企图调用B实例的last方法"); // ③b.last();}public synchronized void last() {//同步监视器:A类的对象:aSystem.out.println("进入了A类的last方法内部");}
}class B {public synchronized void bar(A a) {//同步监视器:bSystem.out.println("当前线程名: " + Thread.currentThread().getName()+ " 进入了B实例的bar方法"); // ②try {Thread.sleep(200);} catch (InterruptedException ex) {ex.printStackTrace();}System.out.println("当前线程名: " + Thread.currentThread().getName()+ " 企图调用A实例的last方法"); // ④a.last();}public synchronized void last() {//同步监视器:bSystem.out.println("进入了B类的last方法内部");}
}public class DeadLock implements Runnable {A a = new A();B b = new B();public void init() {Thread.currentThread().setName("主线程");// 调用a对象的foo方法a.foo(b);System.out.println("进入了主线程之后");}public void run() {Thread.currentThread().setName("副线程");// 调用b对象的bar方法b.bar(a);System.out.println("进入了副线程之后");}public static void main(String[] args) {DeadLock dl = new DeadLock();new Thread(dl).start();//副线程启动dl.init();//主线程启动}
}
从JDK 5.0开始, Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
java.util.concurrent.locks.Lock
接口是控制多个线程对共享资源进行访问的工具。 锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock
类实现了 Lock ,它拥有与synchronized
相同的并发性和内存语义, 在实现线程安全的控制中,比较常用的是ReentrantLock
, 可以显式加锁、释放锁。
注意:Lock方式中并没有同步监视器这个概念,但是我们可以把private ReentrantLock lock = new ReentrantLock();
中的lock视为同步监视器,如果线程间没有使用同一个lock对象,就相当于没有使用同一把锁。lock不可调用wait()
、notify()
、notifyAll()
方法,但是可以通过相关的Condition
对象来实现更多操作。
使用方式
//1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();@Override
public void run() {while(true){try{//2.调用锁定方法lock()lock.lock();//需要同步的代码}finally {//3.调用解锁方法:unlock()lock.unlock();}}
}
因为最后一步一定要解锁,所以使用try--finally
的方式
优先使用顺序:
Lock --> 同步代码块(已经进入了方法体,分配了相应资源)–> 同步方法(在方法体之外)
银行有一个账户。
有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
public class DepositTest {public static void main(String[] args) {Account account = new Account();Customer c1 = new Customer(account);Customer c2 = new Customer(account);c1.setName("Tom");c2.setName("Jerry");c1.start();c2.start();}
}class Account{double balance;public synchronized void deposit(double awt){if (awt <= 0) {return;}balance += awt;try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ":sucessful deposit.The balance is " + balance);}
}class Customer extends Thread{Account acct;public Customer(Account acct) {this.acct = acct;}@Overridepublic void run() {for (int i = 0; i < 3; i++) {acct.deposit(1000);}}
}
public void deposit(double awt){if (awt <= 0) {return;}lock.lock();try {balance += awt;try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ":sucessful deposit.The balance is " + balance);} finally {lock.unlock();}
}
import java.util.concurrent.locks.ReentrantLock;
public class DepositTest {public static void main(String[] args) {Account account = new Account();Customer c1 = new Customer(account);Customer c2 = new Customer(account);Thread t1 = new Thread(c1);Thread t2 = new Thread(c2);t1.setName("Tom");t2.setName("Jerry");t1.start();t2.start();}
}class Account{double balance;ReentrantLock lock = new ReentrantLock();public synchronized void deposit(double awt){if (awt <= 0) {return;}balance += awt;try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ":sucessful deposit.The balance is " + balance);}class Customer implements Runnable{Account acct;public Customer(Account acct) {this.acct = acct;}@Overridepublic void run() {for (int i = 0; i < 3; i++) {acct.deposit(1000);}}
}
import java.util.concurrent.locks.ReentrantLock;
public class DepositTest {public static void main(String[] args) {Account account = new Account();Customer c1 = new Customer(account);Customer c2 = new Customer(account);Thread t1 = new Thread(c1);Thread t2 = new Thread(c2);t1.setName("Tom");t2.setName("Jerry");t1.start();t2.start();}
}class Account{double balance;ReentrantLock lock = new ReentrantLock();public void deposit(double awt){if (awt <= 0) {return;}lock.lock();try {balance += awt;try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ":sucessful deposit.The balance is " + balance);} finally {lock.unlock();}}
}class Customer implements Runnable{Account acct;public Customer(Account acct) {this.acct = acct;}@Overridepublic void run() {for (int i = 0; i < 3; i++) {acct.deposit(1000);}}
}
使用两个线程打印 1-100。线程1, 线程2 交替打印
/*** 使用两个线程打印 1-100。线程1, 线程2 交替打印*/
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("线程一");t2.setName("线程二");t1.start();t2.start();}
}class Number implements Runnable{private int number = 1;@Overridepublic void run() {while (true) {synchronized (this) {notify();//将阻塞的进程唤醒if (number > 100) {return;}System.out.println(Thread.currentThread().getName() + ":" + number++); try {wait();//令当前进程阻塞,等待唤醒。} catch (InterruptedException e) {e.printStackTrace();}}}}
}
涉及到的三个方法:
wait()
:一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
notify()
:一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
notifyAll()
:一旦执行此方法,就会唤醒所有被wait的线程。
说明:
wait()
,notify()
,notifyAll()
三个方法必须使用在同步代码块或同步方法中,lock锁的方式都不可以。意味着这三个方法是依赖于同步监视器的。
wait()
,notify()
,notifyAll()
三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
否则,会出现IllegalMonitorStateException
异常。
wait()
,notify()
,notifyAll()
三个方法三个方法是定义在java.lang.Object
类中。
相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
不同点:
两个方法声明的位置不同:
Thread类中声明sleep()
Object类中声明wait()
调用的要求不同:sleep()
可以在任何需要的场景下调用。 wait()
必须使用在同步代码块或同步方法中
关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()
不会释放锁,wait()
会释放锁。
经典例题:生产者/消费者问题
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
分析:
- 是否是多线程问题?是,生产者线程,消费者线程
- 是否有共享数据?是,店员(或产品)
- 如何解决线程的安全问题?同步机制,有三种方法
- 是否涉及线程的通信?是,生产者通知消费者进行消费,消费者通知生产者进行生产
public class CommunicationTest {public static void main(String[] args) {Clerk clerk = new Clerk();Customer customer = new Customer(clerk);Productor productor = new Productor(clerk);customer.setName("customer");productor.setName("productor");productor.start();customer.start();}
}class Clerk{private int number = 0;public synchronized void produce(){if (number >= 20) {//等待消费try {wait();} catch (InterruptedException e) {e.printStackTrace();}return;}System.out.println(Thread.currentThread().getName() + "开始生产第" + ++number + "个产品");notify();//生产后,唤醒消费者}public synchronized void custom(){if (number <= 0) {//等待生产try {wait();} catch (InterruptedException e) {e.printStackTrace();}return;}System.out.println(Thread.currentThread().getName() + "开始消费第" + number-- + "个产品");notify();//消费后,唤醒生产者}}class Productor extends Thread{Clerk clerk;public Productor(Clerk clerk) {this.clerk = clerk;}@Overridepublic void run() {while (true) {try {Thread.sleep(250);} catch (InterruptedException e) {e.printStackTrace();}clerk.produce();}}
}class Customer extends Thread{Clerk clerk;public Customer(Clerk clerk) {this.clerk = clerk;}@Overridepublic void run() {while (true) {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}clerk.custom();}}
}
JDK5.0 新增
与使用Runnable相比, Callable功能更强大些
run()
方法,可以有返回值FutureTask
类,比如获取返回结果实现步骤为:
创建一个实现Callable
的实现类
实现call方法,将此线程需要执行的操作声明在call()
中
创建Callable
接口实现类的对象
将此Callable
接口实现类的对象作为传递到FutureTask
构造器中,创建FutureTask
的对象
将FutureTask
的对象作为参数传递到Thread
类的构造器中,创建Thread
对象,并调用start()
获取Callable
中call方法的返回值(可选)
get()
返回值即为FutureTask
构造器参数Callable
实现类重写的call()
的返回值。
//1.创建一个实现`Callable`的实现类
class Number implements Callable{private int count = 0;//2.实现call方法,将此线程需要执行的操作声明在call()中@Overridepublic Object call() throws Exception {for (int i = 0; i < 100; i++) {if (i % 2 == 0){System.out.println(i);}count += i;}return count;}
}public class NewThread {public static void main(String[] args) {//3.创建Callable接口实现类的对象Number number = new Number();//4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象FutureTask futureTask = new FutureTask(number);//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()Thread thread = new Thread(futureTask);thread.start();//6.获取Callable中call方法的返回值//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。try {Object o = futureTask.get();System.out.println("总和为:" + o);} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}}
}
如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
call()
可以有返回值的。call()
可以抛出异常,被外面的操作捕获,获取异常的信息Callable
是支持泛型的背景: 经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
思路: 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具
好处:
corePoolSize
:核心池的大小maximumPoolSize
:最大线程数keepAliveTime
:线程没有任务时最多保持多长时间后会终止线程池相关API
JDK 5.0起提供了线程池相关API: ExecutorService
和Executors
ExecutorService
:真正的线程池接口。常见子类ThreadPoolExecutor
void execute(Runnable command)
:执行任务/命令,没有返回值,一般用来执行Runnable
Future submit(Callable task)
:执行任务,有返回值,一般又来执行Callable
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool()
:创建一个可根据需要创建新线程的线程池Executors.newFixedThreadPool(n)
; 创建一个可重用固定线程数的线程池Executors.newSingleThreadExecutor()
:创建一个只有一个线程的线程池Executors.newScheduledThreadPool(n)
:创建一个线程池,它可安排在给定延迟后运使用实例:
public static void main(String[] args) {//1. 创建线程池ExecutorService service = Executors.newFixedThreadPool(10);//2. 放入线程并启动service.execute(new OddNum());//Runnable线程启动FutureTask futureTask = new FutureTask(new EvenNum());service.submit(futureTask);//Callable线程启动//3. 关闭线程池service.shutdown();
}
线程管理
由于我们接收线程池对象时是使用多态方式接收的,我们可以查看newFixedThreadPool
源码,看到它返回的是ThreadPoolExecutor
,若想使用线程管理,就必须先进行强转。
/*** Creates a thread pool that reuses a fixed number of threads* operating off a shared unbounded queue. At any point, at most* {@code nThreads} threads will be active processing tasks.* If additional tasks are submitted when all threads are active,* they will wait in the queue until a thread is available.* If any thread terminates due to a failure during execution* prior to shutdown, a new one will take its place if needed to* execute subsequent tasks. The threads in the pool will exist* until it is explicitly {@link ExecutorService#shutdown shutdown}.** @param nThreads the number of threads in the pool* @return the newly created thread pool* @throws IllegalArgumentException if {@code nThreads <= 0}*/
public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue());
}
转换为ThreadPoolExecutor
再进行管理
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
service1.setCorePoolSize(3);//核心池的大小
上一篇:PyTorch学习笔记:nn.MarginRankingLoss——排序损失
下一篇:Oracle Dataguard(主库为 Oracle rac 集群)配置教程(03)—— 创建 dataguard 数据库之前的准备工作