聊聊你理解的线程与并发

 

目录

        1、进程和线程的由来

2、进程和线程的区别

3、Thread中start和run方法的区别

4、Thread和Runnable是什么关系

5、如何给run()方法传参

6、如何实现处理线程的返回值

7、线程的六个状态

8、sleep和wait区别

9、锁池EntryList

10、等待池WaitSet

11、notify和notifyAll的区别

 12、yield函数

13、如何中断线程

14、线程安全问题的主要诱因

15、互斥锁的特性

16、synchronized的基础

17、Monitor锁的竞争、获取与释放

18、自旋锁与自适应自旋锁

19、synchronized的四种状态

20、偏向锁

21、轻量级锁

22、偏向锁、轻量级锁、重量级锁比较汇总

23、Synchronized 和 Reentrantlock区别总结

24、JMM与JAVA内存区域划分是不同概念层次

25、JMM如何解决可见性问题

26、happens-before的八大原则

27、volatile:JVM提供轻量级同步机制

28、volatile变量为何立即可见?

29、单例双重检测实现

30、volatile和synchronized的区别

31、CAS (Compare and Swap) 乐观锁

32、CAS是一个种高效是高效线程安全性的方法

33、CAS思想

34、CAS缺点

35、 Java线程池

36、 线程池中的Fork/Join框架

37、为什么要使用线程池

38、线程池中Executor的框架

39、ThreadPoolExecutor构造函数(最重要)

40、handler:线程池的饱和策略

41、新任务提交execute执行后的判断

42、线程池的状态

43、线程池大小如何选定


1、进程和线程的由来

  1. 串行:初期的计算机智能串行执行任务,并且需要长时间等待用户输入。
  2. 批处理:预先将用户的指令集中成清单,批量串行处理用户指令,仍然无法并发执行。
  3. 进程:进程独占内存空间,保存各自运行状态,相互间不干扰且可以互相切换,为并发处理任务提供可能。
  4. 线程:共享进程的内存资源,相互间切换更快速,支持更细粒度的任务控制,使进程内的子任务得以并发执行。

2、进程和线程的区别

  1. 线程不能看作独立应用,而进程可看做独立应用
  2. 进程有独立的地址空间,相互不影响,线程只是进程的不同执行路径
  3. 线程没有独立的地址空间,多线程的程序比多线程程序健壮。一个线程挂掉就等于整个线程都挂掉
  4. 进程的切换比线程的切换开销大。对一些要求同时进行并且共享某些变量并发操作,只能用线程。每个独立线程有一个程序运行入口,顺序执行序列和程序出口。但是线程不能独立运行,必须依存于某个应用程序当中,由应用程序对多个线程的执行控制。

3、Thread中start和run方法的区别

考察我们有没有用过线程

public class TreadTest {
    private static void attack(){
        System.out.println("Fight");
        System.out.println("current Thread is:" + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        Thread t = new Thread(){
            public void  run(){//thread里实现run方法
                attack();
            }
        };
        System.out.println("current Thread is" + Thread.currentThread().getName());
    }
}

输出为
current Thread is:main
Fight
current Thread is:main
  •  将t.run()改成 t.start()
public class TreadTest {
    private static void attack(){
        System.out.println("Fight");
        System.out.println("current Thread is:" + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        Thread t = new Thread(){
            public void  run(){//thread里实现run方法
                attack();
            }
        };
        System.out.println("current Thread is:" + Thread.currentThread().getName());
        t.start();
    }
}

输出
current Thread is:main
Fight
current Thread is:Thread-0
  1. 调用start()方法会创建一个新的子线程并启动
  2. run()方法只是Thread的一个普通方法的调用,还是主线程中执行不存在任何可比性。

4、Thread和Runnable是什么关系

public class Mythread extends Thread {
    private String name;
    public Mythread(String name){
        this.name= name;
    }

    @Override
    public void run() {
        for (int i = 0; i <10 ; i++) {
            System.out.println("Thread Start:" + this.name + ",i = " + i);
        }
    }

    public static void main(String[] args) {
        Mythread mt1 = new Mythread("Thread1");
        Mythread mt2 = new Mythread("Thread2");
        Mythread mt3 = new Mythread("Thread3");
        mt1.start();
        mt2.start();
        mt3.start();
    }
}
输出
Thread Start:Thread1,i = 0
Thread Start:Thread1,i = 1
Thread Start:Thread1,i = 2
此处的thread2跑到thread1前面去了
Thread Start:Thread2,i = 0
Thread Start:Thread1,i = 3
Thread Start:Thread1,i = 4
Thread Start:Thread1,i = 5
Thread Start:Thread1,i = 6
Thread Start:Thread1,i = 7
Thread Start:Thread1,i = 8
。。。。。
Thread Start:Thread3,i = 1
Thread Start:Thread3,i = 2
Thread Start:Thread3,i = 3
Thread Start:Thread3,i = 4
Thread Start:Thread3,i = 5
  • Runnable

public class MyRunnable implements Runnable {
    private String name;
    public MyRunnable(String name){
        this.name= name;
    }

