LCODER之多线程系列: Java多线程,看这一篇就够了

LCODER之多线程系列: Java多线程,看这一篇就够了

Android小彩虹2021-08-17 11:23:40270A+A-

一、概念

进程

进程是程序运行资源分配的最小单位。

进程是程序在计算机上的一次执行活动。 当你运行一个程序,你就启动了一 个进程。 显然,程序是死的、 静态的,进程是活的、 动态的。 进程可以分为系统进 程和用户进程。 凡是用于完成操作系统的各种功能的进程就是系统进程,它们就 是处于运行状态下的操作系统本身,用户进程就是所有由你启动的进程。

进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、 内存空间、 磁盘 IO 等,同一进程中的多条线程共享该进程中的全部系统资源,而进程和进程 之间是相互独立的。进程是具有一定独立功能的程序关于某个数据集合上的一次 运行活动,进程是系统进行资源分配和调度的一个独立单位。

线程

线程是 CPU 调度的最小单位,必须依赖于进程而存在。

线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的、 能独立运行的基本单位。 线程自己基本上不拥有系统资源,只拥有一点在运行中 必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其 他的线程共享进程所拥有的全部资源

CPU时间片轮转机制(又叫RR调度、属于并发)

CPU核心数和线程数的关系

多核心: 也指单芯片多处理器( Chip Multiprocessors,简称 CMP),CMP 是由美国 斯坦福大学提出的,其思想是将大规模并行处理器中的 SMP(对称多处理器)集成 到同一芯片内,各个处理器并行执行不同的进程。

多线程:Simultaneous Multithreading.简称 SMT.让同一个处理器上的多个线 程同步执行并共享处理器的执行资源。

核心数、 线程数:目前主流 CPU 都是多核的。 增加核心数目就是为了增加线 程数,因为操作系统是通过线程来执行任务的,一般情况下它们是 1:1 对应关系,也 就是说四核 CPU 一般拥有四个线程。但 Intel 引入超线程技术后,使核心数与线程 数形成 1:2 的关系

CPU时间片轮转机制

时间片轮转调度是一种最古老、 最简单、 最公平且使用最广的算法,又称 RR 调度。 每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。 百度百科对 CPU 时间片轮转机制原理解释如下:

如果在时间片结束时进程还在运行,则 CPU 将被剥夺并分配给另一个进程。 如果进程在时间片结束前阻塞或结来,则 CPU 当即进行切换。 调度程序所要做的 就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾。

时间片轮转调度中唯一有趣的一点是时间片的长度。从一个进程切换到另一 个进程是需要定时间的,包括保存和装入寄存器值及内存映像,更新各种表格和队 列等。 假如进程切( processwitch),有时称为上下文切换( context switch),需要 5ms, 再假设时间片设为 20ms,则在做完 20ms 有用的工作之后,CPU 将花费 5ms 来进行 进程切换。 CPU 时间的 20%被浪费在了管理开销上了。

为了提高 CPU 效率,我们可以将时间片设为 5000ms。 这时浪费的时间只有 0.1%。 但考虑到在一个分时系统中,如果有 10 个交互用户几乎同时按下回车键, 将发生什么情况?假设所有其他进程都用足它们的时间片的话,最后一个不幸的 进程不得不等待 5s 才获得运行机会。 多数用户无法忍受一条简短命令要 5 才能 做出响应,同样的问题在一台支持多道程序的个人计算机上也会发

结论可以归结如下:时间片设得太短会导致过多的进程切换,降低了 CPU 效率: 而设得太长又可能引起对短的交互请求的响应变差。 将时间片设为 100ms 通常 是一个比较合理的折衷。

二、使用线程

1、开启线程

两种方式:Thread、Runnable

方式一:Thread
 class UserThread extends Thread{
        @Override
        public void run() {
            super.run();
            ...
        }
    }
    
 UserThread thread = new UserThread();
 thread.start();
 
 方式二: Runnable
  new Thread(new Runnable() {
       @Override
       public void run() {
            ...

          }
         }).start();

深入理解run()和start()

Thread类是 Java里对线程概念的抽象,可以这样理解:我们通过 new Thread() 其实只是 new 出一个 Thread 的实例, 还没有操作系统中真正的线程挂起钩来。

只有执行了 start()方法后, 才实现了真正意义上的启动线程。 start()方法让一个线程进入就绪队列等待分配 cpu,分到cpu 后才调用实现的run()方法, start()方法不能重复调用, 如果重复调用会抛出异常。而 run 方法是业务逻辑实现的地方, 本质上和任意一个类的任意一个成员方法并没有任何区别,可以重复执行,也可以被单独调用。

