并发编程面试题

第一关: 初出茅庐

1.什么是进程?

进程是系统中正在运行的一个程序,程序一旦运行就是进程。

进程可以看成程序执行的一个实例。进程是系统资源分配的独立实体,每个进程都拥有独立的地址空间一个进程无法访问另一个进程的变量和数据结构,如果想让一个进程访问另一个进程的资源,需要使用进程间通信,比如管道,文件,套接字等。

2.什么是线程?

是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

3.线程的实现方式?

1.继承Thread2.实现runnable接口
3.实现callable接口
4.线程池

4.线程的状态?

public enum State {
    
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,  
        TERMINATED;
    }
  • NEW状态:new创建一个Thread对象时, 并没处于执行状态,因为没有调用start方法启动改线程,那么此时的状态就是新建状态。
  • RUNNABLE状态:线程对象通过start方法进入runnable状态,启动的线程不一定会立即得到执行,线程的运行与否要看cpu的调度,我们把这个中间状态叫可执行状态(RUNNABLE)。
  • RUNNING状态:一旦cpu通过轮询或其他方式从任务可以执行队列中选中了线程,此时它才能真正的执行自己的逻辑代码。
  • BLOCKED状态:处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。
  • TIMED_WAITING状态:处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。
  • TERMINATED状态:当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。

5.run方法和start方法的区别

  1. start ()方法来启动线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码;通过调用Thread类的star()方法来启动一个线程,这时此线程是处于就绪状态,并没有运行。然后通过此Thread类调用方法run()来完成其运行操作的,这里方法run()称为线程体,它包含了要执行的这个线程的内容,Run方 法运行结束,此线程终止。然后CPU再调度其它线程。
  2. run () 方法当作普通方法的方式调用。程序还是要顺序执行,要等待run方法体执行完毕后,才可继续执行下面的代码;程序中只有主线程一这 一个线程,其程序执行路径还是只有一条,这样就没有达到多线程的目的。

6.获取当前线程的名字?

System.out.println(Thread.currentThread().getName());

7.判断线程是否存活?

线程.isAlive();

8.sleep()方法的作用?

方法sleep()的作用是在指定的毫秒数内让当前的“正在执行的线程”休眠(暂停执行)。

9.线程的种类

java中线程分为用户线程和守护线程(GC就是一个守护线程)

守护线程的特点:守护线程是一个比较特殊的线程,主要被用做程序中后台调度以及支持性工作。当Java虚拟机中不存在非守护线程时,守护线程才会随着JVM一同结束工作。

//设置为守护线程
thread.setDaemon(true)

Daemon属性需要再启动线程之前设置,不能再启动后设置。

10.什么是synchronized?

synchronized是java中的一个关键字可以用来修饰方法和变量来保证线程的同步。

普通同步方法一> 锁的是当前实例对象。
静态同步方法一>锁的是当前类的Class对象。
同步方法块一>锁的是synchonized括号里配置的对象。

11.线程的基本方法

线程相关的基本方法有 wait,notify,notifyAll,sleep,join,yield 等。
在这里插入图片描述

方法名功能
sleep()强迫一个线程睡眠N毫秒。
isAlive()判断一个线程是否存活。
join()等待线程终止。
activeCount()程序中活跃的线程数。
enumerate()枚举程序中的线程。
currentThread()得到当前线程。
isDaemon()一个线程是否为守护线程。
setDaemon()设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
setName()为线程设置一个名称。
wait()强迫一个线程等待。
notify()通知一个线程继续运行。
setPriority()设置一个线程的优先级。
getPriority():获得一个线程的优先级。
yieid()yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。

12.为什么 wait 和 notify 方法要在同步块中调用?

Java API 强制要求这样做,如果你不这么做,你的代码会抛出
IllegalMonitorStateException 异常。还有一个原因是为了避免 wait 和 notify之间产生竞态条件。

当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 notify()方法。同样的,当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。

13.怎么检测一个线程是否拥有锁?

在 java.lang.Thread 中有一个方法叫 holdsLock(),它返回 true 如果当且仅当当前线程拥有某个具体对象的锁。

14.volatile 变量和 atomic 变量有什么不同?

Volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用 volatile 修饰 count 变量那么 count++ 操作就不是原子性的。

而 AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。

15.为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里?

Java 的每个对象中都有一个锁(monitor,也可以成为监视器) 并且 wait(),notify()等方法用于等待对象的锁或者通知其他线程对象的监视器可用。

在 Java 的线程中并没有可供任何对象使用的锁和同步器。这就是为什么这些方法是 Object 类的一部分,这样 Java 的每一个类都有用于线程间通信的基本方法。

16.为什么 Thread 类的 sleep()和 yield ()方法是静态的?

Thread 类的 sleep()和 yield()方法将在当前正在执行的线程上运行。所以在其他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。

17.并发编程三要素?

  1. 原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。
  2. 可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。
  3. 有序性,即程序的执行顺序按照代码的先后顺序来执行。

18.Executors 类是什么?

Executors 为 Executor,ExecutorService,ScheduledExecutorService,ThreadFactory 和 Callable 类提供了一些工具方法。Executors 可以用于方便的创建线程池

第二关:小试牛刀

1.如何优雅的设置睡眠时间

TimeUnit.HOURS.sleep(3);
TimeUnit.MINUTES.sleep(22);
TimeUnit.SECONDS.sleep(55);
TimeUnit.MILLISECONDS.sleep(899);

2.如何停止一个线程

1.使用退出标志使线程正常退出

public class ThreadSafe extends Thread {

 public volatile boolean exit = false; 
 
   public void run() { 
    while (!exit){
        //do something
    }
  } 
}

2.使用stop方法不过该方法已经被标记为过时的方法(因为会造成死锁)

3.使用interrupt()方法中断线程

public class TestThread {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                if (Thread.interrupted()) {
                    System.out.println("线程被停止了,我要退出");
                    try {
                        throw new InterruptedException();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        System.out.println("线程已经被停止了");
                    }
                }
            }
        }, "");
        thread.start();
         try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
        thread.interrupt();

    }
}

3.yield()方法和join()的作用

yield()方法

放弃当前cpu资源,将它让给其他的任务占用cpu执行时间。但放弃的时间不确定,有可能刚刚放弃,马上又获得cpu时间片。

join()方法

join是指把指定的线程加入到当前线程,比如join某个线程a, 会让当前线程b进入等待,直到a的生命周期结束,此期间b线程是处于blocked状态。

public class TestThread {
    public static void main(String[] args) throws Exception{
        Thread thread = new Thread(() -> {
          try {
               try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
          }finally {
              System.out.println(Thread.currentThread().getName());
          }
        }, "a");
        thread.start();
        Thread.currentThread().join();
        System.out.println("等待a线程执行完成才会执行");

    }
}

4.线程的优先级

在操作系统中,线程可以划分优先级,优先级较高的线程得到cpu资源比较多,也就是cpu有限执行优先级较高的线程对象中的任务,但是不能保证一定 优先级高,就先执行。

Java的优先级分为1~ 10个等级,数字越大优先级越高,默认优先级大小为5。超出范围则抛出:

java.lang. IlegalArgumentException.

线程的优先级具有继承性,比如a线程启动b线程,b线程与a优先级是一样的。

5.interrupted方法和isInterrupted方法的区别?

//该方法是判断当前线程是否中断(即执行该方法的线程)
 public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }
//该方法是指this关键字所在类的对象是否中断
public boolean isInterrupted() {
        return isInterrupted(false);
    }

举一个例子

ThreadA threadA=new ThreadA();
threadA.interrupt();
System.out.println(threadA.interrupted());//false 判断的是主线程main
System.out.println(threadA.isInterrupted());//true 判断的是threadA线程

6.Java虚拟机退出时Daemon线程中的finally块一定会执行吗?

public class TestThread {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
          try {
               try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
          }finally {
              System.out.println(Thread.currentThread().getName());
          }
        }, "aaaa");

        thread.setDaemon(true);
        thread.start();
    }
}

控制台没有任何输出说明finally中的语句没有执行

7.设置线程上下文类加载器

public void setContextClassLoader(ClassLoader cl)
public ClassLoader getContextClassLoader() 

8.什么是原子操作?

不可中断的一个或一系列操作

8.并发和并行

  • 并发:一个处理器同时处理多个任务。

  • 并行:多个处理器或者是多核的处理器同时处理多个不同的任务.前者是逻辑上的同时发生(simultaneous),而后者是物理上的同时发生.

并发性(concurrency),又称共行性,是指能处理多个同时性活动的能力,并发事件之间不一定要同一时刻发生。

并行(parallelism)是指同时发生的两个并发事件,具有并发的含义,而并发则不一定并行。

来个比喻:并发和并行的区别就是一个人同时吃三个馒头和三个人同时吃三个馒头。

9.什么是多线程中的上下文切换?

多线程会共同使用一组计算机上的 CPU,而线程数大于给程序分配的 CPU 数量时,为了让各个线程都有执行的机会,就需要轮转使用 CPU。不同的线程切换使用 CPU发生的切换数据等就是上下文切换。

10.死锁与活锁的区别,死锁与饥饿的区别?

死锁: 是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

产生死锁的必要条件:

1、互斥条件:所谓互斥就是进程在某一时间内独占资源。
2、请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3、不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。 4、循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

活锁: 任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。

活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。

饥饿: 一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。

Java 中导致饥饿的原因:

  1. 高优先级线程吞噬所有的低优先级线程的 CPU 时间。
  2. 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。
  3. 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。

11.Java 中用到的线程调度算法是什么?

采用时间片轮转的方式。可以设置线程的优先级,会映射到下层的系统上面的优先级上,如非特别需要,尽量不要用,防止线程饥饿。

12.什么是线程组,为什么在 Java 中不推荐使用?

ThreadGroup 类,可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式。

为什么不推荐使用?因为使用有很多的安全隐患吧,没有具体追究,如果需要使用,推荐使用线程池。

13.sleep 与 wait 的区别

  1. 对于 sleep()方法,我们首先要知道该方法是属于 Thread 类中的。而 wait()方法,则是属于Object 类中的。

  2. sleep()方法导致了程序暂停执行指定的时间,让出 cpu执行其他线程,但是他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态。

  3. 在调用 sleep()方法的过程中,线程不会释放对象锁。

  4. 而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

14.Java后台线程

  1. 定义:守护线程–也称“服务线程”,他是后台线程,它有一个特性,即为用户线程提供公共服务,在没有用户线程可服务时会自动离开。
  2. 优先级:守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。
  3. 设置:通过 setDaemon(true)来设置线程为“守护线程”;将一个用户线程设置为守护线程的方式是在 线程对象创建 之前 用线程对象的 setDaemon 方法。
  4. 在 Daemon 线程中产生的新线程也是 Daemon 的。
  5. 线程则是 JVM 级别的,以 Tomcat 为例,如果你在 Web 应用中启动一个线程,这个线程的生命周期并不会和 Web 应用程序保持同步。也就是说,即使你停止了 Web 应用,这个线程依旧是活跃的。
  6. example: 垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是 JVM 上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
  7. 生命周期:守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。当 JVM 中所有的线程都是守护线程的时候,JVM 就可以退出了;如果还有一个或以上的非守护线程则 JVM 不会退出。

15.死锁案例和死锁分析

在这里插入图片描述

public class TestDeadLock {
    public static void main(String[] args) {
        MyResources resources = new MyResources();
        new Thread(()->{
            resources.printA();
        },"A").start();

        new Thread(()->{
            resources.printB();
        },"B").start();

    }
}
class MyResources{
    public String A="A";
    public String B="B";

    public void printA(){
        synchronized (this.A){
            System.out.println(Thread.currentThread().getName()+"\t 输出AA");
             try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
             synchronized (this.B){
                 System.out.println(Thread.currentThread().getName()+"\t 输出AA");
             }
        }
    }
    public void printB(){
        synchronized (this.B){
            System.out.println(Thread.currentThread().getName()+"\t 输出BB");
            try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
            synchronized (this.A){
                System.out.println(Thread.currentThread().getName()+"\t 输出BB");
            }
        }
    }
}

分析:jps -l+jstack

C:\Users>jps -l
12516
30168 chapter10.TestDeadLock
33128 org.jetbrains.jps.cmdline.Launcher
34860 jdk.jcmd/sun.tools.jps.Jps

C:\Users>jstack 30168
2021-09-12 09:46:14
Full thread dump Java HotSpot(TM) 64-Bit Server VM (11.0.10+8-LTS-162 mixed mode):
Found one Java-level deadlock:
=============================
"A":
  waiting to lock monitor 0x000001b9aa5a2980 (object 0x0000000089f95e08, a java.lang.String),
  which is held by "B"
"B":
  waiting to lock monitor 0x000001b9ab178700 (object 0x0000000089f95dd8, a java.lang.String),
  which is held by "A"

Java stack information for the threads listed above:
===================================================
"A":
        at chapter10.MyResources.printA(TestDeadLock.java:27)
        - waiting to lock <0x0000000089f95e08> (a java.lang.String)
        - locked <0x0000000089f95dd8> (a java.lang.String)
        at chapter10.TestDeadLock.lambda$main$0(TestDeadLock.java:9)
        at chapter10.TestDeadLock$$Lambda$14/0x0000000100066840.run(Unknown Source)
        at java.lang.Thread.run(java.base@11.0.10/Thread.java:834)
"B":
        at chapter10.MyResources.printB(TestDeadLock.java:36)
        - waiting to lock <0x0000000089f95dd8> (a java.lang.String)
        - locked <0x0000000089f95e08> (a java.lang.String)
        at chapter10.TestDeadLock.lambda$main$1(TestDeadLock.java:13)
        at chapter10.TestDeadLock$$Lambda$15/0x0000000100066c40.run(Unknown Source)
        at java.lang.Thread.run(java.base@11.0.10/Thread.java:834)

Found 1 deadlock.

16.ThreadLocal 作用(线程本地存储)

ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

使用场景
最常见的 ThreadLocal 使用场景为 用来解决 数据库连接、Session 管理等。

private static final ThreadLocal threadSession = new ThreadLocal(); 
public static Session getSession() throws InfrastructureException { 
 Session s = (Session) threadSession.get(); 
 try { 
 if (s == null) { 
 s = getSessionFactory().openSession(); 
 threadSession.set(s); 
 } 
 } catch (HibernateException ex) { 
 throw new InfrastructureException(ex); 
 } 
 return s; 
}

更多内容可以看:ThreaLocal详细解读

17.Java 中用到的线程调度

抢占式调度:抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。