    @Override
    public void run() {
        for (int i = 0; i <10 ; i++) {
            System.out.println("Thread Start:" + this.name + ",i = " + i);
        }
    }

    public static void main(String[] args) {
        MyRunnable mr1 = new MyRunnable("Runnable1");
        MyRunnable mr2 = new MyRunnable("Runnable2");
        MyRunnable mr3 = new MyRunnable("Runnable3");
        //没有start方法,需要创建thread
        Thread t1 = new Thread(mr1);
        Thread t2 = new Thread(mr2);
        Thread t3 = new Thread(mr3);
        t1.start();
        t2.start();
        t3.start();
    }
}

输出显示
Thread Start:Runnable1,i = 0
Thread Start:Runnable1,i = 1
Thread Start:Runnable1,i = 2
Thread Start:Runnable1,i = 3
Thread Start:Runnable1,i = 4
Thread Start:Runnable1,i = 5
Thread Start:Runnable1,i = 6
Thread Start:Runnable1,i = 7

同样存在thread2在thread1前面
Thread Start:Runnable2,i = 0

Thread Start:Runnable1,i = 8
Thread Start:Runnable2,i = 1
Thread Start:Runnable1,i = 9

  • 通过前面程序看出

  1. Thread是实现了Runnable接口的类,使得run支持多线程
  2. 类的单一继承原则,推荐多使用Runnable接口

5、如何给run()方法传参

  • 跟线程相关的业务逻辑需要放在run方法里执行,而run方法里没有参数,并且也没有返回值
  • 实现的方式主要有三种
  1. 构造函数传参
  2. 成员变量传参(setName方法给name赋值)
  3. 回调函数传参

6、如何实现处理线程的返回值

因为有的程序执行依赖于子任务返回值执行的,当子任务交给子线程完成时,需获取他们的返回值,整个时候该怎么做。

实现方式有三种

  1. 主线程等待法:让主线程循环等待,直到目标子线程返回值为止。缺点:让自己实现循环等待逻辑,代码会繁多,等待多久,没法做到精准。
  2. 使用Thread类的join()阻塞当前线程以等待子线程处理完毕。缺点:密度不够细。
  3. 通过Callable接口实现:通过Future Task or线程池获取。

7、线程的六个状态

  1. 新建(new):创建后尚未启动的线程的状态。即新建一个线程还没有被调用,start方法,将处于new状态
  2. 运行(Runnable):包含Running和Ready。处于此状态线程可能正在执行,也可能正在等待着cpu为他分配执行时间。比如线程对象创建后,主线程调用该线程的start方法后,他们的线程 处于Runnable状态。处于Running状态线程,处于可运行线程之中,等待被现场调度选中,获取cpu使用权。处于Ready状态线程,处于线程池中,等待被线程调度选中获取cpu使用权,在获取cpu使用权后,变为Running使用权。
  3. 无期限等待(waiting):不会被分配cpu执行时间,需要显示被唤醒。以下方法会让线程无期限等待。

    没有设置Timeout参数的Object.wait()方法

    没有设置Timeout参数的Thread.join()方法、LockSupport.park()方法

  4.  期限等待(Timed Waiting) :在一定时间后会有系统自动唤醒

    thread.sleep()方法

    设置Timeout参数的Object.wait()方法

    设置Timeout参数的Thread.join()方法、

    LockSupport.parkNanos()方法

    LockSupport.parkUntil()方法

  5. 阻塞(Blocked):等待获取排它锁 。阻塞状态与等待状态区别是,阻塞状态在等待着获取到一个排它锁,这个事件将在这个线程放弃锁时发生。等待状态则在等待一段时间或者有唤醒动作时发生,在程序等待进入同步区域时线程将进入locked状态,比如当某个线程进入c关键字修饰时,方法和代码块及获取锁执行时候,其他想进入此方法或代码块线程只能等待着他们的状态。
  6. 结束(Terminated):已终止线程的状态,线程已经结束执行

8、sleep和wait区别

  1. sleep是Tread类的方法,wait是object类中定义的方法
  2. sleep()方法可以在任何地方使用
  3. wait()方法只能在synchronized方法或者synchronized块中使用

最本质区别

