本站消息

站长简介/公众号

  出租广告位,需要合作请联系站长


+关注
已关注

分类  

暂无分类

标签  

暂无标签

日期归档  

2024-11(1)

并发编程-初级

发布于2021-03-13 14:24     阅读(1200)     评论(0)     点赞(5)     收藏(4)


并发编程-初级

进程与线程

进程(Process)
  1. 程序由指令和数据构成。指令需要运行,数据需要读写,就必须将指令加载至CPU,数据加载至内存,在指令运行过程中还需要用到磁盘,网络等设备。
  2. 进程就是用来加载指令,管理内存,管理IO的。
  3. 当一个程序被执行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
  4. 进程可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(如打开多个记事本),但是也有的程序只能启动一个实例进程。(360安全卫士)
线程(Thread)
  1. 一个进程之内可以分为一至多个线程。
  2. 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行
  3. Java中,线程作为最小调度单位,进程作为资源分配的最小单位。Windows中进程是不活动的,只是作为线程的容器。
区别
  • 进程基本相互独立,而线程存在进程内,是进程的子集。
  • 进程有共享的资源,如内存空间,内部线程可以共享。
  • 不同计算机的进程通信需要通过网络,共同遵守网络协议如HTTP。
  • 线程通信相对简单,共享进程的内存,多个线程可以访问同一个共享变量。
  • 线程更加轻量,其上下文切换成本低于进程。

并行与并发

单核CPU下,线程实际还是串行执行的,操作系统中有一个组件名为任务调度器,将CPU的时间片(Windows下时间片最小约为15毫秒)分给不同的线程使用,只是由于CPU在线程间(时间片很短)的切换非常快,人类感觉就是同时运行的,即微观串行,宏观并行

线程轮流使用CPU的做法被成为并发(Concurrent)

CPU时间片1时间片2时间片3时间片4
core线程1线程2线程3线程4

多核CPU下,每个核(Core)都可以调度运行线程,称之为并行(parallel)

下述两个核心并行,每个核心并发执行线程

CPU时间片1时间片2时间片3时间片4
core1线程1线程2线程3线程4
core2线程2线程4线程1线程3

同步与异步

从方法调用角度讲
  • 需要等待结果返回才能继续执行为同步
  • 不需要等待结果返回就能继续执行为异步

Java多线程

创建和运行线程的方式

一、直接new Thread()
@Slf4j
public class ThreadExecrise {
    public static void main(String[] args) {
        //创建线程1
         Thread thread1 = new Thread(){
            public void run(){
                //要执行的任务
               log.debug("我是新线程");
            }
        };
         //修改新线程名字
         thread1.setName("线程1");
        // 启动线程1
        thread1.start();
        log.debug("我是main线程");
    }
}
二、实现Runnable接口创建线程

优点:把线程和任务(执行的代码)分开

Runnable runnable = new Runnable() {
    public void run() {
        log.debug("我是新线程");
    }
};
Thread thread1 = new Thread(runnable,"线程1"); //创建线程
thread1.start();

lambda简化

因为Runnable是函数式接口,即接口中只含有一个抽像方法,因此可以简化。

//lambda简化
Runnable runnable = () -> log.debug("我是新线程");
Thread thread1 = new Thread(runnable,"线程1"); //创建线程
thread1.start();
三、FutureTask接口

接收Callable类型的参数,用来处理有返回值的情况

FutureTask<Integer> task = new FutureTask<Integer>(() -> {
    log.debug("我是新线程!");
    Thread.sleep(2000);
    return 100;
});
new Thread(task,"线程1").start();
//main线程获取返回值,main线程阻塞,同步等待task执行完毕
Integer res = task.get();
log.debug("the result is {}",res); //100

线程常见的方法与区别

run与start
@Slf4j
public class runAndStart {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> log.debug("running。。。"));
//        t1.run();//并没有开启新线程
        t1.start(); //开启了新线程
//        t1.start(); //异常,不能多次开启一个线程
    }
}

小结

直接调用 run() 是在主线程中执行了 run(),没有启动新的线程
使用 start() 是启动新的线程,通过新的线程间接执行 run()方法中的代码

sleep 与 yield

sleep

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,那么被打断的线程这时就会抛出 InterruptedException异常【注意:这里打断的是正在休眠的线程,而不是其它状态的线程】
  3. 睡眠结束后的线程未必会立刻得到执行(需要分配到cpu时间片)
  4. 建议用 TimeUnit 的 sleep() 代替 Thread 的 sleep()来获得更好的可读性

yield

  1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
  2. 具体的实现依赖于操作系统的任务调度器(就是可能没有其它的线程正在执行,虽然调用了yield方法,但是也没有用)

小结

