文章目录
什么是线程
线程是进程中的一个实体,线程本身是不会独立存在的。进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中线程共享进程 资源。
操作系统在分配资源时,是把资源分配给CPU的, 但是 CPU 资源比较特殊, 它是被分配到线程的,因为真正要占用CPU运行的是线程,所以也说线程是 CPU 分配的基本单位。
在Java 中,当我们启动 main 函数时,其实就启动了一个JVM进程, main 函数所在的线程就是这个进程中的一个线程,也称为主线程。
一个进程中有多个线程,多个线程共用进程的堆和方法区资源, 但是每个线程有自己的程序计数器和栈区域 。
程序计数器是一块内存区域,用来记录线程当前要执行的指令地址。那么为何要将程序计数器设计为线程私有的呢?因为线程是占用CPU 执行的基本单位,而CPU一般是使用时间片轮转方式让线程轮询占用的,所以当前线程 CPU 时间片用完后,要让出CPU ,等下次轮到自己的时候再执行。那么如何知道之前程序执行到哪里了呢?其实程序计数器就是为了记录该线程让出 CPU 时的执行地址的,待再次分配到时间片时线程就可以从自己私有的计数器指定地址继续执行。另外需要注意的是,如果执行的是 native 方法,那么 pc 计数器记录的是 undefined 地址,只有执行的是 Java 代码时 pc 计数器记录的才是下一条指令的地址。
另外 每个线程 有自己的栈资源,用于存储该线程的局部变量 ,这些局部变量是该线程私有的,其它线程是访问不了的,除此之外栈还用来存放线程的调用栈帧。
堆是一个进程中最大的一块内存,堆是被进程中的所有线程共享的,是进程创建时分配的,堆里面主要存放使用 new 操作创建的对象实例。
方法区则用来存放 JVM 加载的类、常量及静态变量等信息,也是线程共享的。
创建线程与运行
创建线程的三种方式:
- 继承Thread类并重写run方法
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("MyThread");
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
需要注意,创建完Thread 对象后该线程并没有被启动执行,直到调用了start 方法后才真正启动了线程。其实调用 start 方法后线程并没有马上执行而是处于就就绪状态, 这个就绪状态是指线程已经获取了除 CPU 资源外的其它资源,等待获取 CPU 资源后才会真正处于运行状态。一旦 run 方法执行完毕, 该线程就处于终止状态。
- 实现Runnable接口的run方法
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("MyRunnable");
}
}
public class ThreadTest {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
new Thread(myRunnable).start();
}
}
- FutureTask方式
上面介绍的两种方式都有一个缺点,就是任务没有返回值,下面看最后一种,即使用 FutureTask 方式。
public class MyFutureTask implements Callable<String> {
@Override
public String call() throws Exception {
return "MyFutureTask";
}
}
public class ThreadTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> futureTask = new FutureTask<>(new MyFutureTask());
// 启动线程
new Thread(futureTask).start();
// 获取执行结果
String result = futureTask.get();
System.out.println(result);
}
}
线程的通知与等待
wait()函数
当一个线程调用一个共享变量的wait()方法时, 该调用线程会被阻塞挂起,直到发生下面几件事情 之一才返回
- 线程调用了该共享对象 notify()或者 notifyAll ()方法
- 其他线程调用了该线程的interrupt() 方法,该线程抛出 InterruptedException 异常返回。
public class ThreadTest {
public static volatile Object resourceA = new Object();
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread threadA = new Thread(()->{
synchronized (resourceA) {
System.out.println("threadA....begin");
try {
resourceA.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("threadA....end");
}
});
Thread threadB = new Thread(()->{
synchronized (resourceA) {
System.out.println("threadB....begin");
resourceA.notify();
System.out.println("threadB....end");
}
});
// 启动线程
threadA.start();
// 休眠1s,让threadA执行
Thread.sleep(2000);
threadB.start();
// 等待线程执行完毕
threadA.join();
threadB.join();
System.out.println("main...end");
}
}
控制台打印如下
threadA....begin
threadB....begin
threadB....end
threadA....end
main...end
上面代码启动了两个线程threadA和threadB,先让threadA调用共享资源resourceA的wait方法处于阻塞状态,然后让threadB调用共享变量resourceA的notify来唤醒一个因为调用了resourceA的wait方法而阻塞的线程。
public class ThreadTest {
public static volatile Object resourceA = new Object();
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread threadA = new Thread(()->{
synchronized (resourceA) {
System.out.println("threadA....begin");
try {
resourceA.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("threadA....end");
}
});
// 启动线程
threadA.start();
// 休眠1s,让threadA执行
Thread.sleep(2000);
// 中断线程
threadA.interrupt();
System.out.println("main...end");
}
}
控制台输出
threadA....begin
java.lang.InterruptedException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at com.lzc.thread.ThreadTest.lambda$main$0(ThreadTest.java:19)
at java.lang.Thread.run(Thread.java:748)
threadA....end
main...end
notify()函数
一个线程调用共享对象的 notify()方法后,会唤醒一个在该共享变量上调用 wait 系列方法后被挂起的线程。 一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。
此外,被唤醒的线程不能马上从 wait 方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回,也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争,只有该线程竞争到了共享变量的监视器锁后才可以继续执行。
如果调用wait方法的线程事先没有获取到该对象的监视器锁,则调用wait方法的线程将会抛出IllegalMonitorStateException异常
public class ThreadTest {
public static volatile Object resourceA = new Object();
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread threadA = new Thread(()->{
synchronized (resourceA) {
System.out.println("threadA....begin");
try {
resourceA.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("threadA....end");
}
});
Thread threadB = new Thread(()->{
synchronized (resourceA) {
System.out.println("threadB....begin");
try {
resourceA.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("threadB....end");
}
});
Thread threadC = new Thread(()->{
synchronized (resourceA) {
System.out.println("threadC....begin");
resourceA.notify(); // 只唤醒一个阻塞的线程
System.out.println("threadC....end");
}
});
// 启动线程
threadA.start();
threadB.start();
// 休眠1s,让threadA和threadB执行
Thread.sleep(2000);
threadC.start();
// 等待线程执行完毕
threadA.join();
threadB.join();
threadC.join();
System.out.println("main...end");
}
}
控制台输出如下:
threadA....begin
threadB....begin
threadC....begin
threadC....end
threadA....end
可以看到,threadC调用resourceA.notify()方法后,只唤醒了线程A,线程B一直处于阻塞中。
修改一下threadC的代码
Thread threadC = new Thread(()->{
synchronized (resourceA) {
System.out.println("threadC....begin");
resourceA.notifyAll(); // 唤醒所有阻塞中的线程
System.out.println("threadC....end");
}
});
控制台输出如下:
threadA....begin
threadB....begin
threadC....begin
threadC....end
threadB....end
threadA....end
main...end
可以看到,threadA和threadB都被唤醒了
notifyAll()函数
notifyll()方法则会唤醒所有在该共享变量上由于调用 wait 系列方法而被挂起的线程。
守护线程与用户线程
Java 中的线程分为两类,分别为 daemon 线程(守护线程〉和 user 线程(用户线程)。在JVM启动时会调用 main 函数, main函数所在的线程就是一个用户线程,其实在 JVM内部同时还启动了 好多守护线程, 比如垃圾回收线程。那么守线程和用户线程有什么别呢?区别之一是当最后一个非守护线程结束时,JVM正常退出,而不管当前是否存在守护线程,也就是说守护线程是否结束并不影响JVM退出。言外之意,只要有一个用户线程还没结束,正常情况下JVM就不会退出。
如果你希望在主线程结束后JVM进程马上结束,那么在创建线程时可以将其设置为守护线程,如果你希望在主线程结束后子线程继续工作,等子线程结束后再让JVM进程结束,那么就将子线程设置为用户线程。
ThreadLocal
多线程访问同一个共享变量时特别容易出现并发问题,特别是在多个线程需要对共享变量进行写入时。
ThreadLocal提供了线程本地变量,也就是如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。
创建一个ThreadLocal 变量后,每个线程都会复制一个变量到自己的本地内存。
Java中的线程安全问题
谈到线程安全问题,我们先说说什么是共享资源。所谓共享资源,就是说该资源被多个线程所持有或者说多个线程都可以去访问该资源。
线程安全问题是指当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果的问题。
Java并发机制
volatile
如果一个字段被声明为volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。
有volatile修饰的共享变量在进行写操作的时候,在多核处理器下会引发两件事情:
- 将当前处理器缓存行的数据写回到系统内存。
- 这个写回内存的操作会使其它CPU里缓存了该内存地址的数据无效。
synchronized
Java中每一个对象都可以作为锁,具体表现为以下三种形式:
- 对于普通同步方法,锁是当前实例对象
- 对于静态同步方法,锁是当前的Class对象
- 对于同步方法块,锁是synchronized括号里配置的对象
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或者抛出异常时必须释放锁。
JVM基于进入和退出Monitor对象来实现方法 同步和代码块同步。代码同步块使用monitorenter和monitorexit指令来实现。monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之匹配。任何对象都有一个Monitor与之关联,当一个Monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取Monitor对象所对应的所有权,即尝试获取对象锁。
编写Demo.java
public class Demo {
public static final String LOCK = "LOCK";
public synchronized void test1() {
System.out.println("123");
}
public void test2() {
synchronized (LOCK) {
System.out.println("456");
}
}
}
使用javac Demo.java生成字节码文件,然后使用javap -c Demo查看class文件
public class Demo {
public static final java.lang.String LOCK;
public Demo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public synchronized void test1();
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String 123
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
public void test2();
Code:
0: ldc #6 // String LOCK
2: dup
3: astore_1
4: monitorenter
5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #7 // String 456
10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: return
Exception table:
from to target type
5 15 18 any
18 21 18 any
}
进入 synchronized 块的内存语义是把在synchronized 块内使用到的变量从线程的工作内存中清除,这样在synchronized 块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取,退出synchronized 块的内存语义是把在synchronized 块内对共享变的修改刷新到主内存。
其实这也是加锁和释放锁的语义,当获取锁后会清空锁块内本地内存中将会被用到的共享变量 ,在使用这些共享变量时从主内存进行加载,在释放锁时将本地内存中修改的共享变量刷新到主内存。
LockSupport 工具类
JDK 中的 11.jar 里面的 LockSupport 是个工具类,它的主要作用是挂起和唤醒线程,该工具类是创建锁和其他同步类的基础。
当一个线程正在等待某个操作时,JVM调用Unsafe的park()方法来阻塞此线程。
当阻塞中的线程需要再次运行时,JVM调用Unsafe的unpark()方法来唤醒此线程。
LockSupport 类与每个使用它的线程都会关联一 个许可证,在默认情况下调用LockSupport 类的方法的线程是不持有许可证的。 LockSupport 是使用 Unsafe 类实现的,下面介绍 LockSupport 中的几个主要函数 。
park方法
如果调用 park 方法的线程已经拿到了与LockSupport 关联的许可证,则调用Locksupport. park()时会马上返回,否则调用线程会被禁止参与线程的调度, 也就是会被阻塞挂起。 如下代码直接在 main 函数里面调用 park 方法,最终只会输出 begin park !然后当前线程被挂起,这是因为在默认情况下调用线程是不持有许可证的。
public class Demo {
public static void main(String[] args) {
System.out.println("begin park");
LockSupport.park();
System.out.println("end park");
}
}
unpark方法
public class Demo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
System.out.println("t1线程阻塞中.....");
// 阻塞当前线程
LockSupport.park();
System.out.println("t1线程运行结束");
});
t1.start();
// 主线程睡眠2秒,让线程t1运行
Thread.sleep(2000);
// 唤醒阻塞的线程t1
LockSupport.unpark(t1); // 如果注释这行代码,那么线程t1将一直处于阻塞中。
// t1.interrupt(); // 如果不调用unpark唤醒线程,中断线程t1可以让它运行结束
}
}