JAVA八股文面试必会-基础篇-1.3 并发编程

1.3.1 并行和并发有什么区别?

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

并发:指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行 , 把时间分成若干段,使多个进程快速交替的执行。

举个例子 : 进办公室有两个门(两CPU),如果两同学分别从不同的门进入,不管先后性,两者互相独立,那么是并行;如果两同学从同一个们先后进入,就是并发

1.3.2 线程和进程的区别?

线程与进程的区别如下:

  1. 进程是资源分配的最小单位,线程是资源调度的最小单位
  2. 线程是在进程下运行的。一个进程可以包含多个线程
  3. 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间。而线程是共享进程中的数据的,使用相同的地址空间
  4. 同一进程下不同线程间数据容易共享,不同进程间数据很难共享
  5. 一个进程死掉并不会对另外一个进程造成影响

举个例子 : 一个应用程序在计算机上运行 , 会创建至少一个进程 , 在进程中开启多个线程去执行指令

1.3.3 创建线程的方式有哪些

  1. 继承 Thread 类;
  2. 实现 Runnable 接口;
  3. 实现 Callable 接口
  4. 使用线程池创建 ThreadPoolExcutor
  5. 使用匿名内部类方式
public class CreateRunnable {
    public static void main(String[] args) {
        //创建多线程创建开始
        Thread thread = new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("i:" + i);
                }
            }
        });
        thread.start();
    }
}

1.3.4 runnable 和 callable 有什么区别

Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个泛型,和Future、 FutureTask配合可以用来获取异步执行的结果

Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息

注意 :Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到, 此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。

1.3.5 线程的 run()和 start()有什么区别

每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,run()方法称为线程体

通过调用Thread类的start()方法来启动一个线程

总结来说 : run()方法是线程对象的执行体 , 调用run方法不会开启线程 , start方法用于开启线程 , 线程开启后自动执行线程的run方法

1.3.6 线程包括哪些状态,状态之间是如何变化的

线程的状态可以参考JDK中的Thread类中的枚举State , 一共有六种

public enum State {
    /**
     * 尚未启动的线程的线程状态
     */
    NEW,

    /**
     * 可运行线程的线程状态。处于可运行状态的线程正在 Java 虚拟机中执行,但它可能正在等待来自操作系统的其他资源,例如处理器。
     */
    RUNNABLE,

    /**
     * 线程阻塞等待监视器锁的线程状态。处于阻塞状态的线程正在等待监视器锁进入同步块/方法或在调用Object.wait后重新进入同步块/方法。
     */
    BLOCKED,

    /**
     * 等待线程的线程状态。由于调用以下方法之一,线程处于等待状态:
    * Object.wait没有超时
     * 没有超时的Thread.join
     * LockSupport.park
     * 处于等待状态的线程正在等待另一个线程执行特定操作。
     * 例如,一个对对象调用Object.wait()的线程正在等待另一个线程对该对象调用Object.notify()或Object.notifyAll() 。已调用Thread.join()的线程正在等待指定线程终止。
     */
    WAITING,

    /**
     * 具有指定等待时间的等待线程的线程状态。由于以指定的正等待时间调用以下方法之一,线程处于定时等待状态:
    * Thread.sleep
    * Object.wait超时
    * Thread.join超时
    * LockSupport.parkNanos
    * LockSupport.parkUntil
     * </ul>
     */
    TIMED_WAITING,

    /**
     * 已终止线程的线程状态。线程已完成执行
     */
    TERMINATED;
}

1.3.7 wait() 和 sleep() 方法有什么区别

共同点 : wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态

不同点

  1. 方法归属不同
  • sleep(long) 是 Thread 的静态方法
  • 而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有
  1. 醒来时机不同
  • 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来
  • wait(long) 和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去
  • 它们都可以被打断唤醒
  1. 锁特性不同(重点)
  • wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
  • wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用)
  • 而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)

