你连多线程都没弄明白,还想拿高薪?

很多同学面对多线程的问题都很头大,因为自己做项目很难用到,但是但凡高薪的职位面试都会问到。。毕竟现在大厂里用的都是多线程高并发,所以这块内容不吃透肯定是不行的。

今天这篇文章,作为多线程的基础篇,先来谈谈以下问题:

  1. 为什么要用多线程?
  2. 程序 vs 进程 vs 线程
  3. 创建线程的 4 种方式?

为什么要用多线程

任何一项技术的出现都是为了解决现有问题。

之前的互联网大多是单机服务,体量小;而现在的更多是集群服务,同一时刻有多个用户同时访问服务器,那么会有很多线程并发访问。

比如在电商系统里,同一时刻比如整点抢购时,大量用户同时访问服务器,所以现在公司里开发的基本都是多线程的。

使用多线程确实提高了运行的效率,但与此同时,我们也需要特别注意数据的增删改情况,这就是线程安全问题,比如之前说过的 HashMap vs HashTableVector vs ArrayList

要保证线程安全也有很多方式,比如说加锁,但又可能会出现其他问题比如死锁,所以多线程相关问题会比较麻烦。

因此,我们需要理解多线程的原理和它可能会产生的问题以及如何解决问题,才能拿下高薪职位。

进程 vs 线程

程序 program

说到进程,就不得不先说说程序。

程序,说白了就是代码,或者说是一系列指令的集合。比如「微信.exe」这就是一个程序,这个文件最终是要拿到 CPU 里面去执行的。

进程 process

当程序运行起来,它就是一个进程

所以程序是“死”的,进程是“活”的

比如在任务管理器里的就是一个个进程,就是“动起来”的应用程序。

Q:这些进程是并行执行的吗?

单核 CPU 一个时间片里只能执行一个进程。但是因为它切换速度很快,所以我们感受不到,就造成了一种多进程的假象。(多核 CPU 那真的就是并行执行的了。)

Q:那如果这个进程没执行完呢?

プロセスAがタイムスライスの実行を終了したが、実行が終了していない場合、次の実行を容易にするために、「保存サイト」と呼ばれる、実行したばかりのデータ情報を保存します。

次に、実行のためにリソースが取得されたときに、最初に「シーンを復元」してから、実行を続けます。

これは行き来します。

このような繰り返しの保存と復元はすべて追加のコストであり、プログラムの実行を遅くします。

Q:より効率的な方法はありますか?

2つのスレッドが同じプロセスに属している場合、シーンを保存して復元する必要はありません。

これはNIOモデルのアイデアであり、NIOモデルがBIOモデルよりもはるかに効率的である理由です。これについては後で説明します。

スレッドは、実際に機能するプロセス内の特定の実行パスです。

プロセスでは、タイムスライスは1つのスレッドでしか実行できませんが、タイムスライスの切り替え速度は非常に速いため、同時に発生するようです。

プロセスには少なくとも1つのスレッドがあります。たとえば、メインスレッドは、通常はmain()関数を記述するユーザースレッドでありgcJVMが生成されるスレッドがあり、ガベージコレクションを担当し、スレッドのガーディアンです

各スレッドには独自のスタック stackがあり、スレッド内のメソッドの相互呼び出し関係が記録されます。

ただし、プロセス内のすべてのスレッドがヒープを共有します heap

そうすると、異なるプロセスは互いにメモリにアクセスできなくなり、各プロセスには独自のメモリ空間 memeory space仮想メモリ)があります virtual memory

この仮想メモリを通じて、各プロセスはメモリ空間全体を所有していると感じます。

仮想メモリのメカニズムは、物理メモリの制限を保護することです。

Q:物理メモリが使い果たされた場合はどうなりますか?

Windowsシステムのページングファイルなどのハードディスクを使用すると、仮想メモリの一部がハードディスクに格納されます。

これに対応して、ハードディスクの読み取りと書き込みの速度はメモリよりもはるかに遅いため、プログラムはこの時点で非常に遅く実行されます。これは、私たちが感じるほど遅いメモリです。これが、プログラムを開いたときにコンピュータが動かなくなる理由です。

