Java 中线程创建的方式(一)

争取每周至少写一篇,要么是技术分享,要么写点其他的东西。但写作我会坚持下去,真心希望看到这篇文章的童鞋也可以坚持写作,你会发现写作真的很有锻炼价值,当然这篇文章主要是写一写 Java 中的线程的一些知识。

程序、进程和线程

在说线程之前,我觉得有必要来说一下程序、进程和线程的概念。

  • 程序:我们现在编写的都是程序。程序可以是java,c语言,c++,c#,php 等。
  • 进程:程序运行起来会产生一个进程。
    比如:QQ等。当 QQ 静止不运行的时候,只是程序,这个程序提供我们快捷方式,可以双击运行这个程序,然后就会产生一个进程。我们可以通过任务管理器查看进程:

任务管理器—进程

  • 那什么是线程呢?比如在 QQ 中,你正在分别和 A、B 聊天,那么你和 A 聊天是一个线程,和 B 聊天是另外一个线程。

程序运行起来,产生一个进程,进程会被加载到内存中运行。那么进程和线程之间是什么关系呢?

我们都知道计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个CPU一次只能运行一个任务。进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。一个车间里,可以有很多工人。他们协同完成一个任务。线程就好比车间里的工人,一个进程可以包括多个线程。车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。

那么既然说单个 CPU(这里只以单个 CPU 举例)任意时刻只能进行一项任务,那么为什么我们平常用电脑听歌的同时还能和别人在网上聊天然后还能浏览网页呢?

因为 CPU 会给每个进程分配时间片(CPU 将时间片分配给进程,进程又分配给下面的线程),即该进程允许运行的时间,使各个程序从表面上看是同时进行的。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。

举个栗子:假如一台计算机只有A,B 两个程序在运行,那么这两个程序都会被分配时间片,然后 CPU 会先执行 A 的时间片(其实先执行哪个无所谓),当 A 的时间片被执行完之后 CPU 就会去执行 B 的时间片,当 B 的时间片被执行完之后 CPU 又去执行 A 的时间片,就这样依次交替运行,但是呢,CPU 的切换速度特别快,所以在宏观上:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。但在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。

1.线程的创建,启动方式一

线程创建的第一种方式是继承 Thread 类,代码如下:

1
2
3
4
5
6
7
8
9
10
1 public class TestThread001 extends Thread{//只是创建了一个普通类。继承了Thread类之后,代表你这个类具备了多线程的能力。
2 //我们要重写Thread类中的run方法,代表假如你有线程启动了,我就会执行run方法中的内容----线程体
3 @Override
4 public void run() {
5 for (int i = 1; i <=10; i++) {
7 System.out.print(i+" ");
8 }
9 }
10 }

这段代码仅仅代表创建了一个普通类 TestThread001,只是这个类继承了 Thread 类,然后具备了多线程的能力。需要注意的是,继承了 Thread 类之后需要重写Thread类中的run方法,代表假如你有线程启动了,就会执行run方法中的内容(又叫线程体)。

(接上段代码)

1
2
3
4
5
6
7
8
9
1 public class Test {
2 public static void main(String[] args) {
3 TestThread001 tt=new TestThread001();//创建了一个线程对象tt。
4 tt.start();//启动线程
5 for (int i = 1; i <=10; i++) {
6 System.out.print("main"+i+" ");
7 }
8 }
9 }

第三行代码表示仅表示创建了一个线程对象 tt,这个对象需要等待被启动,第四行表示启动这个线程,一定要记住了,若要启动创建的线程,一定要用 start() 方法!!如果第四行写成 tt.run(),那这就成了创建一个普通的对象,然后调用它的 run() 方法。那么有一个问题出现了,这段代码中有几个线程呢?其实是两个线程,一个是主方法线程,另外一个就是新创建的线程。

那么这段代码的运行为:

运行结果
图中红色区域表示 tt 线程的运行结果。从运行结果可以看出来,tt 线程和主方法线程是同时运行的,这里的同时运行是指宏观上的同时运行,微观上,CPU 对于两个线程是交替执行的(前面提到的时间片的概念)。

我们可以画一下这段程序的大概流程图如下所示:

程序流程(大概)

首先主线程开始启动,运行主方法里的代码,运行的过程中发现新创建的 tt 线程启动了,于是,tt 线程和主方法线程并驾齐驱的运行,也就是上面提到的宏观上的同时运行。

2 给线程起名字

2.1 通过setName方法设置线程的名字

代码如下(只写出取名的代码):

1
2
TestThread001 tt=new TestThread001();//创建了一个线程对象tt。
tt.setName("--线程1--");

tt 线程就被命名为 “–线程1–”。

2.2 通过构造器

