Loading

Java · 并发篇

目标:

  • Java线程的状态
  • Java线程状态之间的转换
  • 两种说法:六种/五种状态

Java线程的六种状态:

image

NEW 新建

不会被cpu执行
调用start之后变成RUNNABLE
通过new Thread()创建了线程,但还没有调用start()启动线程。

创建线程的方式:

  1. new Thread(new Thread(){}) 继承Thread
  2. new Thread(new Runnable(){@Override run()}) 实现Runnable并重写run()
  3. FutureTask<V> 可以获得异步执行的结果,得到V类型的返回值
  4. 👍线程池 负责线程的创建、维护和分配,提升性能;可以管理线程,高效调度。

RUNNABLE 可运行

Java把Ready就绪Running执行合并为一种Runnable状态。
调用了线程的start()后处于就绪状态;线程获取到CPU时间片后开始执行run(),处于执行状态。

BLOCKED 阻塞

不会被占用CPU资源。
以下情况会让线程进入阻塞状态:

  1. 线程等待获取锁,该锁被另一个线程持有。
  2. IO阻塞:线程发起了一个阻塞式IO操作,但不具备IO操作的条件。例如:线程等待用户输入内容后继续执行。

WAITING 等待

不会被分配CPU时间片。需要被其他线程显式唤醒才会进入就绪状态。
线程调用以下方法让自己进入等待状态:

  1. Object.wait() 唤醒方式:notify() notifyAll()
  2. Thread.join() 唤醒方式:被合入的线程执行完毕
  3. LockSupport.park() 唤醒方式:LockSupport.unpark(Thread)

TIMED_WAITING 限时等待

不会被分配CPU时间片。若一定时间内未被唤醒,会被系统自动唤醒并进入就绪状态。
以下情况会让线程进入限时等待状态:

  1. Thread.sleep(time) 唤醒方式:sleep时间结束
  2. Object.wait(time) 唤醒方式:Object.notify() Object.notifyAll()
  3. LockSupport.parkNanos(time)/parkUntil(time) 唤醒方式: LockSupport.unpark(Thread) 或 线程的停止(park)时限结束

TERMINATED 终结

线程结束任务后 / 线程执行过程中发生了异常而未被处理

五种状态:依据操作系统层面来划分

  • 分到了CPU时间的:运行
  • 可以分到CPU时间的:就绪
  • 分不到CPU时间的:阻塞
    Java中的RUNNABLE涵盖了就绪、运行、阻塞I/O
    image

线程池

private static ExecutorService pool = Executors.newFixedThreadPool(3);

pool.execute(() -> {
	System.out.println(Thread.currentThread().getName() + " 线程运行开始");
	for (int j = 1; j < MAX_TURN; j++) {
		System.out.println(Thread.currentThread().getName() + ", 轮次:" + j);
		try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	System.out.println(Thread.currentThread().getName() + " 线程运行结束");
});

pool.shutdown();	// 线程池不能再添加新任务。待池中任务都处理完后退出
// pool.shutdownNow();	// 立即试图停止所有执行中的线程,并不再处理阻塞队列中等待的任务,并返回那些未执行的任务

Executors工厂类

四个方法:

  1. new SingleThreadExecutor() 只有一个线程
  2. new FixedThreadPool(int nThreads) 固定大小
  3. new CachedThreadPool() 不限制线程数量,提交的任务都立即执行
  4. new SchaduledThreadPool() 定时/延时执行的任务

大厂的开发规范中禁止使用这些方法。

手动创建线程池

核心线程、任务队列、救急线程:
提交的线程放进核心线程执行,放满之后放到任务队列等待,任务队列也放满之后新的任务放到救急线程去执行;最后触发拒绝策略
image

七个参数

  1. corePoolSize 核心线程数目
    核心线程数目

  2. maximumPollSize 最大线程数目
    核心线程+救急线程数目

  3. keepAliveTime, unit 生存时间、时间单位
    针对救急线程

  4. workQueue
    排队任务队列

  5. threadFactory 线程工厂

  6. handler 拒绝策略 四种

    1. AbortPolicy 报异常
    2. CallerRunsPolicy 由调用者线程自己来执行任务
    3. DiscardPolicy 丢弃,不报异常不报错
    4. DiscardOldestPolicy 丢弃任务队列中等待最久的任务,将新任务加入队列中

提交任务

  1. void execute(Runnable command)
  2. <T> Future<T> submit(Runnable task, T result)

sleep 和 wait

共同点: wait(), wait(long), sleep(long)效果都是让当前线程暂时放弃CPU的使用权、进入阻塞状态

不同点:

  • 方法归属不同
    1. sleep(long) 是 Thread 的静态方法
    2. wait(), wait(long) 是 Object 的成员方法,每个对象都有
  • 醒来时机不同
    1. 执行sleep(long)和wait(long)的线程都会在等待相应毫秒后醒来
    2. wait(long) 和 wait() 还可以被notify唤醒,wait()如果不唤醒就一直等下去
    3. 它们都可以被打断唤醒
  • 锁特性不同
    1. wait方法的调用必须先获取wait对象的锁,而sleep无此限制
    2. wait方法执行后会释放对象锁,允许其他线程获得该对象锁(我放弃,你们还可以用)
    3. sleep如果在synchronized代码块中执行,不会释放对象锁(我放弃,你们也用不了)

lock和synchronized

  • 语法层面

    • synchronized是关键字,源码在JVM中,用C++实现
    • Lock是接口,源码由JDK提供,用Java实现
    • 使用synchronized时退出同步代码块锁会自动释放,而使用Lock时需要手动调用unlock方法释放锁
  • 功能层面

    • 都属于悲观锁,具备基本的互斥、同步、锁重入功能
    • Lock提供了许多synchronized不具备的功能:获取等待状态、公平锁、可打断、可超时、多条件变量
    • Lock有许多适合不同场景的实现,如ReentrantLock,ReentrantReadWriteLock;而synchronized没有
  • 性能层面

    • 没有竞争时,synchronized做了很多优化(如偏向锁、轻量级锁),性能挺好
    • 竞争激烈时,Lock的实现通常会提供更好的性能

公平锁、非公平锁

乐观锁、悲观锁

posted @ 2021-10-16 22:05  ljs9  阅读(53)  评论(0)    收藏  举报