JAVA线程

49 阅读13分钟

JAVA线程:

前言:

简单介绍一下,我所有的文章都会按照:学习 -> 回顾 -> 提问 -> 学习 -> 总结的章节结构来进行;

1. 回顾:

说到多线程,先谈谈我初步能想到的几个概念 这里先写一个小demo,这是最简单的多线程 ->

new Thread(() ->
                System.out.println("nanbeom-thread-1")
        ).start();

通过这个demo可知,java多线程主要为 执行者 & 任务 这两部分;

让我们先来谈一谈任务(Thread):

上面的demo中,任务是直接丢给Thread的,那么我们看看他的构造器,经过查询有以下几种比较重要:

  1. Thread(Runnable target, String name)
  2. Thread(Runnable target, AccessControlContext acc)
  3. public Thread(ThreadGroup group, Runnable target

题外话:这里发现,虽然我们知道Runnable Callable,但是在最早的线程管理类Thread类中使用的是Runnable

所以我们发现可能的2个任务对象:Runnable 和 ThreadGroup:

其中:ThreadGroup 看名字就显然不是啊,更像是一个线程的管理类。我点进去看了下这个组的概念,差不多就是一个管理线程的类,这里感觉并不重要,这里用的也很少,不过多介绍。

我想了下我们看看run方法吧,因为不管怎么样,线程都是执行run方法的内容,直接看那个就行了:

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

自此我们可以得出,target(Runnable)就是我们的任务对象,而任务内容就是他的run方法;

这里我贴一下target的run方法接口:

public abstract void run();

再来回顾一下执行者:

在这里其实执行者很简单就是我们的Thread,我们的java对系统线程的包装类。start方法就是开一个新线程,那么我们看下源码:

  /**
   * 使得该线程开始执行; Java虚拟机调用该线程的run方法。
   * 结果是两个线程同时运行:当前线程(从调用start方法返回)和另一个线程(执行其run方法)。
   * 多次启动一个线程永远是不合法的。特别是,线程一旦完成执行就不能重新启动。
   * 投掷:
   * IllegalThreadStateException – 如果线程已启动。
   * 也可以看看:
   * run() , stop()
   */
    public synchronized void start() {
        /**
         * 不允许一个线程2次启动
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
​
        /*
          * 注意这里的线程组概念,这里添加的进去的是main线程的线程组
          * 具体逻辑在init方法中(可以稍微了解一下,这里我觉得比较重要的知识是安全的通信,其他的更多偏向逻辑层          * 面的线程管理)
        */
        group.add(this);
​
        boolean started = false;
        try {
          /* 真正的执行,调用的是本地方法start0,这里不对C++代码做更多的学习。*/
            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 */
            }
        }
    }
​
    private native void start0();
这里还有一些比较重要的方法和参数这里做一些介绍:
方法:
  1. registerNatives()-> 静态块中的必须执行的注册方法。可以理解为本地方法注册,stop()这些本地方法的前期准备,了解即可。
  2. currentThread()->返回当前正在占用cpu的线程对象
  3. yield()-> 让出cpu的执行权,注意看一看他的介绍:使用这种方法很少是合适的。它对于调试或测试目的可能很有用,它可能有助于重现由于竞争条件而导致的错误。在设计并发控制结构(例如java.util.concurrent.locks包中的结构)时,它也可能很有用。)这里其实还是感觉有东西的,但是应该是属于系统层面的cpu分片时间这一类的,但是涉及到资源释放,怎么释放的呢,和sleep一样吗?
  4. sleep()-> 使当前正在执行的线程休眠(暂时停止执行)指定的毫秒数,具体取决于系统计时器和调度程序的精度和准确性。该线程不会失去任何监视器的所有权。监视器是什么?
  5. interrupt()-> 通过interrupt0 对该线程设置中断标志。(我认为只需要记住这个就好了,关于一些例外的情况遇到了再看看就行)
  6. join()-> 等待,最多等待millis毫秒该线程终止。超时为0意味着永远等待。底层调用的是Object的wait()方法,这里又一次提到了类的监视器。
  7. checkAcess() ->SecurityException 记住这个异常 && AccessControlContext这个成员 。操作权限。这个也在构造器中。
