JAVA并发编程-线程基础(Java高并发核心编程读书笔记)

201 阅读10分钟

查看线程基本信息

public class ThreadDemo01 {
	public static void main(String[] args) {
		Thread thread = Thread.currentThread();
		System.out.println("当前线程名称: " + thread.getName());
		System.out.println("当前线程ID: " + thread.getId());
		System.out.println("当前线程组名称: " + thread.getThreadGroup().getName());
		System.out.println("当前线程状态: " + thread.getState());
		System.out.println("当前线程优先级: " + thread.getPriority());
		fun1();

	}

	private static void fun1() {
		fun2();
	}

	private static void  fun2() {
		int i, j = 0;
		System.out.println();
	}
}

## 输出
当前线程名称: main
当前线程ID: 1
当前线程组名称: main
当前线程状态: RUNNABLE
当前线程优先级: 5

先看线程栈帧信息, 把debug端点打在fun2方法中. 查看栈帧

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b4238b836f5140f3b9f1bcd89176f3bb~tplv-k3u1fbpfcp-zoom-1.image

可以在每个线程栈帧中查看帧中的局部变量信息. 可看到这里有三个帧, 分别是代表了三个方法.

什么是线程

CPU调度的最小单位, 进程是操作系统分配资源的最小单位.

一个进程最少包含一个线程或者拥有多个线程.

各个进程之间是互相独立的, 同一个进程内的不同线程之间可以有关联. 且可以共享进程下的资源信息.

其他结论

同一时刻, 一个cpu内核上只会有一个线程在执行.

在java中, 每个线程都对应一个Thread对象实例. 线程的具体描述信息都保存在Therad类实例的属性中.

Java中三个创建线程的方法

在Java中有三种创建线程的方式, 但是每种方式都和Thread类有关系.

Thread类详解

  • 线程id

属性定义: private final long tid;

获取方法: getId()

  • 线程名称

private volatile String name;

getName();

  • 线程优先级

private int priority;

getPriority()

  • 是否为守护线程

private boolean daemon = false;

isDaemon();

  • 线程状态

private volatile int threadStatus;

public State getState() {
        // get current thread state
        return jdk.internal.misc.VM.toThreadState(threadStatus);
    }
  • 线程的启动和运行

public synchronized void start();

start方法会调用Thread实例的run方法, run()方法作为用户代码逻辑的入口, 由start方法调用.

具体的运行时机, 由操作系统分配.

public void run() {
        if (target != null) {
            target.run();
        }
    }

target属性是当前实例的一个重要实例属性. 默认这个实例属性为null, 所以直接new Thread, 并调用其start方法, 什么也没有执行, 就是因为这个target属性为null;

  • 获取当前线程

Thread.currentThread();

继承Thread类创建线程

public class MyThread01 extends Thread {

	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName());
	}
}

MyThread01类的实例的start方法, 就会执行这里重写的run方法, 作为用户逻辑的入口方法.

优点: 方便快捷

缺点: java单继承属性, 不方便扩展.

实现Runnable接口创建线程目标类

public class MyRunnable01 implements Runnable {
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName());
	}
}

Thread类也实现了Runnable接口, 并且实现了其run方法, 并且在run方法内执行了target实例属性的run方法. 默认这个target实例属性为null.

什么时候这个target属性不为空?

Thread类有多个构造方法可以初始化该属性

public Thread(Runnable target) {
        this(null, target, "Thread-" + nextThreadNum(), 0);
    }
public Thread(Runnable target, String name) {
        this(null, target, name, 0);
    }
public Thread(ThreadGroup group, Runnable target, String name) {
        this(group, target, name, 0);
    }
public Thread(ThreadGroup group, Runnable target, String name,
                  long stackSize) {
        this(group, target, name, stackSize, null, true);
    }
public Thread(ThreadGroup group, Runnable target, String name,
                  long stackSize, boolean inheritThreadLocals) {
        this(group, target, name, stackSize, null, inheritThreadLocals);
    }

所以可以通过自定义实现类实现Runnable接口, 并重写run方法. 然后通过Thread类的构造方法来创建一个线程

Thread thread = new Thread(new MyRunnable01());

两种更优雅的方式通过Runnable接口创建线程

  • 匿名类
Thread thread = new Thread(new Runnable() {
			@Override
			public void run() {
				System.out.println(Thread.currentThread().getName());
			}
		});
  • Lambda
Thread thread = new Thread(() -> System.out.println(Thread.currentThread().getName()));

这种使用方式需要目标接口Runnable是一个函数式接口, 即只有一个抽象方法.

优点: 逻辑和数据的分离, 可以更方便复用. 解决了单继承的缺陷

缺点: 没返回值.

Callable和FutureTask

private static void thread3() throws ExecutionException, InterruptedException {
		Callable<String> callable = new Callable<String>() {
			@Override
			public String call() throws Exception {
				return "hello thread";
			}
		};
		FutureTask<String> task = new FutureTask<>(callable);
		Thread thread = new Thread(task);
		thread.start();
		System.out.println(task.get());
	}

