并发编程基础
什么是线程
进程是操作系统中的一个实体,是操作系统资源分配的基本单位,在Java中,一个进程必然至少有一个线程,这个线程被称为主线程。进程下的多个线程共享进程的资源。
操作系统分配CPU资源是以进程下的线程为基本单位而分配的,因为线程才是主要执行任务的。
undefined。每个线程都有一个相对应的栈区,用于存储当前线程所用到的局部变量,以及线程内函数调用的栈帧数据。
如下图所示,我们的一个main方法,输出HelloWorld会创建以下几个线程:
public class Main {
public static void main(String[] args) {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
for (ThreadInfo threadInfo : threadInfos) {
System.out.println(threadInfo.getThreadId() + "\t" + threadInfo.getThreadName());
}
}
}
一个CPU核心只能同时运行一个线程,但是在现代CPU中由于使用了超线程技术,使得可以将一个物理核心拆成两个逻辑核心来使用,也就是一个逻辑核心可以使用一个线程。
如下图所示内核中标识的就是我们的物理核心数,而逻辑核心数则是20个。
在Java中,如果想要获取计算机系统中的逻辑核心数可以使用Runtime类中的availableProcessors方法获取到,如下代码所示:
public class Main {
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
int i = runtime.availableProcessors();
System.out.println(i);
}
}
上下文切换
线程的创建
继承Thread
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Hello world");
}
}
public class Main {
private static void isCompletedByTaskCount(ThreadPoolExecutor threadPool) {
while (threadPool.getTaskCount() != threadPool.getCompletedTaskCount()) {
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
通过继承Thread类并重写run方法的方式来实现线程类的创建,在run方法中编写具体的业务代码。在main方法中创建线程对象并调用start方法就可以将线程执行起来。调用start方法之后线程处于就绪状态,如果CPU为线程分配了时间片,线程才会进行之后执行,执行完毕之后线程处于终止状态。
优点是是如果想要在run方法中获取当前线程的话,无需调用Thread.currentThread()来获取线程,直接使用this关键字就可以获取当前线程。
缺点是线程业务代码与线程耦合度较高且无非继承其他类,如果其他的业务功能需要多线程,需要重新继承Thread并重写run方法。
实现runnable
Java虽然不能不能对类进行多继承,但是可以继承多个接口。Runnable接口可以优化继承Thread无法继承其他类的,与Thread类似,在run方法中编写业务代码,然后新建一个Thread对象,并调用start方法即可,两个线程可以同时引用同一个Runnable来完成业务功能。
class MyThread implements Runnable {
@Override
public void run() {
System.out.println("Hello world");
System.out.println(this);
}
}
public class Main {
public static void main(String[] args) {
Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();;
Thread thread1 = new Thread(runnable);
thread1.start();;
}
}
futureTask
通过futureTask创建的线程可以获取线程执行后的回调数据,如下所示,定义了一个task,返回值类型为string,执行线程调用后调用FutureTask.get()方法可以获取线程的执行结果。
class CallerTask implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("call me");
return "test";
}
}
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> stringFutureTask = new FutureTask<>(new CallerTask());
new Thread(stringFutureTask).start();
String s = stringFutureTask.get();
System.out.println(s);
}
}
总结
- 继承Thread的方式可以很方便的在类中定义成员变量并使用set方法对成员变量进行设置,并在执行多线程中使用。缺点是无法实现多线程且业务代码与线程耦合严重。
- 使用Runnable的好处是,解决了Thread无法继承的问题。缺点是大多数使用Runnable的时候是只能new Thread(() -> {}), 这种lambda表达式的方式创建对象使得匿名对象无法引用外部的非final的成员变量。如果使用实现runnable接口的方式的话就与继承Thread方式类似了。
- 前两种创建线程的方式都无法获取到线程的回调结果,而futureTask可以获取线程的回调。
线程通知与等待
wait
监视器锁: 每个对象都有一个当线程进入被synchronized修饰的语句块,得先获取当前对象的监视器锁才能继续执行,否则一直会被阻塞。
如下所示, 以下狱中中三个线程,只有获取到a的线程锁才能执行synchronized语句内容,a的监视器锁只有一个,所以这三个线程执行是互斥的,只有一个线程执行完毕,其他线程才能继续执行。
Integer a = 100;
new Thread(() -> {
synchronized (a) {
System.out.println("thread a");
}
}).start();
new Thread(() -> {
synchronized (a) {
System.out.println("thread b");
}
}).start();
new Thread(() -> {
synchronized (a) {
System.out.println("thread c");
}
}).start();
只有获取到监视器锁才能调用wait方法,否则会抛出异常,在调用线程中的使用的共享变量的wait()方法时,这个线程就会被挂起,如下代码所示
new Thread(() -> {
synchronized (a) {
System.out.println("Hello world");
try {
a.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
只有在其他线程中调用共享变量的notify或notifyAll方法才能唤醒线程继续执行。
Integer a = 100;
new Thread(() -> {
synchronized (a) {
System.out.println("Hello world");
try {
a.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
new Thread(() -> {
synchronized (a) {
a.notifyAll();
}
}).start();
或是直接调用线程的interrupt方法直接抛出异常也会使线程中断,解除阻塞,继续往下执行。
Integer a = 100;
Thread thread = new Thread(() -> {
synchronized (a) {
System.out.println("Hello world");
try {
a.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread.start();
thread.interrupt();
需要注意的是, 有时会出现虚假唤醒的问题,虚假唤醒就是没有被唤醒,但是无缘无故被唤醒了,为了解决这个问题,我们需要循环使线程一直wait(), 等满足条件之后再由其他线程进行唤醒操作。
synchronized(a) {
while (xxx == ture) {
a.wait();
}
}
如下代码所示,创建两个线程,分别是消费者和生产者,如果队列满了生产者会挂起当前线程,如果队列空了消费者会挂起当前线程。
Queue<String> queue = new ArrayDeque<>();
AtomicInteger i = new AtomicInteger();
// 生产者线程
new Thread(() -> {
synchronized (queue) {
while (true) {
// 如果队列满了则阻塞该线程
while (queue.size() >= 50) {
try {
queue.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
i.getAndIncrement();
queue.add(i.get() + "");
System.out.println("生产者生产数据");
// 如果未满则释放监视器锁供其他线程使用
queue.notify();
}
}
}).start();
// 消费者线程
new Thread(() -> {
while (true) {
// 获取贡献变量queue的监视器锁
synchronized (queue) {
// 如果队列为空则阻塞该线程
while (queue.isEmpty()) {
try {
queue.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 如果线程不为空则释放共享变量的监视器锁
queue.notify();
String poll = queue.poll();
System.out.println("消费者消费数据," + poll);
}
}
}).start();
调用共享变量的wait方法只会释放当前当前变量的锁,其他变量的锁则依旧保留,如下所示,线程A中拿到了资源A和资源B的锁,只会调用资源A的wait方法释放了资源A的锁,资源B的锁并未释放,所以线程B获取资源B的锁会一直阻塞。
private static volatile Object resourceA = new Object();
private static volatile Object resourceB = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
// 获取资源A的锁
synchronized (resourceA) {
System.out.println("thread get A lock.");
synchronized (resourceB) {
System.out.println("thread get B lock");
try {
// 释放资源A的锁 但未释放资源B的锁
resourceA.wait();
System.out.println("thread B relase A lock");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
Thread t2 = new Thread(() -> {
// 休眠一秒 避免线程2先执行
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 由于资源B的锁未释放,所以进入不到语句中
synchronized (resourceB) {
System.out.println("get B lock");
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
如果直接调用interrupt来打断等待状态的线程则会抛异常并直接返回。
private static volatile Object resourceA = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("begin");
synchronized (resourceA) {
try {
resourceA.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("end");
}
});
thread.start();
thread.interrupt();
}
notify
唤醒调用共享变量的wait方法的线程,由于一个共享变量可以被多个线程引用,所以具体唤醒哪个线程,这个是随机的。调用notify方法的前提是,获取到该贡献变量的监视器锁。被唤醒的线程也不一定立即能执行,首先得等调用notify方法的线程释放锁,其次得和其他使用到共享变量锁的线程竞争成功才可以执行。
notifyAll
唤醒所有被共享变量等待的线程,如下所示线程1和线程2获取到资源A的锁之后进行等待,在线程3中将线程1和线程2全部唤醒。
private static volatile Object resourceA = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (resourceA) {
try {
System.out.println("get a lock begin");
resourceA.wait();
System.out.println("get a lock end");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Thread t2 = new Thread(() -> {
synchronized (resourceA) {
try {
System.out.println("get b lock begin");
resourceA.wait();
System.out.println("get b lock end");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Thread t3 = new Thread(() -> {
synchronized (resourceA) {
resourceA.notifyAll();
}
});
t1.start();
t2.start();
Thread.sleep(1000);
t3.start();
t1.join();
t2.join();
t3.join();
}
join
如果调用了线程的join方法,那么将会阻塞,等待线程执行完毕才会继续向下执行,如下代码所示,三个线程都调用了join方法,等待三个线程执行完毕才会输出最后一句。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println("线程1");
});
Thread t2 = new Thread(() -> {
System.out.println("线程2");
});
Thread t3 = new Thread(() -> {
System.out.println("线程3");
});
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println("joined");
}
线程停止
- 业务代码执行完成
- 业务代码出现异常
- 调用中断方法
调用中断方法时,通知线程当前线程要中断,具体是否中断,还要看实际情况,不是一点会发生中断。为了避免直接调用stop方法引发的一系列问题,那么可以使用线程中断来保证线程可以正常停止。
当线程需要停止时,调用线程的interrupt方法发送中断信号,将中断标志位设置成ture;当中断业务操作执行完成时调用,如下代码所示,在main方法中调用调用t1线程的中断方法,然后在具体的业务处理中判断当前线程是否发生中断,如果发生中断则进行业务处理,可以在业务处理中将线程获取的资源释放掉,这样就解决了直接stop无法释放资源的问题了。
package com.lyra;
import javax.imageio.plugins.tiff.TIFFImageReadParam;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RunnableFuture;
public class Main {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
// 系统业务代码
System.out.println("Hello world");
// 获取当前线程
Thread thread = Thread.currentThread();
while (!thread.isInterrupted()) {
System.out.println(thread.getName() + "\t" + thread.isInterrupted());
}
// 处理中断 关闭资源等等
System.out.println(thread.getName() + "\t" + thread.isInterrupted());
});
t1.start();
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t1.interrupt();
}
}
需要注意的是,不建议使用取消标志位来中止线程,如下代码所示,在线程类中有个cancel方法,当cancel为true就表示发生中断了,然后可以进行业务处理,但是这种方法如果在线程中sleep的话,没办法感知到,只能等待线程下次唤醒才能处理中断,这样的中断不及时。
package com.lyra;
import javax.imageio.plugins.tiff.TIFFImageReadParam;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RunnableFuture;
public class Main {
public static void main(String[] args) {
Runnable helloWorld = new Runnable() {
public void setCancel(Boolean cancel) {
this.cancel = cancel;
}
public Boolean getCancel() {
return cancel;
}
private Boolean cancel = true;
@Override
public void run() {
System.out.println("Hello World");
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
while (cancel) {
System.out.println("处理异常");
cancel = false;
}
}
};
Thread t1 = new Thread(helloWorld);
t1.start();
}
}
而使用interrupt方法时,如果线程中中断了且睡眠了,则会直接幻想抛出异常,这样就保证了异常中断的及时处理。
package com.lyra;
import javax.imageio.plugins.tiff.TIFFImageReadParam;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RunnableFuture;
public class Main {
public static void main(String[] args) {
Runnable helloWorld = new Runnable() {
public void setCancel(Boolean cancel) {
this.cancel = cancel;
}
public Boolean getCancel() {
return cancel;
}
private Boolean cancel;
@Override
public void run() {
System.out.println("Hello World");
Thread thread = Thread.currentThread();
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
while (!thread.isInterrupted()) {
System.out.println(thread.getName() + "\t" + thread.isInterrupted());
}
System.out.println(thread.getName() + "\t" + thread.isInterrupted());
}
};
Thread t1 = new Thread(helloWorld);
t1.start();
t1.interrupt();
}
}
- 手动停止线程
线程类提供了stop对象,但是已经过时了。
过期原因官方文档:https://docs.oracle.com/en%2Fjava%2Fjavase%2F11%2Fdocs%2Fapi%2F%2F/java.base/java/lang/doc-files/threadPrimitiveDeprecation.html
当执行stop方法后,操作系统会将线程进行中止,如果占用的资源未被释放则会导致资源异常。所以方法就被弃用了。