参数:
  1. threadLocals()线程私有的一个map容器,这里要记得clear,不然会内存泄漏的哦(应用的话,看情况啦,挺好用的)
  2. daemon()守护线程,简而言之:最后死的那个线程
  3. target()任务
  4. contextClassLoader:contextClassLoader` 有助于在多线程环境中更灵活地管理类加载器,解决类加载的委托问题。这个双亲委派,后面再讨论吧。例如你在ThreadA中加载了一个类,但是在ThreadB中要使用这个时候这个加载器就会派上用场。

2. 提问:

ThreadGroup有什么用?组的概念有什么意义?
Runnable和Callable有什么区别?为什么Runnable的返回对象是void?这样够用吗?
对象监视器是什么?
yield?sleep?这两者有什么区别?
锁是什么东西?

3. 学习:

OK!开始我们对ThreadGroup的学习:

class类介绍:线程组代表一组线程。另外,一个线程组还可以包含其他线程组。线程组形成一棵树,其中除了初始线程组之外的每个线程组都有一个父线程组。 允许线程访问有关其自己的线程组的信息,但不允许访问有关其线程组的父线程组或任何其他线程组的信息

那么可知他的设计的初衷:

  1. 线程的集合
  2. 允许线程访问有关其自己的线程组的信息 -> 线程的安全通信

看一下它的一些成员变量

image.png

vmAllowSuspension:

介绍:Used by VM to control lowmem implicit suspension.

VM 使用它来控制 lowmem 隐式暂停,这里的VM应该是虚拟机,lowmen我理解为下级线程或者下级线程组;

查找该变量,我发现一个方法:

@Deprecated
/*
* 被弃用的代码,因为方法suspend(暂停线程)
*/
public boolean allowThreadSuspension(boolean b) {
    this.vmAllowSuspension = b;
    if (!b) {
        VM.unsuspendSomeThreads();
    }
    return true;
}

既然该方法因为调用方法suspend的弃用被弃用了,那么我们看看suspend方法:

    @Deprecated
    @SuppressWarnings("deprecation")
    public final void suspend() {
        if (stopOrSuspend(true))
            Thread.currentThread().suspend();
    }
/**
java.lang.ThreadGroup 
@Deprecated 
public final void suspend()
挂起该线程组中的所有线程。
首先,不带参数调用该线程组的checkAccess方法;这可能会导致安全异常。
然后,此方法对该线程组及其所有子组中的所有线程调用suspend方法。
已弃用
这种方法本质上很容易出现死锁。有关详细信息,请参阅Thread.suspend 。
投掷:
SecurityException – 如果不允许当前线程访问此线程组或线程组中的任何线程。
自从:
JDK1.0
也可以看看:
Thread.suspend() , SecurityException , checkAccess()
*/

这里说这个方法被弃用啦,因为会造成死锁,以及部分资源的释放啊咋咋咋的有问题。基于这个我们来探究下为什么以及现在怎么办了:

请参阅为什么不推荐使用 Thread.stop、Thread.suspend 和 Thread.resume? (其实废弃的不只是suspen,我发现)

那么来,写个demo:

/**
 * @创建人 Nanbeom
 * @创建时间 2024/1/3
 * @描述
 */
public class Demo {
    static class Person {
        private String name = "name";
        private String age = "age";
​
        public synchronized void personBuilder(String name, String age) throws InterruptedException {
            this.name = name;
            Thread.sleep(500000);
            this.age = age;
        }
​
        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + ''' +
                    ", age='" + age + ''' +
                    '}';
        }
    }
​
    public static void main(String[] args) {
        Person person = new Person();
        Thread thread = new Thread(() -> {
            try {
                person.personBuilder("hhh", "hhh");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        thread.start();
        thread.stop();
        System.out.println(person);
    }
}
/**
stop弃用官方原因:
这种方法本质上是不安全的。使用 Thread.stop 停止线程会导致它解锁所有已锁定的监视器(这是未经检查的ThreadDeath异常向上传播堆栈的自然结果)。如果先前受这些监视器保护的任何对象处于不一致的状态,则损坏的对象将对其他线程可见,从而可能导致任意行为。 stop的许多用法应替换为仅修改某些变量以指示目标线程应停止运行的代码。目标线程应该定期检查该变量,如果该变量指示它将停止运行,则以有序的方式从其 run 方法返回。如果目标线程等待很长时间(例如,在条件变量上),则应使用interrupt方法来中断等待。
*/

运行可发现:

Person{name='hhh', age='age'}
​
Process finished with exit code 0

在停止后会虽然该线程会直接停止但是他的一些行为似乎没有回滚或者顺利结束:Person的age的赋值并没有被顺利的进行下去。(这也就是官方注释里面的不一致的问题)

官方也给出了建议怎么去终止这个线程:stop的许多用法应替换为仅修改某些变量以指示目标线程应停止运行的代码。目标线程应该定期检查该变量,如果该变量指示它将停止运行,则以有序的方式从其 run 方法返回。

直接上代码:

ThreadGroup group = new ThreadGroup("MyThreadGroup");
​
        Thread thread = new Thread(group, () -> {
            while (!stopRequested) {
                // Some task
                System.out.println("Thread is running...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
​
        thread.start();
​
        // 模拟在某个时刻设置停止标志位
        stopRequested = true;

如何安全正确的结束一个线程,通俗来讲,让线程然的死去,从run()方法返回

我这里还会继续模拟死锁的场景,在模拟过程中我有了一个疑问:什么是锁?我在模拟后会进行一个学习以及解答:

这里会使用println来操作,那么先来看看println的代码嘛做点解释:

    public void println(String x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }

发现他是一个同步块,在执行前会获取锁,才能打印。

为什么是同步块原因:

System.out.println 是一个同步方法,其底层使用了 synchronized 关键字。这主要是因为在多线程环境下,多个线程同时向控制台输出内容可能会导致输出混乱。

同步块会引发的问题,在实际开发中,必须杜绝System.out.println这种代码:

上面代码可以看到,System.out.println是全局的输出流,可以点一下到底层他的输出流是BufferedWriter,看到Buffered那么就不多说啦,System.out.println是一个全局的共享资源,可以这么理解,如果是多线程环境下,那么必然会进行锁竞争,会出现性能问题。

死锁的demo:

    public static void main(String[] args) throws InterruptedException {
        ThreadGroup group = new ThreadGroup("MyThreadGroup");
​
        Thread thread = new Thread(group, () -> {
            while (true) {
                System.out.println("Thread is running...");
            }
        });
        thread.start();
        Thread.sleep(10);
        thread.suspend();
        // 模拟在某个时刻设置停止标志位
        System.out.println("Main thread end");
    }

运行结果如下:

image.png 我们可以看见暂停的线程,并没有释放锁,导致了主线程的阻塞,导致了死锁。如果子线程没有恢复的话,那么就会一直占用资源。

至此再来看看剩下的一些成员变量:

maxPriority:最大优先级 --竞争的时候用的
daemon:是否是守护线程
destroyed:是否被销毁
ngroups,nthreads,nUnstartedThreads:未开始的线程(这里可以看看start方法中,稍微看了一下代码,他用于跟踪某些线程的情况,可以不用多考虑)
说一些ThreadGroup的题外话,所有的线程都在组内哦,main线程在System的线程下哦,子线程在main线程下:
    /**
     * Creates an empty Thread group that is not in any Thread group.
     * This method is used to create the system Thread group.
     */
    private ThreadGroup() {     // called from C code
        this.name = "system";
        this.maxPriority = Thread.MAX_PRIORITY;
        this.parent = null;
    }
Thread的init方法:
  

image.png

至此除了一些我认为不太重要的东西,其他都7788啦。那么我们的执行者到此为止;

接下来就是我们的任务Target啦(What will be run. )

这里介绍的比较简单,这块应该是线程池那里会说的多一些;

runnable:

接口介绍:Runnable接口应该由其实例旨在由线程执行的任何类来实现。该类必须定义一个名为run的无参数方法。 该接口旨在为希望在活动时执行代码的对象提供通用协议。例如, Runnable是由Thread类实现的。处于活动状态仅仅意味着线程已经启动并且尚未停止。 此外, Runnable提供了使类处于活动状态而不需要子类化Thread方法。实现Runnable的类可以通过实例化Thread实例并将其自身作为目标传递来运行,而无需子类化Thread 。在大多数情况下,如果您只计划重写run()方法而不重写其他Thread方法,则应使用Runnable接口。这很重要,因为除非程序员打算修改或增强类的基本行为,否则不应对类进行子类化。

通俗意义上来讲:如果你只需要这个线程帮你做事,那么你就实现这个接口就行了,不要去向下方demo一样子类化一个类出来重写run()方法:

public class MyThread extends Thread {
​
    public void run() {
        System.out.println("Nanbeom_test_Thread_Run_Method");
    }
​
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

正确的demo:

// 偷懒
new Thread(() -> {
            System.out.println("hhh");
        }).start();
    }
​
// runnable接口
public class MyRunnableClass implements Runnable {
    @Override
    public void run() {
        System.out.println("MyRunnableClass");
    }
}
​
// 执行
    public static void main(String[] args) {
        new Thread(new MyRunnableClass()).start();
    }
callable:

类介绍:

返回结果并可能引发异常的任务。实现者定义一个没有参数的单一方法,称为call 。 Callable接口与Runnable类似,因为两者都是为实例可能由另一个线程执行的类设计的。然而, Runnable不返回结果,也不能抛出已检查的异常。 Executors类包含从其他常见形式转换为Callable类的实用方法。

这里其实区别依旧很明显啦:然而, Runnable不返回结果,也不能抛出已检查的异常;

这里不做过多的解释,李老爷子在在jdk1.5引入了线程池的概念,callable出现:

Future      Runnable
   \           /
    \         /
   RunnableFuture
          |
          |
      FutureTask
​
FutureTask 通过 RunnableFuture 间接实现了 Runnable 接口,
所以每个 Runnable 通常都先包装成 FutureTask,
然后调用 executor.execute(Runnable command) 将其提交给线程池
  

详情可见:AbstractExecutorService.newTaskFor()

这里我想了想为什么runnable是个void的返回值。那我们程序的入口包括Spring,public static void main(String[] args)都是void的返回值。所以在整体上而言这样的设计是没有问题的,配合juc的部分工具。确实可以满足很多的功能。但是总感觉runnable并不是面向对象的程序设计方案,感觉他是独立于主线程之外的程序入口,和主线程的兼容性较小。callable则是弥补了这一点。

4. 解答问题&&总结:

ThreadGroup有什么用?组的概念有什么意义?

答:ThreadGroup现在看来,是在逻辑上管理线程,让所有的线程处于一个树的结构下,便于管理。在stop扽方法被废弃后,该类对于统一管理线程,更多的留存在逻辑上了,我认为了解即可,不用钻牛角尖。

Runnable和Callable有什么区别?为什么Runnable的返回对象是void?这样够用吗?

答: Runnable不返回结果,也不能抛出已检查的异常,但是Callable返回。Runnable返回void,可以将其看成一个程序的入口,在当时来看够用。

对象监视器是什么?

答:累啦,明天说,这块还蛮多的

yield?sleep?这两者有什么区别?

答:累啦,明天说

总结:累啦,明天说