协同式调度:协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行,这种模式就像接力赛一样,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。

JVM 的线程调度实现(抢占式调度)

java 使用的线程调使用抢占式调度,Java 中线程会按优先级分配 CPU 时间片运行,且优先级越高越优先执行,但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片,反之,优先级低的分到的执行时间少但不会分配不到执行时间。

线程让出 cpu 的情况:

  1. 当前运行线程主动放弃 CPU,JVM 暂时放弃 CPU 操作(基于时间片轮转调度的 JVM 操作系统不会让线程永久放弃 CPU,或者说放弃本次时间片的执行权),例如调用 yield()方法。
  2. 当前运行线程因为某些原因进入阻塞状态,例如阻塞在 I/O 上。
  3. 当前运行线程结束,即运行完 run()方法里面的任务。

18.一个线程运行时发生异常会怎样?

如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候 JVM 会使用 Thread.getUncaughtExceptionHandler()来查询线程的 UncaughtExceptionHandler 并将线程和异常作为参数传递给handler 的 uncaughtException()方法进行处理。

19.同步方法和同步块,哪个是更好的选择?

同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁。

同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,这样从侧面来说也可以避免死锁。

20.多线程的价值?

1、发挥多核 CPU 的优势
多线程,可以真正发挥出多核 CPU 的优势来,达到充分利用 CPU 的目的,采用多线程的方式去同时完成几件事情而不互相干扰。

2、防止阻塞
从程序运行效率的角度来看,单核 CPU 不但不会发挥出多线程的优势,反而会因为在单核 CPU 上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核 CPU 我们还是要应用多线程,就是为了防止阻塞。试想,如果单核 CPU 使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。

3、便于建模
这是另外一个没有这么明显的优点了。假设有一个大的任务 A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务 A 分解成几个小任务,任务 B、任务 C、任务 D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。

21. 创建线程的三种方式的对比?

1、采用实现 Runnable、Callable 接口的方式创建多线程。

优势是:线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。在这种方式下,多个线程可以共享同一个 target 对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将 CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

劣势是:编程稍微复杂,如果要访问当前线程,则必须使用 Thread.currentThread()方法。

2、使用继承 Thread 类的方式创建多线程

优势是:编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread()方法,直接使用 this 即可获得当前线程。

劣势是:线程类已经继承了 Thread 类,所以不能再继承其他父类。

3、Runnable 和 Callable 的区别

  1. Callable 规定(重写)的方法是 call(),Runnable 规定(重写)的方法是 run()。

  2. Callable 的任务执行后可返回值,而 Runnable 的任务是不能返回值的。

  3. Call 方法可以抛出异常,run 方法不可以。

  4. 运行 Callable 任务可以拿到一个 Future 对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过 Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

22. Java 线程数过多会造成什么后果?

1、线程的生命周期开销非常高

2、消耗过多的 CPU 资源
如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争 CPU资源时还将产生其他性能的开销。

3、降低稳定性
JVM 在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError 异常。

23.线程的调度策略

线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:

  1. 线程体中调用了 yield 方法让出了对 cpu 的占用权利
  2. 线程体中调用了 sleep 方法使线程进入睡眠状态
  3. 线程由于 IO 操作受到阻塞
  4. 另外一个更高优先级线程出现
  5. 在支持时间片的系统中,该线程的时间片用完

24.什么是 Future?

在并发编程中,我们经常用到非阻塞的模型,在之前的多线程的三种实现中,不管是继承 thread 类还是实现 runnable 接口,都无法保证获取到之前的执行结果。

通过实现 Callback 接口,并用 Future 可以来接收多线程的执行结果。Future 表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加Callback 以便在任务执行成功或失败后作出相应的操作。

25.写一个Callable案例

public class TestFuture {
    public static void main(String[] args) throws Exception{
        FutureTask<String> task=new FutureTask<>(new CallableThread());
        task.run();
        System.out.println(task.get());
    }
}

class CallableThread implements Callable<String> {
    @Override
    public String call() throws InterruptedException {
        System.out.println("hello world");
        return "hello world";
    }
}

第三关:过关斩将

1.synchronized底层的两个jvm指令

monitor enter 和 monitor exit

2.java对象头

synchronized用的锁是存在Java对象头里的。对象如果是数组类型,虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,用2字宽存储对象头。

长度内容说明
32/64bitMark Word存储对象的hashCode或锁信息等
32/64bitClass Metadata Address存储到对象类型数据的指针
32/64bitArray length数组的长度(如果当前对象是数组)

在这里插入图片描述

3.锁的升降级规则

Java SE1.6为了提高锁的性能。引入了“偏向锁”和轻量级锁”。

Java SE 1.6中锁有4种状态。级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。

锁只能升级不能降级。

4.偏向锁

Hotspot 的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线 程多次获得。偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起 来让这个线程得到了偏护。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级 锁执行路径,因为轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所 以偏向锁的撤销操作的性能损耗必须小于节省下来的 CAS 原子指令的性能消耗)。上面说过,轻 量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进 一步提高性能。

//设置jvm参数用来关闭偏向锁,锁会变为轻量级锁
-XX:-UseBiasedLocking=false

//关闭偏向锁延迟
-XX:BiasedLockingStartupDelay=0

5.轻量级锁

锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。 锁升级 随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的, 也就是说只能从低到高升级,不会出现锁的降级)。 “轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是, 轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量 级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场 景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀 为重量级锁。

6.重量级锁

Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又 是依赖于底层的操作系统的 Mutex Lock 来实现的。而操作系统实现线程之间的切换这就需要从用 户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized 效率低的原因。

因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为 “重量级锁”。JDK 中对 Synchronized 做的种种优化,其核心都是为了减少这种重量级锁的使用。 JDK1.6 以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和 “偏向锁”。

7.锁优化

减少锁持有时间
只用在有线程安全要求的程序上加锁

减小锁粒度
将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。最最典型的减小锁粒度的案例就是ConcurrentHashMap。

锁分离
最常见的锁分离就是读写锁 ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能。读写分离思想可以延伸,只要操作互不影响,锁就可以分离。比如LinkedBlockingQueue 从头部取出,从尾部放数据。

锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化 。

锁消除
锁消除是在编译器级别的事情。在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作,多数是因为程序员编码不规范引起。

8.锁对比

优点缺点适用场景
偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距如果线程间存在锁竞争,会带来额外的锁撤销的消耗适用于只有一 个线程访问同步块场景
轻量级锁竞争的线程不会阻塞,提高了程序的响应速度如果始终得不到锁竞争的线程,使用自旋会消耗CPU追求响应时间同步块执行速度非常快
重量级锁线程竞争不使用自旋,不会消耗CPU线程阻塞,响应时间缓慢追求吞吐量同步块执行速度较长

9.生产者消费者模型

实现如下操作线程一加一减
product1 正在操作Number=1
consumer2 正在操作 Number=0
product2 正在操作Number=1
consumer4 正在操作 Number=0
product4 正在操作Number=1
consumer5 正在操作 Number=0
product5 正在操作Number=1
consumer1 正在操作 Number=0
product3 正在操作Number=1
consumer3 正在操作 Number=0

1.0版本synchronized+wait+notifyAll

