Java -- 线程池 基本知识

1、基本概念

1、共享资源

多个线程对同一份资源进行访问(读写操作),该资源被称为共享资源。如何保证多个线程访问到的数据是一致的,则被称为数据同步或资源同步。

2、线程通信

线程通信,又叫进程内通信,和网络通信等进程间通信不同;多个线程实现互斥资源访问的时候,会互相发送信号。

2、同步、异步、阻塞、非阻塞

同步和异步是 获取结果的方式,阻塞和非阻塞是 等待结果中是否能够完成其他事情。

同步阻塞(BIO),需要等待读取完客户端的数据,同时需要阻塞的判断客户端是否有数据。

同步非阻塞(NIO),需要等待读取完客户端的数据,但是轮询的方式判断客户端是否有数据,可以去做其他事情。

异步阻塞,等待结果回调通知,cpu处于等待的休眠中。

异步非阻塞(AIO),操作系统来完成读取的操作,读取完毕通知回调,使用线程池的方式去轮询客户端是否有数据,可以去做其他事情。

并发,单个 cpu 可以处理多个任务,但是同一时刻只有一个任务在运行。

并行,多个任务在多个 cpu 上运行,实现真正的同一时刻运行。

2、生命周期

//当new一个Thread的时候,此时只是一个简单的java对象
NEW

//调用start方法之后,进入该状态,此时只是处于可执行状态,但还未执行
RUNNABLE

//处于执行状态,可由于调用sleep/join和wait、阻塞IO操作、尝试获取锁,进入 BLOCKED 状态
//可由于cpu调度、调用yield,进入 RUNNABLE 状态
RUNNING   

//在sleep完毕和wait唤醒、阻塞IO操作完成、获得了锁、阻塞过程被interrupt,进入 RUNNABLE 状态
BLOCKED 

3、守护线程

一般使用 new Thread 创建的线程都是非守护线程,也称用户线程。设置为守护进行的方式就是,在 run 之前,调用setDaemon(true)。守护线程,仅仅是为用户线程提供服务,那么一旦所有的用户线程都运行完毕,守护进行也会结束。相反,只要有非守护线程存在,那么守护线程就不会终止。

守护线程的应用场景:垃圾回收、心跳检测、拼音检查线程

package com.vim;

import java.util.concurrent.TimeUnit;

