Java的多线程-基础知识(1)

今天整理一下java的多线程部分的基础知识

废话篇

今年7月中旬的时候从我的大学毕业了,这一段时间在家里带了几天娃,希望我的博客不倒,小外甥女长大能看到。(笑哭

前一段时间学习了Java的多线程,Java多线程编程的基础内容并不难,但是刚学完多线程我对这个多线程有了一些疑问——Java的多线程程序在计算机中究竟是怎么运行的呢?带着这个疑问开始了下面的探寻之路。

Java多线程编程回顾

java的多线程在Java8中总共有3种实现方式,即通过继承thread类,实现Runable接口,使用callable、线程池executor、future来创建可以返回值的线程。下面简单看一下这三者实现的一个demo。

1、继承thread类来创建线程

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
35
36
37
class ThreadMethod extends Thread {

private String name;

ThreadMethod(String name) {
this.name = name;
}

// 这里通过重写Thread类的run()方法达到起多线程的目的
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name + ":" + i);
}
try {
sleep((int) (Math.random() * 10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

class TestMain1 {
public static void main(String[] args) {

// 这里就起了两个线程,线程的代码顺序和线程的执行顺序无关
ThreadMethod threadClass1 = new ThreadMethod("AA");
ThreadMethod threadClass2 = new ThreadMethod("BB");

// 这里可以通过调用run方法来看看线程和非线程的区别,如果是调用run方法时,得到的结果是顺序执行的,
// 但是start()方法是线程执行的方法,因此如果用多线程的方法就要使用start()
// 注意:start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。
threadClass1.start();
threadClass2.start();

}
}

根据Java的单继承模式(即一个类只能继承一个类),所以如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话(因为java的接口是多继承关系,即一个类可以实现多个接口),这样则很容易的实现资源共享(这里的资源共享是什么意思)。

2、实现Runable接口来创建线程

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class RunnableThreadMethod implements Runnable {

private String name;

RunnableThreadMethod(String name) {
this.name = name;
}

public void run() {
System.out.println(Thread.currentThread().getName()+"运行开始");
for (int i = 0; i < 10; i++) {
System.out.println(name + ":" + i);

if (i==8 && Thread.currentThread().getName().equals("Thread-0")){
System.out.println("yield start");
// yield只是从程序上控制线程的运行状态,即将其在程序层面上转为可运行状态也就是就绪状态,但是硬件层面CPU调度还是有
// 可能将同时处于可运行状态中的该线程拿出来跑。
Thread.yield();
}
}
try {
sleep((int) (Math.random() * 10));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"运行结束");
}
}

class TestMain2 {
public static void main(String[] args) {

System.out.println(Thread.currentThread().getName()+"运行开始");

// Thread2类通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个约定。
// 所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。
RunnableThreadMethod runnableThreadMethod1 = new RunnableThreadMethod("C");
RunnableThreadMethod runnableThreadMethod2 = new RunnableThreadMethod("D");

// 在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,
// 然后调用Thread对象的start()方法来运行多线程代码。
// 实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是扩展Thread类还是
// 实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。
Thread thread1 = new Thread(runnableThreadMethod1);
thread1.start();
new Thread(runnableThreadMethod2).start();


try {
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(Thread.currentThread().getName()+"运行结束");
}
}

3、使用call、线程池executor、future来创建可以返回值的线程

如果对一些场合需要线程返回的结果。就要使用用Callable、Future、FutureTask、CompletionService这几个类。Callable只能在ExecutorService的线程池中跑,但有返回结果,也可以通过返回的Future对象查询执行状态。
Future 本身也是一种设计模式,它是用来取得异步任务的结果。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class CallThreadMethod {

static class CallThread implements Callable<Object> {
private String taskName;

CallThread(String name) {
this.taskName = name;
}

@Override
public Object call() throws Exception {

System.out.println(Thread.currentThread().getName() + " start");

int sum = 0;
Date date1 = new Date();
for (int i = 0; i < 10; i++) {
sum = sum + i;
System.out.println(taskName + ":" + i);
}
Date date2 = new Date();
long time = date2.getTime() - date1.getTime();

System.out.println(Thread.currentThread().getName() + " end");

return sum;
}
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
int taskSize = 2;
// 创建一个线程池
ExecutorService executorServicePool = Executors.newFixedThreadPool(taskSize);
// 创建具有返回值的任务
Callable callThreadMethod1 = new CallThread("AAa");
Callable callThreadMethod2 = new CallThread("BBA");

// way-1:执行任务并获取 Future对象
Future future1 = executorServicePool.submit(callThreadMethod1);
Future future2 = executorServicePool.submit(callThreadMethod1);

System.out.println("通过方式1获得的10的和为:" + future1.get().toString());
System.out.println("通过方式1获得的10的和为:" + future2.get().toString());

// =============================================
// way-2 通过invokeAll执行任务
List<Future<Object>> futureList = executorServicePool.invokeAll(asList(new CallThread("AAAA"), new CallThread("BBB")));

// 关闭线程池
executorServicePool.shutdown();

for (Future<Object> item : futureList) {
System.out.println("通过方式2获得的结果:"+item.get());
}
}
}

下面再说一说这三者的共同点和差异。

1、继承thread类和实现runnable接口的两种方式最终都是通过调用thread类的start方法启动线程的;
2、前两者(thread、runnable)都不能返回结果;
3、runnable实现的多线程 最终还是归一到thread上,这就表明runnable实现的多线程对象的方法是和thread实现的多线程对象的方法api是一致的;
4、单继承机制导致多线程类不能继承多个类,而runnable是一个接口类,因此可以实现类继承一个类和多个接口;
5、call方法可以返回结果,是使用了线程池,是继承了callable()接口,因此可以像runnable方法一样实现继承多个类。
6、call方法开启线程有两种方式,一种是使用submit方法接收CallThread的具体对象,一种是使用invokeAll方法接收一个CallThread对象列表。

如果需要线程返回结果,那么就选择call方式;如果不需要结果,那么有两种选择,那么建议使用runnable或者call方式。

Java多线程的进一步思考

针对java的多线程,是不是这样写出来的代码就一定是并行运行的?

回想了操作系统相关的知识,进程是操作系统分配资源的最小单位,线程是操作系统调度的最小单位,那么就是说CPU资源是按照线程进行调度的,我们再回到目前的多核多线程的概念中去,目前购买CPU的时候,都会有一个双核4线程、4核8线程这种说法,就是说一个线程运行在CPU的核心上,这样的一般都是一个核心上运行2个线程。对于4核4线程,意味着该CPU可以同时运行4个线程,就是我们写一个4线程程序的时候,如果只有这一个程序使用,理想情况下CPU会全部为该程序服务。也就是说程序(进程)并发,线程在多核心CPU上是并行运行的,多线程程序在单核心单线程的CPU上是并发运行的。

Google一下“CPU多核多线程”keyword,可以搜到一堆关于这方面的讨论,不清楚的话可以多看几篇文章。


项目的源码已开放在github上,项目地址:https://github.com/flowerlake/java-learning ,这个项目包含了学习java的所有代码,感谢star。

参考文献