  1. thread.sleep 只会让出cpu,不会导致锁行为改变。即当前线程拥有锁,Thread.sleep不会让线程释放锁,而主动会让出cpu,让出cpu后,cpu就会执行其他任务了。因此调用Thread.sleep 不会影响锁相关行为
  2. Object.wait不仅让出cpu,还会释放已经占有的同步资源锁以便其他等待资源的线程得到该资源,进而去运行。

9、锁池EntryList

假设线程A已经拥有了某个对象(不是类)的锁,而其他线程B、C想要调用这个对象的某个synchronized方法或者(块),由于B、C线程在进入对象synchronized方法或者块之前必须先获得对象锁的拥有权,而恰巧该对象的锁目前正被线程A所占用,此时B、C线程就会阻塞,进入一个地方等待锁的释放,这个地方便是该对象的锁池。

10、等待池WaitSet

假设线程A调用某个对象的wait()方法,线程A就会释放该对象的锁,同时线程A就进入到了该对象的等待池中,进入到等待池中的线程不会去竞争对象的锁。

11、notify和notifyAll的区别

  1. notify会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会,没有获取到锁而已经等待在锁池中的线程只能等待其他机会获取锁,而不能主动回到等待池中
  2. notify只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会

 12、yield函数

当调用Thread.yield()函数时,会给线程调度器一个当前线程愿意让出cpu的使用的暗示,但是线程调度器可能会忽略这个暗示

13、如何中断线程

(1)调用interrupt(),通知线程应该中断。通知线程应该中断,具体时中断还是执行,应该由线程自己处理

  1.  如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出一个InterruptException异常
  2. 如果线程处于正常活动状态,那么会将该线程中断标志设置为true,被设置中断标志的线程将继续正常运行,不受影响。

(2)需要被调用的线程配合中断

  1. 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程
  2. 如果线程处于正常活动状态,那么会将该线程的中断标志设置位true,被设置中断标志的线程将继续正常运行,不受影响。

14、线程安全问题的主要诱因

  • 存在共享数据(临界资源)
  • 存在多条线程共同操作这些数据
  • 解决方法:同一时刻有且只有一个线程在操作共享数据,其他线程必须等待到该线程处理完数据后再对共享数据进行操作

15、互斥锁的特性

  • 互斥性

即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块进行访问。互斥性也称为操作的原子性

  • 可观性

必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的,否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起不一致。

16、synchronized的基础

  • java对象头
  • Monitor

17、Monitor锁的竞争、获取与释放

Moitor 存在于每个java对象的对象头中,synchronized锁就是靠这种方式获取锁,这是为什么java任意对象可以作为锁的原因

18、自旋锁与自适应自旋锁

  • 自旋锁

许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得在多处理器条件下,让另一个没有获取锁的线程在门外等待一会,但不放弃cpu的执行时间。这个不放弃cpu的执行的等待时间就是自旋。

缺点:若锁被其他线程长时间占用,会带来许多性能上的开销,在线程自旋时始终占用cpu的时间片,占用时间太长,字片时间会白消耗,如果超过,可以用PreBlockSpin来更改。

  • 自适应自旋锁

  1. 自旋次数不再固定
  2. 由前一次再同一个锁上的自旋时间及锁的拥有着的状态来决定,如果在同一个锁对象上,自旋等待刚刚获取锁的并持有锁的线程正在运行中,jvm认为该锁自旋获取到锁的可能性很大,会自动增加等待时间。比如增加到50次循环,相反某个锁自旋很少,成功获取到锁,在以后获取到锁时,将可能省略到自旋过程,以避免浪费处理器资源。

19、synchronized的四种状态

无锁、偏向锁、轻量级锁、重量级锁

锁膨胀方向:无锁——》偏向锁——》轻量级锁——》重量级锁

无锁:并没有加入任何锁,此时目标塑胶并没有任何线程占用。

20、偏向锁(CAS) 

减少同一线程获取锁的代价

大多数情况下,锁不存在多线程竞争,总是由同一线程多次获取。

核心思想:

如果一个线程获得了锁,那么锁就进入偏向模式,此时MarkWord的结构也变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,及获取锁的过程只需要检查MarkWord的锁标记记位为偏向锁以及当前线程id等于MarkWord的ThreadId即可,这样省去大量有关锁申请的操作

不适用锁竞争比较激烈的多线程场景

21、轻量级锁

轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁进程时候,偏向素就会升级为轻量级锁。

适应场景:线程交替执行同步块

若存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。

22、偏向锁、轻量级锁、重量级锁比较汇总

偏向锁