public class ProductConsumerLock {
    public static void main(String[] args) {
        Sources source = new Sources();

        for (int i = 1; i <= 5; i++) {
            new Thread(()->{
                try {
                    source.addNumber();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },"product"+i).start();
        }

        for (int i = 1; i <= 5; i++) {
            new Thread(()->{
                try {
                    source.subNumber();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },"consumer"+i).start();
        }

    }
}
class Source{
    public volatile int number=0;

    public synchronized void  addNumber() throws Exception{

        while (number==1){
            this.wait();
        }
        number+=1;
        System.out.println(Thread.currentThread().getName()+"\t正在操作Number="+number);
        this.notifyAll();
    }

    public synchronized void subNumber() throws Exception{
        while (number==0){
            this.wait();
        }
        number-=1;
        System.out.println(Thread.currentThread().getName()+"\t正在操作\tNumber="+number);
        this.notifyAll();
    }
}

2.0版本lock+await+sginalall

class Sources{
    public volatile int number=0;
    public Lock lock=new ReentrantLock();
    Condition condition=lock.newCondition();

    public  void  addNumber() throws Exception{
        try {
            lock.lock();
            while (number==1){
                condition.await();
            }
            number+=1;
            System.out.println(Thread.currentThread().getName()+"\t正在操作Number="+number);
            condition.signalAll();
        }finally {
            lock.unlock();
        }
    }

    public synchronized void subNumber() throws Exception{
        try {
            lock.lock();
            while (number==0){
                condition.await();
            }
            number-=1;
            System.out.println(Thread.currentThread().getName()+"\t正在操作\tNumber="+number);
            condition.signalAll();
        }finally {
            lock.unlock();
        }

    }
}

10.synchronized和lock的区别lock有什么好处?

1.原始构成:

synchronized是关键字属于JVM层面,
monitorenter(底层是通过monitor对象来完成,其义wait/notify等方法也依赖于monitor对象只有在同步块或方法中才能调wait/notify
monitorexit

Lock是具体类( java. util. concurrent . locks . Lock)是api层面的锁

2.使用方法:

synchronized不需要用户去手动释放锁,当synchronized代码执行完后系统会自动让线程释放对锁的占用。

Reentrantlock则需要用户去手动释放锁若没有主动释放锁,就有可能导致出现死锁现象。
需要Llock()和unlock()方法配合try/finally语句块来完成。

3.等待是否可中断

synchronized不可中断,除非抛出异常或者正常运行完成
ReentrantLock可中断

  1. 设置超时方法tryLock(long timeout, TimeUnit unit)
  2. lockInterruptibly()放代码块中,调用interrupt() 方法可中断

4.加锁是否公平

synchronized非公平锁
Reentrantlock两者都可以,默认非公平锁,构造方法可以传入boolean值,true为公 平锁,false 为非公平锁

5.锁绑定多个条件condition

synchronized没有

Reentrantlock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

lock的优势

整体上来说 Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操作。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。

11.乐观锁

乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。

java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

12.悲观锁

悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。

java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如 RetreenLock。

13.自旋锁

自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

线程自旋是需要消耗 cpu 的,说白了就是让 cpu在做无用功,如果一直获取不到锁,那线程也不能一直占用 cpu 自旋做无用功,所以需要设定一个自旋等待的最大时间。

如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

自旋锁的优缺点

自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!

但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用 cpu 做无用功,占着 XX 不 XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要 cpu的线程又不能获取到 cpu,造成 cpu 的浪费。所以这种情况下我们要关闭自旋锁;

自旋锁时间阈值(1.6 引入了适应性自旋锁)

自旋锁的目的是为了占着 CPU 的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用 CPU 资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!

JVM 对于自旋周期的选择,jdk1.5 这个限度是一定的写死的,在 1.6 引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时 JVM 还针对当前 CPU 的负荷情况做了较多的优化,如果平均负载小于 CPUs 则一直自旋,如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞,如果正在自旋的线程发现 Owner 发生了变化则延迟自旋时间(自旋计数)或进入阻塞,如果 CPU 处于节电模式则停止自旋,自旋时间的最坏情况是 CPU的存储延迟(CPU A 存储了一个数据,到 CPU B 得知这个数据直接的时间差),自旋时会适当放弃线程优先级之间的差异。

自旋锁的开启

JDK1.6-XX:+UseSpinning 开启;
-XX:PreBlockSpin=10 为自旋次数;
JDK1.7 后,去掉此参数,由 jvm 控制;

14.可重入锁(递归锁)

最大作用就是避免死锁

指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁也即是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块。

在 JAVA 环境下 ReentrantLock 和 synchronized 都是 可重入锁。

public class Phone {
    public static synchronized void sendSMS(){
        System.out.println(Thread.currentThread().getId()+"sendSMS() invoked");
        sendEmail();
    }
    public static synchronized void sendEmail(){
        System.out.println(Thread.currentThread().getId()+"sendEmail() invoked");
    }

    public static void main(String[] args) {

        new Thread(()->{
           sendSMS();
        }).start();
        new Thread(()->{
            sendSMS();
        }).start();
    }
}
14sendSMS() invoked
14sendEmail() invoked
15sendSMS() invoked
15sendEmail() invoked

ReentrantLock 版本

 public void set(){
        Lock lock=new ReentrantLock();
        lock.lock();
        System.out.println(Thread.currentThread().getId()+"set() invoked");
        get();
        lock.unlock();
    }

    public void get(){
        Lock lock=new ReentrantLock();
        lock.lock();
        System.out.println(Thread.currentThread().getId()+"get() invoked");
        lock.unlock();
    }

注意:lock()和unlock()要配对 可以使用多个

//正常
public void get(){
        Lock lock=new ReentrantLock();
        lock.lock();
   	    lock.lock();
        System.out.println(Thread.currentThread().getId()+"get() invoked");
        lock.unlock();
        lock.unlock();
    }
//不正常 后面的持有锁的方法代码不会执行因为为释放锁
public void get(){
        Lock lock=new ReentrantLock();
        lock.lock();
   	    lock.lock();
        System.out.println(Thread.currentThread().getId()+"get() invoked");
        lock.unlock();
    }

15.公平锁与非公平锁

公平锁(Fair)
加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得

非公平锁(Nonfair)
加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待

  1. 非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护一个队列
  2. Java 中的 synchronized 是非公平锁,ReentrantLock 默认的 lock()方法采用的是非公平锁。

16.ReadWriteLock 读写锁

为了提高性能,Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。读写锁分为读锁和写
锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的,你只要上好相应的锁即可。

读锁:如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁

写锁:如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上

读锁,写的时候上写锁!

public class ReadWriteLockDemo {
    /*
    * 读读共享
    * 读写互斥
    * 写写互斥
    * */
    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        //写线程
        for (int i = 0; i < 5; i++) {
           final String temp=i+"";
            new Thread(()->{
                myCache.put(temp,temp);
            },String.valueOf(i)).start();
        }
        //读线程
        for (int i = 0; i < 5; i++) {
            final String temp=i+"";
            new Thread(()->{
                myCache.get(temp);
            },String.valueOf(i)).start();
        }
    }
}

class MyCache{
    private volatile Map<String,Object> map=new HashMap<>();
    //读写锁
    private ReadWriteLock readWriteLock=new ReentrantReadWriteLock();

    //添加数据
    public void put(String key,Object value){
        Lock lock = readWriteLock.writeLock();
        lock.lock();
        System.out.println("线程"+Thread.currentThread().getName()+"\t正在写入");
         try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
        map.put(key,value);
        System.out.println("线程"+Thread.currentThread().getName()+"\t写入完成");
        lock.unlock();
    }
    //查询数据
    public Object get(String key){
        Lock lock = readWriteLock.readLock();
        lock.lock();
        System.out.println("线程"+Thread.currentThread().getName()+"\t正在读取");
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        Object result = map.get(key);
        System.out.println("线程"+Thread.currentThread().getName()+"\t读取完成");
        lock.unlock();
        return result;
    }

}

17.共享锁和独占锁

独占锁
独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock 就是以独占方式实现的互斥锁。

独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。

共享锁
共享锁则允许多个线程同时获取锁,并发访问 共享资源,如:ReadWriteLock。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。

  1. AQS 的内部类 Node 定义了两个常量 SHARED 和 EXCLUSIVE,他们分别标识 AQS 队列中等待线程的锁获取模式。
  2. java 的并发包中提供了 ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问,或者被一个 写操作访问,但两者不能同时进行。

18.分段锁

分段锁也并非一种实际的锁,而是一种思想 ConcurrentHashMap 是学习分段锁的最好实践。

ConcurrentHashMap,它内部细分了若干个小的 HashMap,称之为段(Segment)。默认情况下一个 ConcurrentHashMap 被进一步细分为 16 个段,既就是锁的并发度。

如果需要在 ConcurrentHashMap 中添加一个新的表项,并不是将整个 HashMap 加锁,而是首先根据 hashcode 得到该表项应该存放在哪个段中,然后对该段加锁,并完成 put 操作。在多线程环境中,如果多个线程同时进行 put操作,只要被加入的表项不存放在同一个段中,则线程间可以做到真正的并行。

ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成

ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。Segment 是一种可重入锁 ReentrantLock,在ConcurrentHashMap 里扮演锁的角色,HashEntry 则用于存储键值对数据。一个 ConcurrentHashMap 里包含一个 Segment 数组,Segment 的结构和 HashMap类似,是一种数组和链表结构, 一个 Segment 里包含一个 HashEntry 数组,每个 HashEntry 是
一个链表结构的元素, 每个 Segment 守护一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得它对应的 Segment 锁。
在这里插入图片描述

19.tryLock 和 lock 和 lockInterruptibly 的区别

  1. tryLock 能获得锁就返回 true,不能就立即返回 false,tryLock(long timeout,TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁,返回 false
  2. lock 能获得锁就返回 true,不能的话一直等待获得锁
  3. lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程, lock 不会抛出异常,而 lockInterruptibly 会抛出异常。

20.什么是工作窃取算法?

是指某个线程从其他队列里窃取任务来执行。当大任务被分割成小任务时,有的线程可能提前完成任务,此时闲着不如去帮其他没完成工作线程。此时可以去其他队列窃取任务,为了减少竞争,通常使用双端队列,被窃取的线程从头部拿,窃取的线程从尾部拿任务执行。

工作窃取算法的优缺点

优点:充分利用线程进行并行计算,减少了线程间的竞争。

缺点:有些情况下还是存在竞争,比如双端队列中只有一个任务。这样就消耗了更多资源。

21.Java中的原子更新类

原子操作更新基本类型

AtomicBoolean:原子更新布尔类型
AtonicInteger:原子更新整型
AtomicLong:原子更新长整型

原子操作更新数组

AtomicIntegerArray:原子更新整型数据里的元素
AtomicLongArray:原子更新长整型数组里的元素
AtomicReferenceArray:原子更新引用类型数组里的元素
AtomicIntegerArray:主要提供原子方式更新数组里的整型

原子操作更新引用类型

如果原子需要更新多个变量,就需要用引用类型了。

AtomicReference:原子更新引用类型
AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
AtomicMarkableReference:
原子更新带有标记位的引用类型。标记位用boolean类型表示,构造方法时
AtomicMarkableReference(V initialRef,boolean initialMark)

原子操作更新字段类

AtomiceIntegerFieldUpdater:原子更新整型字段的更新器
AtomiceLongFieldUpdater:原子更新长整型字段的更新器
AtomiceStampedFieldUpdater:原子更新带有版本号的引用类型,将整数值

22.SynchronizedMap 和 ConcurrentHashMap 有什么区别?

SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为 map。

ConcurrentHashMap 使用分段锁来保证在多线程下的性能。ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将hash 表分为 16 个桶,诸如 get,put,remove 等常用操作只锁当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有 16 个写线程执行,并发性能的提升是显而易见的。另外 ConcurrentHashMap 使用了一种不同的迭代方式。在这种迭代方式中,当iterator 被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时 new 新的数据从而不影响原有的数据 ,iterator 完成后再将头指针替换为新的数据 ,这样 iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。

23.多线程与单例模式

多线程与单例模式

24.什么是 Java Timer 类?如何创建一个有特定时间间隔的任务?

全面了解Java Timer定时器类

第四关:登峰造极

1.什么是CAS

CAS的全称为Compare-And-Swap(比较并交换),它是一条CPU并发原语。

它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。

CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。

这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

2.UnSafe类(jdk.internal.misc.UnSafe)

是CAS的核心类,由于Java 方法无法直接访问底层系统,需要通过本地(native) 方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe 类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。

注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。

3.CAS原理分析

用AtomicInteger类中的方法来说明CAS的底层原理和思想

//原子上的加一操作
public final int getAndIncrement() {
        return U.getAndAddInt(this, VALUE, 1);
    }

底层调用jdk.internal.misc.UnSafe类中的方法

//o-->AtomicInteger对象
//offset-->地址偏移量
//delta-->要加的数字
//v-->通过AtomicInteger对象和偏移地址得到的AtomicInteger在主内存具体数值
   public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
        //通过AtomicInteger对象和偏移地址得到的AtomicInteger在主内存具体数值
            v = getIntVolatile(o, offset);
            //用该对象当前的值与v比较:
           //如果相同,更新v + delta并且返回true,
           //如果不同,继续取值然后再比较,直到更新完成。
        } while (!weakCompareAndSetInt(o, offset, v, v + delta));
        return v;
    }

举例:假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上) :

  1. AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3, 根据JMM模型,线程A和线程B各自持有-份值为3的value的副本分别到各自的工作内存。
  2. 线程A通过getIntVolatile(var1, var2)拿到value值3, 这时线程A被挂起。
  3. 线程B也通过getlIntVolatile(var1, var2)方法获取到value值3, 此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。
  4. 这时线程A恢复,执行compareAndSwapInt方法比较, 发现自己手里的值数字3和主内存的值数字4不一致, 说明该值已经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
  5. 线程A重新获取value值, 因为变量value被volatile修饰, 所以其它线程对它的修改,线程A总是能够看到,线程A继续执
    行compareAndSwaplnt进行比较替换,直到成功。

