1. 进程和线程
提到并发,首先需要了解下进程和线程。
1.1 进程
进程,可以理解为就是一个应用程序,如当我们听音乐时,开启的程序就是一个进程;当我们听着音乐,写着代码,这个时候就开启了两个程序,有两个进程在运行。此时,相当于 CPU 在同时处理两个任务,属于并发,对用户来说,两个进程就像在同时运行一样。现代的多核处理器,确实可以做到多个进程并行。一个进程开启,操作系统会为这个进程分配独立的资源,不同的进程之间相互隔离,所以进程是资源分配的基本单元,进程间的隔离,会使得进程在运行时,就像该进程占据着全部的资源。进程拥有自己独立的虚拟空间,包括从文件加载的程序代码,程序数据,用户栈和运行时堆等。正是因为进程间是隔离的,但是在进程进行切换时,需要保存当前进程的上下文,这部分工作由操作系统维护的进程控制块(PCB)来完成,维护进程编号,进程状态,程序计数器,寄存器,CPU 调度信息,内存管理,打开的文件列表等,当切换回来时,再恢复这些数据继续执行。以前的单核 CPU,一个时间点只能执行一个任务,由于切换的过程很快,所以用户感觉像是在同时运行多个任务。
1.2 线程
线程是 CPU 调度的最小单元,一个进程至少有一个线程,当然,也可以有多个线程,现在的应用程序基本都是多线程的。那么为什么需要多个线程,多个线程有哪些好处呢?一个进程可能需要多个子任务,如用户点击界面上的一个按钮,点击后,需要进行网络请求,网络请求可能需要 10s,那么用户点击后如果在 10s后才有反应,那么对用户来说,可能是程序出了问题。所以在用户点击后,可以在一个线程中显示一个提示界面或者加载界面,同时用另外一个线程去执行网络请求,请求完成后,再切换到另一个线程显示界面,告知用户。这样开启多个线程能够大大提高效率,进程让操作系统的并发性成为可能,而线程让进程的内部并发成为可能。
一个进程的不同线程共享进程的资源,线程也有自己的寄存器和堆栈,属于线程独有,其他线程不能访问。
2.线程同步
同步的问题也是来源于线程之间的共享变量,如果不同线程bu操作共享变量也就不会出现数据安全性问题。这里的变量只是指实例变量,静态变量,并不包括局部变量,因为局部变量是线程私有的,并不存在共享。
Java 中出现数据安全性问题,也是由于 Java 内存模型导致的,每条线程都有自己的工作内存,工作内存当中会有共享变量的副本。线程只能对自己工作内存的当中的共享变量副本进行操作,不能直接操作主内存的共享变量。不同线程之间无法直接操作对方的工作内存的变量,只能通过主线程来协助完成。由于缓存的存在,一方面带来了性能上的提升,CPU 不用每次都去内存中取数据,可以从缓存中获取数据;另一方面,缓存也造成了数据不同步的问题,一个线程在工作内存中拿到的数据,可能是一个旧值,此时主内存中的数据可能已经被其他线程修改过。
为了保证数据的安全性问题,需要满足 3 个条件:原子性,可见性,有序性。
3.线程状态
(1) 初始(NEW):新创建了一个线程对象,但还没有调用 start() 方法。
(2) 可运行状态/就绪(RUNNABLE):线程对象创建后,其他线程调用了该对象的 start() 方法。该状态的线程位于可运行线程池中,变得可运行,等待获取 CPU 的使用权。
(3) 运行状态(Running):就绪状态的线程获取了 CPU,执行程序代码。
(4) 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃 CPU 使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
等待阻塞:运行的线程执行 wait() 方法,JVM 会把该线程放入等待池中。(wait 会释放持有的锁)
同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池中。
其他阻塞:运行的线程执行 sleep() 或 join() 方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep() 状态超时、join() 等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。(注意,sleep 是不会释放持有的锁)
(5) 终止(TERMINATED):表示该线程已经执行完毕。
4.基本用法
4.1 创建
(1) 继承 Thread,重写 run 方法
public class TheadTest {
private static class MyThread extends Thread {
@Override
public void run() {
int count = 0;
while (count < 5) {
System.out.println(Thread.currentThread().getName() + " - " + count);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
}
}
public static void main(String[] args) {
Thread myThread = new MyThread();
myThread.start();
}
}
(2) 实现 Runnable 接口,实现 run 方法
public class ThreadRunnable {
private static class MyRunnable implements Runnable{
@Override
public void run() {
int count = 0;
while (count < 5) {
System.out.println(Thread.currentThread().getName() + " - " + count);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
}
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
(3) 实现 Callable 接口
Callable 是 jdk 1.5 版本之后增加的任务接口,实现 call 方法,并且能够从线程返回结果,但是 Callable 只能通过线程池来提交执行。
public class ThreadCallable {
private static class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
int count = 0;
while (count < 5) {
System.out.println(Thread.currentThread().getName() + " - " + count);
Thread.sleep(1000);
count++;
}
return "Thread is finished,the result is Hello World!";
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService service = Executors.newCachedThreadPool();
Future<String> future = service.submit(new MyCallable());
while (!future.isDone()) {
System.out.println("waiting ------");
Thread.sleep(500);
}
System.out.println(future.get());
service.shutdown();
}
}
(3) 通过 FutureTask 创建任务
FutureTask 也是 jdk 1.5 版本之后增加的任务类,除了能够获取线程返回的结果,还能够对线程进行更多的操作,如取消,查询线程状态,结果等。FutureTask 既可以用线程池提交执行,也可以使用 Thread 来开启。
private static class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
int count = 0;
while (count < 5) {
System.out.println(Thread.currentThread().getName() + " - " + count);
Thread.sleep(1000);
count++;
}
return "Thread is finished,the result is Hello World!";
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1.线程池方式提交
ExecutorService service = Executors.newCachedThreadPool();
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
service.submit(futureTask);
service.shutdown();
}
通过普通线程开启 FutureTask 任务
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 2.FutureTask 支持线程方式
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
Thread thread = new Thread(futureTask);
thread.start();
while (!futureTask.isDone()) {
System.out.println("waiting ------");
Thread.sleep(500);
}
System.out.println(futureTask.get());
}
实现 Runnable 接口比继承 Thread 类所具有的优势:
适合多个相同的程序代码的线程去处理同一个资源
可以避免 java 中的单继承的限制
增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
线程池只能放入实现 Runable 或 Callable 类线程,不能直接放入继承 Thread 的类
4.2 基本操作
(1) start : 这个不用多讲,上面例子已经提到了,线程创建之后开启线程,此时线程处于就绪状态,等待操作系统分配时间片,获取到时间片就可以运行
(2) setPriority 设置线程优先级:Java线程有优先级,优先级高的线程会获得较多的运行机,但并不表明一定能够运行,只是获取时间片的概率高。
Java线程的优先级用整数表示,取值范围是 1~10,Thread类有以下三个静态常量:
static int MAX_PRIORITY 线程可以具有的最高优先级,取值为10。
static int MIN_PRIORITY 线程可以具有的最低优先级,取值为1。
static int NORM_PRIORITY 分配给线程的默认优先级,取值为5。
每个线程都有默认的优先级。主线程的默认优先级为 Thread.NORM_PRIORITY。
线程的优先级有继承关系,比如 A 线程中创建了 B 线程,那么 B 将和 A 具有相同的优先级。
JVM 提供了 10 个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用 Thread 类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。
(3) setDaemon 设置后台线程
守护线程和用户线程的区别在于:守护线程依赖于创建它的线程,而用户线程则不依赖。如果在 main 线程中创建了一个守护线程,当 main 方法运行完毕之后,守护线程也会随着消亡。而用户线程则不会,用户线程会一直运行直到其运行完毕。在JVM中,像垃圾收集器线程就是守护线程。
此外,守护线程中创建的线程也是守护线程。
public class ThreadDaemon {
private static class PrintRunnable implements Runnable {
private String name;
PrintRunnable(String name) {
this.name = name;
}
@Override
public void run() {
int count = 0;
System.out.println(name + " - start - isDaemon - " + Thread.currentThread().isDaemon()¬);
while (true) {
System.out.println(name + " - " + count);
count++;
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
Thread daemon = new Thread(new PrintRunnable("daemon"));
daemon.setDaemon(true);
daemon.start();
int count = 5;
while (count > 0) {
System.out.println(Thread.currentThread().getName() + " - " + count);
count--;
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
守护线程没有运行完,随着主线程退出而退出。
(4) yield,线程礼让。
调用 yield 方法会让当前线程交出 CPU 权限,让 CPU 去执行其他的线程。它跟 sleep 方法类似,同样不会释放锁。但是 yield 不能控制具体的交出 CPU 的时间,另外,yield 方法只能让拥有相同优先级的线程有获取 CPU 执行时间的机会。
注意,调用 yield 方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取时间片,这一点是和 sleep 方法不一样的。
public class ThreadYield {
private static class PrintRunnable implements Runnable {
private String name;
PrintRunnable(String name) {
this.name = name;
}
@Override
public void run() {
int count = 100;
System.out.println(name + " - start");
while (count > 0) {
if ("yield".equals(name)) {
Thread.yield();
}
System.out.println(name + " - " + count);
count--;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread yield = new Thread(new PrintRunnable("yield"));
Thread normal = new Thread(new PrintRunnable("normal"));
yield.setPriority(Thread.MAX_PRIORITY);
yield.start();
normal.start();
Thread.sleep(2000);
}
}
从运行结果看出 yield 方法并不一定立即交出 CPU 使用权,另外,yield 方法增加其他线程获取时间片的机会,并不意味着调用 yield 的线程就获取不到时间片。
yield - start
normal - start
yield - 100
normal - 100
yield - 99
normal - 99
yield - 98
normal - 98
yield - 97
normal - 97
yield - 96
normal - 96
normal - 95
normal - 94
yield - 95
normal - 93
yield - 94
...
(5) join
join 方法有三个重载函数:
join()
join(long millis) //参数为毫秒
join(long millis,int nanoseconds) //第一参数为毫秒,第二个参数为纳秒
调用 Thread 的 join 方法,表明会等待指定的线程执行完毕,或者等待这个 线程执行一段时间后再开始执行。下面例子中 A 线程会等待 B 线程执行结束后再继续执行。如果执行时间,表明会等待一段时间后继续执行。
public class ThreadJoin {
private static class PrintRunnable implements Runnable {
private String name;
private Thread joinThread;
public PrintRunnable(Thread joiner, String name) {
this.name = name;
this.joinThread = joiner;
}
public PrintRunnable(String name) {
this.name = name;
}
@Override
public void run() {
int count = 5;
System.out.println(name + " - start");
while (count > 0) {
if (joinThread != null) {
try {
joinThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(name + " - " + count);
count--;
}
}
}
public static void main(String[] args) {
Thread B = new Thread(new PrintRunnable("thread-B"));
Thread A = new Thread(new PrintRunnable(B, "thread-A"));
A.start();
B.start();
}
}
(6) sleep 线程睡眠。
sleep方法有两个重载版本:
sleep(long millis) //参数为毫秒
sleep(long millis,int nanoseconds) //第一参数为毫秒,第二个参数为纳秒
sleep 相当于让线程睡眠,交出 CPU,让 CPU 去执行其他的任务。
有一点要非常注意,sleep 方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用 sleep 方法,其他线程也无法访问这个对象。
public class ThreadSleep {
private static int i = 10;
private static final Object object = new Object();
public static void main(String[] args) throws IOException {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
thread1.start();
thread2.start();
}
private static class MyThread extends Thread{
@Override
public void run() {
synchronized (object) {
i++;
System.out.println("i:"+i);
try {
System.out.println("线程"+Thread.currentThread().getName()+"进入睡眠状态");
Thread.sleep(10000);
} catch (InterruptedException e) {
// TODO: handle exception
}
System.out.println("线程"+Thread.currentThread().getName()+"睡眠结束");
i++;
System.out.println("i:"+i);
}
}
}
}
(7) 线程中断,interrupt 、isInterrupted、interrupted
这里需要注意,调用 interrupt 方法相当于将中断标志位置为 true,并不是直接中断线程,isInterrupted 是用来判断中断标志位的,interrupted 同样也是用来判断中断标志位的,但是多一个操作,会将中断标志位置为 false。
interrupt 方法调用后,如果线程处于运行状态,仅仅表现为中断标识位为 true,并不能中断线程,这时需要自己通过 isInterrupted 或者 interrupted 判断来中断线程;如果线程处于阻塞状态,会抛出 InterruptedException 异常,此时线程会继续执行,如果是循环执行的线程,需要中断的话,需要在异常出现时自己做中断处理。
下面来看下中断线程的几种方法:
public class ThreadInterruption {
private static class Interruption1 implements Runnable {
@Override
public void run() {
int count = 0;
try {
while (!Thread.currentThread().isInterrupted()) {
Thread.sleep(500);
count++;
System.out.println(Thread.currentThread().getName() + " --- " + count + " ---");
}
// 如果不睡眠,会走这里正常结束
System.out.println(Thread.currentThread().getName() + " is interrupted normal");
} catch (InterruptedException e) {
// 睡眠,会走这里,报异常
e.printStackTrace();
System.out.println(Thread.currentThread().getName() + " is interrupted abnormal!");
}
}
}
private static class Interruption2 implements Runnable {
@Override
public void run() {
int count = 0;
while (!Thread.currentThread().isInterrupted()) {
count++;
System.out.println(Thread.currentThread().getName() + " --- " + count + " ---");
// 如果不加睡眠,可以中断
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " is interrupted abnormal!");
e.printStackTrace();
// 加睡眠,会走异常,需要在这里处理,结束线程,否则线程继续执行
break;
}
}
}
}
private static class Interruption3 implements Runnable {
private volatile boolean isStop = false;
public void setStop(boolean stop) {
isStop = stop;
}
@Override
public void run() {
int count = 0;
while (!isStop) {
count++;
System.out.println(Thread.currentThread().getName() + " --- " + count + " ---");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " is stopped normal");
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Interruption1());
Thread thread2 = new Thread(new Interruption2());
Interruption3 target = new Interruption3();
Thread thread3 = new Thread(target);
thread1.start();
thread2.start();
thread3.start();
Thread.sleep(2000);
thread1.interrupt();
Thread.sleep(2000);
thread2.interrupt();
Thread.sleep(2000);
target.setStop(true);
}
}
Interruption1 中 try-catch 包裹 while 循环的外面,线程阻塞时被中断,抛出异常,线程会结束;如果线程在非阻塞时中断,通过线程的中断状态来结束 while 循环,正常结束线程。
Interruption2 中 while 循环包裹 try-catch 的外面,线程阻塞时被中断,抛出异常,此时如果不加 break 或者 return 之类的操作,线程会继续执行;如果线程在非阻塞时中断,通过线程的中断状态来结束 while 循环,正常结束线程。
Interruption3 中自己设置一个变量来控制线程的中断。
(8) wait 、notif、notifyAll
调用某个对象的 wait() 方法能让当前线程阻塞,并且当前线程必须拥有此对象的 monitor(即锁),所以调用 wait() 方法必须在同步块或者同步方法中进行(synchronized 块或者 synchronized 方法)。
调用某个对象的 notify() 方法能够唤醒一个正在等待这个对象的 monitor 的线程,如果有多个线程都在等待这个对象的 monitor,则只能唤醒其中一个线程;
调用 notifyAll() 方法能够唤醒所有正在等待这个对象的 monitor 的线程;
调用某个对象的 wait() 方法,相当于让当前线程交出此对象的 monitor,然后进入等待状态,等待后续再次获得此对象的锁(Thread 类中的 sleep 方法使当前线程暂停执行一段时间,从而让其他线程有机会继续执行,但它并不释放对象锁);
notify() 方法能够唤醒一个正在等待该对象的 monitor 的线程,当有多个线程都在等待该对象的 monitor 的话,则只能唤醒其中一个线程,具体唤醒哪个线程则不得而知。
下面演示一个线程的小算法,通过两个线程实现交替打印奇偶数。
public class ThreadOddEven {
private static class OddEvenRunnable implements Runnable {
private final Object object = new Object();
private int count = 0;
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
synchronized (object) {
System.out.println(Thread.currentThread().getName() + " --- " + count++);
try {
Thread.sleep(300);
object.notifyAll();
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
}
}
public static void main(String[] args) throws InterruptedException {
OddEvenRunnable oddEvenRunnable = new OddEvenRunnable();
Thread even = new Thread(oddEvenRunnable);
Thread odd = new Thread(oddEvenRunnable);
even.start();
odd.start();
Thread.sleep(5000);
even.interrupt();
odd.interrupt();
}
}
4.3 同步
同步这部分涉及的内容较多,后面单独来讲,基础的主要就是 synchronized、lock 的使用以及 volatile 关键字。