  • 优点:加锁和解锁不需要CAS操作,没有额外内存性能消耗,和执行非同步方法相比不存在纳秒级差距
  • 缺点:如果线程间存在锁竞争,会带来额外的锁撤销的消耗。
  • 使用场景:只有一个线程访问同步块或者同步方法场景

轻量级锁

  • 优点:竞争的线程,不会阻塞,提高了响应速度。
  • 缺点:若线程长时间不到锁,自旋锁会消耗cpu性能。
  • 使用场景:线程交替执行同步块或者同步方法场景

重量级锁

  • 优点:线程竞争不使用自旋,不会消耗cpu
  • 缺点:线程阻塞,响应时间缓慢,再多线程下频繁的获取释放锁,会带来巨大性能的消耗。
  • 使用场景:追求吞吐量,同步块,同步方法执行时间较长的场景。

23、Synchronized 和 Reentrantlock区别总结

  1. Synchronized 是关键字,Reentrantlock是类,
  2. Reentrantlock可以对获取锁的等待时间进行设置,避免死锁
  3. Reentrantlock可以获取各种锁信息
  4. Reentrantlock可以灵活的实现多路通知
  5. 机制:Synchronized操作MarkWord,Reentrantlock通用unsafe类的 park() 方法

24、JMM与JAVA内存区域划分是不同概念层次

  1. JMM描述场景的是一组规则,围绕原子性,有序性,可见性展开,控制程序中各个变量在共享数据区和私有数据区的访问方式
  2. 相似点:存在共享区域和私有区域

在JMM中主内存属于共享数据区域,以某个程度来讲,包括堆和方法区。而工作内存数据线程私有区域,从某程度讲,包括程序接受器,虚拟机栈和本地机栈。

25、JMM如何解决可见性问题

(1)指令重排序需满足条件

  • 在单线程环境下不能改变程序运行的结果
  • 存在数据依赖关系的不允许重排序
  • 无法通过happens-before原则推到出来的,才能进行指令的重排序。

(2)A操作的结果需要对B操作可见,则A与B存在happens-before关系

  • happens-before原则是判断数据是否存在竞争,线程是否安全主要依据,依靠这个原则便能解决在并发环境下两个曹操之间存在的冲突的问题。

26、happens-before的八大原则

  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作(只对单线程有用)。
  2. 锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。
  3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的赌操作。
  4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  5. 线程启动规则:Thread对象的start() 方法先行发生于此线程的每一个动作。
  6. 线程中断规则:对线程interrupt()方法调用先行发生被中断线程的代码检测到中断事件的发生。
  7. 线程终结规则:线程中所有的操作都先行发生于线程的终点检测,我们可以通过Thread.join()结束,Thread.isAlive()的返回值手段检测到线程依据终止执行。
  8. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。

27、volatile:JVM提供轻量级同步机制

  1. 保证被volatile修饰变量对所有线程总是可见的
  2. 禁止指令重排序优化

28、volatile变量为何立即可见?