通过实现Callable接口, 创建一个实现类.

将实现类传递给FutureTask的构造方法. 构造FutureTask实例,

再把FutureTask实例传递给Thread构造函数.

其中FutureTask就是一个中间类, 将Callable和Runnable接口关联起来, 典型的设计模式中的适配器模式. 返回值通过FutureTask实例中的实例属性: outcome返回.

通过线程池创建

private static void thread4() throws ExecutionException, InterruptedException {
		ExecutorService executorService = Executors.newFixedThreadPool(10);
		executorService.execute(() -> System.out.println("今天天气不错"));
		Future<String> submit = executorService.submit(new Runnable() {
			@Override
			public void run() {
				System.out.println("今天阳光明媚");
			}
		}, "hello");
		System.out.println(submit.get());
		Future<String> future = executorService.submit(new Callable<String>() {

			@Override
			public String call() throws Exception {
				return "hello submit callable";
			}
		});
		System.out.println(future.get());

		Future<String> call = executorService.submit(new Callable<String>() {
			@Override
			public String call() throws Exception {
				return "hello world";
			}
		});
		System.out.println(call.get());
		executorService.shutdown();

	}

在线程池中提交任务有三种方式

  • result = submit(Runnable, result)
  • result = submit(Callable)
  • result = void submit(Runnable)
  • execute()

线程原理

线程的调度和时间片

由于cpu的速度很快, 每秒能够执行十亿次计算, 所以把cpu的计算按照毫秒分开, 例如20ms为一个时间片, 不同的操作系统的时间片长度是不一样的.

线程的调度就是基于时间片来调度的, 只有被分配到时间片的线程, 才能够得到执行, 否则就是就绪状态, 由于速度太快, 在多个线程之间切换时间片对于用户来说是无感知的, 感觉就像是在并发执行.

时间片的分配策略:

  • 分时调度模型, 线程轮流执行, 每个线程占用的时间片都是相等的.
  • 抢占式调度, 线程区分优先级, 优先级高的线程能够获得更多的时间片. (主流操作系统采取的方式)

java的线程操作是委托给操作系统的, 所以java的线程调度模型也是抢占式的.

线程的优先级

在Thread类中提供了两个方法用来操作线程的优先级, priority, get set

优先级越高的线程获得cpu时间片的机会越高, 但这不是绝对的. 不能依赖此来控制线程的执行顺序.

线程的生命周期

6中状态:

  • NEW

线程对象创建好, 还没有调用其start方法.

  • RUNNABLE

创建好的线程在调用start方法后, 如果被分配到cpu时间片, 就处于运行状态, 如果时间片用完, 等待再次分配时间片, 在这种执行中和等待时间片的状态称为: runnable

  • BLOCK

阻塞状态, 等待被唤醒.

  • TIMED_WITTING

线程阻塞指定的时间段, 时间结束后进入runnable状态.

  • WITTING
  • TERMINATED

线程执行结束

代码演示线程的不同状态

public static void main(String[] args) throws InterruptedException {
		Thread thread = new Thread(){
			@Override
			public void run() {
				for (int i = 0; i < 100000000; i++) {

				}
				LockSupport.parkNanos(1000 * 1000 * 100);
			}
		};
		System.out.println("new Thread 之后的线程状态: " + thread.getState());
		thread.start();
		System.out.println("thread.start() 之后的线程状态: " + thread.getState());
		LockSupport.parkNanos(1000 * 1000 * 10);
		System.out.println("LockSupport.park() 之后的线程状态: " + thread.getState());
		thread.join();

		System.out.println("thread执行结束之后的线程状态: " + thread.getState());

	}
## 输出log
new Thread 之后的线程状态: NEW
thread.start() 之后的线程状态: RUNNABLE
LockSupport.park() 之后的线程状态: TIMED_WAITING
thread执行结束之后的线程状态: TERMINATED

线程状态查看

jps查看java进程

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4cbf226e059c4b8e933633023282c1d5~tplv-k3u1fbpfcp-zoom-1.image

jstack查看状态

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/080887e46a1d433fb2bd48c40c3c729d~tplv-k3u1fbpfcp-zoom-1.image

线程的基本操作

  • 名称的设置和获取

getName(); setName();

  • 线程的sleep操作

Thread.sleep(1000);

调用后, 线程会处于TIMED_WAITING状态.

  • interrupt操作

线程终止操作: stop, 已经过时, 不安全, 可能造成数据不一致问题, 锁无法释放问题等等.

Thread.interrupt()方法, 本质不是用来中断一个线程, 而是把线程设置为中断状态. 调用iterrupt方法的作用:

  1. 如果线程处于阻塞状态(例如调用了线程的Object.wait(), Thread.join(), Thread.sleep()方法), 会立马退出阻塞, 并抛出InterruptedException异常. 线程捕获这个异常, 并做一定的处理, 然后让线程安全的退出.