4.CAS出现的问题分析与解决

1) ABA问题

CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。

比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。

尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。

解决方案:添加版本号,每次更新的时候追加版本号,A-B-A- -> 1A-2B-3A。

从jdk1.5开始,Atomic包提供了一个类AtomicStampedReference来解决ABA的问题。

public class ABA {
    static AtomicReference<Integer> atomicReference=new AtomicReference<>(100);
    //初始化时间戳为1
    static AtomicStampedReference<Integer> asr=new AtomicStampedReference<>(100,1);

    public static void main(String[] args) {
        System.out.println("引出ABA问题");
        new Thread(()->{
            atomicReference.compareAndSet(100,101);
            atomicReference.compareAndSet(101,100);
        },"t1").start();

        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(1);
                System.out.println(atomicReference.compareAndSet(100,2019)+"\t"+atomicReference.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"t2").start();
        System.out.println("解决ABA问题");
        new Thread(()->{
            int stamp= asr.getStamp();
            //睡一秒保证t4能够获得和t3一样的初始时间戳
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            asr.compareAndSet(100,101,1,asr.getStamp()+1);
            asr.compareAndSet(101,100, asr.getStamp(), asr.getStamp()+1);
        },"t3").start();

        new Thread(()->{
            int stamp= asr.getStamp();
            //睡3秒保证t3的改变能够被t4察觉
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
            boolean result = asr.compareAndSet(100, 230, stamp, asr.getStamp() + 1);
            System.out.println("修改结果:"+result);
            System.out.println("现在的值:"+asr.getReference());
            System.out.println("现在的版本号:"+asr.getStamp());
        },"t4").start();
    }
}

