笨蛋学JUC并发编程-进程与线程

发布时间:2024年01月17日

1.进程与线程

1.1概念

  • 进程:程序由指令和数据组成,当程序从磁盘加载到程序的代码中再到内存中,就已经开启了一个进程。

    进程可以看做是程序的一个实例,大部分程序都可以同时运行多个实例进程,也有程序只能启动一个进程

  • 线程:一个进程可以分为一到多个线程;一个线程就是一个指令流,将指令流中的指令按一定的顺序交给CPU执行

    线程作为最小的调度单位,进程作为资源分配的最小单位


1.1.1进程线程之间对比

  • 进程基本上是相互独立的,而线程存在于进程内,是进程的一个子集
  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享
  • 进程间通信较为复杂
    • 同一台计算机的进程通信称为 IPC(Inter-process communication)
    • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议
  • 线程通信相对简单,因为能共享进程内的内存,一个实例由多个线程功能访问同一个共享变量
  • 线程更轻量,且上下文切换成本一般要比进程上下文切换低

1.1.2并发与并行

  • 并发concurrent:在同一时刻,有多个指令在单个CPU上交替执行(线程轮流使用CPU)

    同一时间应对多件事情的能力

  • 并行parallel:在同一时刻,有多个指令在多个CPU上同时执行(多核CPU,每个核都可以调度运行线程)

    同一时间动手做多件事情的能力

1.1.3同步与异步

  • 同步:需要等待结果返回,才能继续运行
  • 异步:不需要等待结果返回,就能继续运行

1.1.4单线程与多线程

  • 在单核CPU下,多线程不能实际提高程序运行效率,也只是不同线程轮流使用CPU

  • 在多核CPU下,可以并行跑多个线程,但是能否提高程序运行效率,还是需要分情况

    根据任务的不同,决定是否拆分任务,然后来并行执行,以此来提高程序运行效率

1.2创建和运行线程

1.2.1直接使用Thread

  • 将线程和任务合并在了一起

    1. 创建线程对象

    2. 启动线程

Thread t =new Thread(){
    @Override
    public void run() {
        log.debug("running");
    }
};
t.setName("t1");
t.start();
log.debug("main-running");

1.2.2使用Runnable配合Thread

  • 将线程和任务(要执行的代码)分开
    • Thread代表线程
    • Runnable可运行的任务(线程要执行的代码)
Runnable runnable = new Runnable() {
    @Override
    public void run() {
        log.debug("running");
    }
};
Thread t = new Thread(runnable,"t2");
t.start();

1.2.3使用lambda语法配合Thread

Runnable runnable = () -> {log.debug("running");};
Thread t = new Thread(runnable,"t2");
t.start();

--------------------------------------------------------
Thread t = new Thread(() -> {log.debug("running");},"t2");
t.start();

1.2.4总结

  • 用Runnable更容易与线程池等高级API配合

  • 用Runnable让任务类脱离了Thread继承体系,更灵活

1.2.5FutureTask配合Thread

  • FutureTask能够接受Callable类型的参数,用来处理有返回结果的情况
FutureTask<Integer> integerFutureTask = new FutureTask<Integer>(new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        log.debug("running...");
        Thread.sleep(1000);
        return 1000;
    }
});

//参数1是任务对象,参数2是线程名字
Thread t = new Thread(integerFutureTask,"t1");
t.start();
//获取FutureTask中的值
//主线程阻塞,同步等待FutureTask执行完毕的结果
log.debug("{}",integerFutureTask.get());

1.2.6多线程同时运行

new Thread(()->{
   while (true){
       log.debug("running");
   }
},"t1").start();
new Thread(()->{
    while (true){
        log.debug("running");
    }
},"t2").start();

1.2.7查看进程线程的方法

windows
  • 任务管理器可以查看进程和线程数,通过查看进程ID来杀死进程
  • tasklist查看进程
  • taskkill杀死进程
linux
  • ps -fe查看所有进程
  • ps -fT -p 查看某个进程(PID)的所有线程
  • kill杀死进程
  • top按大写H切换是否显示线程
  • top -H -p 查看某个进程(PID)的所有线程
Java
  • jps命令查看所有Java进程
  • jstack 查看某个Java进程(PID)的所有线程状态
  • jconsole来查看某个Java进程中线程的运行情况(图形界面)

jconsole远程监控配置

  • 需要以如下方式运行Java类

    java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote -
    Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接(true/false) -
    Dcom.sun.management.jmxremote.authenticate=是否认证 java类(true/false)
    
  • 修改/etc/hosts文件将127.0.0.1映射至主机名

