以微服务注册中心为背景学习Java并发

本文介绍了如何手动实现一个注册中心,涉及register-server和register-client的角色。文章详细讲解了Java中的工作线程和daemon线程,以及它们对JVM进程退出的影响。同时,讨论了线程的创建、线程组、线程优先级以及Eureka的类似机制。还提到了线程的中断、sleep和yield方法,以及join的概念。
摘要由CSDN通过智能技术生成

文章作者:老钟
在这里插入图片描述
手动模拟实现一个注册中心,以该案例为背景进行Java并发的相关知识学习。
(1)register-server:负责接收各个服务的请求,是可以独立部署和启动的,启动了以后,他会以一个web工程的方式来启动,启动之后就是监听各个服务发送过来的http请求,注册、心跳、下线
(2)register-client:组件,依赖包,各个服务需要引入这个依赖,在服务启动的时候就可以去让register-client来运行,来跟register-server进行通信,比如说完成这个注册,或者是心跳的通知。
**register-client **他不是独立启动的,他其实是一个依赖包,你可以把这个东西打包发布到maven nexus私服里去,你的公司里各个服务,必须依赖这个register-client,然后启动服务的时候,一般会调用regsiter-client的API,创建一个组件,启动这个组件;由register-client组件去跟register-server进行交互。

人家Spring Cloud的微服务注册中心,eureka,大概就是这个意思,人家也是分eureka-server,是独立部署和启动的,就是一个web工程;eureka-client,各个服务都需要依赖eureka-client,服务启动就创建一个eureka-client实例;eureka-client帮各个服务跟eureka-server进行通信,注册、心跳、下线

1.工作线程和daemon线程

我们启动了一个jvm进程,main线程,RegisterClientWorker线程。
main线程负责启动了RegisterClientWorker线程,其实干完这些事情以后,main线程就结束了,结束了以后但是jvm进程不会退出?为什么呢,有一个工作线程,就是RegisterClientWorker线程一直在运行,所以jvm进程是不会退出的,会一直存在!

只要有工作线程一直在运行,没有结束,那么jvm进程是不会退出的
在这里插入图片描述

啥是daemon线程,啥是非daemon线程?
简单来说,一般工作线程是非daemon线程,后台线程是daemon线程.
默认创建的线程就是非daemon的,我们称之为工作线程. 而daemon线程不会阻止JVM进程的退出,如果没有工作线程了,那么daemon线程会随着JVM进程的退出而退出。

假如说微服务注册中心负责接收请求的核心工作线程不知道为啥都停止了,那么说明这个微服务注册中心必须停止啊,结果你的那个监控微服务存活状态的线程一直在那儿运行着,卡着,会导致微服务注册中心没法退出的!因为jvm进程没法结束

所以说针对这种情况,一般会将后台运行的线程设置为daemon线程,如果jvm里只剩下了daemon线程,那么就会进程退出,所有daemon线程一起销毁了,不会阻止jvm进程退出。所以我们应该将微服务存活状态监控的线程,设置为daemon线程,这样如果工作线程都死了,那么jvm也就退出了,daemon线程也销毁了

设置daemon线程的方式:
在这里插入图片描述

2.创建线程的两种方式

方法一,直接使用 Thread
// 创建线程对象
Thread t = new Thread() {
    public void run() {
        // 要执行的任务
    }
};
// 启动线程
t.start();

例如:

// 构造方法的参数是给线程指定名字,推荐
Thread t1 = new Thread("t1") {
    @Override
    // run 方法内实现了要执行的任务
    public void run() {
        log.debug("hello");
    }
};
t1.start();

输出
19:19:00 [t1] c.ThreadStarter - hello

方法二,使用 Runnable 配合 Thread

把【线程】和【任务】(要执行的代码)分开

  • Thread 代表线程
  • Runnable 可运行的任务(线程要执行的代码)