Q:この仮想メモリはどのくらいの大きさですか?

对于 64 位操作系统来说,每个程序可以用 64 个二进制位,也就是 2^64 这么大的空间!

如果还不清楚二进制相关内容的,公众号内回复「二进制」获取相应的文章哦~

总结

总结一下,在一个时间片里,一个 CPU 只能执行一个进程。

CPU 给某个进程分配资源后,这个进程开始运行;进程里的线程去抢占资源,一个时间片就只有一个线程能执行,谁先抢到就是谁的。

多进程 vs 多线程

每个进程是独立的,进程 A 出问题不会影响到进程 B;

虽然线程也是独立运行的,但是一个进程里的线程是共用同一个堆,如果某个线程 out of memory,那么这个进程里所有的线程都完了。

所以多进程能够提高系统的容错性 fault tolerance ,而多线程最大的好处就是线程间的通信非常方便。

进程之间的通信需要借助额外的机制,比如进程间通讯 interprocess communication - IPC,或者网络传递等等。

如何创建线程

上面说了一堆概念,接下来我们看具体实现。

Java 中是通过 java.lang.Thread 这个类来实现多线程的功能的,那我们先来看看这个类。

从文档中我们可以看到,Thread 类是直接继承 Object 的,同时它也是实现了 Runnable 接口。

官方文档里也写明了 2 种创建线程的方式:

一种方式是从 Thread 类继承,并重写 run()run() 方法里写的是这个线程要执行的代码;

启动时通过 new 这个 class 的一个实例,调用 start() 方法启动线程。

二是实现 Runnable 接口,并实现 run()run() 方法里同样也写的是这个线程要执行的代码;

稍有不同的是启动线程,需要 new 一个线程,并把刚刚创建的这个实现了 Runnable 接口的类的实例传进去,再调用 start(),这其实是代理模式

如果面试官问你,还有没有其他的,那还可以说:

  1. 实现 Callable 接口

  2. 通过线程池来启动一个线程。

但其实,用线程池来启动线程时也是用的前两种方式之一创建的。

这两种方式在这里就不细说啦,我们具体来看前两种方式。

继承 Thread 类

public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("小齐666:" + i);
        }
    }
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        for (int i = 0; i < 100; i++) {
            System.out.println("主线程" + i + ":齐姐666");
        }
    }
}

在这里,

  • main 函数是主线程,是程序的入口,执行整个程序;

  • 程序开始执行后先启动了一个新的线程 myThread,在这个线程里输出“小齐”;

  • 主线程并行执行,并输出“主线程i:齐姐”。

来看下结果,就是两个线程交替夸我嘛~

Q:为啥和我运行的结果不一样?

多线程中,每次运行的结果可能都会不一样,因为我们无法人为控制哪条线程在什么时刻先抢到资源。

当然了,我们可以给线程加上优先级 priority,但高优先级也无法保证这条线程一定能先被执行,只能说有更大的概率抢到资源先执行。

实现 Runnable 接口

这种方式用的更多。

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for(int i = 0; i < 100; i++) {
            System.out.println("小齐666:" + i);
        }
    }

    public static void main(String[] args) {
        new Thread(new MyRunnable()).start();

        for(int i = 0; i < 100; i++) {
            System.out.println("主线程" + i + ":齐姐666");
        }
    }
}

结果也差不多:

像前文所说,这里线程启动的方式和刚才的稍有不同,因为新建的的这个类只是实现了 Runnable 接口,所以还需要一个线程来“代理”执行它,所以需要把我们新建的这个类的实例传入到一个线程里,这里其实是代理模式。这个设计模式之后再细讲。

小结

那这两种方式哪种好呢?

使用 Runnable 接口更好,主要原因是 Java 单继承。

另外需要注意的是,在启动线程的的时候用的是 start(),而不是 run()

run()通常のメソッド呼び出しであるこのメソッドを呼び出すだけでstart()、スレッドが開始され、JVMによってこのスレッドを呼び出しますrun()

おすすめ

転載: blog.csdn.net/weixin_45784983/article/details/108568062