引出ABA问题
解决ABA问题
true	2019
修改结果:false
现在的值:100
现在的版本号:3

2)循环时间长开销大(do while 循环一直自旋直到修改成功为止,会给cpu带来大开销)

如果jvm能支持处理器提供的pause指令,那么效率会有一定的提升。

原因一、它可以延迟流水线执行指令(de-pipeline),使cpu不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,有些处理器延迟时间是0。

原因二、它可以避免在退出循环的时候因内存顺序冲突而引起的cpu流水线被清空,从而提高cpu执行效率。

3)只能保证一个变量的原子操作

一、对多个共享变量操作时,可以用锁。

二、可以把多个共享变量合并成一一个共享变量来操作。比如,x=1,k=a, 合并xk=1a,然后用cas操作xk。

java 1.5开始;jdk提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象来进行cas操作。

5.JMM模型(java内存模型规范不是真实存在)

必须满足 原子性 可见性 有序性

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
在这里插入图片描述


在这里插入图片描述
Java 的 内 存 模 型 定 义 了 8 种 内 存 间 操 作 :

lock 和 unlock

  • 把 一 个 变 量 标 识 为 一 条 线 程 独 占 的 状 态 。
  • 把 一 个 处 于 锁 定 状 态 的 变 量 释 放 出 来 ,释 放 之 后 的 变 量 才 能 被 其 他 线 程 锁定 。

read 和 write

  • 把 一 个 变 量 值 从 主 内 存 传 输 到 线 程 的 工 作 内 存 , 以 便 load。
  • 把 store 操 作 从 工 作 内 存 得 到 的 变 量 的 值 , 放 入 主 内 存 的 变 量 中 。

load 和 store

  • 把 read 操 作 从 主 内 存 得 到 的 变 量 值 放 入 工 作 内 存 的 变 量 副 本 中 。
  • 把 工 作 内 存 的 变 量 值 传 送 到 主 内 存 , 以 便 write。

use 和 assgin

  • 把 工 作 内 存 变 量 值 传 递 给 执 行 引 擎 。
  • 将 执 行 引 擎 值 传 递 给 工 作 内 存 变 量 值 。

6.如何实现一个自旋锁

是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

public class SpinLockDemo {
    //原子线程引用
    AtomicReference<Thread> atomicReference=new AtomicReference<>();

    public void myLock(){
        Thread thread=Thread.currentThread();
        System.out.println(thread.getName()+"come in");
        //开始自旋
        while (!atomicReference.compareAndSet(null,thread)){

        }
    }
    public void myUnLock(){
        Thread thread=Thread.currentThread();
        atomicReference.compareAndSet(thread,null);
        System.out.println(thread.getName()+"invoke myUnlock()");
    }

    public static void main(String[] args) {
        SpinLockDemo spinLockDemo = new SpinLockDemo();
        new Thread(()->{
            spinLockDemo.myLock();
            try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
            spinLockDemo.myUnLock();
        },"AA").start();
        //保证A先执行
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        new Thread(()->{
            spinLockDemo.myLock();
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            spinLockDemo.myUnLock();
        },"BB").start();
    }
}

AAcome in
BBcome in
AAinvoke myUnlock()
BBinvoke myUnlock()

7.volatile的作用

JVM提供的轻量级锁机制有以下三个特点

1)保证可见性

//验证可见性
public class TestVolatile {
    
    public volatile int number=0;

    public void add(){
        number=60;
    }
    public static void main(String[] args) {
        TestVolatile testVolatile = new TestVolatile();
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"start update");
            try {
                Thread.sleep(3000);
                testVolatile.add();
                System.out.println(Thread.currentThread().getName()+"over update");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        },"AAA").start();

        while (testVolatile.number==0){

        }
        System.out.println("等待输出");
    }
}

没有volatile修饰变量的输出

AAAstart update
AAAover update

有volatile修饰变量的输出—》验证了volatile能保证可见性

AAAstart update
等待输出
AAAover update

2)不保证原子性

 public static void main(String[] args) {
        TestVolatile testVolatile = new TestVolatile();
        for (int i = 0; i < 20; i++) {
            //10个线程加number1000次
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    testVolatile.number++;
                }
            },String.valueOf(i)).start();
        }

        while (Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println("最终结果:"+testVolatile.number);
    }
最终结果:19200

解决办法:使用原子类AtomicInteger

3)禁止指令重排

计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种
在这里插入图片描述
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。

处理器在进行重排序时必须要考虑指令之间的数据依赖性

//语句3依赖语句1
int X=11; //语句1
int Y= 12; //语句2
X=X+5;//语句3
Y=X*X;//语句4

指令可以重排为1234 2134 1324 
但是要考虑数据之间的依赖性不能为4123

多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证–致性是无法确定的,结果无法预测

8.线程池相关问题

线程池内容较多,可以参考我的另一篇文章:
看了就能学会的线程池技术

9.Synchronized 同步锁分析

synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重入锁。

Synchronized 作用范围

  1. 作用于方法时,锁住的是对象的实例(this);
  2. 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
  3. synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

Synchronized 核心组件

  1. Wait Set:哪些调用 wait 方法被阻塞的线程被放置在这里;
  2. Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
  3. Entry List:Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中;
  4. OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck;
  5. Owner:当前已经获取到所资源的线程被称为 Owner;
  6. !Owner:当前释放锁的线程。

Synchronized 实现
在这里插入图片描述

  1. JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList 会被大量的并发线程进行 CAS 访问,为了降低对尾部元素的竞争,JVM 会将一部分线程移动到 EntryList 中作为候选竞争线程。

  2. Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定EntryList 中的某个线程为 OnDeck 线程(一般是最先进去的那个线程)。

  3. Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck,OnDeck 需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM 中,也把这种选择行为称之为“竞争切换”。

  4. OnDeck 线程获取到锁资源后会变为 Owner 线程,而没有得到锁资源的仍然停留在 EntryList中。如果 Owner 线程被 wait 方法阻塞,则转移到 WaitSet 队列中,直到某个时刻通过 notify或者 notifyAll 唤醒,会重新进去 EntryList 中。

  5. 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux 内核下采用 pthread_mutex_lock 内核函数实现的)。

  6. Synchronized 是非公平锁。 Synchronized 在线程进入 ContentionList 时,等待的线程会先尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁资源。

  7. 每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的

  8. synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁消耗的时间比有用操作消耗的时间更多。

  9. Java1.6,synchronized 进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后推出的 Java1.7 与 1.8 中,均对该关键字的实现机理做了优化。引入了偏向锁和轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。

  10. 锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀;

  11. JDK 1.6 中默认是开启偏向锁和轻量级锁,可以通过-XX:-UseBiasedLocking 来禁用偏向锁。