Runnable runnable = new Runnable() {
    public void run(){
        // 要执行的任务
    }
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();

例如:

// 创建任务对象
Runnable task2 = new Runnable() {
    @Override
    public void run() {
        log.debug("hello");
    }
};

// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();

输出
19:19:00 [t2] c.ThreadStarter - hello
Java 8 以后可以使用 lambda 精简代码

// 创建任务对象
Runnable task2 = () -> log.debug("hello");

// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();

3.线程组ThreadGroup

ThreadGroup就是线程组,其实意思就是你可以把一堆线程加入一个线程组里,好处大概就是,你可以将一堆线程作为一个整体,统一的管理和设置。
实际上在java里,每个线程都有一个父线程的概念,就是在哪个线程里创建这个线程,那么他的父线程就是谁。举例来说,java都是通过main启动的,那么有一个主要的线程就是mian线程。在main线程里启动的线程,父线程就是main线程,就这么简单。
然后每个线程都必然属于一个线程组,默认情况下,你要是创建一个线程没指定线程组,那么就会属于父线程的线程组了,main线程的线程组就是main ThreadGroup。
我们创建线程组的时候,如果没有手动指定他的父线程组,那么其实默认的父线程组就是main线程的线程组。
相关API方法:

enumerate():复制线程组里的线程
activeCount():获取线程组里活跃的线程
getName()getParent()list(),等等
interrupt():打断所有的线程
destroy():一次性destroy所有的线程

把握住一点:默认线程会加入父线程的ThreadGroup,或者你自己手动创建ThreadGroup,ThreadGroup也有父ThreadGroup,ThreadGroup可以包裹一大堆的线程,然后统一做一些操作,比如统一复制、停止、销毁,等等

JDK虽然提供了ThreadGroup,但是一般平时自己开发,或者是很多的开源项目里,ThreadGrdoup很少用,其实如果你要自己封装一堆线程的管理组件,我觉得你完全可以自己写

4.线程优先级

设置线程优先级,理论上可以让优先级高的线程先尽量多执行,但是其实一般实践中很少弄这个东西,因为这是理论上的,可能你设置了优先级,人家cpu结果也还是没按照这个优先级来执行线程

这个优先级一般是在1~10之间

而且ThreadGroup也可以指定优先级,线程优先级不能大于ThreadGroup的优先级

但是一般就是用默认的优先级就ok了,默认他会用父线程的优先级,就是5

    public final void setPriority(int newPriority) {
        ThreadGroup g;
        checkAccess();
        if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
            throw new IllegalArgumentException();
        }
        if((g = getThreadGroup()) != null) {
            if (newPriority > g.getMaxPriority()) {
                newPriority = g.getMaxPriority();
            }
            setPriority0(priority = newPriority);
        }
    }

5.Thread源码

(1)初始化过程

public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }

/**
 * Initializes a Thread with the current AccessControlContext.
 * @see #init(ThreadGroup,Runnable,String,long,AccessControlContext,boolean)
 */
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize) {
    init(g, target, name, stackSize, null, true);
}

无论是哪个构造方法都会调用init这个重载方法进行初始化。
默认情况下,如果你不指定线程的名称,那么自动给你生成的线程名称就是,Thread-0,Thread-1,以此类推的一大堆的线程。
继续深入init方法内部:

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;
        //获取到的parent就是当前创建该线程的线程
        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        //默认未指定threadGroup那么就为null
        if (g == null) {
            /* Determine if it's an applet or not */

            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }

            /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
            if (g == null) {
                //获取父线程的threadGroup
                g = parent.getThreadGroup();
            }
        }

        /* checkAccess regardless of whether or not threadgroup is
           explicitly passed in. */
        g.checkAccess();

        /*
         * Do we have the required permissions?
         */
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        g.addUnstarted();

        this.group = g;    
        //默认情况下,如果你没有指定你是否为daemon的话,那么你的daemon的状态是由父线程决定的,
        //就是说如果你的父线程是daemon线程,那么你也是daemon线程
        this.daemon = parent.isDaemon();
        //同理,你的优先级如果没有指定的话,那么就跟父线程的优先级保持一致
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

总结一下Thread初始化的过程你需要知道的一些点:
(1)创建你的线程,就是你的父线程
(2)如果你没有指定ThreadGroup,你的ThreadGroup就是父线程的ThreadGroup
(3)你的daemon状态默认是父线程的daemon状态
(4)你的优先级默认是父线程的优先级
(5)如果你没有指定线程的名称,那么默认就是Thread-0格式的名称
(6)你的线程id是全局递增的,从1开始