public class App {
    public static void main( String[] args ) throws Exception{
        Thread t = new Thread(()->{
            while (true){
                try {
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println("......");
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        });
        //只有t设置了此处,才会随着父进程的退出而退出
        t.setDaemon(true);
        t.start();
        TimeUnit.SECONDS.sleep(5);
        System.out.println("main is over!");
    }
}

4、线程 yield、sleep

yield,称为线程让步,从 RUNNING 状态转为 RUNNABLE 状态,有可能切换之后,再次抢到执行权,进入 RUNNING 状态。

sleep,会阻塞该线程,进入 BLOCKED 状态,此时是挂起,让出cpu;只有阻塞时间到了,才会进入 RUNNABLE 状态,并不一定马上获取到 cpu 的执行权。

sleep(0) 的作用是,触发操作系统立刻重新进行一次CPU竞争,竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权。

package com.vim;

public class App {
    public static void main( String[] args ) throws Exception {
        new Thread(()->{
            try {
                while (true){
                    Thread.sleep(0);
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }).start();
    }
}

5、线程 interrupt

wait、sleep、join  使当前线程进入阻塞状态,而 interrupt 可以打断阻塞,不过仅仅是打算当前的阻塞状态。当前线程会抛出一个 InterruptException 异常,就像一个信号通知。此通知会将 interrupt flag 置为 true,不过针对阻塞状态下的中断,该状态位会被重置为 false。通过 thread.isInterrupted() 来判断,当然,阻塞状态下的,会被清除,从而影响该方法的结果。

package com.sample.modules.test;

import java.util.concurrent.TimeUnit;

public class Test {

    //中断一个线程,中断位 true
    public static void test1() throws Exception{
        Thread t = new Thread(()->{
            //2.获取当前的 interrupt flag 状态为 true
            System.out.println(Thread.currentThread().isInterrupted());
        });
        t.start();

        //1.中断线程
        t.interrupt();
        TimeUnit.MINUTES.sleep(4);
    }

    //中断一个线程,中断位 false
    public static void test2() throws Exception{
        Thread t = new Thread(()->{
            //2.清除中断位
            Thread.interrupted();
            //3.获取当前的 interrupt flag 状态为 false
            System.out.println(Thread.currentThread().isInterrupted());
        });
        t.start();

        //1.中断线程
        t.interrupt();
        TimeUnit.MINUTES.sleep(4);
    }

    //中断一个线程,中断位 false
    public static void test3() throws Exception{
        Thread t = new Thread(()->{
            //2.中断wait、sleep、join导致的阻塞
            try {
                TimeUnit.SECONDS.sleep(5);
            }catch (InterruptedException e){
                System.out.println("i am interrupted");
            }
            //3.获取当前的 interrupt flag 状态为 false
            System.out.println(Thread.currentThread().isInterrupted());
        });
        t.start();

        //1.中断线程
        t.interrupt();
        TimeUnit.MINUTES.sleep(4);
    }

    //阻塞之前中断结果:false、true、i am interrupted、false
    public static void test4(){
        Thread.interrupted();
        System.out.println("interrupt flag: "+Thread.currentThread().isInterrupted());
        Thread.currentThread().interrupt();
        System.out.println("interrupt flag: "+Thread.currentThread().isInterrupted());
        try {
            TimeUnit.SECONDS.sleep(1);
        }catch (InterruptedException e){
            System.out.println("i am interrupted");
        }
        System.out.println("interrupt flag: "+Thread.currentThread().isInterrupted());
    }

    public static void main(String[] args) throws Exception{
        test3();
    }
}

6、线程 join

parent 线程调用 child 线程的 join 方法,实际上调用的是 join(0) ,该方法加了锁,循环判断 child 线程的存活状态,当然,每次循环中调用 wait(0) 方法,这样的话可以让其他线程也进入 join(0) 方法。

//当前方法没有上锁
public final void join() throws InterruptedException {
    join(0);
}

//此方法上锁,此时其他线程可以进入 join(),但不可以进入 join(0)
//不断的检查线程是否 alive,调用 wait(0),这样就释放了锁,其他的线程就可以进入 join(0),也就是可以有多个线程等待某个线程执行完毕
//一旦线程不在 alive,那么就会返回到 join() 方法,调用的线程就可以继续执行下去
public final synchronized void join(long millis){
    if (millis == 0) {
         while (isAlive()) {
            wait(0);
        }
    }
}

7、线程通知 notify、wait

这两个方法,来源于 Object 类,两者配合使用。wait 方法属于对象方法,在调用之前必须先获取该对象的 monitor 锁,调用之后,就会释放该对象的 monitor 锁,从而进入该对象关联的 waitset 中,等待其他线程使用 notify 唤醒。

典型的生产消费场景:

生产者,在生产产品时,对仓库(同步资源)进行上锁,如果当前仓库没有满,则放入产品,使用 notifyAll 通知消费者;如果当前仓库满了,则使用 wait 释放锁,进入 waitset 阻塞等待 notifyAll 通知。

消费者,来到仓库消费,对仓库(同步资源)进行上锁,如果当前仓库有产品,则拿走产品,使用 notifyAll 通知生产者;如果当前仓库是空的,则使用 wait 释放锁,进入 waitset 阻塞等待 notifyAll 通知。

当 notifyAll 来临的时候,针对所有的生产者和消费者来说,都有拿到仓库钥匙的机会,就会再去竞争,再次进入以上判断逻辑。

8、wait 和 sleep 的区别

相同之处:

使线程进入到阻塞状态;可以被 interrupt 中断;

不同之处:

wait 是 Object 共有,sleep 是 Thread 特有。

wait 必须运行在同步方法中,sleep不需要。

wait 会释放锁,如果sleep放在同步方法中,并不会释放锁。

9、线程异常处理

package com.sample.modules.test;

public class Test {

    public static void main(String[] args) throws Exception{
        Thread t = new Thread(()->{
            int i = 1/0;
        });
        t.setUncaughtExceptionHandler((thread, e)->{
            System.out.println("exception...");
        });
        t.start();
    }
}

10、计算机内存模型 和 Java 内存模型

原理追溯:cpu 在执行指令的时候,数据来源于主内存(RAM),由于两者速度的严重不对等,之间出现了缓存 cache;一般分为 L1、L2、L3 缓存,每个 CPU 核心包含一套 L1,共享 L2 和 L3 缓存。

缓存一致性问题:当多个处理器的运算任务都涉及同一块主内存区域时,每个 cpu 从主内存中取出变量,放到本地 cache 中,进行计算之后,写入到 cache 中,再由 cache 刷新到主内存中。

  • 读取主内存 i 到 cache 中
  • 对 i 进行 ++
  • 将结果写回 cache
  • 将 cache 刷新回主内存。

缓存一致性协议:cpu 在操作 cache 中的数据时,如果发现是一个共享变量,那么在写入的时候,会发出信号通知其他的 cpu 将该变量的 cache line 置为无效状态,其他 cpu 在进行该变量读取的时候,就需要去主内存中读取。

相比计算机内存模型,Java 内存模型: 线程 == CPU, 工作内存 == CPU cache,主内存 == 主内存。

12、并发编程三大特性

  • 原子性,多次操作中,要么全部得到执行,要么全部不执行。
  • 可见性,一个线程对共享变量,作了修改,那么其他线程立即可以看到修改后的值。
  • 有序性,程序代码在执行过程中的先后顺序。

13、synchronized 关键字

synchronized 关键字提供了一种锁的机制,能够保证共享变量的互斥访问,即同一时刻,只能有一个线程访问同步资源。

内存方面,monitor enter 和 monitor exit 两个 JVM 指令,保证了任何线程在 monitor enter 之前必须从主内存中获取数据,在 monitor exit 之后,必须把更新的值刷新到主内存中。这两个 JVM 指令,严格的遵守 happends-before 原则,即一个 monitor exit 指令之前必须有一个 monitor enter 指令存在。

该关键字对于同步资源的排他性访问,很有效,但是其他没有获取到 monitor 的线程,到底阻塞多久,能不能提前解除阻塞,这些都是未知的。为此,java 为我们提供了其他的解决方案,显式锁。如 ReentrantLock。

猜你喜欢

转载自blog.csdn.net/sky_eyeland/article/details/93177820
今日推荐