10.线程中断(interrupt)

中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这 个线程本身并不会因此而改变状态(如阻塞,终止等)。

  1. 调用 interrupt()方法并不会中断一个正在运行的线程。也就是说处于 Running 状态的线 程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。
  2. 若调用 sleep()而使线程处于 TIMED-WATING 状态,这时调用 interrupt()方法,会抛出 InterruptedException,从而使线程提前结束 TIMED-WATING 状态。
  3. 许多声明抛出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),抛出异 常前,都会清除中断标识位,所以抛出异常后,调用 isInterrupted()方法将会返回 false。
  4. 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止 一个线程 thread 的时候,可以调用 thread.interrupt()方法,在线程的 run 方法内部可以 根据 thread.isInterrupted()的值来优雅的终止线程。

引起线程上下文切换的原因

  1. 当前执行任务的时间片用完之后,系统 CPU 正常调度下一个任务;
  2. 当前执行任务碰到 IO 阻塞,调度器将此任务挂起,继续下一任务;
  3. 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务;
  4. 用户代码挂起当前任务,让出 CPU 时间;
  5. 硬件中断;

11.线程上下文切换

巧妙地利用了时间片轮转的方式, CPU 给每个任务都服务一定的时间,然后把当前任务的状态保存下来,在加载下一任务的状态后,继续服务下一任务,任务的状态保存及再加载, 这段过程就叫做上下文切换。时间片轮转的方式使多个任务在同一颗 CPU 上执行变成了可能。
在这里插入图片描述
进程:(有时候也称做任务)是指一个程序运行的实例。在 Linux 系统中,线程就是能并行运行并且与他们的父进程(创建他们的进程)共享同一地址空间(一段内存区域)和其他资源的轻量级的进程。

上下文: 是指某一时间点 CPU 寄存器和程序计数器的内容。

寄存器: 是 CPU 内部的数量较少但是速度很快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主内存)。寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速度。

程序计数器: 是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体依赖于特定的系统。

PCB-“切换桢:上下文切换可以认为是内核(操作系统的核心)在 CPU 上对于进程(包括线程)进行切换,上下文切换过程中的信息是保存在进程控制块(PCB, process control block)中的。PCB 还经常被称作“切换桢”(switchframe)。信息会一直保存到 CPU 的内存中,直到他们被再次使用。

上下文切换的活动:

  1. 挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处。
  2. 在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复。
  3. 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程在程序
    中。

引起线程上下文切换的原因:

  1. 当前执行任务的时间片用完之后,系统 CPU 正常调度下一个任务;
  2. 当前执行任务碰到 IO 阻塞,调度器将此任务挂起,继续下一任务;
  3. 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务;
  4. 用户代码挂起当前任务,让出 CPU 时间;
  5. 硬件中断;

12.JAVA 阻塞队列

阻塞队列,关键字是阻塞,先理解阻塞的含义,在阻塞队列中,线程阻塞有这样的两种情况:

  1. 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。
  2. 当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。
    在这里插入图片描述

Java 中的阻塞队列

1.ArrayBlockingQueue(公平、非公平)

用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平的访问队列,所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。

//我们可以使用以下代码创建一个公平的阻塞队列:
ArrayBlockingQueue fairQueue =
 new ArrayBlockingQueue(1000,true);

2.LinkedBlockingQueue(两个独立锁提高并发)

基于链表的阻塞队列,同 ArrayListBlockingQueue 类似,此队列按照先进先出(FIFO)的原则对元素进行排序。而 LinkedBlockingQueue 之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

LinkedBlockingQueue 会默认一个类似无限大小的容量
(Integer.MAX_VALUE)。

3.PriorityBlockingQueue(compareTo 排序实现优先)

是一个支持优先级的无界队列。默认情况下元素采取自然顺序升序排列。可以自定义实现compareTo()方法来指定元素进行排序规则,或者初始化 PriorityBlockingQueue 时,指定构造参数 Comparator 来对元素进行排序。需要注意的是不能保证同优先级元素的顺序。

4.DelayQueue(缓存失效、定时任务 )

是一个支持延时获取元素的无界阻塞队列。队列使用 PriorityQueue 来实现。队列中的元素必须实现 Delayed 接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。我们可以将 DelayQueue 运用在以下应用场景:

  1. 缓存系统的设计:可以用 DelayQueue 保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从 DelayQueue 中获取元素时,表示缓存有效期到了。

  2. 定时任务调度:使用 DelayQueue 保存当天将会执行的任务和执行时间,一旦从DelayQueue 中获取到任务就开始执行,从比如 TimerQueue 就是使用 DelayQueue 实现的。

5.SynchronousQueue(不存储数据、可用于传递数据)

是一个不存储元素的阻塞队列。每一个 put 操作必须等待一个 take 操作,否则不能继续添加元素。
SynchronousQueue 可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合于传递性场景,比如在一个线程中使用的数据,传递给
另 外 一 个 线 程 使 用 , SynchronousQueue 的 吞 吐 量 高 于 LinkedBlockingQueue 和ArrayBlockingQueue。

6.LinkedTransferQueue是 一 个 由 链 表 结 构 组 成 的 无 界 阻 塞 TransferQueue 队 列 。

相 对 于 其 他 阻 塞 队 列 ,LinkedTransferQueue 多了 tryTransfer 和 transfer 方法。