yield使cpu调用其它线程,但是cpu可能会再分配时间片给该线程;而sleep需要等过了休眠时间之后才有可能被分配cpu时间片

线程优先级

线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它

  • cpu 比较忙,那么优先级高的线程会获得更多的时间片,

  • cpu 闲时,优先级几乎没作用

join

join再当前线程方法中由其他线程调用,会等待调用方法的线程结束任务后执行

private static void test1() throws InterruptedException {
    log.debug("开始");
    Thread t1 = new Thread(() -> {
        log.debug("开始");
        sleep(1);
        log.debug("结束");
        r = 10;
    },"t1");
    t1.start();
    t1.join(); // 不加join的情况下,多核CPU并行执行,r=0,在主线程main方法中加入t1线程调用join方法后,主线程需等待t1线程任务结束执行
    log.debug("结果为:{}", r);// r=10
    log.debug("结束");
}

interrupt 方法

打断 sleep,wait,join和正常运行的线程

sleep,wait,join 的线程,这几个方法都会让线程进入阻塞状态, interrupt 方法打断这些线程的阻塞,抛出异常并且清空打断状态,即调用

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        log.debug("t1线程任务开始sleep...");
        try {
            Thread.sleep(1000); // wait, join 方法都如此
        } catch (InterruptedException e) {
            //e.printStackTrace();
            log.debug("t1线程被打断了"); //被打断后,返回异常处理结果
        }
    }, "t1");
    t1.start();
    Thread.sleep(500);
    log.debug("主线程打断t1线程");
    t1.interrupt(); //打断t1线程的睡眠,并清除打断状态
    Thread.sleep(10); //阻塞的线程打断之后需要等会会被清除打断状态
    log.debug("线程t1的状态为{}",t1.isInterrupted()); //false
}
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        log.debug("t1线程任务开始循环...");
        while (true){
            boolean flag = Thread.currentThread().isInterrupted();
            //如果打断状态为真,就停止程序
            if(flag)
                break;
        }
    }, "t1");
    t1.start();
    Thread.sleep(1000);
    log.debug("主线程开始打断");
    t1.interrupt(); //打断t1线程的睡眠,并清除打断状态
    Thread.sleep(10); //正常运行的线程打断之后需要等会并不会会清除打断状态
    log.debug("线程t1的状态为{}",t1.isInterrupted()); //true
}

小结

sleep,yiled,wait,join 对比

关于join的原理和这几个方法的对比:看这里

补充:

  1. sleep,join,yield,interrupted是Thread类中的方法
  2. wait/notify是object中的方法

sleep 不释放锁、释放cpu
join 释放锁、抢占cpu
yiled 不释放锁、释放cpu
wait 释放锁、释放cpu

终止模式之两阶段终止模式

Two Phase Termination,就是考虑在一个线程T1中如何优雅地终止另一个线程T2?这里的优雅指的是给T2一个料理后事的机会(如释放锁)。

如下所示:那么线程的isInterrupted()方法可以取得线程的打断标记,

如果线程在睡眠sleep期间被打断,打断标记是不会变的,为false,但是sleep期间被打断会抛出异常,我们据此

手动设置打断标记为true

如果是在程序正常运行期间被打断的,那么打断标记就被自动设置为true

处理好这两种情况那我们就可以放心地来料理后事啦!

@Slf4j
public class Test01 {
    public static void main(String[] args) throws InterruptedException {
        TwoParseTermination twoParseTermination = new TwoParseTermination();
        twoParseTermination.start();
        Thread.sleep(3000);  // 让监控线程执行一会儿
        twoParseTermination.stop(); // 停止监控线程
    }
}

@Slf4j
class TwoParseTermination {
    private Thread thread;

    public void start() {
        thread = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    log.debug("线程结束。。正在料理后事中");
                    break;
                }
                try {
                    Thread.sleep(500);
                    log.debug("正在执行监控的功能");
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt(); //避免监控线程睡眠时被监控线程被打断
                }
            }
        },"monitorThread");
        thread.start();
    }

    public void stop() {
        thread.interrupt();
    }
}

park方法停止线程

@Slf4j
public class Test02 {
    public static void main(String[] args) throws InterruptedException {
        test3();
    }
    private static void test3() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("park停止该线程...");
            LockSupport.park();//停止线程后,该线程一直处于park状态,直到被打断
            log.debug("unPark");
            log.debug("打断状态{}", Thread.currentThread().isInterrupted());
            LockSupport.park(); //此时该线程被打断,但因其正常运行不会标记为false,因此失效(标记为true即失效)
            log.debug("unPark1");
            log.debug("打断状态{}", Thread.interrupted()); //返回为假,park方法会再次生效,类似i++,此行为true,但是下行已改false
            LockSupport.park();
            log.debug("unPark2");
        },"t1");
        t1.start();
        Thread.sleep(1000);
        t1.interrupt();  //被打断后t1线程会恢复继续运行
    }