若需要认证访问,还需要做如下步骤

  • 复制jmxremote.password文件
  • 修改jmxremote.password和较明显remote.access文件的权限为600即文件所有者可读写
  • 连接时填入controlRole(用户名),R&D(密码)

1.3线程运行原理

1.3.1栈与栈帧

Java Virtual Machine Stacks(Java虚拟机栈)

JVM由堆、栈、方法区组成,栈内存主要是给线程用的,每个线程启动后,虚拟机就会为其分配一块栈内存

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

  • 每个线程有自己独立的栈帧,每个线程都是独立的且互不干扰

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

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

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

当Context Switch线程上下文发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态。

程序计数器就是记住下一条JVM指令的执行地址的作用,是线程私有的

  • 状态信息包括程序计数器、虚拟机栈中的每个栈帧信息,如局部变量表、操作数栈、动态连接、方法返回地址等
  • Context Switch线程上下文切换频繁发生会影响性能

1.3.4常用方法

方法名static功能说明注意
start()启动一个新线程,在新的线程运行run 方法中的代码start 方法只是让线程进入就绪,里面代码不一定立刻 运行(CPU 的时间片还没分给它)。每个线程对象的 start方法只能调用一次,如果调用了多次会出现 IllegalThreadStateException
run()新线程启动后会调用的方法如果在构造 Thread 对象时传递了 Runnable 参数,则 线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为
join()等待线程运行结束
join(long n)等待线程运行结 束,最多等待 n 毫秒
getId()获取线程长整型 的 idid 唯一
getName()获取线程名
setName(String)修改线程名
getPriority()获取线程优先级
setPriority(int)修改线程优先级java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率
getState()获取线程状态Java 中线程状态是用 6 个 enum 表示,分别为: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
isInterrupted()判断是否被打断不会清除打断标记
isAlive()线程是否存活(还没有运行完 毕)
interrupt()打断线程如果被打断线程正在 sleep、wait、join 会导致被打断的线程抛出 InterruptedException,并清除打断标记;
如果打断的正在运行的线程,则会设置打断标记;
park 的线程被打断,也会设置打断标记
interrupted()static判断当前线程是否被打断会清除打断标记
currentThread()static获取当前正在执行的线程
sleep(long n)static让当前执行的线 程休眠n毫秒,休眠时让出 cpu 的时间片给其它 线程
yield()static提示线程调度器 让出当前线程对 CPU的使用主要是为了测试和调试
1.3.4.1strart和run
  • 使用run()方法,程序仍在 main 线程运行, FileReader.read() 方法调用是同步的
  • 使用start()方法,程序在 t1 线程运行, FileReader.read() 方法调用是异步的

  • 直接调用 run 是在主线程中执行了 run,没有启动新的线程

  • 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码

    public static void main(String[] args) {
        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                log.debug(Thread.currentThread().getName());
                FileReader.read(Constants.MP4_FULL_PATH);
            }
        };
        
        t1.run(); //t1.start()
        log.debug("do other things ...");
    }
    
1.3.4.2sleep和yield

sleep(有一个等待的时间)

  1. 调用 sleep() 会让当前线程从 Running进入 Timed Waiting 状态(阻塞)

    Thread t1 = new Thread("t1") {
                @Override
                public void run() {
                    //让t1线程休眠2秒
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                }
            };
            t1.start();
            log.debug("t1 state: {}", t1.getState());
    
            //让主线程也休眠500毫秒
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("t1 state: {}", t1.getState());
    
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException

    @Test
        public void test1() throws InterruptedException {
            Thread t1 = new Thread("t1") {
                @Override
                public void run() {
                    //让t1线程休眠2秒
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        log.debug("wake up...");
                        e.printStackTrace();
                    }
    
                }
            };
            t1.start();
    
            Thread.sleep(1000);
            log.debug("interrupt...");
            t1.interrupt();
        }
    -------------------------------------------------------------
    14:43:27.175 [main] DEBUG c.ThreadMethod - interrupt...
    14:43:27.177 [t1] DEBUG c.ThreadMethod - wake up...
    java.lang.InterruptedException: sleep interrupted
    	at java.lang.Thread.sleep(Native Method)
    	at com.technologystatck.juc.threads.ThreadMethod$1.run(ThreadMethod.java:53)
    
  3. 睡眠结束后的线程未必会立刻得到执行

  4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性

    @Test
        public void test2() throws InterruptedException {
            log.debug("enter");
            TimeUnit.SECONDS.sleep(1);
            log.debug("end");
        }
    14:53:34.486 [main] DEBUG c.ThreadMethod - enter
    14:53:35.499 [main] DEBUG c.ThreadMethod - end
    