2、结束线程

2.1 stop()suspend()resume()

暂停、 恢复和停止操作对应在线程 Thread 的 API 就是 suspend()、 resume() 和 stop()。 但这些方法已被废弃,原因是:suspend()在调用时,线程不会释放已占用的资源,如锁,而是占着资源进入睡眠状态,这样容易发生死锁的问题。stop()方 法在终结一个线程时不会保证线程的资源正常释放, 通常是没有给予线程完成资 源释放工作的机会, 因此会导致程序可能工作在不确定状态下。

2.2 中断的一系列方法interrupt()、isInterrupted()、Thread.interrupted()

安全的中断是指,通过调用线程A的interrupt()对其进行中断操作。当调用了A的interrupt()时,就好像跟A打了个招呼:“A,你要中断了”,这个时候把A线程的中断标志位置为true,此时使用该线程的isInterrupted()可以获取到这个中断标志位的值,通过这个值来进行线程的中断操作。这样就可以给线程一个释放资源等的机会,当isInterrupted()为true时,去做释放资源,终止线程等操作。

也可以使用Thread.interrupted()来判断标志位,这个方法是静态的,它和isInterrupted()不同的是,它在调用完成之后,会将标志位置为false。

当线程处于阻塞状态时,比如线程调用了sleep、join、wait等方法时,如果线程在检查中断标志位时发现中断标志为true,会在这些阻塞方法调用处抛出InterruptedException异常,并在抛出异常后立即将线程的中断标志位清除,重新置为false。

处于死锁状态下的线程无法被中断。

3、线程常用的方法和线程的状态

3.1 yield()

使当前线程让出 CPU 占有权,但让出的时间是不可设定的,也不会释放锁资源。在调用yield()让出CPU的占有权之后,CPU会在所有的线程中重新挑选线程执行,所以执行yield()的线程有可能在进入到就绪状态后会被操作系统再次选中马上又被执行。

3.2 join()

当A线程正在执行时,调用B线程的join(),A线程就会被挂起,让出CPU,CPU去执行B线程。等到B线程执行完毕之后,A线程再继续执行。

3.3 线程的状态

3.3.1 初始状态

新创建了一个线程,但还没有调用start()

3.3.2 运行状态

Java线程中讲就绪状态和运行中状态的线程,统称为“运行”。 线程创建好之后,其他线程(如main线程)调用了该对象的start()方法。该状态的线程位于可运行的线程池中,等待被线程调度选中,获取CPU的使用权,此时线程处于就绪状态,就绪状态的线程,获取CPU时间片后,变成运行中的状态。

3.3.3 阻塞状态

线程阻塞于锁。

3.3.4 等待状态

进入该状态的线程需要等待其他线程做出一些特定的动作(通知或中断)。

3.3.5 超时等待

该状态,可以在指定的时间后自行返回。

3.3.6 终止状态

线程执行完毕。

3.4 线程的优先级

