并发学习(三) Java并发编程基础

Java并发编程基础

为什么要使用多线程

  • 更多的处理器核心

  • 更快的响应时间

  • 更好的编程模型

线程状态

状态名称 说明
NEW 初始状态,线程被构建,但是还没调用start()方法
RUNNABLE 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中”
BLOCKED 阻塞状态,表示线程阻塞于锁
WAITING 无限期等待状态,进入该状态表示当前线程需要等待其它线程做出一些特定动作(通知或中断)
TIME_WAITING 限期等待状态,可以在指定的时间自行返回
TERMINATED 终止状态,表示当前线程已经执行完毕

Daemon 线程

Daemon线程(守护线程)是一种支持性线程,,他主要被作用程序中后台调度以及支持性工作。

当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会推出。可以调用Thread.setDaemon(true)设置线程位Daemon线程。

当Java虚拟机中已经没有非Daemon线程,虚拟机将会退出。Java虚拟机中的所有Daemon线程都需要立即终止,并且他们的finally块并不会执行。

守护线程可用于实时监控和管理系统中的可回收资源。例如,Java垃圾回收线程就是一个典型的守护线程,当我们的程序中不再有任何运行中的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是Java虚拟机上仅剩的线程时,Java虚拟机会自动退出。

使用线程

有三种使用线程的方法:

  • 实现 Runnable 接口;
  • 实现 Callable 接口;
  • 继承 Thread 类。

实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。

实现 Runnable 接口

需要实现 run() 方法。

通过 Thread 调用 start() 方法来启动线程。

1
2
3
4
5
6
7
8
9
10
11
public class MyRunnable implements Runnable {
public void run() {
// ...
}
}

public static void main(String[] args) {
MyRunnable instance = new MyRunnable();
Thread thread = new Thread(instance);
thread.start();
}

实现 Callable 接口

与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyCallable implements Callable<Integer> {
public Integer call() {
return 123;
}
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}

继承 Thread 类

同样也是需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口。

当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。

1
2
3
4
5
6
7
8
9
10
public class MyThread extends Thread {
public void run() {
// ...
}
}

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

实现接口 VS 继承 Thread

实现接口会更好一些,因为:

  • Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
  • 类可能只要求可执行就行,继承整个 Thread 类开销过大。

线程间通信

volatile 和 synchronized 关键字 (同步)

在并发学习(一)和(二)中了解到 volatile 和 synchronized 关键字的基本原理

volatile写操作的时候,通过LOCK前缀指令将变量写到内存,而其他处理器通过嗅探总线判断是否无效,在处理器对这个数据进行修改的时候会强制从系统内存读取数据到处理器缓存实现了线程通信。

而synchronized则是通过加锁的方式,当两个线程对同一个变量进行操作的时候,synchronized锁机制会让两个线程有序地执行,通过共享内存进行通信

等待/通知机制

等待/通知机制是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。以下为等待/通知的经典范式:

1
2
3
4
5
6
7
8
9
10
11
12
13
//等待方
synchronized(对象){
while(条件不满足){
对象.wait();
}
对应的处理逻辑;
}

//通知方
synchronized(对象){
改变条件
对象.notifyAll();
}

管道输入/输出流

管道输入/输出流包括:PipedOutputStream、PipedInputStream、PipedReader、PipedWriter实现,前两种面向字节,后两种面向字符。Piped类型的流必须要进行绑定,也就是调用connect()方法将输入输出流绑定起来,否则会抛出异常

Thread.join()的使用

在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。

ThreadLocal 的使用

ThreadLocal即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。可以通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。

参考资料

  • Java并发编程的艺术[M].机械工业出版社
  • CS-Note