并发学习记录02:Java线程

创建和运行线程

方法一,直接使用Thread

@Slf4j(topic = "ch.Test1")
public class Test01 {
    public static void main(String[] args) {
        //创建线程子类
        Thread t = new Thread(){
            //重写任务方法,run方法中写要执行的任务
            @Override
            public void run() {
                log.debug("running");
            }
        };
        //线程命名
        t.setName("t1");
        t.start();
        log.debug("running");
    }
}

方法二,使用Runnable配合Thread

把线程和任务分开
Thread代表线程
Runnable代表任务,run()方法中是线程要执行的代码

@Slf4j(topic = "ch.Test02")
public class Test02 {
    public static void main(String[] args) {
        //创建任务对象
        Runnable r = () -> log.debug("running");
        //创建线程对象,参数1是任务对象,参数2是任务名称
        Thread t2 = new Thread(r, "t2");
        t2.start();
    }
}

Thread和Runnable之间的关系

方法1是把线程和任务合并在了一起,方法2是线程和任务分开了
用Runnable更容易与线程池等API配合
用Runnable让任务类脱离了Thread继承体系,更灵活
Java中有组合大于继承的设计传统,使用Runnable相当于是用一个任务对象去和线程对象进行组合,可扩展性更高。

方法三,FutureTask配合Thread

FutureTask能够接收Callable类型的参数,用来处理有返回结果的情况

@Slf4j(topic = "ch.Test03")
public class Test03 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                log.debug("call");
                Thread.sleep(3000);
                return 55;
            }
        });
        Thread t = new Thread(task,"t1");
        t.start();
        log.debug("返回结果是{}",task.get());
    }
}

查看进程线程的方法

Windows

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

Linux

  • ps -fe 查看所有进程
  • ps -fT -p 查看某个进程的所有线程
  • kill 杀死进程
  • top 按大写H切换是否显示线程
  • top -H -p 查看某个进程的所有线程

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=是否安全连接 -
Dcom.sun.management.jmxremote.authenticate=是否认证 java类

修改/etc/hosts文件将127.0.0.1映射到主机名

原理之线程运行

栈与栈帧

JVM中由堆,栈,方法区组成,其中栈内存就是给线程用的,每个线程启动后,虚拟机就会为其分配一块栈内存。
每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。

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

因为下面一些原因,导致CPU不再执行当前的线程,转而执行另一个线程的代码
线程的CPU时间片用完
垃圾回收
有更高优先级的线程需要运行
线程自己调用了sleep,yield,wait,join,park,synchronized,lock等方法

当线程上下文切换发生时,需要操作系统保持当前线程的状态,恢复另一个线程的状态

状态包括程序计数器,虚拟机栈中每个栈帧的信息,如局部变量,操作数栈和返回地址等
线程上下文切换频繁会影响性能
程序计数器的作用是记住下一条JVM指令的执行地址,它是线程私有的。

线程常见方法

方法名 static 功能说明 注意事项
start() 启动一个新线程,在新的线程中允许run方法中的代码 start方法只是让线程进入就绪状态,里面的代码不一定立刻运行(因为还没得到CPU的时间片)。每个线程对象的start方法只能调用一次,如果调用了多次会出现异常
run() 新线程启动后会调用的方法 如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为

start与run

利用一个demo测试run和start:

@Slf4j(topic = "ch.TestStart")
public class TestStart {
    public static void main(String[] args) {
        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                log.debug("Thread name is {}",Thread.currentThread().getName());
                FileReader.read(Constants.TEST_ADDRESS);
            }
        };
        //run
//        t1.run();
        //start
        t1.start();
        log.debug("running");
    }
}

执行可以发现,当调用run方法时,其实也只是在主线程中执行了Thread的run方法,没有启动新的线程
而使用start是启动新的线程,通过新的线程去执行Thread中的run方法

sleep和yield