transfer 方法:
如果当前有消费者正在等待接收元素
(消费者使用 take()方法或带时间限制的

poll()方法,transfer 方法可以把生产者传入的元素立刻 transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer 方法会将元素存放在队列的 tail 节点,并等到该元素被消费者消费了才返回。

tryTransfer 方法。则是用来试探下生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回 false。和 transfer 方法的区别是 tryTransfer 方法无论消费者是否接收,方法立即返回。而 transfer 方法是必须等到消费者消费了才返回。
对于带有时间限制的 tryTransfer(E e, long timeout, TimeUnit unit)方法,则是试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没消费元素,则返回 false,如果在超时时间内消费了元素,则返回true。

7.LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列。所谓双向队列指的你可以从队列的两端插入和移出元素。

双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。相比其他的阻塞队列,LinkedBlockingDeque 多了 addFirst,addLast,offerFirst,offerLast,peekFirst,peekLast 等方法,以 First 单词结尾的方法,表示插入,获取(peek)或移除双端队列的第一个元素。以 Last 单词结尾的方法,表示插入,获取或移除双端队列的最后一个元素。另外插入方法 add 等同于 addLast,移除方法 remove 等效于 removeFirst。但是 take 方法却等同于 takeFirst,不知道是不是 Jdk 的 bug,使用时还是用带有 First 和 Last 后缀的方法更清楚。在初始化 LinkedBlockingDeque 时可以设置容量防止其过渡膨胀。另外双向阻塞队列可以运用在“工作窃取”模式中。

阻塞队列的主要方法:
在这里插入图片描述
添加数据的操作:

public abstract boolean add(E paramE)
将指定元素插入此队列中(如果立即可行且不会违反容量限制),
成功时返回 true,如果当前没有可用的空间,
则抛出 IllegalStateException。
如果该元素是 NULL,则会抛出NullPointerException 异常。
public abstract boolean offer(E paramE)
将指定元素插入此队列中(如果立即可行且不会违反容量限制),
成功时返回 true,如果当前没有可用的空间,则返回 false
public abstract void put(E paramE) throws InterruptedException
将指定元素插入此队列中,将等待可用的空间(如果有必要)
offer(E o, long timeout, TimeUnit unit)
可以设定等待的时间,如果在指定的时间内,
还不能往队列中加入 BlockingQueue,则返回失败。

获取数据操作:

poll(time):取走 BlockingQueue 里排在首位的对象,
若不能立即取出,则可以等 time 参数规定的时间,取不到时返回 null;
poll(long timeout, TimeUnit unit)BlockingQueue 取出一个队首的对象,如果在指定时间内,
队列一旦有数据可取,则立即返回队列中的数据。
否则直到时间超时还没有数据可取,返回失败。
take()
取走 BlockingQueue 里排在首位的对象,BlockingQueue 为空,
阻断进入等待状态直到 BlockingQueue 有新的数据被加入。
drainTo()
一次性从 BlockingQueue 获取所有可用的数据对象
(还可以指定获取数据的个数),
通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。

13.CyclicBarrier、CountDownLatch、Semaphore ,Exchanger的用法

CyclicBarrier、CountDownLatch、Semaphore 的用法

Exchanger用于两个线程之间交换数据。

public class ExchangerTest {
    public static final Exchanger<String> ex=new Exchanger<>();
    private static ExecutorService pool= Executors.newFixedThreadPool(2);
    public static void main(String[] args) {
        pool.execute(()->{
            String A="銀行流水A";
            try {
                ex.exchange(A);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        pool.execute(()->{
            String B="銀行流水B";
            try {
                String A=ex.exchange(B);
                System.out.println("AB錄入的是否一致"+A.equals(B)+
                        "A:"+A+"B"+B);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
}

14.LockSupport

是一个JUC中的类用于创建锁和其他同步类的基本线程阻塞原语。

LockSupport类使用了一 种名为Permit (许可)的概念来做到阻塞和唤醒线程的功能, 每个线程都有一个许可(permit)permit只有两个值1和等, 默认是零。

可以把许可看成是一种(0,1)信号量( Semaphore) ,但与Semaphore不同的是,许可的累加上限是1.

//permit默认是0,所以一开始调用park()方法, 当前线程就会阻塞
//直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,
//然后会将permit再次设置为0并返回。
   public static void park() {
        U.park(false, 0L);
    }
//调用unpark(hread)方法后,就会将thread线程的许可permit设置成1
//(注意多次调用unpark方法,不会累加,permit值还是1)会自动唤醒
//thread线程,即之前阻塞中的LockSupport.park(方法会立即返回。
 public static void unpark(Thread thread) {
        if (thread != null)
            U.unpark(thread);
    }

LockSupport不需要锁支持,可以先唤醒后等待

public class TestLockSupport {
    public static void main(String[] args) {

        Thread a=new Thread(()->{
              System.out.println(Thread.currentThread().getName()+"\t线程进入了");
               try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
              LockSupport.park();
              System.out.println(Thread.currentThread().getName()+"\t线程被唤醒了");
        },"AA");
        a.start();

        Thread b=new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t线程发出了唤醒通知");
            LockSupport.unpark(a);
        },"BB");
        b.start();
    }
}

为什么可以先唤醒线程后阻塞线程?
因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。

为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行。

15.Java并发容器有哪些?

ConcurrentHashMap CopyOnWriteArrayList
 CopyOnWriteArraySet
ConcurrentLinkedQueue 
ConcurrentLinkedDeque ConcurrentSkipListMap 
ConcurrentSkipListSetArrayBlockingQueue
LinkedBlockingQueue LinkedBlockingDeque 
PriorityBlockingQueue
SynchronousQueue
LinkedTransferQueue DelayQueue

16.什么是写时复制

Copyonwrite容器即写时复制的容器。往一个容器添加元 素的时候,不直接往当前容器Object[]添加,而是先将当前容器object[ ]进行copy,复制出一个新的容器0bject[] newElements, 然后新的容器0bject[] newElements 里添加元素,添加完元素之后,再将原容器的引用指向新的容器setArray(newElements);. 这样做的好处是可以对CopyOnwrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyonWrite 容器也是一“种读写分离的思想, 读和写不同的容器

  public boolean add(E e)
    {
        final Reentrantlock lock = this. lock;
        lock. lock();
        try{
            object[] ele ments = getArray();
            int Len = elements. length;
            object[] newElements = Arrays. copyof(elements, len + 1);
            newELements[len] = e;
            setArray(newElements );
            return true;
        }finally {
            lock. unlock();
        }
    }

17.线程类的构造方法、静态块是被哪个线程调用的

线程类的构造方法、静态块是被 new这个线程类所在的线程所调用的,而 run 方法里面的代码才是被线程自身所调用的。

举个例子,假设 Thread2 中 new 了Thread1,main 函数中 new 了 Thread2,那么:

1、Thread2 的构造方法、静态块是 main 线程调用的,Thread2 的 run()方法是Thread2 自己调用的.

2、Thread1 的构造方法、静态块是 Thread2 调用的,Thread1 的 run()方法是Thread1 自己调用的.

18.什么是 AQS(抽象的队列同步器)

在这里插入图片描述
AbstractQueuedSynchronizer 类如其名,抽象的队列式的同步器,AQS 定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的
ReentrantLock/Semaphore/CountDownLatch
在这里插入图片描述
它维护了一个 volatile int state(代表共享资源)和一个 FIFO 线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里 volatile 是核心关键词,具体 volatile 的语义,在此不述。state 的访问方式有三种:

getState()
setState()
compareAndSetState()

AQS 定义两种资源共享方式

Exclusive 独占资源-ReentrantLock
Exclusive(独占,只有一个线程能执行,如 ReentrantLock)

Share 共享资源-Semaphore/CountDownLatch
Share(共享,多个线程可同时执行,如 Semaphore/CountDownLatch)。

AQS 只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现,AQS 这里只定义了一个接口,具体资源的获取交由自定义同步器去实现了(通过 state 的 get/set/CAS)之所以没有定义成abstract ,是 因 为独 占模 式 下 只 用实现 tryAcquire-tryRelease ,而 共享 模 式 下 只用 实 现
tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS 已经在顶层实现好了。自定义同步器实现时

主要实现以下几种方法:

  1. isHeldExclusively():该线程是否正在独占资源。只有用到 condition 才需要去实现它。
  2. tryAcquire(int):独占方式。尝试获取资源,成功则返回 true,失败则返回 false。
  3. tryRelease(int):独占方式。尝试释放资源,成功则返回 true,失败则返回 false。
  4. tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0 表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  5. tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回 false。

同步器的实现是 ABS 核心(state 资源状态计数)

同步器的实现是 ABS 核心,以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。

以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次,state会 CAS 减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程就会从 await()函数返回,继续后余动作。

ReentrantReadWriteLock 实现独占和共享两种方式

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现 tryAcquiretryRelease、tryAcquireShared-tryReleaseShared 中的一种即可。但 AQS 也支持自定义同步器
同时实现独占和共享两种方式,如 ReentrantReadWriteLock。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