笔记源代码地址:B站 库米豪斯巴达锅铲祖师 空间
最近学校的信息技术暑假作业来了,去做了一下,so没更文
(怎么暑假了也事这么多qwq)
线程与进程的区别
进程,也就是平常非专业人员所说的应用,从你打开这个程序开始,到程序运行完毕自动退出,这是进程的生命周期
线程,是一个进程中的执行单元,在进程里独立运行,一般情况下互不干扰
一个进程可以同时启动多个线程。在Java中,main方法控制的线程表示主线程;主线程的结束不代表进程的结束
对于一个hello world程序来说,至少有两个并行线程:除了我们看到的主线程,还存在一个垃圾回收线程来清理程序不要的东西
两个进程的内存是互相独立的,而一个进程中的多个线程内存可能相互影响。在一个进程中,所有线程的方法区内存和堆内存共用,但每个线程都有自己的栈内存
线程的生命周期
对于多核CPU,可以真正实现多线程并行:每个核都运行自己的线程,互不干扰
而对于单核CPU或者CPU的核的数量比线程数量少的时候,真正的多线程并行是不能做到的,那么这个CPU会去这样调用线程:运行一会儿这个线程,然后撒手去运行下一个线程,再撒手去运行别的线程。这个动作的切换速度很快,人眼是不能看出来区别的,但是当线程太多并且每个线程都比较麻烦的时候,可能会出现掉帧的现象
(所以不要在开着QQ的情况下玩起床战争,心态要没了)
一个线程从创建到没用,就是这样被CPU调用的。至于每个线程调用的时间,靠JVM来决定。从线程出现开始,它会经历这样的大起大落 ->
- 新建状态:一个线程刚刚被创建的时候,没有人对它进行任何操作,它不会自己运行,它会在哪里等你叫它
- 就绪状态:你现在把这个线程叫出来运行了,但是这个线程不会马上开始运行。它会试图让CPU注意到自己,专业术语叫试图抢夺CPU时间片。如果当前有别的线程也在运行,它会尽最大努力让CPU看到自己
- 运行状态:现在这个线程成功让CPU注意到自己了,它会在CPU里面运行一段时间。如果运行时间到了,它会被CPU轰出来,进入就绪状态;但是如果它没有被CPU轰出来,是被其他东西强行拖出来了(比如线程进入了休眠),它会进入阻塞状态
- 阻塞状态:这个线程由于不可控因素,从就绪状态或运行状态中被拖出来,堵在外面,等待把它拖出来的事件结束。这段时间内,这个线程不能抢夺CPU时间片,也会放弃原来占用的CPU时间片。阻塞状态结束后,线程将回到就绪状态重新叫CPU看它
- 死亡状态:线程里的代码都运行完了,它的利用价值没有了,然后它悲痛欲绝(bushi)然后它就结束了
线程的三种创建方式
Java很好心地给广大即将秃头的Java程序员提供了线程类,用它创建了对象就是一个可运行的线程。这就是最基础的创建方式 ->
直接创建并运行
这个Java提供的类叫做Thread
,用它创建的对象可以直接开始运行,也就是我们可以这样弄一个线程:
Thread t = new Thread();
但是!你觉得Java都不知道你要干什么,它给你的线程里面会有些什么东西?我们要弄一个线程,还要让这个线程做事情,相信一般人都会想到:创建子类,这样它也是一个Thread,也可以运行,但是这是我们可以更改的内容,我们可以操控它的行为
public MyThread extends Thread {
}
然后之前想到这办法的一般人都陷入了沉思,因为IDE不报错,不知道需要干什么了
这里需要知道,线程是根据里面的run()
方法来运行的。这个run方法相当于主线程的main()
方法。启动线程后,这个run方法里的代码就会同步运行。由此,我们需要重写一下run方法
为了测试线程的并行,我们给它写一个循环语句
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
在这个输出语句的参数中,Thread.currentThread()
的作用是获取当前的线程对象。比如这个语句在线程t1
的run方法里面,这一句就会返回t1
对象;getName()
可以获取线程的名字,你也可以用t1.setName()
来自己给线程t1
起名字。如果你拒绝给它一个名字,它也会有默认的名字,看起来像这样:Thread-0
,Thread-1
所以这个语句的输出效果是:当前线程的名字: 数字
然后我们给这个Thread的子类创建一个对象(你想给它起名字也是可以的)然后问题又来了:我们没有叫它,它不会运行,那我们怎么让它动起来呢?
之前的一般人又讲话了,说直接运行run方法就好了。。?
然后这人在main方法里也写了一个一样的for循环,运行,发现:他的线程运行了一遍,完事main里面的for循环运行了一遍,控制台里面明显的两块先后运行,重启都不管用
实际上,要把线程叫出来运行,需要用到一个start()
方法。这个方法会让线程进入就绪状态,然后它会自己抢CPU时间片,自己运行run方法。我们在创建线程后加上这样一行代码(假设对象名是t1):
t1.start();
如果你的主线程里也有一个一样的for循环,你会发现这次的控制台数据很乱,两个线程交替输出,那是线程成功开启了(没有现象的也没关系,多运行几次就有了)
依靠Runnable接口运行
Runnable接口表示“可运行的”,它可以用于线程。我们将一个类实现Runnable接口,然后重写一下它的run方法(我好像应该早点说这种方法,这样就会少很多对着Thread子类沉思的人)
刚才写的那个子类没删吧?没删就好,把extends Thread
改成implements Runnable
就没事了。
然后在主方法里,我们可以直接用Thread类创建对象,在构造器里面填上你刚才写的Runnable实现类(假设类名为MyRunnable
)
Thread t = new Thread(new MyRunnable());
当然你也可以考虑用匿名内部类来实现。而且这里只需要重写一个run方法,也可以用匿名函数来实现,两种办法差别不大
依靠Callable接口实现
Callable接口同样是用于线程的。与Runnable接口的区别在于Callable会返回运行结果,依靠call()
方法运行线程,而且call()
可以抛出异常
但是这个Callable接口不能直接给Thread用(noooooooo!)
在给Thread用之前,它需要让FutureTask
类包装一下。这个FutureTask是有泛型的,Callable也是有泛型的,两个填一样的泛型,就可以指定call方法的返回值类型
这个有点难描述,直接上代码
FutureTask<String> task = new FutureTask<String>(new Callable<String>() {
@Override
public String call() throws Exception {
return "qwq";
}
});
Thread t = new Thread(task);
这里指定了call方法的返回值类型是String
那既然它有返回值,我们肯定能获取这个值。谁开启了这个线程?
(主线程:我!)
于是我们在主线程里面调用 task
对象里面的get()
方法 来获取这个值
String result = task.get();
// 记得自己捕捉异常
(主线程:*乖乖的等在那里,盯着线程运行完了把结果给它)
对,它会在那里一直等着线程运行完,不是bug,它一定会等这个线程运行完了再自己运行
finally——
耶!贪吃蛇还有两三天就做好啦!下次断网的时候不用玩谷歌小恐龙啦!(不是,我会去内卷)