  • 当写一个volatile变量时,JMM会把该线程对应的工作内存中共享变量值刷新到主内存中
  • 当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效。

29、单例双重检测实现

public class Singleton {
    private static Singleton instance;
    private Singleton(){}
    public static Singleton getInstance() {
        //第一此检测
        if (instance == null) {
            //同步
            synchronized (Singleton.class) {
                if (instance == null) {
                    //多线程环境下可能出现问题的地方
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

上述代码存在多线程环境下仍有隐患,

原因在于某个线程在执行到第一次检测时候,读到Instance不为空时,Instanct的对象可能还没有完全初始化。

30、volatile和synchronized的区别

(1)volatile本质是在告诉JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主内存中读取;

         synchronzied 则是所动当前变量,只有当前线程可以访问该变量,其他线程被阻塞住直到该线程完成变量操作为止。

(2)volatile仅能使用变量级别

         synchronized则可以使用变量,方法和类级别

(3)volatile仅能实现变量的修改可见性,不能保证原子性

         synchronized则可以保存变量修改的可见性和原子性

(4)volatile不会造成线程阻塞

        synchronized可能会造成线程的阻塞

(5)volatile标记的变量不会被编译器优化

         synchronized标记的变量可以被编译器优化

31、CAS (Compare and Swap) 乐观锁

  1. synchronized属于悲观锁,始终假定发生并发冲突,,因此会屏蔽一切可能违法数据完整性的操作
  2. CAS              属于乐观锁,不会发生冲突,因此旨在提交操作时检查,违反数据完整性。如果提交失败,则会继续重试。

32、CAS是一个种高效是高效线程安全性的方法

  1. 支持原子更新操作,适用于计数器,序列发生器等场景,序列发生器就会给变量自增的工具
  2. 属于乐观锁机制,称为lock-free
  3. CAS操作失败时由开发者决定是继续尝试,还是执行别的操作。

33、CAS思想

  • 包含了三个操作数-内存位置(v)、预期原值(A)和新值(B)
  • 操作时,将内存位置值于预期原值进行比较,如果相匹配,处理器会自动将该位置的值,更新为新值,否则处理器不做任何操作。
  • 这里内存重置V即主内存的值。
  • 例子:当一个线程需要修改共享变量的值,完成这个操作,先去取出共享变量,值赋给A,然后基于A的基础进行计算,得到新值B,执行B之后来更新共享变量的值时,就可以调用CAS方法更新变量值。

34、CAS缺点

  1. 若循环时间长、则开销很大
  2. 只能保证一个共享变量的原子操作,用锁保证
  3. ABA问题
  • 如果内存地址V初次读取地址A,并且准备赋值时检查到它得到值A,就可以说他的值没有被其他线程改变过了嘛?
  • 如果这段时间它的值曾经被改为了B,后来改为了A,那CAS 操作就会默认为从来没有被改变过,这个漏洞称为CAS的ABA问题
  • 解决方法:引入AtomicStampedReference,它可以通过控制变量值的版本来保证CAS的正确性。

35、 Java线程池

web开发中,服务器需要接受并处理请求,所以为一个请求来分配一个线程来进行处理,如果并发的线程数量非常打,但执行的时间很短,这样就会频繁创建和销毁线程。日次就会大大降低系统效率,可能出现服务器在为每个请求创建新线程和销毁线程花费时间,消耗系统资源要比是西安处理的用户请求的时间和资源更多。

36、 线程池中的Fork/Join框架

  • 把大任务分割成若干个小任务并行执行,最终汇总每个小任务结果后得到大任务结果的框架
  • working-stealing算法:某个线程从其他队列里窃取任务来执行。

37、为什么要使用线程池

  1. 降低资源消耗
  2. 提高线程仍可管理性
  • 无限制创建线程降低系统稳点性,池进行调试

38、线程池中Executor的框架

  • 是一个根据一组执行策略调用,调度,执行,控制,异步任务的框架
  • 目的:是提供一种将任务提交,与任务如何运行分离开来的机制

39、ThreadPoolExecutor构造函数(最重要)

  1. corePoolSize:核心线程数量
  2. maximumPoolSize:线程不够用时能够创建最大线程数
  3. workQueue:任务等待队列
  4. KeepAliceTime:抢占的顺序不一定,看运气
  5. threadFactory:创建新线程,Executors.defaultThreadFactory()

40、handler:线程池的饱和策略

  1. 如果阻塞满了,没有空隙,线程这时继续提交任务,就需要采用一种策略
  2. AbortPolicy:直接抛出异常,这是默认策略
  3. CallerRunsPolicy:用调用若所在的线程来执行任务
  4. DiscardOldestPolicy:丢弃队列中靠最前的任务,并执行当前任务
  5. DiscardPolicy:直接丢弃任务
  6. 实现RejectedExecutionHandler接口的自定义handler

41、新任务提交execute执行后的判断

  1. 如果卞的线程少于corePoolSize,则创建新线程来处理任务,即使线程池中的其他线程是空闲的
  2. 如果线程池中的线程数量大于等于corePoolSize且小于maximumPoolSize,则只有当workQueue满时才创建新的线程去处理任务。
  3. 如果设置的corePoolSize和MaximumPoolSize相同,则创建的线程池的大小时固定等待,这时如果有新任务提交,,若workQueue未满,则将请求放入workQueue中,等待有空闲的线程去从workQueue中去任务并处理。
  4. 如果运行的线程数量大于等于maxumumPoolSize,这是如果workQueue,已经满了,则通过handler所指定策略来处理任务。

42、线程池的状态

  1. Running:能接受新提交的任务,并且也能处理阻塞队列中的任务
  2. ShutDown:不能接受新提交的任务,但可以处理存量任务
  3. Stop:不能接受新提交任务,也不处理存量任务
  4. Tidying:所有任务都已终止,线程数为0
  5. Terminated:terminated()方法执行完后进入该状态,什么都不做,只是一个标识

43、线程池大小如何选定

  1. cpu密集型:线程数 = 按照核数或者核数+1设定
  2. I/O密集型:线程数 =  cpu核数*(1+评价等待时间 / 平均工作时间)

你可能感兴趣的:(java面试,多线程,面试,并发编程,java,经验分享)