守护线程

默认情况下,java进程需要等待所有的线程结束后才会停止。

但是有一种特殊的线程,叫做守护线程,在其他线程全部结束的时候即使守护线程还未结束代码未执行完java进程也会停止。

普通线程t1可以调用t1.setDeamon(true); 方法变成守护线程

注意
  • 垃圾回收器线程就是一种守护线程

  • Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等
    待它们处理完当前请求

线程的状态

五种状态

其划分主要是从操作系统的层面进行划分的

  1. 初始状态,仅仅是在语言层面上创建了线程对象,即Thead thread = new Thead();,还未与操作系统线程关联
  2. 可运行状态,也称就绪状态,指该线程已经被创建,与操作系统相关联,等待cpu给它分配时间片就可运行
  3. 运行状态,指线程获取了CPU时间片,正在运行
    1. 当CPU时间片用完,线程会转换至【可运行状态】,等待 CPU再次分配时间片,会导致我们前面讲到的上下文切换
  4. 阻塞状态
    1. 如果调用了阻塞API,如BIO读写文件,那么线程实际上不会用到CPU,不会分配CPU时间片,会导致上下文切换,进入【阻塞状态】
    2. 等待BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
    3. 与【可运行状态】的区别是,只要操作系统一直不唤醒线程,调度器就一直不会考虑调度它们,CPU就一直不会分配时间片
  5. 终止状态,表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

六种状态

这是从 Java API 层面来描述的,我们主要研究的就是这种。状态转换详情图:地址

根据 Thread.State 枚举,分为六种状态

  1. NEW 跟五种状态里的初始状态是一个意思

  2. RUNNABLE 是当调用了 start() 方法之后的状态,注意,Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【IO阻塞状态】(由于 BIO 导致的线程阻塞(如读取文件),在 Java 里无法区分,仍然认为是可运行)

  3. BLOCKEDWAITINGTIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,

  4. BLOCKED

    • 这个状态,一般是线程等待获取一个锁,来继续执行下一步的操作
  5. WAITING

    • 一个线程处于等待另一个线程执行的时候,需要关注的是,这边的等待是没有时间限制的。
      • Object. Wait() 等待另一个线程执行Object.notify()或者Object.notifyAll()
      • Thread. Join() 等待该线程执行完成
      • LockSupport.park() 在等待执行LockSupport.unpark(thread)
  6. TIMED_WAITING

    等待一段时间之后,会唤醒线程去重新获取锁,区别WAITING永久等待

    • Thread.sleep(long)

      Object.wait(long)

      Thread. Join(long)

      LockSupport.parkNanos()

      LockSupport.parkUntil()

查看线程进程的方法

windows系统

  • 任务管理器可以查看进程和线程数,也可以杀死进程

  • tasklist查看进程

  • taskkill杀死进程

    常用参数

    /F 指定强制终止进程。

    /PID 指定要终止的进程的 PID。(使用 TaskList 取得 PID)

linux系统

  • ps -fe 查看运行的进程

    ps -fe | grep ef 筛选名字含有ef的进程

  • top -H -p 【进程的PID】 查看当前进程的所有线程

Java

  • jps命令查看所有java进程

  • jstack 【PID】查看某个Java进程的所有线程状态

  • jconsole 来查看某个Java进程中线程的运行情况(图形界面)

栈与栈帧(Stack and Frames)

Java Virtual Machine Stacks(Java 虚拟机栈)

JVM中由堆,栈,方法区组成,其中栈内存被线程所使用,每个线程启动后,虚拟机就会为其分配一块栈内存。

  • 每个栈由多个栈帧Frame组成,对应着每次方法调用所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的方法

线程上下文切换(Thread Context Switch)

因为以下一些原因导致CPU不再执行当前的线程,转而执行另一个线程的代码

  • 线程的CPU时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要执行
  • 线程自己调用了sleep,yield,wait,join,synchronized,lock等方法

当Context Switch发送时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的就是程序计数器(Program Counter Register),其作用是记住下一条jvm指令的执行地址,是线程私有的。

  • 状态包括程序计数器,虚拟栈中每个栈帧的信息,如局部变量,操作数栈,返回地址等。
  • Context Switch 频繁发送时会影响性能。

原文链接:https://blog.csdn.net/m0_46975599/article/details/114702798



所属网站分类: 技术文章 > 博客

作者:天天在家

链接:http://www.javaheidong.com/blog/article/114270/7f21c0afad412550b607/

来源:java黑洞网

任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任

5 0
收藏该文
已收藏

评论内容:(最多支持255个字符)