美文网首页
Java 多线程——原理概述

Java 多线程——原理概述

作者: BitterOutsider | 来源:发表于2020-06-23 10:36 被阅读0次

    为什么需要多线程?

    • 任何事物对于现代CPU来说都实在是太慢了,3GHz的CPU一个时钟周期为3纳秒,而内存寻址过程约为10微秒,而IO操作(包括网络IO,文件IO)对于CPU来说更是慢得一塌糊涂。
    • 我们无法一直缩短CPU的时钟周期,因为这会增加CPU的发热量。所以聪明的人类,找到了让CPU更快的方法:增加核心数。于是现代CPU都是多核的。
    • Java的执⾏模型是同步/阻塞(block)的。一条指令会等待上一条指令执行完成(无论多么耗时)才会执行。如果一条指令“卡”住了,那么下面的指令都将无法完成。
    • 默认情况下只有⼀个线程也符合人类的思想,处理问题⾮常⾃然,但是具有严重的性能问题。
    • 多线程是这样一种机制,它允许在程序中并发执行多个指令流,每个指令流都称为一个线程,彼此间互相独立。线程又称为轻量级进程,它和进程一样拥有独立的执行控制,由操作系统负责调度。
    • 一句话总结多线程:相同的一份代码,你雇佣了很多工人去执行他。

    什么是Thread

    • 在 Java 中,Thread类的每一个实例代表一个JVM中的线程,需要注意的是Runnable/Callable都不是线程(它们只是一小段代码,描述线程完成的任务,它可以被线程执行)。相较于Callable来说,Runnable有局限性,表现在:Runnable不能返回值,Runnable不能抛出checked exception.
    • Thread.start()之后,JVM中就会增加一个执行流(工人)和一套方法栈。
    • 不同执行流的同步执行是一切线程问题的来源。

    Java线程模型 & 开启一个新线程

    • Java中使用Thread开启一个线程,使用start方法并发执行。(run方法在我看来就是顺序执行)
    for (int i = 0; i < 3; i++) {
        new Thread(new Runnable() {
            @Override
                public void run() {
                    // do some thing
                }
         }).start();
    }
    
    • 每多开⼀个线程,就多⼀个执⾏流(工人)。每一线程栈的栈底都是Thread.run方法。

    • ⽅法栈(局部变量)是线程私有的,静态变量/类变量是被所有线程共享的。如下图所示:


    • 当一个线程去修改堆里的变量时,那么它是否立刻生效呢?答案:不是的。在现代的多核CPU上,CPU自己有个缓存,调度器可以轻松地将多个线程分配给多个CPU去执行,这样效率会特别高。但是这样会有一个问题,CPU和主内存的交换是很慢的(一次内存寻址大概需要1微秒实现,差不多相当于1000-3000个时钟周期),所以 Java 线程模型允许每一个线程自己有一份私有的共享变量的副本,CPU定期会把副本同步回主内存。

      但是这样会带来一些问题。举一个例子,如下面代码所示:开启一个线程去做一些初始化操作,并将initSignal设置为true。但是main中的initSignal和thread中的initSignal是同一个吗?理论上存在一种情况,就是thread中的initSignal是它自己缓存中的,还没有同步到主内存中去,这样有可能就会出现错误。还有一个问题,编译器和处理器都可能对指令进⾏重排,导致问题。简单来说init()initSignal=true这两行代码是没有相互依赖的,可能会颠倒执行。这就出现大问题了,明明还没有初始化,却将会去做接下来的任务。这两个问题都可以用volatile关键词去解决。
    public class Main {
        static boolean initSignal = false;
    
        private static void init() {
            // 初始化操作
        }
    
        public static void main(String[] args) throws InterruptedException {
            new Thread(()->{
               init();
               initSignal=true;
            }).start();
    
            while (true){
                if(initSignal){
                    //如果初始化完成,则做接下来的任务
                }else {
                    Thread.sleep(500);
                }
            }
        }
    }
    

    如何解决线程自身存在的副本导致可能发生的错误呢?java引入了一个关键词volatile,当你把一个变量声明成一个volatile时,所有线程对共享变量的修改会立刻写回主内存,从变量读取会直接读取主内存 。换句话说,读写都是最新的。这叫做可见性,值得注意的时,可见性并非原子性。volatile也会禁⽌指令重排。有同步操作(synchronized/Lock/AtomicInteger,详情见Java 多线程——线程安全、线程池初步)的时候⽆需volatile,同步操作自带原子性和可见性。

    volatile static boolean init = false;
    

    多线程问题的来源

    • 当多线程试图去修改一个静态变量时,噩梦也就开始了,很容易造成数据错误。如下代码所示,我们期望它打印的最后一个值为30000,而最终结果是29997。可怕的是,每次运行的结果都是无法确定的。
    class Main {
        private static int num = 0;
        public static void main(String[] args) {
            new Thread(Main::addOne).start();
            new Thread(Main::addOne).start();
            new Thread(Main::addOne).start();
        }
        public static void addOne(){
            for (int i = 0; i < 10000; i++) {
                num++;
                System.out.println(num);
            }
        }
    }
    
    • CPU会给线程分配时间,如果时间到了,不会等待某一线程的指令执行完,而是直接终止,等待下一次轮到再执行。


    • 上文中,num++不是一个原子操作。这可以看成三个步骤。1.得到num的值。2.num自增。3.将自增结果写回num。如果在2、3两个步骤中间,该线程的时间正好到了,没有成功将结果写回,下一个线程开始工作。此时num的值并没有成功被更新。
    • 多线程问题的本质来源是,同⼀份代码(非原子操作),被不同的工人⼈在疯狂地乱序执⾏。

    线程的生命周期

    • 建议直接看Java自带的文档Thread.state,最权威。
         * <ul>
         * <li>{@link #NEW}<br>
         *     一个线程还没有开始执行,刚new出来。
         *     A thread that has not yet started is in this state.
         *     </li>
         * <li>{@link #RUNNABLE}<br>
         *     一个线程正在Java虚拟机中被执行
         *     A thread executing in the Java virtual machine is in this state.
         *     </li>
         * <li>{@link #BLOCKED}<br>
         *     一个线程正在等待一个monitor lock。
         *     我们使用synchronized块锁住一个对象,有多个线程没有拿到锁,进入阻塞状态。
         *     A thread that is blocked waiting for a monitor lock
         *     is in this state.
         *     </li>
         * <li>{@link #WAITING}<br>
         *     处于wait状态的线程(调用了wait方法)
         *     A thread that is waiting indefinitely for another thread to
         *     perform a particular action is in this state.
         *     </li>
         * <li>{@link #TIMED_WAITING}<br>
         *     处于wait状态的线程(调用了wait方法),有时间限制。
         *     A thread that is waiting for another thread to perform an action
         *     for up to a specified waiting time is in this state.
         *     </li>
         * <li>{@link #TERMINATED}<br>
         *     一个线程退出了的状态。
         *     A thread that has exited is in this state.
         *     </li>
         * </ul>
    

    适合使用多线程的场合

    • 对于IO密集型应⽤极其有⽤:
      • ⽹络IO(通常包括数据库)。
      • ⽂件IO。
    • 对于CPU密集型(疯狂地进行运算)应⽤有折扣。
    • 性能提升的上限在哪⾥?
      • 单核CPU上限为 100%
      • 多核CPU上限为 N * 100%

    本文完。

    相关文章

      网友评论

          本文标题:Java 多线程——原理概述

          本文链接:https://www.haomeiwen.com/subject/korrxktx.html