第二种给对象起名字的方法就是通过构造器了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1 public class TestThread001 extends Thread{
2 public TestThread001(String name) {
3 super(name);
4 }
5 //重写Thread类中的run方法,线程体
6 @Override
7 public void run() {
8 for (int i = 1; i <=10; i++) {
9 System.out.println(Thread.currentThread().getName()+i);
10 }
11 }
12 }
13 public class Test {
14 public static void main(String[] args) {
15 for (int i = 1; i <=10; i++) {
16 System.out.println(i);
17 }
18 TestThread001 tt=new TestThread001("线程1-----");//创建了一个线程对象tt。
19 tt.start();//我们启动线程 ,用的方法是start()方法。只能通过start启动,你要是只调用run的话,并没有启动成功。
20 for (int i = 1; i <=10; i++) {
21 System.out.println(Thread.currentThread().getName()+i);
22 }
23 }
24 }

代码第 18 行表示通过构造器的方法来给线程对象命名。那么我们可以通过 Thread.currentThread().getName() 方法来获取当前线程对象的名字。

至于第 3 行为什么会调用 super(name),感兴趣的同学可以看一下 Thread 的源码,在这里就不解释啦,不重要。只需要知道怎么给对象命名就行。

3 龟兔赛跑

下面带大家写一个龟兔赛跑的故事来感受一下线程:
下面其实是三段代码,为了节省空间,放在一块,希望大家不要误解啊

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//第一段代码:
1 public class WuGui extends Thread {
2 public WuGui(String name) {
3 super(name);
4 }
5 @Override
6 public void run() {
7 while(true){
8 System.out.println("我是乌龟。。我会跑。。。");
9 }
10 }
11 }
//第二段代码:
12 public class Tuzi extends Thread{
13 public Tuzi(String name) {
14 super(name);
15 }
16 @Override
17 public void run() {
18 while(true){
19
20 System.out.println("我是兔子 我不睡觉了。。。");
21 }
22 }
23 }
//第三段代码:
24 public class Test {
25 public static void main(String[] args) {
26 WuGui wg=new WuGui("乌龟");//创建线程对象
27 wg.start();//启动
28 Tuzi tz=new Tuzi("兔子");
29 tz.start();
30 }
31 }

这段代码运行结果如下 :
龟兔赛跑
wg 线程和 tz 线程是在微观上是交替运行的,即 “我是乌龟。。我会跑。。。” 和 “我是兔子 我不睡觉了。。。”这两句话是交替出现的(又用到了上面提到的时间片的概念)。

4 火车票

下面以我们平常买票为例,再次感受下线程的 “魅力”… …

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//第一段代码
1 public class HuoChePiao extends Thread{
2 static int ticketNum=10;
3 public HuoChePiao(String name) {
4 super(name);
5 }
//每个窗口相当于一个线程,排队排了100人
6 @Override
7 public void run() {
8 for (int i = 1; i <=100; i++) {
9 if(ticketNum>0){
10 System.out.println("我在"+Thread.currentThread().getName()+",买了第"+(ticketNum--)+"张车票");
11 }
12 }
13 }
14 }
//第二段代码
15 public class Test {
16 public static void main(String[] args) {
17 HuoChePiao hcp001=new HuoChePiao("窗口1");//创建线程hcp001
18 hcp001.start();//启动线程
19 HuoChePiao hcp002=new HuoChePiao("窗口2");//创建线程hcp002
20 hcp002.start();//启动线程
21 HuoChePiao hcp003=new HuoChePiao("窗口3");//创建线程hcp003
22 hcp003.start();//启动线程
23 }
24 }

运行结果如下:
火车票运行结果1

这样的运行结果应该是正确的,先不解释。

那么,把代码第 2 行的 static 去掉呢,运行结果又如何?

火车票运行结果2
惊不惊喜,刺不刺激,意不意外,按照生活常识每张票只能被买一次,而现在运行结果表明同一张票被卖了三次,这显然是不符合逻辑的。

下面来分析一下:

火车票线程原理简图

由上图可以看出,在主线程之后,会创建三个火车票线程对象并依次启动,然后这四个线程并行运行(当然只是宏观上看是一起运行)。

特别需要注意的是: ticketNum 变量为 static!就是说,这个变量属于类类型的变量,它被这个类所创建的所有对象共享,共享的意思就是说,这个类创建的所有对象中只要有一个对象修改了这个变量,那么,其他对象所拥有的就是修改过的变量。

拿这个代码举栗子:起初 ticketNum=10,当 hcpoo1 线程对象使用了 ticketNum 变量之后 ticketNum=9,那么,当 hcpoo2 线程对象再使用 ticketNum 时,这时的 ticketNum就等于 9,这就是 static 类型的变量被这个类所创建的所有对象共享,一旦这个变量被修改,其他对象所拥有的就是修改过的变量。我想我应该表达清楚了吧?


##关于线程的第一种创建方式今天先写到这里,未完待续,敬请期待。