在Java线程中,通过一个整型成员变量 priority 来控制优先级,优先级的范围从1~10,在线程构建的时候可以通过 setPriority(int)方法来修改优先级, 默认优先级是5,优先级高的线程分配时间片的数量要多于优先级低的线程。 设置线程优先级时,针对频繁阻塞(休眠或者 I/O 操作)的线程需要设置较高优先级,而偏重计算( 需要较多 CPU 时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。在不同的 JVM 以及操作系统上,线程规划会 存在差异, 有些操作系统甚至会忽略对线程优先级的设定。

3.5 守护线程

Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个 Java 虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置 为Daemon线程。我们一般用不上,比如垃圾回收线程就是Daemon线程。 Daemon线程被用作完成支持性工作,但是在Java虚拟机退出时Daemon线程中的finally块并不一定会执行。在构建 Daemon线程时,不能依靠 finally 块中的内容来确保执行关闭或清理资源的逻辑。

4、线程的生命周期

线程的生命周期 如上图所示: 新建一个线程并且调用其Start()之后,线程处于就绪状态,此时线程在等待CPU的时间片轮到它,时间片轮到了线程或者是执行了join()之后,线程开始运行,当CPU分配给该线程的时间片到期或者是调用了yield(),线程回到就绪状态,继续等待。当run()结束或者是调用了stop(),线程终止。在线程运行时,如果调用了sleep()或者wait(),线程就会进入阻塞状态,当sleep()时间到或者是调用了notify()、notifyAll(),线程再次回到就绪状态,等待运行。

三、线程安全

1、什么是线程间的共享?

线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码一步一步地执行,直到终止。但是,每个运行中的线程,如果仅仅是孤立地运行,那么没有一点儿价值,或者说价值很少,如果多个线程能够相互配合完成工作,包括数据之间的共享,协同处理事情。这将会带来巨大的价值。

2、synchronized关键字

2.1 synchronized关键字的作用

Java支持多个线程同时访问一个对象或者对象的成员变量,关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。

2.2

// TODO 还没写完

3、volatile关键字

volatile保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

4、ThreadLocal

4.1 什么是ThreadLocal?

从名字我们就可以看到ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

4.2 ThreadLocal原理

关于ThreadLocal的原理,在我的博客LCODER之多线程系列:从源码的角度分析Android中的线程通信中有很详细的讲解,在这里把这部分内容搬过来。

ThreadLocal最重要的作用就是做到线程之间的数据隔离,这是它存在的意义。那么ThreadLocal是如何做到线程间的数据隔离的呢?先上结论:

  1. 往ThreadLocal中放数据:每次放数据的时候,首先第一件事获取当前线程,然后获取当前线程中的ThreadLocalMap,往ThreadLocalMap中放数据key = ThreadLocal 、vaule= 要放入的值
  2. 从ThreadLocal中取数据:也是首先获取线程,再获取线程中的ThreadLocalMap,再通过ThreadLocal这个key值从Map中拿东西。
  3. ThreadLocalMap是Thread类中的一个全局变量,在每一个Thread中都有一份。所以,即使ThreadLocal这个key一样,拿出来的数据分别是每个线程的数据,不会冲突,就做到了数据的隔离。

这个结论,是我分析ThreadLocal的源码,从源码中得出来的。下面就来分析一下ThreadLocal的源码。首先我们追踪源码,看一下ThreadLocal是怎么放置和取出数据的。

1.1 从ThreadLocal中取数据:get()
     public T get() {
        //拿到当前线程 
        Thread t = Thread.currentThread();
        //拿到当前线程中的 ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //通过下面两步,追踪可以看到,map此时为空。
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

点击进入源码发现这个方法首先是获取到当前的线程,然后拿到当前线程中的ThreadLocalMap。我们追踪其中的getMap(t):

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

返回的是threadLocals,这是Thread类中的一个全局变量。追踪进去可以看到: Thread.java

 ThreadLocal.ThreadLocalMap threadLocals = null;
  threadLocals = null;

在Thread.java中,对这个全局变量的定义均为null。因此在get()中,map为空,会走到setInitialValue()中。我们继续追踪到setInitialValue()中。看看setInitialValue()在源码中如何实现的:

    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

首先调用初始化方法initialValue(),得到valuse,如果用户重写了initialValue(),那么获得到就是用户定义的返回值。再往下走,得到的Map仍然是null,因此会走到createMap(t, value)中。继续追踪下去,看看createMap()是怎么实现的。

  void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

在这个方法中,定义了threadLocals这个变量。创建了ThreadLocalMap。

1.2 往ThreadLocal中添加数据。set()

   public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

仍然是这样:首先获取当前线程,再通过当前线程获取到 ThreadLocalMap,在之前通过对getMap(t)的分析可以知道,此时的map = null,因此set(t)最终也会走 createMap(t, value)。之前对 createMap(t, value)的源码进行分析过, createMap(t, value)会创建一个ThreadLocalMap,并将value放入ThreadLocalMap中。

看到这里,我们发现,ThreadLocal取数据和拿数据,都是通过一个叫做ThreadLocalMap的类,那么这个类到底是个啥呢? 阅读源码发现,ThreadLocalMap是ThreadLocal中的一个静态内部类:

 static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;
      .......

这个静态内部类中,还有一个静态内部类,这个类叫做Entry,这个类比较简单,它其中维护了两个变量,分别是ThreadLocal类型的Key和Object类型的Value。在每个ThreadLocalMap中都有一个Entry类型的数组Entry[],用来存储数据。

源码阅读到这里,再结合到上面分析的ThreadLocal源码中set()的实现:

 if (map != null)
 map.set(this, value);

这里的Map指的是ThreadLocalMap,追踪到ThreadLocalMap中的set()

 /**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be set
         */
        private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

通过代码可以看到,ThreadLocalMap装数据的过程,就是把数据放入Entry中的过程,而Entry就是把当前的ThreadLocal作为Key值,用户要放入的值作为Value,存入Entry中。

那么,ThreadLocal是如何做到数据隔离的呢? 来看下面这个图:线程间的数据隔离

每个线程中有一个自己的ThreadLocalMap,这句话,看源码可以看出来,Thread的源码中有这样一句话:

/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

在Thread中,threadLocals是null的,它是在哪里被赋值的呢? 答案是在ThreadLocal中被赋值的。我们上面分析ThreadLocal的set()和get()的源码可以知道,当我们在ThreadLocal中set()或者get()时,如果初始值为空,或者要get()的值为空,都会判断ThreadLocalMap是否为空,如果ThreadLocalMap为空,会走到createMap中,而createMap的源码:

/**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread 在前面的代码中: Thread t = Thread.currentThread(); 可以得到这一结论
     * @param firstValue value for the initial entry of the map
     */

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

一目了然,每个线程中都会new 一个新的ThreadLocalMap,每一个ThreadLocalMap的key值都是ThreadLocal,对于同一个ThreadLocal变量来说,这个key是一样的,但是并不会冲突,因为存储数据的是ThreadLocalMap中的Entry数组,即使Key相同,每个线程中的ThreadLocalMap不同,是两个完全不相干的ThreadLocalMap,其中的Entry[]更不同。这样,ThreadLocal就做到了线程间的数据隔离。

简单来说,为什么ThreadLocal可以做到线程间的数据隔离,因为每个线程中都有一个自己的ThreadLocalMap来存储数据。

4.3 ThreadLocal引发内存泄漏分析

4.3.1 为什么会导致内存泄露呢?

查看ThreadLocal的源码,我们会发现,ThreadLocal本身并不存储值,真正存储值的地方是在ThreadLocalMap中,ThreadLocal只是作为Map的key值,让线程从ThreadLocalMap中取值。在源码中我们可以知道的是:ThreadLocalMap是使用ThreadLocal的弱引用作为Key的。弱引用的对象,在GC时会被回收。(关于弱引用的内容,会在下一系列的LCODER之JVM系列中详细讲解)。
作为Key的ThreadLocal,在发生GC时,即被回收。但是,被这些ThreadLocal作为Key的Entry却被强引用着。Entry是ThreadLocalMap中的静态内部类,被ThreadLocalMap引用着,而ThreadLocalMap是Thread中的一个全局变量,被Thread引用着,Thread -> ThreadLocalMap -> Entry ,因此,只要Thread不结束,Entry就一直被强引用着,而Entry的key:ThreadLocal却被回收了,Entry所在的这块内存,永远都不会被访问到了,就这样,造成了内存泄露。
因此,想要不造成内存泄露,在使用完ThreadLocalMap之后,调用它的remove()将数据回收掉。在remove()中,显性的调用了expungeStaleEntry(),用来清除key值为null的Entry。

  /**
         * Expunge a stale entry by rehashing any possibly colliding entries
         * lying between staleSlot and the next null slot.  This also expunges
         * any other stale entries encountered before the trailing null.  See
         * Knuth, Section 6.4
         *
         * @param staleSlot index of slot known to have null key
         * @return the index of the next null slot after staleSlot
         * (all between staleSlot and this slot will have been checked
         * for expunging).
         */
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

那么既然使用弱引用的ThreadLocal会导致内存泄露为什么还要用弱引用呢? 答案是,虽然使用弱引用会导致内存泄露,但在调用remove()、set()、get()时,是可以把key值为null的Entry回收掉的。但假如使用强引用,即使引用ThreadLocal的对象被回收了,但ThreadLocal被ThreadLocalMap强引用着,也会导致内存泄露,并且只有等到线程结束后才会释放,因此,虽然弱引用会导致内存泄露,但它比强引用还是要安全一些。

Thread与Synchonized的比较

四、 线程的协作

线程之间相互配合,完成某项工作,比如:一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,而最终执行又是另一个线程。

4.1 等待和通知机制

是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的 wait()和 notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

notify()

通知一个在对象上等待的线程,使其从wait方法返回,而返回的前提是该线程获取到了对象的锁,没有获得锁的线程重新进入WAITING状态。

notifyAll()

通知所有等待在该对象上的线程.

wait()

调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回.需要注意,调用wait()方法后,会释放对象的锁.

操作系统限制每个进程的线程数: Linux 1000个 Windows:2000个

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1
本网站由 提供CDN/云存储服务

联系我们