zoukankan      html  css  js  c++  java
  • [Java复习] 多线程 Multithreading

    Q1多线程基础

    进程和线程?

    进程: 1. 一段程序执行过程,动态的,相对而言程序是静态的。(与类和对象的关系类似)

                2. CPU资源分配最小单元,包括CPU调度和资源管理。

                3. 一个进程可以有多个线程。

    线程: 1. 程序执行流的最小单元。

                2. CPU调度的最小单位,只负责CPU调度,不负责资源管理。

                3. 可以拥有自己的堆栈,程序计数器和局部变量。

    多线程:并不是多个线程一起执行,而是线程之间切换速度非常快,看起像不间断的执行。可以使同一个进程中可以同时并发处理多个任务。

    并发(Concurrency)与并行(Parallel)?

    并发:同一时刻只有一条指令执行,但多个线程的指令被快速切换执行,在宏观上具体有多个进程同时执行的效果。

    并行:同一时刻多条指令在多个处理上同时执行。

    高并发与多线程区别?

    高并发包括硬件、网络、系统架构、开发语言的选取、数据结构的运用、算法优化、数据库优化等等,多线程只是其中的解决方法之一。

    多线程对应的是CPU,高并发对应的是访问请求。

    多线程是处理高并发的一种编程方法,即并发需要用多线程实现。

    创建线程的几种方法?

    1. 继承Thread类:重写run方法,调用对象引用的start方法进入Runnable状态。

    2. 实现Runnable接口:重写run方法,创建Runnable实例,并作为Thread类的target来创建Thread对象。

    3. 使用Callable和Future接口: 使用FutureTask类包装Callable实现类的对象,并且以FutureTask对象作为Thread对象的target来创建线程。

    start()和run()的区别?

    start() 方法则是 Thread类的方法,用来异步启动一个线程,然后主线程立刻返回。

    该启动的线程不会马上运行,会放到等待队列中等待 CPU 调度,只有线程真正被 CPU 调度时才会调用 run() 方法执行。

    所以 start() 方法只是标识线程为就绪状态的一个方法。

    run()可重复调用,单独调用run会在当前线程执行,不会启新线程。

    Object的wait, notify, notifyAll的理解?

    wait:让持有该对象锁的线程等待;

    notify: 唤醒任何一个持有该对象锁的线程;

    notifyAll: 唤醒所有持有该对象锁的线程;

    它们 3 个的关系是,调用对象的 wait 方法使线程暂停运行,通过 notify/ notifyAll 方法唤醒调用 wait 暂时的线程。

    它们并不是 Thread 类中的方法,而是 Object 类中的,为什么呢!? 因为每个对象都有监视锁,线程要操作某个对象当然是要获取某个对象的锁了,而不是线程的锁。

    注意点:

    1、调用对象的 wait, notify, notifyAll 方法需要拥有对象的监视器锁,即它们只能在同步方法(块)中使用;

    2、调用 wait 方法会使用线程暂停并让出 CPU 资源,同时释放持有的对象的锁;

    3、多线程使用 notify 容易发生死锁,一般使用 notifyAll;

    让步yield()的理解?

    让当前线程由运行状态进入就绪状态,让其它线程获取执行权,但并不保证其它同优先级线程一定能获取执行权。

    与wait区别:1. Yield是运行->就绪,Wait是运行->等待阻塞。2. Yield不会释放锁,wait要释放锁。

    休眠sleep()的理解?

    让线程从运行->休眠(阻塞)。

    sleep()与wait()的区别? 

    1. sleep()属于Thread类,wait()属于Object类。
    2. slee()线程不会释放锁,wait()线程会放弃对象锁。

    Join()的理解?

    让”主线程”(当前线程)等待“子线程”(join进来的线程)结束之后才能继续运行。

     join的意思是使得放弃当前线程的执行,并返回对应的线程,例如下面代码的意思就是:

    程序在main线程中调用t1线程的join方法,则main线程放弃cpu控制权,并返回t1线程继续执行直到线程t1执行完毕,

    结果是t1线程执行完后,才到主线程执行,相当于在main线程中同步t1线程,t1执行完了,main线程才有执行的机会。

    终止线程的方式?

    1. 终止处于阻塞状态的线程:

    当线程由于被调用了sleep(), wait(), join()等方法而进入阻塞状态;若此时调用线程的interrupt()将线程的中断标记设为true。

    由于处于阻塞状态,中断标记会被清除(即isInterrupted()会返回false),同时产生一个InterruptedException异常。

    将InterruptedException放在适当的为止就能终止线程。

     说明:在while(true)中不断的执行任务,当线程处于阻塞状态时,调用线程的interrupt()产生InterruptedException中断。中断的捕获在while(true)之外,这样就退出了while(true)循环!

    @Override
    public void run() {
        try {
            while (true) {
                // 执行任务...
            }
        } catch (InterruptedException ie) {  
            // 由于产生InterruptedException异常,退出while(true)循环,线程终止!
        }
    }

    2. 终止处于运行状态的线程:

        1. java.lang.Thread#interrupt
         中断目标线程,给目标线程发一个中断信号,线程被打上中断标记。

         2. java.lang.Thread#isInterrupted()
         判断目标线程是否被中断,不会清除中断标记。

         3.java.lang.Thread#interrupted
         判断目标线程是否被中断,会清除中断标记。

    通过“标记”方式终止处于“运行状态”的线程。其中,包括“中断标记”和“额外添加标记”。
    (01) 通过“中断标记”终止线程。

    @Override
    public void run() {
        while (!isInterrupted()) {
        // 执行任务...
        }
    }

    说明:isInterrupted()是判断线程的中断标记是不是为true。当线程处于运行状态,并且我们需要终止它时;可以调用线程的interrupt()方法,使用线程的中断标记为true,即isInterrupted()会返回true。此时,就会退出while循环。
    注意:interrupt()并不会终止处于“运行状态”的线程!它会将线程的中断标记设为true。

    (02) 通过“额外添加标记”。

    private volatile boolean flag= true;
    protected void stopTask() {
        flag = false;
    }
    
    @Override
    public void run() {
        while (flag) {
            // 执行任务...
        }
    }

    说明:线程中有一个flag标记,它的默认值是true;并且我们提供stopTask()来设置flag标记。当我们需要终止该线程时,调用该线程的stopTask()方法就可以让线程退出while循环。
    注意:将flag定义为volatile类型,是为了保证flag的可见性。即其它线程通过stopTask()修改了flag之后,本线程能看到修改后的flag的值。

    非静态同步方法使用什么锁?

       this锁

    静态同步方法使用什么锁?

       当前类的字节码文件

    死锁和活锁?

    死锁:多个线程因竞争资源而造成的一种僵局(互相等待的现象)。若无外力作用,他们都将无法推进下去。

    线程1已经持有了A锁并想要获得B锁的同时,线程2持有B锁并尝试获取A锁,那么这两个线程将永远地等待下去。

    活锁:为了彼此间的响应而相互礼让,使得没有一个线程能够继续前进,就发生活锁。

    死锁的产生条件与避免?

    死锁产生的4个必要条件:互斥条件不可抢占条件占有且申请条件循环等待条件

    死锁的预防:只要破坏4个必要条件中的任意一个,死锁就不会发生。

    预防常见方法:1.避免一个线程同时获取多个锁;2.避免一个线程在锁内同时占用多个资源;3.尝试使用定时锁,使用lock.tryLock来代替使用内置锁。

    什么是伪共享以及如何解决?

    定义:CPU缓存系统中是以缓存行(cache line)为单位存储的。目前主流的CPU Cache的Cache Line大小都是64Bytes。在多线程情况下,如果需要修改“共享同一个缓存行的变量”,就会无意中影响彼此的性能,这就是伪共享(False Sharing)。

    例子:假设在多线程情况下,x,y两个共享变量在同一个缓存行中,核a修改变量x,会导致核b,核c中的x变量和y变量同时失效。

    此时对于在核a上运行的线程,仅仅只是修改了了变量x,却导致同一个缓存行中的所有变量都无效,需要重新刷缓存。假设此时在核b上运行的线程,正好想要修改变量y,那么就会出现相互竞争,相互失效的情况,这就是伪共享。

    Java对于伪共享的解决方案:

    我们只需要填6个无用的长整型补上6*8=48字节, 让不同的VolatileLong对象处于不同的缓存行, 就可以避免伪共享了(64位系统超过缓存行的64字节也无所谓,只要保证不同线程不要操作同一缓存行就可以)。这个办法叫做补齐(Padding)。

    Java8中新增了一个注解:@sun.misc.Contended。加上这个注解的类会自动补齐缓存行,需要注意的是此注解默认是无效的,需要在jvm启动时设置-XX:-RestrictContended才会生效。

    Java内存模型(JMM)是什么?

    内存模型:为保证共享内存的正确性(可见性、原子性、有序性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。

    内存模型解决并发问题主要采用两种方式:限制处理器优化使用内存屏障

    JMM是一种机制和规范。符合内存模型规范,屏蔽各种硬件和操作系统访问差异,保证Java程序在各平台下对内存的访问都能保证效果一致的机制和规范。

    目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。

    内存屏障是什么?
    硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。

    内存屏障的作用:
    1.阻止屏障两侧的指令重排序;
    2.强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。

    为什么会有内存屏障(Memory barrier)?
    每个CPU都会有自己的缓存(有的甚至L1,L2,L3),缓存的目的就是为了提高性能,避免每次都要向内存取。但是这样的弊端也很明显:不能实时的和内存发生信息交换,分在不同CPU执行的不同线程对同一个变量的缓存值不同。

    用volatile关键字修饰变量可以解决上述问题,那么volatile是如何做到这一点的呢?那就是内存屏障,内存屏障是硬件层的概念,不同的硬件平台实现内存屏障的手段并不是一样,java通过屏蔽这些差异,统一由jvm来生成内存屏障的指令。

    java的内存屏障通常所谓的四种即LoadLoad,StoreStore,LoadStore,StoreLoad实际上也是上述两种的组合,完成一系列的屏障和数据同步功能。

     

    什么是内存不可见性以及如何避免?

    线程之间有互相独立的缓存, 当多个线程对共享数据进行操作时, 其操作彼此不可见。

    例:A线程先读取共享变量a, B修改了共享变量a为a1, 推送给住内存并改写,但主内存不会推送给A线程,这样A线程和B线程的变量会不同步。

    解决方案:1. synchronized关键字,互斥锁,在被锁代码块只能有一个线程访问共享变量。

                       2. volatile关键字, 会使主内存的共享变量每一次变更后都会推送给其他线程,其他线程会使用最新的变量副本。

    volatile读写的内存语义?解决什么问题?

    当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。

    当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

    解决线程可见性问题。

    volatile内存语义的实现?

    当第二个操作为volatile写操作时,不管第一个操作是什么(普通读写或者volatile读写),都不能进行重排序。这个规则确保volatile写之前的所有操作都不会被重排序到volatile写之后;

    当第一个操作为volatile读操作时,不管第二个操作是什么,都不能进行重排序。这个规则确保volatile读之后的所有操作都不会被重排序到volatile读之前;

    当第一个操作是volatile写操作时,第二个操作是volatile读操作,不能进行重排序。

    JMM中什么时数据依赖?

     A和C之间存在数据依赖关系,同时B和C之间也存在数据依赖关系。因此在最终执行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的结果将会被改变)。但A和B之间没有数据依赖关系,编译器和处理器可以重排序A和B之间的执行顺序。

    JMM中什么是重排序,如何避免?

    重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

    代码执行顺序会改变,但是执行结果不会变。

    通过变量增加volatile关键字来避免。为了实现 volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

    锁(同步关键字Synchronized)内存语义?解决什么问题?

    当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中;

    当线程获取锁时,JMM会当前线程拥有的本地内存共享变量置为无效,从而使得被监视器保护的临界区代码必须要从主内存中去读取共享变量;

    解决线程安全问题。

    JVM内存结构、 Java内存模型和Java对象模型 三个概念?

    JVM内存结构,线程共享:堆和方法区(包括运行时常量池),线程独享:Java虚拟机栈,本地方法栈,程序计数器。
    和Java虚拟机的运行时区域有关。描述的是Java程序执行过程中,由JVM管理的不同数据区域。各个区域有其特定的功能。

    Java内存模型(JMM)是和多线程相关的,他描述了一组规则或规范,这个规范定义了一个线程对共享变量的写入时对另一个线程是可见的。
    JMM定义了一些语法集,这些语法集映射到Java语言中就是volatile、synchronized等关键字。

    Java对象模型,是指Java类在被JVM加载时,JVM为类创建instanceKlass保持在方法区,在JVM里表示该类。使用new创建一个对象时,JVM创建一个instanceOopDesc对象,包括对象头和实例数据等。

    什么是CAS,为解决什么问题?ABA问题是什么?

    CAS: CompareAndSwap or CompareAndSet(比较并交换). CAS操作涉及到三个操作数,一个是内存值,一个是旧的预期值,一个是更新后的值,如果内存值和旧的预期值没有发生变化,才设置成新的值。

    (解决什么问题)用途:可以用CAS在无锁的情况下实现原子操作,但要明确应用场合,非常简单的操作且又不想引入锁可以考虑使用CAS操作,当想要非阻塞地完成某一操作也可以考虑CAS。

    ABA问题:如果一个值原来A, 变成B,又变成了A,使用CAS检查是会发现它的值没有变化,但实际上却变化了。

    解决思路:使用版本号。A->B->A 变为1A->2B->3A。

    JUC里提供的AtomicStampedReference来解决ABA问题,即检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志。

    独占锁,共享锁,公平锁,非公平锁?

    独占锁:该锁每一次只能被一个线程所持有,ReentrantReadWriteLock里的写锁。

    共享锁:该锁可被多个线程共有,  ReentrantReadWriteLock里的读锁。

    共享锁:其内部维护了一个FIFO的队列,先申请的线程优先获取锁。

    非公平锁:申请锁的线程可能插队,后申请锁的线程有可能先拿到锁。

    为什么弃用stop, suspend, resume方法?

    Stop方法天生就不安全。该方法终止所有未结束的方法,包括run方法。当线程被终止,立即释放被它锁住的所有对象锁,会导致对象处于不一致的状态。例子,转账中途被终止,钱款已转出,但未转入目标账户,现在银行对象就被破坏了。

    在希望停止线程的时候应该中断线程interrupt(),被中断的线程会在安全的时候停止。

    Suspend与stop不同,它不会破坏对象。但如果线程t1调用suspend阻塞一个持有一个锁的线程t2,那么,该锁在线程t2恢复之前是不可用的。如果t1试图获得同一个锁,那么t1也阻塞,程序死锁。

    suspend(),resume()--线程会停下来,但该线程,并没有放弃对象的锁。

    destroy()--强制终止线程,但该线程不会释放对象锁。

     附上思维导图:

    参考资料:《Java并发编程的艺术》

  • 相关阅读:
    奇数阶魔方问题
    《DSP using MATLAB》示例9.3
    《DSP using MATLAB》示例9.2
    《DSP using MATLAB》示例9.1
    找个目标很重要
    《DSP using MATLAB》示例Example 8.30
    《DSP using MATLAB》示例Example 8.29
    《DSP using MATLAB》示例Example 8.28
    《DSP using MATLAB》示例Example 8.27
    《DSP using MATLAB》示例Example 8.26
  • 原文地址:https://www.cnblogs.com/fyql/p/11117226.html
Copyright © 2011-2022 走看看