(2)start启动过程

    public synchronized void start() {
        /**
         * 永远都不能对一个线程多次调用和执行start()方法,这个是不对的;
        	如果你的线程一旦执行过一次以后,那么他的threadStatus就一定会变为非0的一个状态,
			如果threadStatus是非0的状态,说明他之前已经被执行过了,所以这里会有一个判断
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* group就是之前给分配的,如果你自己指定了那么就是你自己创建的那个ThreadGroup,
		否则的话就是你的父线程的threadGroup,这行代码,其实就是将当前线程加入了他属于的那个线程组. */
        group.add(this);

        boolean started = false;
        try {
            start0(); //会结合底层的一些代码和机制,实际的启动一个线程
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

一旦是start0()成功的启动之后,他就会去执行我们覆盖掉的那个run()方法,或者是如果你传入进去的是那个Runnalbe对象,人家就会执行那个Runnable对象的方法:

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

如果target不为null的话,那么此时就会执行target的run方法。反之,如果你是直接自己用Thread类继承了一个子类的话,那么你会重写这个run()方法,start0()启动线程之后,就会来执行你的run()方法.

大家从这里需要注意的几个点:
(1)一旦启动了线程之后,就不能再重新启动了,多次调用start()方法,因为启动之后,threadStatus就是非0的状态了,此时就不能重新调用了
(2)你启动线程之后,这个线程就会加入之前处理好的那个线程组中
(3)启动一个线程实际上走的是native方法,start0(),会实际的启动一个线程
(4)一个线程启动之后就会执行run()方法

6.sleep 与 yield

sleep

  1. 调用 sleep 会让当前线程从**Running **进入 Timed Waiting 状态(阻塞)
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
    在这里插入图片描述
  3. 睡眠结束后的线程未必会立刻得到执行
  4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性

yield

  1. 调用 yield 会让当前线程从 **Running **进入 **Runnable **就绪状态,然后调度执行其它线程
  2. 具体的实现依赖于操作系统的任务调度器

注意:

任务调度器是不会将时间片分给阻塞状态的线程,但是有可能会分给就绪状态的线程,比如当前已经没有可以执行的任务,那么就会给当前处于就绪状态的线程进行分配执行。yield没有等待时间,立马进入就绪状态,没有等待时间。

![image.png](https://img-blog.csdnimg.cn/img_convert/d5bdd1c5b40d103613ed816c388aba3e.png#averageHue=#fefefc&clientId=u747c64ea-d28a-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=164&id=ufac95567&margin=[object Object]&name=image.png&originHeight=164&originWidth=680&originalType=binary&ratio=1&rotation=0&showTitle=false&size=30051&status=done&style=none&taskId=u505e386d-e54b-4bb9-97c4-ca3cafc892c&title=&width=680)

7.join概念

main线程里面,如果开启了一个其他线程,这个时候只要你一旦开启了其他线程之后,那么main线程就会跟其他线程开始并发的运行,一会执行main线程的代码,一会儿会执行其他线程的代码。

main线程里面开启了一个线程,main线程如果对那个线程调用了join的方法,那么就会导致main线程会阻塞住,他会等待其他线程的代码逻辑执行结束,那个线程执行完毕,然后main线程才会继续往下走。
优化后的代码:
在这里插入图片描述

8. interrupt 方法

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

private static void test1() throws InterruptedException {
    Thread t1 = new Thread(()->{
        sleep(1);
    }, "t1");
    t1.start();

    sleep(0.5);
    t1.interrupt();
    log.debug(" 打断状态: {}", t1.isInterrupted());
}

输出:
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at cn.itcast.n2.util.Sleeper.sleep(Sleeper.java:8)
at cn.itcast.n4.TestInterrupt.lambda$test1$3(TestInterrupt.java:59)
at java.lang.Thread.run(Thread.java:745)
21:18:10.374 [main] c.TestInterrupt - 打断状态: false

打断正常运行的线程
打断正常运行的线程, 不会清空打断状态:

private static void test2() throws InterruptedException {
    Thread t2 = new Thread(()->{
        while(true) {
            Thread current = Thread.currentThread();
            boolean interrupted = current.isInterrupted();
            if(interrupted) {
                log.debug(" 打断状态: {}", interrupted);
                break;
            }
        }
    }, "t2");
    t2.start();

    sleep(0.5);
    t2.interrupt();
}

输出:
20:57:37.964 [t2] c.TestInterrupt - 打断状态: true

今天这篇文章我们先将一些线程相关的基础知识打牢,后续我们再逐渐深入,直到我们自己一起手动实现一个微服务注册中心。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

墨家巨子@俏如来

你的鼓励是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值