【Java笔记分享】多线程基础 & 线程对象的创建

笔记源代码地址: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-0Thread-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——

耶!贪吃蛇还有两三天就做好啦!下次断网的时候不用玩谷歌小恐龙啦!(不是,我会去内卷)

java
126 views
Comments
登录后评论
Sign In
·

弄完贪吃蛇踢一下我