yield(没有等待时间)

  1. 调用 yield() 会让当前线程从 Running 进入 Runnable就绪状态,然后调度执行其它线程

  2. 具体的实现依赖于操作系统的任务调度器

线程优先级 (调度器可忽略)

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

  • 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用

    @Test
        public void test3() throws InterruptedException {
            Runnable task1 = () -> {
                int count = 0;
                for (;;) {
                    System.out.println("---->1 " + count++);
                }
            };
            Runnable task2 = () -> {
                int count = 0;
                for (;;) {
                    // Thread.yield();
                    System.out.println("    ---->2 " + count++);
                }
            };
            Thread t1 = new Thread(task1, "t1");
            Thread t2 = new Thread(task2, "t2");
      	    // t1.setPriority(Thread.MIN_PRIORITY);
    	    // t2.setPriority(Thread.MAX_PRIORITY);
            t1.start();
            t2.start();
        }
    
1.3.4.3join()(让等待同步)
  • join会等待线程结束然后再执行后面的内容
  • 而当线程执行结束会导致 join 结束
static int r = 0;
public static void main(String[] args) throws InterruptedException {
    test1();
}

private static void test1() throws InterruptedException {
    log.debug("开始");
    Thread t1 = new Thread(() -> {
        log.debug("开始");
        sleep(1);
        log.debug("结束");
        r = 10;
    });
    t1.start();
    log.debug("结果为:{}", r);
    log.debug("结束");
}
  • 因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 r=10
  • 而主线程一开始就要打印 r 的结果,所以只能打印出 r=0

  1. 最好别用 sleep ,因为主线程可以等t1执行完,继续再执行;但是这期间不能保证t1线程需要运行的时间,所以不能确定

  2. 用 join,加在 t1.start() 之后即可

  • 未等够时间
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
    test3();
}
public static void test3() throws InterruptedException {
    Thread t1 = new Thread(() -> {
        sleep(1);
        r1 = 10;
    });
    long start = System.currentTimeMillis();
    t1.start();
    // 线程执行结束会导致 join 结束
    t1.join(1500);
    long end = System.currentTimeMillis();
    log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
  • 等够时间
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
    test3();
}
public static void test3() throws InterruptedException {
    Thread t1 = new Thread(() -> {
        sleep(1);
        r1 = 10;
    });
    long start = System.currentTimeMillis();
    t1.start();
    // 线程执行结束会导致 join 结束
    t1.join(3000);
    long end = System.currentTimeMillis();
    log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}

