线程笔记(1)

线程和进程

主要区别

根本区别:

  • 进程是是系统进行资源分配和调度的基本单位
  • 线程是进程中的一个实体,是被系统独立调度和分派的基本单位

在开销方面:

  • 每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销
  • 线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

资源和内存方面:

  • 进程是系统中一个活动的实体,它可以拥有自己独立的资源。每个进程都拥有自己独立的内存空间。
  • 除了CPU外,系统不会为线程分配内存。线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源

并发与并行的区别

多个进程在单个处理器上并发执行,同一进程中的多个线程之间可以并发执行。
说到并发,还有一个与之相关的并行。并发与并行是两个既相似而又不相同的概念:

  • 并发性,又称共行性,是指能处理多个同时性活动的能力。是在同一个cpu上同时(不是真正的同时,而是看来是同时,因为cpu要在多个程序间切换)运行多个程序。;
  • 并行是指同时发生的两个并发事件,即每个cpu运行一个程序。并行具有并发的含义,而并发则不一定并行,也亦是说并发事件之间不一定要同一时刻发生。

    总的来说,并发指在同一时刻只能有一条指令执行,但多个指令被快速轮换执行,使得在宏观上具有多个活动同时进行的效果。而并行指在同一时刻,有多条指令在多个处理器上同时执行。

包含关系

  • 线程是进程的组成部分,一个进程可以拥有多个线程,一个进程必须有一个父进程。
  • 如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的。
  • 一个程序运行后至少包含一个进程,一个进程里可以包含多个线程,但至少要包含一个线程。

多线程的优势

  • 进程之间不能共享内存,但线程之间共享内存非常容易。(因为多个进程共享父进程的内存空间)
  • 系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多(同样是因为同一个进程下的线程共享进程的资源),因此使用多线程来实现多任务并发比多进程效率高。
  • Java内置了多线程功能支持,从而简化了Java的多线程编程。

创建多线程

继承Thread类创建线程类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class FirstThread extends Thread {
int i = 0; //不同线程对象不能共享该成员变量

/**
* 重写run方法,该方法为线程的执行体
*/
@Override
public void run() {
for (; i < 10; i++) {
System.out.println(getName() + ": i = " + i);
}
}
}
1
2
3
4
5
6
7
8
public class TestFirstThread {

public static void main(String[] args) {
new FirstThread().start();
new FirstThread().start();
}

}

在main方法中创建了两次该线程类,并调用start方法开启线程,然后执行线程中的run方法。

注意:使用继承Thread类创建线程类时,不同线程对象不能共享该线程类的成员变量

上述代码输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Thread-0: i = 0
Thread-1: i = 0
Thread-1: i = 1
Thread-0: i = 1
Thread-1: i = 2
Thread-1: i = 3
Thread-1: i = 4
Thread-1: i = 5
Thread-1: i = 6
Thread-1: i = 7
Thread-1: i = 8
Thread-1: i = 9
Thread-0: i = 2
Thread-0: i = 3
Thread-0: i = 4
Thread-0: i = 5
Thread-0: i = 6
Thread-0: i = 7
Thread-0: i = 8
Thread-0: i = 9

实现Runnable接口创建线程

1
2
3
4
5
6
7
8
9
10
11
12
public class SecondThread implements Runnable {

int i = 0;

@Override
public void run() {
for (; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ": i = " + i);
}
}

}
1
2
3
4
5
6
7
8
9
public class TestSecondThread {

public static void main(String[] args) {
SecondThread st = new SecondThread(); //作为target传入Thread的构造方法
new Thread(st).start();
new Thread(st).start();
}

}

运行结果

1
2
3
4
5
6
7
8
9
10
11
Thread-0: i = 0
Thread-0: i = 1
Thread-1: i = 0
Thread-0: i = 2
Thread-0: i = 4
Thread-0: i = 5
Thread-0: i = 6
Thread-1: i = 3
Thread-1: i = 8
Thread-1: i = 9
Thread-0: i = 7

从运行结果可以发现用这种方法创建的多个线程可以共享线程类(严格来说不是线程类,只是和线程类一样实现了Runnable接口,说它是线程类的target更合适)的成员变量。下面会分析为什么会这样。

从源码角度分析以上两种方法

其实查看源码可以发现,以上这两种方法虽然实现不同,但内在的原理是一样的。

继承Thread

1
2
3
4
5
6
7
8
9
10
class Thread implements Runnable {
//........

@Override
public void run() {
if (target != null) {
target.run();
}
}
}

