多线程
一、概念
1、程序(program)、进程(process)与线程(thread)
程序(program):是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
进程(process):是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。——生命周期
如:
-
运行中的QQ,运行中的MP3播放器
-
程序是静态的,进程是动态的
-
进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域、
线程(thread):进程可进一步细化为线程,是一个程序内部的一条执行路径。
- 若一个进程同一时间并行执行多个线程,就是支持多线程的
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
- 一个进程中的多个线程共享相同的内存单元/内存地址空间
- 它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。
2、单核CPU和多核CPU
单核CPU:其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。
多核CPU:能更好的发挥多线程的效率。多个核心同时工作
3、并行与并发
并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。
二、线程创建方式
1、继承Thread类
a、核心步骤
- 定义子类继承Thread类。
- 子类中重写Thread类中的run方法。
- 创建Thread子类对象,即创建了线程对象。
- 调用线程对象start方法:启动线程,调用run方法。
实例:
package Day1_1;
/*
*项目名: OneDayLearn1
*文件名: Threadlen
*创建者: SWY
*创建时间:2023/5/27 下午9:20
*描述: TODO
*/
public class Threadlen extends Thread {
public static void main(String[] args) {
Threadlen threadlen = new Threadlen();
threadlen.start();
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() +": "+i);
}
}
public void run(){
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() +": "+i);
}
}
}
b、注意点
- 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式,想要启动多线程,必须调用start方法。
- run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
- 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上的异常“
IllegalThreadStateException
”。
c、线程中常用构造器
public Thread() :分配一个新的线程对象
public Thread(String name) : 分配一个指定名字的新的线程对象
public Thread(Runnable target) :创建指定线程的目标对象,它实现了Runnable接口的run方法。
public Thread(Runnable target String name) ; fp一个指定线程的目标对象并且带有线程名称。
d、常用方法
1.static Thread currentThread()(静态方法)
用于获取当前正在执行的线程对象。在多线程程序中,每个线程都有一个唯一的线程对象,可以通过
currentThread()
方法获取当前线程的引用。通过这个引用,你可以访问和操作当前线程的属性和方法。
String threadName = Thread.currentThread().getName();
System.out.println("当前线程名称:" + threadName);
2.void start
启动线程,并执行对象的run()方法
3.String getName
返回线程的名称
4.void setName(String Name)
设置线程的名称
5.static void yield (线程让步)
暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
若队列中没有同优先级的线程,忽略此方法
6.static void sleep(long millis)
使得当前进程睡眠指定的毫秒
抛出InterruptedException异常
7.join()
join(long millis): 等待该线程终止时间最长millis毫秒,如果超过该时间则不再等待。
join(long millis,long nanos): nanos纳秒
当某个程序执行流中调用其他线程的 join() 方法时,调用线程将
被阻塞,直到 join() 方法加入的 join 线程执行完为止低优先级的线程也可以获得执行
8.stop()
强制线程生命期结束,不推荐使用
9.boolean isAlive()
返回boolean,判断线程是否还活着
2、实现Runnable接口
a、核心步骤
- 定义子类,实现Runnable接口。
- 子类中重写Runnable接口中的run方法。
- 通过Thread类含参构造器创建线程对象。
- 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
- 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
实例:
package Day1_1;
/*
*项目名: OneDayLearn1
*文件名: RunnableOne
*创建者: SWY
*创建时间:2023/5/28 下午12:48
*描述: TODO
*/
//定义子类实现Runnable接口
public class RunnableOne implements Runnable{
//重写run方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+" : "+ i);
}
}
}
public class Main {
public static void main(String[] args) {
RunnableOne runnableOne = new RunnableOne();
//通过Thread类含参构造器创建线程对象。
//将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
Thread thread = new Thread(runnableOne);
//调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
thread.start();
Thread thread1 = new Thread(runnableOne);
thread1.start();
}
}
3、两种方式的区别与联系
a、区别
- 继承Thread:线程代码存放Thread子类run方法中。
- 实现Runnable:线程代码存在接口的子类的run方法。
b、Runnable实现方式的好处
-
避免了单继承的局限性
-
多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。
因为实现的方法可以多个线程传递同一个子类,但是继承
Thread
类就不能调用多个run
方法,只能重新创建对象。 -
实现代码与数据的分离
c、与Thread的联系
public class Thread extends Object implements Runnable(代理模式)
调用Thread的run方法然后,Thread 的run方法又调用Runnable的run方法
4、实现Callable接口
a、核心步骤
- 创建一个实现
Callable
类的实现类 - 实现
call
方法,将线程需要执行的操作声明在call
中 - 创建
Callable
接口实现类的对象 - 将
Callable
接口实现类的对象,作为参数传递到FutureTask
构造器中,创建FutureTask
类的对象 - 将
FutureTask
类的对象传递到Thread
构造器中,创建Thread
类的对象 - 调用
Thread
的start
方法
使用FutureTask类的get方法,返回实现Callable接口的call方法。
为什么要放入FutureTask类中???
实际上FutrurTask类也实现了Runnable接口。
主要原因有两点:
- 封装任务:
Callable
接口代表了一个可以在其他线程中执行的任务,通过将它的实现类对象传入FutureTask
类中,可以将该任务封装起来,以便提交给线程池或执行器执行。- 获取结果:
FutureTask
类提供了方法来管理任务的执行状态和获取任务的结果。通过创建FutureTask
对象,并将Callable
实现类对象传入,可以获取该任务的执行结果。通过get()
方法可以阻塞当前线程,并等待任务执行完成并返回结果。或者使用isDone()
方法检查任务是否完成,通过cancel()
方法取消任务的执行。
import java.util.concurrent.Callable;
//1、创建一个实现`Callable`类的实现类
public class Callable1 implements Callable {
//2、实现`call`方法,将线程需要执行的操作声明在`call`中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i < 100; i++) {
if(i%2 == 0){
System.out.println(Callable.class + " :" +i);
sum += i;
}
}
return sum;
}
}
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Mian {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//3、创建`Callable`接口实现类的对象
Callable1 callable1 = new Callable1();
//4、将`Callable`接口实现类的对象,作为参数传递到`FutureTask`构造器中,创建`FutureTask`类的对象
FutureTask futureTask = new FutureTask(callable1);
//5、将`FutureTask`类的对象传递到`Thread`构造器中,创建`Thread`类的对象
Thread thread = new Thread(futureTask);
//6、执行start方法
thread.start();
//在主线程中调用FutrueTask.get方法此时,主线程是被堵塞的
System.out.println("sum = "+futureTask.get());
}
}
在主线程中调用FutrueTask.get方法此时,主线程是被堵塞的
b、主要方法
1.call();
比起
run
方法来说,call
方法更加灵活,他可以直接抛出异常,并且还提供了返回值。
public class Callable1 implements Callable {
@Override
//提供了抛出异常的功能,不必要在call方法中解决异常
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i < 100; i++) {
if(i%2 == 0){
System.out.println(Callable.class + "i");
sum += sum;
}
}
//返回了一个int值
return sum;
}
}
c、与Runnable接口相比的好处
- call方法可以有返回值更加灵活
- call可以抛出异常
- Callable使用了泛型参数,可以指明call的返回值类型
5、使用线程池
a、好处
- 提高程序执行效率,因为线程已经被创建好了
- 提高资源的复用率
- 可以设置相关参数管理线程
三、线程优先级
1、等级
2、方法
a、getPriority()
输出线程优先级
b、setPriority()
设置线程优先级
3、说明
-
线程创建时继承父线程的优先级
-
低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
4、线程的分类
a、用户线程
用户线程(User Thread)是指在程序中由用户创建和控制的线程。用户线程是相对于守护线程(Daemon Thread)而言的。
在Java中,当启动一个线程时,默认情况下创建的是用户线程。用户线程的特点是当所有用户线程都执行完毕时,即使还有守护线程在运行,JVM也会退出。
用户线程在程序执行过程中承担着实际的业务逻辑任务,比如处理用户请求、计算数据、与外部系统交互等。用户线程的生命周期由用户代码控制,线程的创建、启动、暂停、恢复、停止等操作都由用户代码实现。
可以通过Thread
类的setDaemon
方法来设置线程的守护属性。默认情况下,线程的守护属性为false
,即用户线程。
Thread userThread = new Thread(() -> {
// 用户线程的任务逻辑
});
userThread.setDaemon(false); // 设置为用户线程
userThread.start(); // 启动线程
b、守护线程
守护线程(Daemon Thread)是在后台运行的线程,它的任务是为其他非守护线程提供服务。当所有非守护线程都结束运行时,守护线程会自动退出。
守护线程通常用于执行一些后台任务,例如垃圾回收、自动保存等。它们不会阻止程序的退出,即使守护线程仍然在运行。
在 Java 中,可以通过将线程的 setDaemon(true)
方法设置为 true
来将线程设置为守护线程。默认情况下,线程是非守护线程。
package Day1_1;
/*
*项目名: OneDayLearn1
*文件名: Gurad
*创建者: SWY
*创建时间:2023/5/28 下午3:08
*描述: TODO
*/
public class Gurad {
public static void main(String[] args) {
Thread daemonThread = new Thread(() -> {
while (true) {
System.out.println(Thread.currentThread().getName()+" 守护线程执行中...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
daemonThread.setDaemon(true); // 设置为守护线程
daemonThread.start();
// 主线程休眠5秒后退出
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 主线程退出");
}
}
在上述代码中,我们创建了一个守护线程 daemonThread
,它会不断地输出一条消息。主线程休眠5秒后退出,而守护线程会随着主线程的退出而结束。
四、生命周期
新建、准备、运行、死亡、堵塞
1、线程的状态
五、同步机制
1、概念引入
例如:一个车站有三个窗口卖票。
我们先看使用Runnable
方法创建线程的结果
package Day2.Runnable;
/*
*项目名: OneDayLearn1
*文件名: Main
*创建者: SWY
*创建时间:2023/5/28 下午5:07
*描述: TODO
*/
public class Main {
public static void main(String[] args) {
Windows windows = new Windows();
Thread windows1 = new Thread(windows,"窗口1");
Thread windows2 = new Thread(windows,"窗口2");
Thread windows3 = new Thread(windows,"窗口3");
windows1.start();
windows2.start();
windows3.start();
}
}
public class Windows implements Runnable {
static int ticket = 100;
@Override
public void run() {
while (true){
if(ticket > 0){
System.out.println(Thread.currentThread().getName() +" " + ticket);
}else{
break;
}
ticket--;
}
}
}
通过这个代码我们实现三个窗口同时进行卖票,运行结果如下:
很明显出现了问题,三个窗口同时卖了一张票,为什么会这样?
我们的理想状态应该是这样:
实际可能状态:
多线程出现了安全问题
1.问题的原因:
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
2.解决办法:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
2、同步监视器 synchronized
a、使用synchronized
同步代码块:
synchronized (同步监视器){
//需要被同步的代码
}
同步监视器:可以使用一个类来充当(需要定义为static),但是多个线程必须公用一个同步监视器。
一般说来继承
Thread
类线程的同步监视器使用:subclass.class
实现
Runnable
类线程的同步监视器使用:this
同步方法:
public synchronized void show(String name){
//需要被同步的代码
}
非静态同步方法:默认的同步监视器就是
this
静态同步方法:默认同步监视器就是当前类的反射对象
b、同步锁原理
在《Thinking in Java》中,是这么说的:对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。 防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。
3、同步的范围
a、如何找问题,即代码是否存在线程安全?(非常重要)
(1)明确哪些代码是多线程运行的代码
(2)明确多个线程是否有共享数据
(3)明确多线程运行代码中是否有多条语句操作共享数据
b、如何解决呢?(非常重要)
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
即所有操作共享数据的这些语句都要放在同步范围中
c、注意点
范围太小:没锁住所有有安全问题的代码
范围太大:没发挥多线程的功能
4、释放锁的操作
- 当前线程的同步方法、同步代码块执行结束。
- 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导
致异常结束。 - 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线
程暂停,并释放锁。
在Java中,
wait()
方法是Object
类的一个方法,用于在多线程环境下实现线程的等待和唤醒操作。具体来说,wait()
方法用于使当前线程进入等待状态,并释放该对象的锁,直到其他线程调用相同对象的notify()
或notifyAll()
方法来唤醒该线程。
wait()
方法有以下几个重要的特点:
wait()
方法必须在同步代码块或同步方法中使用,即在使用wait()
方法之前必须获取该对象的锁。- 当调用
wait()
方法时,当前线程会释放对象的锁,使得其他线程可以获取该锁并执行相应的操作。wait()
方法会使当前线程进入等待状态,直到其他线程调用相同对象的notify()
或notifyAll()
方法来唤醒该线程。- 被唤醒的线程会从
wait()
方法返回,并且在返回前会重新获取对象的锁。wait()
方法可以被中断,即在等待过程中,其他线程调用该线程的interrupt()
方法可以中断等待。wait()
方法可以指定等待的时间,在等待超过指定时间后,线程会自动唤醒。使用
wait()
方法可以实现线程之间的协作和同步,允许线程等待某个条件满足后再继续执行,避免了线程的忙等待,提高了线程的效率和资源利用率。通常与notify()
、notifyAll()
等方法配合使用,实现线程间的通信和同步操作。
5、不会释放锁的操作
-
线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
-
线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程
挂起,该线程不会释放锁(同步监视器)。应尽量避免使用suspend()和resume()来控制线程
6、使用synchronized实现售票问题
a、Runnable型
package Day2.Runnable;
/*
*项目名: OneDayLearn1
*文件名: Main
*创建者: SWY
*创建时间:2023/5/28 下午5:07
*描述: TODO
*/
public class Main {
public static void main(String[] args) {
Windows windows = new Windows();
Thread windows1 = new Thread(windows,"窗口1");
Thread windows2 = new Thread(windows,"窗口2");
Thread windows3 = new Thread(windows,"窗口3");
windows1.start();
windows2.start();
windows3.start();
}
}
public class Windows implements Runnable {
static int ticket = 100;
@Override
public void run() {
while (true){
synchronized(this){
if(ticket > 0){
try {
Thread.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() +" " + ticket);
}else{
break;
}
ticket--;
}
}
}
}
b、Thread型
package Day2.Thread;
/*
*项目名: OneDayLearn1
*文件名: Main
*创建者: SWY
*创建时间:2023/5/28 下午5:07
*描述: TODO
*/
public class Main {
public static void main(String[] args) {
Windows windows1 = new Windows("窗口1");
Windows windows2 = new Windows("窗口2");
Windows windows3 = new Windows("窗口3");
windows1.start();
windows2.start();
windows3.start();
}
}
public class Windows extends Thread {
static int ticket = 100;
@Override
public void run() {
while (true){
try {
Thread.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized(Windows.class){
if(ticket > 0){
System.out.println(Thread.currentThread().getName() +" " + ticket);
}else{
break;
}
ticket--;
}
}
}
public Windows(String name) {
super(name);
}
}
六、volastile (保证数据不会被多次修改)
它的主要作用是保证变量在多线程环境下的可见性和禁止指令重排序
当一个变量被声明为volatile
时,意味着它可能会被多个线程同时访问和修改。volatile
关键字提供了一种轻量级的同步机制,确保对被修饰变量的读写操作具有以下特性:
- 可见性:对一个
volatile
变量的写操作会立即被其他线程可见,读操作也会读取最新的值。这意味着当一个线程修改了volatile
变量的值后,其他线程可以立即看到这个变化,而不会使用过期的值。 - 禁止指令重排序:
volatile
关键字禁止编译器和处理器对指令进行重排序,确保被修饰变量的读写操作按照程序的顺序执行。这样可以避免出现意外的并发问题.
七、锁 lock
1、创建方法 RenntrantLock
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
创建Lock的实例
private static final ReentrantLock lock = new ReentrantLock();
执行lock()方法,锁定共享资源
lock.lock();
执行unlock()方法,释放共享数据的锁定
lock.unlock();
伪代码:
class A {
private static final ReentrantLock lock = new ReenTrantLock();
public void m() {
lock.lock();
try {
//保证线程安全的代码;
} finally {
lock.unlock();
}
}
}
注意:如果同步代码有异常,要将unlock()写入finally语句块
2、lock与snychronized的区别
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是
隐式锁,出了作用域自动释放 - Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有
更好的扩展性(提供更多的子类
八、死锁
1、概念
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
2、代码样例
package Day3;
/*
*项目名: OneDayLearn1
*文件名: DeadLockTest
*创建者: SWY
*创建时间:2023/5/29 下午5:31
*描述: TODO
*/
public class DeadLockTest {
public static void main(String[] args){
final StringBuffer s1 = new StringBuffer();
final StringBuffer s2 = new StringBuffer();
//S1 -》 S2
new Thread(() -> {
synchronized (s1) {
s2.append("A");
/**
* 睡一下便于观察死锁现象,此时可能导致该线程握着同步监视器s1,此时需要同步监视器s2,
* 但是s2被另一个线程握住,而另一个线程需要s1。此时就出现了死锁现象。
*/
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (s2) {
s2.append("B");
System.out.print(s1);
System.out.print(s2);
}
}
}).start();
//S2 -》 S1
new Thread(() -> {
synchronized (s2) {
s2.append("C");
synchronized (s1) {
s1.append("D");
System.out.print(s2);
System.out.print(s1);
}
}
}).start();
}
}
3、诱发死锁的原因
- 互斥条件
- 占用且等待
- 不可抢夺(或不可占用)
- 循环等待
以上四个条件,同时出现就会出发死锁。
4、解决死锁
针对条件1:互斥条件基本上无法解决,因为线程需要通过互斥来解决安全问题
针对条件2:可以考虑一次性申请所有资源,这样就不存在等待问题
针对条件3: 占用资源的线程在进一步申请资源时,如果申请不到,就主动释放已经占用的资源
针对条件4:可以将资源改为线性顺序,申请资源时,先申请序号较小的,这样可以避免等待问题
九、进程间通信
1、wait() 与 notify() 和 notifyAll()
wait():
当前线程等待当前线程所拥有的对象的锁.wait()
令当前线程挂起并放弃CPU、同步资源并等待,同时会释放同步监视器的调用,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。
public class Number {
public static void main(String[] args) {
new Thread() {
@Override
public void run() {
int i = 0;
while (true) {
synchronized (this) {
notify();
if (i <= 100) {
System.out.println(Thread.currentThread().getName() + ":" + i++);
} else break;
try {
//线程一旦执行此方法,就进入等待状态,同时会释放同步监视器的调用。
Thread.currentThread().wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}.start();
}
}
notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
notifyAll ():唤醒正在排队等待资源的所有线程结束等待.
当前线程等待当前线程所拥有的对象的锁.notify()
被唤醒的线程从当初wait的位置继续执行
这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报java.lang.IllegalMonitorStateException异常。
因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明。
样例:
package Day3;
/*
*项目名: OneDayLearn1
*文件名: Mian
*创建者: SWY
*创建时间:2023/5/29 下午7:18
*描述: TODO
*/
public class Windows extends Thread {
static int ticket = 10000;
@Override
public void run() {
while (true) {
synchronized (Windows.class) {
Windows.class.notify(); // 在同步代码块内调用 notify() 方法
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + " " + ticket);
ticket--;
} else {
//防止最后一次无法被唤醒
Windows.class.notify();
break;
}
try {
//线程一旦执行此方法,就进入等待状态,同时会释放同步监视器的调用。
Windows.class.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public Windows(String name) {
super(name);
}
}
class Main {
public static void main(String[] args) {
Windows windows1 = new Windows("窗口1");
Windows windows2 = new Windows("窗口2");
Windows windows3 = new Windows("窗口3");
windows1.start();
windows2.start();
windows3.start();
}
}