【并发编程:线程池】深入简出的带你精通java线程

线程与进程

  • 进程:用来加载指令、管理内存、管理IO。操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位。
  • 线程:有时被称为轻量级进程(Lightweight Process,LWP),是操作系统调度(CPU调度)执行的最小单位。

进程间通信的方式

  • 管道(pipe)及有名管道(named pipe):管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
  • 信号(signal):信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致的。
  • 消息队列(message queue):消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限的进程可以按照一定的规则向消息队列中添加新信息;对消息队列有读权限的进程则可以从消息队列中读取信息。
  • 共享内存(shared memory):可以说这是最有用的进程间通信方式。它使的多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。
  • 信号量(semaphore):主要作为进程之间及同一种进程的不同线程之间的同步和互斥手段。
  • 套接字(socket):这是一种更为一般的进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。

线程的同步互斥

  • 线程同步:线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。
  • 线程互斥:对于共享的进程系统资源,在各单个线程访问时的排它性。

线程同步互斥的控制方法

  • 临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。(在一段时间内只允许一个线程访问的资源就称为临界资源)。
  • 互斥量:为协调共同对一个共享资源的单独访问而设计的。
  • 信号量:为控制一个具有有限数量用户资源而设计。
  • 事件:用来通知线程有一些事件已发生,从而启动后继任务的开始。

线程上下文切换(Context switch):一般要5-10毫秒

  • 下文切换是指CPU(中央处理单元)从一个进程或线程到另一个进程或线程的切换。
  • 是多任务操作系统的一个基本特性。
  • 切换的耗时一般为5-10毫秒。
  • 上下文切换只能在内核模式下发生!

操作系统层面线程状态:初始状态、可运行状态、运行状态、休眠状态和终止状态。

  • 初始状态:指的是线程已经被创建,但是还不允许分配 CPU 执行。
  • 可运行(就绪)状态:指的是线程可以分配 CPU 执行。在这种状态下,真正的操作系统线程已经被成功创建了,所以可以分配 CPU 执行。
  • 运行状态:获取到CPU的时间片。
  • 休眠状态:运行状态的线程如果调用一个阻塞的 API(例如以阻塞方式读文件)或者等待某个事件(例如条件变量),那么线程的状态就会转换到休眠状态,同时释放 CPU 使用权,休眠状态的线程永远没有机会获得 CPU 使用权。
  • 终止状态:线程执行完或者出现异常就会进入终止状态,终止状态的线程不会切换到其他任何状态,进入终止状态也就意味着线程的生命周期结束了。

java的线程状态

  • 取自Thread类的内部枚举
    public enum State {
        // 初始化状态
        NEW,

        // 可运行状态+运行状态
        RUNNABLE,

        // 阻塞状态
        BLOCKED,

        // 无时限等待
        WAITING,

        // 有时限等待
        TIMED_WAITING,

        // 终止状态
        TERMINATED;
    }

【并发编程:线程池】深入简出的带你精通java线程_第1张图片

面试中问你线程的状态,你应该如何回答?

  • 在操作系统层面有5种,java中有6种。
  • Java线程中的 BLOCKED、WAITING、TIMED_WAITING 是一种状态,即操作系统的休眠状态。这三种状态永远没有CPU的使用权!
  • Java线程中的 RUNNABLE 状态,在操作系统中分为:可运行(就绪)状态、运行状态。

Java线程的实现方式(4种)

  • 使用 Thread类或继承Thread类
// 创建线程对象
Thread t = new Thread() {
    public void run() {
    // 要执行的任务
    }
};
// 启动线程
  • 实现 Runnable 接口配合Thread:线程(Thread)与任务(Runnable)分开
Runnable runnable = new Runnable() {
    public void run(){
    // 要执行的任务
    }
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
  • 使用有返回值的 Callable
class CallableTask implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        return new Random().nextInt();
    }
}
//创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//提交任务,并用 Future提交返回结果
  • 使用 lambda
new Thread(() -> System.out.println(Thread.currentThread().getName())).start();

Thread常用方法

sleep方法

  • 调用 sleep 会让当前线程从 Running 进入TIMED_WAITING状态,不会释放对象锁
  • 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException,并且会清除中断标志
  • 睡眠结束后的线程未必会立刻得到执行
  • sleep当传入参数为0时,和yield相同

yield方法

  • yield会释放CPU资源,让当前线程从 Running 进入 Runnable状态,让优先级更高(至少是相同)的线程获得执行机会,不会释放对象锁;
  • 假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为没有比它优先级更高的线程;
  • 具体的实现依赖于操作系统的任务调度器

join方法

  • 等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之后才能继续运行的场景。

如何正确优雅的停止线程?

stop方法

  • stop()方法已经被jdk废弃,原因就是stop()方法太过于暴力,强行把执行到一半的线程终止。

Java线程的中断机制

  • 中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理。
  • 被中断的线程拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停止。
  • interrupt(): 将线程的中断标志位设置为true,不会停止线程
  • isInterrupted(): 判断当前线程的中断标志位是否为true,不会清除中断标志位
  • Thread.interrupted():判断当前线程的中断标志位是否为true,并清除中断标志位,重置为fasle
  • sleep可以被中断 抛出中断异常:sleep interrupted, 清除中断标志位
  • wait可以被中断 抛出中断异常:InterruptedException, 清除中断标志位

Java线程间通信方式

  • volatile:两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程之间进行通信。
  • 等待唤醒(等待通知)机制:基于wait和notify方法来实现,在一个线程内调用该线程锁对象的wait方法,线程将进入等待队列进行等待直到被唤醒。
  • LockSupport:JDK中用来实现线程阻塞和唤醒的工具,线程调用park则等待“许可”,调用unpark则为指定线程提供“许可”。
  • 管道输入输出流:PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前两种面向字节,而后两种面向字符。
  • Thread.join:可以简单理解为线程合并。一个线程等待到另一个线程执行完。

为什么说本质上Java中实现线程只有一种方式?


    private Runnable target;

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
  • 继承Thread类,其实就是使用的Runnable对象调用run方法
public interface ThreadFactory {

    Thread newThread(Runnable r);
}
  • 线程池的创建,本质上也是传入一个Runnable对象
  • 使用lambda的方式,本质与继承的方式一致
  • 所以,创建线程的本质只有一种!

简单了解start()方法是如何开启一个线程的(JVM与操作系统层面)?

  • 使用new Thread()创建一个线程,然后调用start()方法进行java层面的线程启动;
  • 调用本地方法start0(),去调用jvm中的JVM_StartThread方法进行线程创建和启动;
  • 调用new JavaThread(&thread_entry, sz)进行线程的创建,并根据不同的操作系统平台调用对应的os::create_thread方法进行线程创建;
  • 新创建的线程状态为Initialized,调用了sync->wait()的方法进行等待,等到被唤醒才继续执行thread->run();
  • 调用Thread::start(native_thread);方法进行线程启动,此时将线程状态设置为RUNNABLE,接着调用os::start_thread(thread),根据不同的操作系统选择不同的线程启动方式;
  • 线程启动之后状态设置为RUNNABLE, 并唤醒第4步中等待的线程,接着执行thread->run()的方法;
  • JavaThread::run()方法会回调第1步new Thread中复写的run()方法。

结束语

  • 获取更多有价值的文章,让我们一起成为架构师!
  • 关注公众号,可以让你逐步对MySQL以及并发编程有更深入的理解!
  • 这个公众号,无广告!!!每日更新!!!
    作者公众号.jpg

你可能感兴趣的:(并发编程,java,并发编程)