static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
    test2();
}
private static void test2() throws InterruptedException {
    Thread t1 = new Thread(() -> {
        sleep(1);
        r1 = 10;
    });
    Thread t2 = new Thread(() -> {
        sleep(2);
        r2 = 20;
    });
    long start = System.currentTimeMillis();
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    long end = System.currentTimeMillis();
    log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
  • 第一个 join:等待 t1 时, t2 并没有停止, 而在运行
  • 第二个 join:1s 后, 执行到此, t2 也运行了 1s, 因此也只需再等待 1s
1.3.4.4interrupt()
  • isInterrupted默认为false,正常打断标记会置为true
  • 打断睡眠状态下的sleep、wait、join、park后,标记会被置为false
private static void test1() throws InterruptedException {
    Thread t1 = new Thread(()->{
        sleep(1);
    }, "t1");
    t1.start();
    sleep(0.5);
    t1.interrupt();
    log.debug(" 打断状态: {}", t1.isInterrupted());
}

  • 使用线程对象的 stop() 方法停止线程 ,stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁,会造成死锁
  • 使用 System.exit(int) 方法停止线程,目的仅是停止一个线程,但这种做法会让整个程序都停止
两阶段终止模式
@Test
    public void test4() throws InterruptedException {
        TPTInterrupt tptInterrupt = new TPTInterrupt();

        tptInterrupt.start();
        Thread.sleep(3500);
        tptInterrupt.stop();
    }

@Slf4j(topic = "c.TPTInterrupt")
class TPTInterrupt {
    private Thread thread;
    public void start(){
        thread = new Thread(() -> {
            while(true) {
                Thread current = Thread.currentThread();
//                log.debug("current.isInterrupted="+current.isInterrupted());
                if(current.isInterrupted()) {
//                    log.debug("isInterrupted="+current.isInterrupted());
                    log.debug("料理后事");
//                    log.debug("isInterrupted="+current.isInterrupted());

                    break;
                }
                try {
                    Thread.sleep(1000);
                    log.debug("将结果保存");
                } catch (InterruptedException e) {
                    current.interrupt();
                }
                // 执行监控操作
            }
        },"监控线程");
        thread.start();
    }
    public void stop() {
        thread.interrupt();
    }
}
  • 使用interrupt打断park()
public static void test5() throws InterruptedException {
        Thread t1 = new Thread(()->{
            log.debug("park...");
            LockSupport.park();

            log.debug("unpark...");
            //使用interrupted打断后恢复标志
            log.debug("打断状态:{}",Thread.interrupted());

            LockSupport.park();
            log.debug("unpark...");
        },"t1");
        t1.start();

        sleep(1);
        t1.interrupt();
    }
不推荐的方法
  • stop():停止线程运行
  • suspend():挂起(暂停)线程运行
  • resume():恢复线程运行

? 容易导致线程死锁,不推荐使用


1.4主线程与守护线程

  • 默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。

    有一种特殊的线程叫做守护线程(线程.setDaemon(true)),只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

log.debug("开始运行...");
Thread t1 = new Thread(() -> {
log.debug("开始运行...");
sleep(2);
log.debug("运行结束...");
}, "daemon");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();

sleep(1);
log.debug("运行结束...");
  • 守护线程的应用场景

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

1.5线程的五种状态

CPU
运行状态
初始状态
可运行状态
终止状态
阻塞状态
  • 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联

  • 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行

  • 【运行状态】指获取了 CPU 时间片运行中的状态

    • 当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】会导致线程的上下文切换
  • 【阻塞状态】

    • 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】
    • 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
    • 【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
  • 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

1.6线程的六种状态

在这里插入图片描述

  • NEW线程刚被创建,但是还没有调用 start() 方法

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

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

  • TERMINATED 当线程代码运行结束

1.6.1案例

	Thread t1 = new Thread("t1") {//NEW
            @Override
            public void run() {
                log.debug("running...");
            }
        };

        Thread t2 = new Thread("t2") {//RUNNABLE
            @Override
            public void run() {
                while (true) {

                }
            }
        };
        t2.start();

        Thread t3 = new Thread("t3") {//TERMINATED
            @Override
            public void run() {
                log.debug("running...");
            }
        };
        t3.start();

        Thread t4 = new Thread("t4") {//TIMED_WAITING
            @Override
            public void run() {
                synchronized (ThreadMethod.class) {
                    try {
                        Thread.sleep(1000000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t4.start();

        Thread t5 = new Thread("t5") {//WAITTING
            @Override
            public void run() {
                try {
                    t2.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        t5.start();

        Thread t6 = new Thread("t6") {//BLOCKED
            @Override
            public void run() {
                synchronized (ThreadMethod.class) {
                    try {
                        Thread.sleep(1000000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t6.start();

        log.debug("t1 state: {}", t1.getState());
        log.debug("t2 state: {}", t2.getState());
        log.debug("t3 state: {}", t3.getState());
        log.debug("t4 state: {}", t4.getState());
        log.debug("t5 state: {}", t5.getState());
        log.debug("t6 state: {}", t6.getState());

1.7烧水泡茶

Thread t1 = new Thread(() -> {
            log.debug("洗水壶");
            try {
                sleep(1);
                log.debug("烧开水");
                sleep(15);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "老王");

        Thread t2 = new Thread(() -> {
            try {
                log.debug("洗茶壶");
                sleep(1);
                log.debug("洗茶杯");
                sleep(2);
                log.debug("拿茶叶");
                sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            try {
                //等待水开
                t1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("泡茶");
        }, "小王");

        t1.start();
        t2.start();
    }
------------------------------------------------------------
19:34:26.084 [老王] DEBUG c.Kettle - 洗水壶
19:34:26.084 [小王] DEBUG c.Kettle - 洗茶壶
19:34:26.090 [老王] DEBUG c.Kettle - 烧开水
19:34:26.090 [小王] DEBUG c.Kettle - 洗茶杯
19:34:26.093 [小王] DEBUG c.Kettle - 拿茶叶
19:34:26.105 [小王] DEBUG c.Kettle - 泡茶

文章来源:https://blog.csdn.net/YSL_Monet/article/details/135659583
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。