sleep

  • 调用sleep会让当前线程从running编程Timed Waiting状态,进入阻塞
  • 其他线程可以使用interrupt方法打断正在睡眠的线程,这时候sleep方法会抛出异常
  • 睡眠结束后的线程未必会立刻执行
  • 建议用TimeUnit的sleep代替Thread的sleep,因为可读性更好
  • sleep方法是哪个线程调用,哪个线程睡眠

yield

  • 调用yield方法会让当前线程从running状态进入Runnable就绪状态,然后调度执行其他线程
  • 具体的实现其实还是要依赖操作系统的任务调度器

线程优先级

  • 线程优先级可以提示调度器优先调度该线程,一般优先级越高,该线程可能得到的时间片就越多
  • 如果CPU比较忙,那么优先级高的线程会获得更多的时间片,但CPU闲时,优先级几乎没作用

join方法详解

为什么需要join

`@Slf4j(topic = "ch.TestJoin")
public class TestJoin {
static int r = 0;

public static void main(String[] args) throws InterruptedException {
    test01();
}

public static void test01() throws InterruptedException {
    log.debug("开始");
    Thread t1 = new Thread(){
        @Override
        public void run() {
            log.debug(Thread.currentThread().getName() + "开始");
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r++;
            log.debug(Thread.currentThread().getName() + "结束");
        }
    };
    t1.start();
    //如果想加1,那么就得等t1线程执行结束
    t1.join();
    log.debug("结果为{}",r);
    log.debug("结束");
}

}`

由于主线程和t1线程是并行的,而t1线程需要sleep1秒才会计算r++
如果主线程不等t1线程执行完成的话,就会输出r = 0
如果主线等t1线程执行完成的话,就会输出r = 1

这里遗留一个小问题:join底层用了wait,wait和sleep有什么区别?是否能相互代替?

有时效的join

t.join(long millis):等待t1线程执行,如果超过时间t1还没执行完成,就放弃等待直接运行。
如下:

@Slf4j(topic = "ch.TestJoinLimit")
public class TestJoinLimit {
    static int r1 = 0;

    public static void main(String[] args) throws InterruptedException {
//        test03();
        test04();
    }

    public static void test03() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r1 = 20;
        }, "t1");
        long start = System.currentTimeMillis();
        t1.start();
        //实际等了1s就等到
        t1.join(1500);
        long end = System.currentTimeMillis();
        log.debug("r1 = {},time gap is {}", r1, end - start);
    }

    public static void test04() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r1 = 20;
        }, "t1");
        long start = System.currentTimeMillis();
        t1.start();
        //等了1.5s没等到,直接执行了
        t1.join(1500);
        long end = System.currentTimeMillis();
        log.debug("r1 = {},time gap is {}", r1, end - start);
    }
}

interrupt方法

interrupt方法可以打断sleep,wait,join的线程
这几个方法都会让线程进入阻塞状态
打断sleep的线程,会清空打断状态,以sleep为例

@Slf4j(topic = "ch.TestInterrupt01")
public class TestInterrupt01 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1");
        t1.start();
        Thread.sleep(1);
        t1.interrupt();
        //打断状态应该是FALSE,因为打断sleep线程,会清空打断状态
        log.debug("打断状态为{}",t1.isInterrupted());
    }
}

当然我们也可以用interrupt方法打断正常运行的线程,不会清空打断的状态

@Slf4j(topic = "ch.TestInterrupt02")
public class TestInterrupt02 {
    //尝试打断正常运行的线程
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (true) {
                log.debug("t1运行中");
                Thread cur = Thread.currentThread();
                boolean interrupted = cur.isInterrupted();
                if (interrupted) {
                    log.debug("被打断");
                    break;
                }
            }
        }, "t1");
        t1.start();
        t1.interrupt();
        log.debug("t1线程的状态是{}",t1.isInterrupted());
    }
}

多线程设计模式之两阶段终止

假如t1和t2并发执行,我们需要在线程t1中终止线程t2,我们该如何做呢?
首先我们可以想到使用线程对象的stop()方法停止线程,但是stop方法会真正杀死线程,如果这时线程锁住了共享资源,那么它被杀死后就再也没有机会释放锁,其他线程将永远无法获取这个锁