1.3.8 notify()和 notifyAll()有什么区别

  1. notifyAll() 会唤醒所有的线程,notify() 只会唤醒一个线程。
  2. notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行, 如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪 一个线程由虚拟机控制。

1.3.9 如何控制某个方法允许并发访问线程的数量?

控制并发访问线程的数量有三种方式 :

第一种 : 使用线程池 , 设置线程池大小 , 那么并发数量就是线程池中的线程数量了

第二种 : 使用CountDownLatch , 它内部维护了一个计数器 , 计数器的值就是允许执行的线程数量 , 每当一个线程完成了自己的任务,计数器的值就相应得减1。当计数器到达0时,表示所有的线程都已执行完毕,然后在等待的线程就可以恢复执行任务

@Test
void name() throws InterruptedException {
    //需求 : 有T1,T2,T3三个线程, 希望T1,T2,T3 按照顺序执行  join方法
    //需求 : T1,T2,T3执行完毕之后, 再执行主线程  CountDownLatch
    //线程计数器
    CountDownLatch latch = new CountDownLatch(3);
    //RCountDownLatch latch = redissonClient.getCountDownLatch("countdown");
    //latch.trySetCount(3);

    Thread t1 = new Thread(() -> {
        try {
            latch.countDown();
            Thread.sleep(1000);
            log.info("t1线程开始执行--------------");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    });

    Thread t2 = new Thread(() -> {
        try {
            latch.countDown();
            Thread.sleep(2000);
            log.info("t2线程开始执行--------------");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    });

    Thread t3 = new Thread(() -> {
        try {
            latch.countDown();
            Thread.sleep(3000);
            log.info("t3线程开始执行--------------");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    });

    t1.start();
    t2.start();
    t3.start();

    log.info("主线程执行-------");
    latch.await();
    Thread.sleep(5000);
}

第三种 : 使用CyclicBarrier , 他和 CountDownLatch 的效果差不多 , 可以让指定数量的线程在某一个临界点等待 , 所有线程都执行到临界点 , 等待线程开始执行任务

package com.heima.redis;

import java.util.concurrent.*;

public class CyclicBarrierExample2 {
  // 请求的数量
  private static final int threadCount = 550;
  // 需要同步的线程数量
  private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5);

  public static void main(String[] args) throws InterruptedException {
    // 创建线程池
    ExecutorService threadPool = Executors.newFixedThreadPool(10);
    for (int i = 0; i < threadCount; i++) {
      final int threadNum = i;
      Thread.sleep(1000);
      threadPool.execute(() -> {
        try {
          test(threadNum);
        } catch (Exception e) {
          e.printStackTrace();
        }
      });
    }
    threadPool.shutdown();
  }

  public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
    System.out.println("threadnum:" + threadnum + "is ready");
    try {
      /**等待60秒,保证子线程完全执行结束*/
      cyclicBarrier.await(60, TimeUnit.SECONDS);
    } catch (Exception e) {
      System.out.println("-----CyclicBarrierException------");
    }
    System.out.println("threadnum:" + threadnum + "is finish");
  }
}

1.3.10 有 T1、T2、T3 三个线程,如何保证它们按顺序执行

有很多种方式 , 例如 :

方式一 : 使用CompletableFuture中的runAsyncthenRun方法

public static void main(String[] args) throws ExecutionException, InterruptedException {
    Thread t1 = new Thread(() -> System.out.println("t1开始执行"));
    Thread t2 = new Thread(() -> System.out.println("t2开始执行"));
    Thread t3 = new Thread(() -> System.out.println("t3开始执行"));

    CompletableFuture.runAsync(t1).thenRun(t2).thenRun(t3);
}

方式二 : 使用线程的join方法

public static void main(String[] args) throws ExecutionException, InterruptedException {
    Thread t1 = new Thread(() -> System.out.println("t1开始执行"));
    Thread t2 = new Thread(() -> System.out.println("t2开始执行"));
    Thread t3 = new Thread(() -> System.out.println("t3开始执行"));

    t1.start();
    t1.join();
    t2.start();
    t2.join();
    t3.start();
    t3.join();
}

方式三 : 使用CountDownLatch , 设置计数器 , t1执行完 , t1计数器-1 , t2执行依次类推

public static void main(String[] args) throws ExecutionException, InterruptedException {

    CountDownLatch latch1 = new CountDownLatch(1);
    CountDownLatch latch2 = new CountDownLatch(1);
    CountDownLatch latch3 = new CountDownLatch(1);

    Thread t1 = new Thread(() -> {
        try {
            latch1.await();
            System.out.println("t1开始执行");
            latch2.countDown();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

    });
    Thread t2 = new Thread(() -> {
        try {
            latch2.await();
            System.out.println("t2开始执行");
            latch3.countDown();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    });
    Thread t3 = new Thread(() -> {
        try {
            latch3.await();
            System.out.println("t3开始执行");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    });

    t1.start();
    t2.start();
    t3.start();

    latch1.countDown();
}

方式四 : 使用单线程线程池 , 任务放到队列排序执行

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> System.out.println("t1开始执行"));
    Thread t2 = new Thread(() -> System.out.println("t2开始执行"));
    Thread t3 = new Thread(() -> System.out.println("t3开始执行"));

    ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 10, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(3));
    executor.execute(t1);
    executor.execute(t2);
    executor.execute(t3);

    Thread.sleep(1000);
    executor.shutdown();
}

1.3.11 有T1,T2,T3三个线程 , 如何保证T1,T2执行完毕后执行T3

使用CountDownLatch , 设置计数器值为2 , T3 await , T1,T2执行完毕之后计数器值为0 , T3执行

@Test
void name() throws InterruptedException {
    //需求 : 有T1,T2,T3三个线程, 希望T1,T2,T3 按照顺序执行  join方法
    //需求 : T1,T2,T3执行完毕之后, 再执行主线程  CountDownLatch
    //线程计数器
    CountDownLatch latch = new CountDownLatch(2);

    Thread t1 = new Thread(() -> {
        try {
            Thread.sleep(1000);
            log.info("t1线程开始执行--------------");
            latch.countDown();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    });

    Thread t2 = new Thread(() -> {
        try {
            Thread.sleep(2000);
            log.info("t2线程开始执行--------------");
            latch.countDown();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    });

    Thread t3 = new Thread(() -> {
        try {
            latch.await();
            Thread.sleep(1000);
            log.info("t3线程开始执行--------------");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    });

    t1.start();
    t2.start();
    t3.start();

    Thread.sleep(5000);
}

思考 : 有T1,T2,T3三个线程 , 如何保证T1执行完毕后执行T2 , T3 ?

1.3.12 线程之间如何通信

多个线程在并发执行的时候,他们在CPU中是随机切换执行的,这个时候我们想多个线程一起来完成一件任务,这个时候我们就需要线程之间的通信了,多个线程一起来完成一个任务,线程通信一般有5种方式:

  1. 使用共享变量 , 需要注意线程安全问题
  2. 通过 volatile 关键字 , 需要考虑到原子性问题
  3. 通过 Object类的 wait/notify 方法
  4. 通过 condition 的 await/signal 方法
  5. 通过 join 的方式

1.3.13 如何获取一个线程的执行结果

获取线程的执行结果也有很多中方式 :

  1. 使用Callable线程 , 可以通过Future获取到线程的执行结果
  2. 使用Future,包括 FutureTask、CompletableFuture
  3. 使用共享变量 , 线程执行完毕之后将结果赋值给一个共享变量 , 通过CountDownLatch或者CyclicBarrier在线程执行完毕之后获取

1.3.14 如何停止一个正在运行的线程

有三种方式可以停止线程

  • 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
public class MyThread extends Thread {

    volatile boolean flag = false ;     // 线程执行的退出标记

    @Override
    public void run() {
        while(!flag) {
            System.out.println("MyThread...run...");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {

        // 创建MyThread对象
        MyThread t1 = new MyThread() ;
        t1.start();

        // 主线程休眠6秒
        Thread.sleep(6000);

        // 更改标记为true
        t1.flag = true ;

    }
}
  • 使用stop方法强行终止(不推荐,方法已作废)
  • 使用interrupt方法中断线程
public class MyThread extends Thread {

    volatile boolean flag = false ;     // 线程执行的退出标记

    @Override
    public void run() {
        while(!flag) {
            System.out.println("MyThread...run...");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {

        // 创建MyThread对象
        MyThread t1 = new MyThread() ;
        t1.start();

        // 主线程休眠2秒
        Thread.sleep(6000);

        // 调用interrupt方法
        t1.interrupt();

    }
}

1.3.15 有没有了解过线程池 , 使用线程池有什么好处

  1. 使用线程池可以实现线程的重用 , 线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用 , 提升性能
  2. 可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃
  3. 线程池可以根据设置对池中的线程进行管理 , 例如 : 线程不够了创建新线程 , 限制线程池中的最大线程数量, 线程空闲时间比较就回收线程等

1.3.16 如何创建线程池

方式一:通过构造方法实现

根据传递的参数不同, 创建适用于不同场景的线程池

方式二:通过 Executor 框架的工具类 Executors 来实现

根据《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 new ThreadPoolExecutor() 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险 , 所以我们在开发过程中创建线程池一般使用new ThreadPoolExecutor()手动设置参数的形式创建

1.3.17 线程池的核心参数有哪些

线程池的7个核心参数

  1. 核心线程数(corePoolSize) : 定义了最小可以同时运行的线程数量。
  2. 最大线程数(maximumPoolSize) : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
  3. 工作队列(workQueue) : 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
  4. 存活时间(keepAliveTime) : 当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
  5. 时间单位(unit) : keepAliveTime 参数的时间单位。
  6. 线程工厂(threadFactory) : executor 创建新线程的时候会用到。
  7. 拒绝策略(handler) : 关于饱和策略下面单独介绍一下

1.3.18 如何确定核心线程数

要合理的分配线程池的大小要根据实际情况来定,简单的来说的话就是根据CPU密集和IO密集来 分配

什么是CPU密集

CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行

CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开 几个模拟的多线程,该任务都不可能得到加速,因为CPU总的运算能力就那样

什么是IO密集

IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费 大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行, 即时在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间

分配CPU和IO密集

  1. CPU密集型时,任务可以少配置线程数,大概和机器的cpu核数相当,这样可以使得每个线程都在执行任务
  2. IO密集型时,大部分线程都阻塞,故需要多配置线程数,(2-3)*cpu核数

1.3.19 讲一讲线程池的任务调度流程

提交一个任务到线程池中,线程池的处理流程如下:

  1. 判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没 有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个 流程。
  2. 线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队 列里。如果工作队列满了,则进入下个流程。
  3. 判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任 务。如果已经满了,则交给饱和策略来处理这个任务。

1.3.20 常用的阻塞队列有哪些

  1. ArrayBlockingQueue: 一种基于数组的有界队列
  2. LinkedBlockingQueue:一种基于链表的无界队列 , 使用该对象时,除非系统资源耗尽,否则不存在入队失败的情况。该队列会保持无限增长 , 有可能会出现OOM (内存溢出)
  3. PriorityBlockingQueue(优先任务队列):是一个特殊的无界队列。ArrayBlockingQueueLinkedBlockingQueue都是按照先进先出处理任务,而该类则可以根据任务自身的优先级顺序先后执行。

1.3.21 线程池拒绝策略有哪些

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,就会执行拒绝策略, 不同的拒绝策略对任务的处理方式不一致 , ThreadPoolTaskExecutor 定义一些策略:

  • ThreadPoolExecutor.AbortPolicy: 抛出 RejectedExecutionException来拒绝新任务的处理(默认)
  • ThreadPoolExecutor.CallerRunsPolicy: 调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果我们需要任何一个任务请求都要被执行的话,可以选择这个策略
  • ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉
  • ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求

具体情况具体分析, 看场景是什么样子的 :

如果任务安全性要求不高, 允许丢失一些任务 : 使用DiscardPolicy或者DiscardOldestPolicy

如果不允许任务丢失 , 使用CallerRunsPolicy , 效率比较低

如果不允许任务丢失还要考率效率问题 : 使用默认AbortPolicy策略, 线程池满了之后抛出异常, 补货异常, 记录日志 , 交给人工处理

1.3.22 submit()和 execute()方法有什么不同

submit()和 execute()都代表提交任务 , 也是有一些差异的 :

  1. execute 没有返回值,如果不需要知道线程的结果就使用 execute 方法,性能会好很多
  2. submit 返回一个 Future 对象,如果想知道线程结果就使用 submit 提交,而且它能在主线程中通过 Future 的 get 方法捕获线程中的异常和执行结果

注意 : get是一个阻塞方法

1.3.23 Java程序中怎么保证多线程的执行安全

线程的安全性问题体现在:

  • 原子性:一个或者多个操作在 CPU 执行的过程中不被其他线程干扰
  • 可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到
  • 有序性:程序执行的顺序按照代码的先后顺序执行

解决办法:

  • 使用JUC中 Atomic开头的原子类、线程安全的并发容器 , synchronized、Lock,可以解决原子性问题
  • 使用 synchronized、volatile、Lock,可以解决可见性问题

1.3.24 Java中有哪些常用的锁

Java提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率。

乐观锁 : 乐观锁并没有给当前线程加锁 , 它认为当前环境读数据的多,写数据的少,并发读多,并发写少。因此,在读数据的时候,并不会给当前线程加锁,在写数据的时候,会进行判断当前的值与期望值时候相同,如果相同则进行更新,更新期间进行加锁,保证原子性

悲观锁 : 相比于乐观锁,悲观锁是一种非常悲观的思想,遇到事总是想到最坏的情况,认为写多读少,因此无论是读取数据还是写入数据,都会当作要修改其他里面的数据,通通上锁,指导这个线程释放锁后其他线程获取。

在java里面悲观锁有两种实现:synchronized、ReentrantLock 都是悲观锁

自旋锁 : 为了让线程进行等待,让线程不断执行一个空操作的循环,类似你去找一个朋友,朋友在家里干活让你等一下,你就在门口徘徊,不去干别的事,徘徊了N次之后发现还没来人,直接先去干别的事,等他打电话叫你

偏向锁 : 偏向锁是在JDK1.6加入的一种锁优化机制。偏向锁里面最重要的一个理解就是:偏心。这个锁会非常偏心对待第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步

轻量级锁 : 轻量级锁是在JDK1.6的时候,为了优化重量级锁,引入了一种优化机制。由于锁的获取默认采用重量级,互斥的开销很大,因此在没有竞争的时候采用CAS去操作以便消除同步使用的互斥锁

重量级锁 : 重量级锁其实是一种称呼,synchronized和Lock 就是一种重量级锁,它是通过内部一个叫做监视器锁来实现,而监视器锁本质上是依赖于系统的Mutex Lock(互斥锁)来实现,当加锁的时候需要用用户态切换为核心态,这样子的成本非常高,因此这种依赖于操作系统Mutex Lock的锁称为重量级锁。为了优化synchronized的性能,引入了轻量级锁,偏向锁

公平锁/非公平锁 : 公平锁是指多个线程获取锁的顺序,是按照申请锁的顺序来获取的 , 先到先得 , 非公平锁是指多个线程获取锁的顺序,并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁

独享锁/共享锁 : 共享锁指的是多个线程可以同时获取锁,以共享的形式持有,独享锁是指是多个线程不可以同时获取锁 , 一个线程持有锁, 那么其他线程就不能再获取到锁了!

Java中的共享锁的实现一般就是ReentrantReadWriteLock , 通过这个对象可以获取读锁和写锁 , 读锁共享, 写锁互斥, 读写互斥 , 还有synchronizedReentrantLock都是独占锁

可重入锁 : 同一个线程可以多次获取同一把锁 , 底层通过一个计数器实现 , 获取锁一次计数器 + 1 , 释放锁一次计数器 - 1 , 直到计数器的值减为0 , 锁才真的被释放 , synchronizedReentrantLock都是可重入锁

分段锁 : 分段锁就是对数据进行拆分, 分成多个部分, 每个部分就是一个分段 , 加锁的时候锁的是某一个分段的数据 , 其他分段不受影响, 相对于全量数据加锁 , 效率更高 !

JDK1.7版本里面的ConcurrentHashMap,它里面就将数据划分了非常多的分段(Segment),默认是16个,如果需要添加一个key-value,并不是将整个HashMap锁住,而是先进行hashcode计算从而得出这个key-value应该放在哪个Segment里面,然后开始对该Segment进行加锁,并完成put操作。不会影响其他分段, 所以性能相比HashTable提升了16倍

1.3.25 讲一讲synchronized的实现原理

synchronized 属于悲观锁 , 锁的本质就是在Java对象的对象头中有一个区域叫MarkWord , 在MarkWord区域中存储的就是锁标识 , 不同的标识代表的是不同级别的锁 , 刚开始没有线程获取锁对象就是一个无锁状态 , 有一个线程获取锁, 就是一个偏向锁状态, 有多个线程竞争锁, 会自动升级为轻量级锁 , 竞争锁的线程数量继续增加会导致多个线程竞争失败 , 会自动升级为重量级锁

在JDK6之前synchronized锁都属于重量级锁,但是在JDK6之后对synchronized做了升级优化,里面主要体现在:CAS自旋、锁消除、锁膨胀、轻量级锁、偏向锁等

CAS自旋 : 获取锁失败之后会执行一个循环 , 尝试重新获取锁 , 尝试一段时间还获取不到锁, 返回获取锁失败

锁消除 : 为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。所以锁消除可以节省毫无意义的请求锁的时间

锁膨胀: 锁膨胀的话比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁

JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作

锁升级 : synchronized在为对象加锁的时候, 首先该对象处于无锁状态 , 在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级

1.3.26 synchronized 和 Lock 有什么区别

语法层面

  • synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现
  • Lock 是接口,源码由 jdk 提供,用 java 语言实现
  • 使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁

功能层面

  • 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
  • Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量
  • Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock

性能层面

  • 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
  • 在竞争激烈时,Lock 的实现通常会提供更好的性能

1.3.27 谈谈你对ThreadLocal的理解

ThreadLocal也叫线程局部变量, 可以理解他就是在线程中开辟一块空间 , 保存整个线程共享的数据

ThreadLocal的作用

  1. ThreadLocal 可以实现【资源对象】的线程隔离,让每个线程各用各的【资源对象】,避免争用引发的线程安全问题
  2. ThreadLocal 同时实现了线程内的资源共享

ThreadLocal实现原理

每个线程内有一个 ThreadLocalMap 类型的成员变量,用来存储资源对象

调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线程的 ThreadLocalMap 集合中

调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值

调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值

注意问题

使用ThreadLocal特别要注意内存泄露问题 , ThreadLocalMap中的key被设计为弱引用类型 , 在内存不足(GC)时释放其占用的内存 , 但是value是强引用类型 , 不会被自动释放 , 所以就有可能出现内存泄露 , 解决方案也非常简单, ThreadLocal中的数据使用完毕之后及时调用remove方法清除即可

登录认证, 保存登录用户信息到ThreadLocal , 提供后面的业务使用

1.3.28 开发中有没有使用过volatile 关键字

volatile是JDK中的一个关键字 , 如果一个变量如果用volatile修饰了,则Java可以确保所有线程看到这个变量的值是一致的,如果某个线程对volatile修饰的共享变量进行更新,那么其他线程可以立马看到这个更新,这就是内存可见性。所有volatile主要用于实现内存可见性

volatile实现内存可见性的过程 :

  1. 线程写volatile变量的过程:
    1. 改变线程本地内存中Volatile变量副本的值;
    2. 将改变后的副本的值从本地内存刷新到主内存
  1. 线程读volatile变量的过程:
    1. 从主内存中读取volatile变量的最新值到线程的本地内存中
    2. 从本地内存中读取volatile变量的副本

1.3.29 volatile 能使得一个非原子操作变成原子操作吗

volatile关键字的主要作用是使变量在多个线程间可见,但无法保证原子性,对于多个线程访问同 一个实例变量需要加锁进行同步。如果要想保证原子性 , 建议使用JUC中提供的`java.util.concurrent.atomic`包下的原子类 , 这些类中提供了一些原子性操作的方法, 通过这些原子类可以避免线程安全问题的发生 , 这些类的底层都是使用CAS机制 , 来保证操作的原子性

1.3.30 有没有了解过CAS

CAS也叫比较交换,是一种无锁原子算法,其作用是让CPU将内存值更新为新值,但是有个条件,内存值必须与期望值相同

CAS虽然高效地解决了原子操作,但是还是存在一些缺陷的 , 主要存在缺陷有 :

  1. ABA问题 : 因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化
  2. 循环时间长开销大 : 自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销
  3. 只能保证一个共享变量的原子操作 : 当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁

1.3.31 有没有了解过AQS

AQS全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架

他有几个特点 :

  1. 用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁 (实现读写锁)
  2. 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
  3. 提供了基于 FIFO 的等待队列 (实现公平锁/非公平锁)
  4. 使用条件变量来实现等待、唤醒机制,支持多个条件变量 实现CountDownLatch

AQS内部维护着一个FIFO队列,该队列就是CLH同步队列,遵循FIFO原则( First Input First Output先进先出)。CLH同步队列是一个FIFO双向队列,AQS依赖它来完成同步状态的管理。

我们常用的的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于 AQS 的

1.3.32 JUC框架有了解过吗

J.U.C 即 java.util.concurrent,是 Java 5 中引入的,而我们最熟悉的线程池机制就在这个包

J.U.C 框架包含的内容有:

  1. AbstractQueuedSynchronizer(AQS框架),J.U.C 中实现锁和同步机制的基础;
  2. Locks & Condition(锁和条件变量),比 synchronized、wait、notify 更细粒度的锁机制;
  3. Executor 框架(线程池、Callable、Future),任务的执行和调度框架;
  4. Synchronizers(同步器),主要用于协助线程同步,有 CountDownLatch、CyclicBarrier、Semaphore、Exchanger;
  5. Atomic Variables(原子变量),方便程序员在多线程环境下,无锁的进行原子操作,核心操作是 CAS 原子操作,所谓的 CAS 操作,即 compare and swap,指的是将预期值与当前变量的值比较(compare),如果相等则使用新值替换(swap)当前变量,否则不作操作;
  6. BlockingQueue(阻塞队列),阻塞队列提供了可阻塞的入队和出对操作,如果队列满了,入队操作将阻塞直到有空间可用,如果队列空了,出队操作将阻塞直到有元素可用;
  7. Concurrent Collections(并发容器),像`ConcurrentHashMap`,`CopyOnWriteArrayList`之类的
  8. TimeUnit 枚举,TimeUnit 是 java.util.concurrent 包下面的一个枚举类,TimeUnit 提供了可读性更好的线程暂停操作,以及方便的时间单位转换方法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吉迪恩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值