可以看到Thread实现了Runnable接口并重写了run方法。FirstThread方法继承了Thread,其实也就是实现了Runnable接口,然后重写了run方法。
第一种方法开启线程是这样的:new FirstThread().start(); 它执行了Thread的无参构造方法

1
2
3
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}

我们重点看第二个参数Runnable target,这里的target为null,也就是说第一种方式实现不需要target,但第二种方式会不一样。

实现Runnable

1
2
3
4
@FunctionalInterface
public interface Runnable {
public abstract void run();
}

可以看到Runnable接口只有一个方法,而SecondThread实现了该接口并重写了这个方法。所以实际上SecondThread也是一个Runnable,也就是说SecondThread可以作为Thread类的一个target。

第二种方法开启线程是这样的:new Thread(st).start(); 它执行了Thread的含有一个Runnable参数的构造方法

1
2
3
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}

这里的第二个参数不在是null,而是传来的参数target,然后在init方法执行过后,SecondThread对象被赋给了Thread的成员变量

1
private Runnable target;

所以现在虽然Thread的run方法没有被重写,但它实际上是调用了target的run方法,也就是SecondThread中重写的run方法。不妨再看一次Thread重写的run方法:

1
2
3
4
5
6
@Override
public void run() {
if (target != null) {
target.run();
}
}

所以说,这两种方法的实质都是重写了Thread中的run方法,只不过是直接重写该方法和调用target所重写的该方法的区别。

两种方式所带来的不同

分析过源码后,现在就更加清楚为什么不同的实现,会产生成员变量是否可以共享的问题。

  • 首先是第一种方式,这里new了两个Thread的子类,也就是说有两个对象,等于实现了两个接口对象,那么接口中的run方法的变量自然不能共享了。

    1
    2
    new FirstThread().start();
    new FirstThread().start();
  • 然后是第二种,这里虽然也是new了两个Thread,但是只有一个接口对象,就是st,那么接口中的run方法的变量自然就可以共享了(因为就只有这一个接口对象,也就是说只有这个变量,当然只能共享了)。

1
2
3
SecondThread st = new SecondThread();	//作为target传入Thread的构造方法
new Thread(st).start();
new Thread(st).start();

其实弄懂了这些后,我们也可以用第二种方式实现第一种的效果,只要多创建一个SecondThread(Runnable)对象就行了。

1
2
3
4
SecondThread st = new SecondThread();
SecondThread st1 = new SecondThread();
new Thread(st).start();
new Thread(st1).start();

改为上述代码之后,再运行就会和第一种方式的效果一样。原因很简单,这里多了一个SecondThread对象,两者的变量当然是不能共享的,因为根本就不在同一块存储空间。

使用Callable和Future创建线程

Callable接口

Callable接口提供了一个call方法作为线程执行体,call方法比run方法更加强大。

  • call方法可以有返回值
  • call方法可以声明抛出异常

但是Callable接口不是Runnable接口的子接口,所以不能作为Thread的target。那么要怎么把Callable和Thread联系起来呢?

Java5提供了Future接口来代表Callable接口里call方法的返回值,并提供了一个FutureTask实现类(该类既实现了Future接口,又实现了Runnable接口,所以可以作为Thread的target)

创建线程步骤

  • 创建一个Callable接口对象,重写call方法
  • FutureTask对象并使用FutureTask来包装Callable对象
  • 将FutureTask对象作为target传入Thread的构造函数
  • 最后可以通过FutureTask对象的get()方法获得Callable线程的返回值

注意

  • FutureTask对象的泛型参数必须和Callable对象的泛型参数一致
  • call方法会导致主线程阻塞,直到call方法结束并返回为止

三种方式的对比

实现Runnable接口、Callable接口的方式基本相同,可以归为一类。它们和直接继承Thread类的优缺点如下。

实现Runnable接口、Callable接口的优缺点

优点

  • 还可以继承其他类
  • 多个线程可以共享一个target,非常适合多个相同的线程来处理同一资源。

    缺点

  • 编程稍微复杂(其实也不算复杂,所以这个缺点可以忽略不计)

直接继承Thread类的优缺点

优点

  • 编写简单

    缺点

  • 不能继承其他父类

总结

一般推荐采用实现Runnable接口、Callable接口的方法创建线程

参考资料

-------------    本文到此结束  感谢您的阅读    -------------
0%