private static void testInBlock() throws InterruptedException {
		Thread thread = new Thread() {
			@Override
			public void run() {
				System.out.println(LocalDateTime.now());
				try {
					Thread.sleep(5000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}

			}
		};
		thread.start();
		Thread.sleep(2000);
		thread.interrupt();
		thread.join();
		System.out.println(LocalDateTime.now());
	}

## 输出

2021-12-06T16:28:25.554670
java.lang.InterruptedException: sleep interrupted
	at java.base/java.lang.Thread.sleep(Native Method)
	at main.java.com.lee.thread.InterruptTest$2.run(InterruptTest.java:52)
2021-12-06T16:28:27.536343

可以看到在thread线程中, sleep的参数为5s, 在主线程sleep 2s后调用了thread的interrupt方法, 导致只sleep了2s,从25→27. 后sleep状态被强行中断.

  1. 如果线程处于运行中. 线程不会受影响, 继续运行, 仅仅是线程的中断标记被设置为true. 如果需要响应中断, 需要在线程适当的位置查看当前线程的中断标识, 并确定是否执行中断操作.
private static void inRunning() throws InterruptedException {
		Thread thread = new Thread() {
			@Override
			public void run() {
				System.out.println("运行中");
			}
		};
		thread.start();
		System.out.println(thread.isInterrupted());
		thread.interrupt();
		System.out.println(thread.isInterrupted());
		thread.join();
	}
## 输出
false
运行中
true

可见对于运行中的线程, 调用线程的interrupt方法, 不会对线程造成任何影响, 只是将thread的interrupted属性设置为true;

  1. 如果先调用interrupt()方法, 后调用阻塞方法来阻塞线程,

在第一次调用阻塞方法进入阻塞状态时, 依旧会抛出InterruptedException, 后续再调用阻塞方法, 就不会再触发InterruptedException.

  • join 线程合并

线程A在执行到某个位置的时候, 需要等待线程B先执行完成, 可以在线程A中需要的位置调用方法threadB.join(); 此时线程A就会处于阻塞状态, 直到线程B执行完成来唤醒线程A, join()方法有包含时间参数的重载版本, 表示可以阻塞一定的时间后自动恢复执行状态. 如果线程A被中断, 会抛出InterruptedException异常.

join()方法是实例方法, 需要使用被合并线程的句柄(指针, 变量)去调用.

join()方法无法直接获取被依赖线程的执行结果.

直接调用join()方法, 会让依赖线程进入waitting状态, cpu不会分配资源给该线程, object的wait方法也是 一样的效果.

通过join(xxxx)的重载版本, 会让线程进入timed_waitting状态, 等待时间结束后被唤醒, 或者主动被其他线程唤醒.

  • yield操作 让步

让正在执行的线程, 放弃持有的cpu执行权限, 线程重新进入runnable状态, 有可能当前线程会再次竞争到cpu资源得到执行.

yield只会让线程从运行状态切换到就绪状态, 不会进入阻塞状态.

不能保证是的当前正在运行的线程迅速转换为就绪状态.

即使迅速的转换到了就绪状态, 下一次的cpu切换还是有可能选中当前线程.

所以通过yield方法严格的控制线程的执行顺序.

  • daemon操作 守护线程.

例如随着jvm启动的gc线程. 守护线程随着jvm的结束才会结束.

可以通过setDaemon来设置一个线程为守护线程.

  1. 守护线程一定要在启动之前设置为守护线程. 如果启动后设置会抛出interruptedException异常.
  2. 守护线程随时可能被jvm强行终止, 不适合用于访问系统资源等操作, 防止造成数据不一致的风险.
  3. 守护线程中创建一个线程, 那个这个线程也是守护线程. 可以通过setDaemon方法设置为用户线程.
  • 线程状态总结

通过new Thread方式创建线程之后还未调用start方法, 线程就是处于new状态, runnable,和callable本质都是通过new thread的方式在创建线程, 只是target对象不同而已.

runnable, 在创建好的线程调用start方法后, 线程进入可执行状态, 表示线程就绪或者运行中.

就绪表示为线程具备运行条件, 但是还没有获得cpu资源. 进入就绪状态的条件包括:

  1. start方法
  2. cpu时间片用完
  3. sleep时间结束
  4. join操作结束
  5. 等待用户输入结束
  6. 线程获取到对象锁
  7. 调用yield方法让出时间片.

运行中: 表示线程在继续状态, 并且被分配到cpu时间片后的状态.

block状态

线程不会占用cpu资源. 进入条件:

  1. 等待获取对象锁
  2. io阻塞

waitting状态

线程在waitting状态会处于无限制的等待, 直到有其他现场显示唤醒该线程.

TimedWaitting

线程处于限时等待状态, 在限时结束或者其他线程显示唤醒该线程即可进入runnable状态. 进入方式:

sleep(time), wait(time)parkNanos(time), join(time)

terminated

线程正常结束, 或者线程异常后没有被处理进入结束状态.