还有一种是System.exit()方法停止线程,但是这种方法会让整个程序都停止

理想的办法就是用两阶段终止,用一个线程间隔一段时间去监视isInterrupted的状态,如果正常运行时被打断,isInterrupted会被置为true,如果睡眠时期被打断,也会抛出异常。

@Slf4j(topic = "ch.InterruptPattern")
public class InterruptPattern {
    public static void main(String[] args) throws InterruptedException {
        InTestInterrupt testInterrupt = new InTestInterrupt();
        testInterrupt.start();

        Thread.sleep(3500);
        log.debug("stop");
        testInterrupt.stop();
    }
}

@Slf4j(topic = "ch.TestInterrupt")
class InTestInterrupt {
    private Thread thread;

    public void start() {
        thread = new Thread(() -> {
            while (true) {
                Thread cur = Thread.currentThread();
                //被打断的情况
                if (cur.isInterrupted()) {
                    log.debug("优雅关闭线程");
                    break;
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {//捕捉到异常说明
                    cur.interrupt();
                    e.printStackTrace();
                }
            }
        }, "t1");
        thread.start();
    }

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

}

打断park线程,park的作用也是让当前线程停下
打断park线程,不会清空打断状态
如果打断标记已经是TRUE,则park会失效

可能破坏同步代码块,造成线程死锁的方法

方法名 功能说明 原因
stop() 停止线程运行
suspend() 挂起线程运行
resume() 恢复线程运行

主线程与守护线程

默认情况下,Java进程需要等待所有线程都运行结束,才会结束,有一种特殊的线程叫做守护线程,只要其他非守护线程运行结束了,即使守护线程的代码没有执行完,守护线程也会强行结束。

@Slf4j(topic = "ch.TestShouhuThread")
public class TestShouhuThread {
    public static void main(String[] args) throws InterruptedException {
        log.debug("main begin");
        Thread t1 = new Thread(() -> {
            log.debug("t1 begin");
            try {
                //按道理是要休眠20s,才结束
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("t1 end");
        }, "t1");
        //设置为守护线程
        t1.setDaemon(true);
        t1.start();
        Thread.sleep(1);
        //守护线程会在其他线程都结束时结束
        log.debug("main end");
    }
}

线程的五种状态

初始状态:语言层面创建了线程对象,但是还未与操作系统的线程关联
可运行状态:也可以叫就绪状态,该线程已经被创建且与操作系统线程关联,可以由CPU调度执行
运行状态:获取了CPU时间片,线程在运行中的状态,如果CPU的时间片用完,线程会从运行状态转换为就绪状态,这时候会有一个线程上下文的切换
阻塞状态:一般是等待系统非CPU资源。比如调用了读写文件的接口,线程就暂时不会用到CPU,会导致线程上下文切换,进入阻塞状态,等到读写操作完毕,操作系统会唤醒阻塞的线程,转换到可运行状态。阻塞状态的线程要先完成等待非CPU资源做的事,变成就绪状态,才会有机会得到CPU的调度
终止状态:表示线程已经执行完毕,生命周期已经结束,不会再转换为其他状态

Java中线程的六种状态

这是根据Thread.State枚举类型变量来分的。

  • NEW:线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE:当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
  • BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分
  • TERMINATED 当线程代码运行结束

扩展习题之华罗庚的烧水泡茶

任务就是想喝茶,但是需要洗水壶,烧开水,洗茶壶,洗茶杯,拿茶叶之后才能泡茶

@Slf4j(topic = "ch.MakeTea")
public class MakeTea {
    public static void main(String[] args) {
        makeTea01();
    }

    public static void makeTea01() {
        Thread t1 = new Thread(() -> {
            log.debug("洗水壶");
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("烧开水");
            try {
                Thread.sleep(15);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            log.debug("洗茶壶");
            log.debug("洗茶杯");
            log.debug("拿茶叶");
            try {
                Thread.sleep(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                t1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("泡茶");
        }, "t2");
        t1.start();
        t2.start();
    }
}
posted @   理塘DJ  阅读(32)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示