JAVA并发(持续更新)

1.并行跟并发有什么区别?

从操作系统的角度来看,线程是CPU分配的最小单位。

  • 并行就是同一时刻,两个线程都在执行。这就要求有两个CPU去分别执行两个线程。
  • 并发就是同一时刻,只有一个执行,但是一个时间段内,两个线程都执行了。并发的实现依赖于CPU切换线程,因为切换的时间特别短,所以基本对于用户是无感知的。

就好像我们去食堂打饭,并行就是我们在多个窗口排队,几个阿姨同时打菜;并发就是我们挤在一个窗口,阿姨给这个打一勺,又手忙脚乱地给那个打一勺。 

2.说说什么是进程和线程?

要说线程,必须得先说说进程。

  • 进程:进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。
  • 线程:线程是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。

操作系统在分配资源时是把资源分配给进程的, 但是 CPU 资源比较特殊,它是被分配到线程的,因为真正要占用CPU运行的是线程,所以也说线程是 CPU分配的基本单位。

比如在Java中,当我们启动 main 函数其实就启动了一个JVM进程,而 main 函数在的线程就是这个进程中的一个线程,也称主线程。

一个进程中有多个线程,多个线程共用进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈 

3.说说线程有几种创建方式?

  • 继承Thread类,重写run()方法,调用start()方法启动线程
package com.kfm.it.Paraller;

public class MyThread {
    public static void main(String[] args) {
        Thread.currentThread().setName("主线程");
        MyThread1 myThread1 = new MyThread1();
        myThread1.setName("子线程");
        myThread1.start();
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+"运行到第"+i+"次");
        }
    }
}
class MyThread1 extends Thread{
    public  void run(){
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+"运行到第"+i+"次");

        }
    }
}
  • 实现 Runnable 接口,重写run()方法
package com.kfm.it.Paraller;

public class MyRunnable {
    public static void main(String[] args) {
Thread.currentThread().setName("主线程");
        Thread thread = new Thread(new MyRunnable1());
        thread.setName("子线程");
        thread.start();
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+"运行到第"+i+"次");

        }
    }

}
class MyRunnable1 implements Runnable{
    public void run(){
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+"运行到第"+i+"次");
        }
    }
}
  • 实现Callable接口,重写call()方法,这种方式可以通过FutureTask获取任务执行的返回值
public class CallerTask implements Callable<String> {
    public String call() throws Exception {
        return "Hello,i am running!";
    }

    public static void main(String[] args) {
        //创建异步任务
        FutureTask<String> task=new FutureTask<String>(new CallerTask());
        //启动线程
        new Thread(task).start();
        try {
            //等待执行完成,并获取返回结果
            String result=task.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

4.线程有哪些常用的调度方法?

 

线程等待与通知

在Object类中有一些函数可以用于线程的等待与通知。

  • wait():当一个线程A调用一个共享变量的 wait()方法时, 线程A会被阻塞挂起, 发生下面几种情况才会返回 :

    • (1) 线程A调用了共享对象 notify()或者 notifyAll()方法;

    • (2)其他线程调用了线程A的 interrupt() 方法,线程A抛出InterruptedException异常返回。

  • wait(long timeout) :这个方法相比 wait() 方法多了一个超时参数,它的不同之处在于,如果线程A调用共享对象的wait(long timeout)方法后,没有在指定的 timeout ms时间内被其它线程唤醒,那么这个方法还是会因为超时而返回。

  • wait(long timeout, int nanos),其内部调用的是 wait(long timout)函数。

上面是线程等待的方法,而唤醒线程主要是下面两个方法:

  • notify() : 一个线程A调用共享对象的 notify() 方法后,会唤醒一个在这个共享变量上调用 wait 系列方法后被挂起的线程。 一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。
  • notifyAll() :不同于在共享变量上调用 notify() 函数会唤醒被阻塞到该共享变量上的一个线程,notifyAll()方法则会唤醒所有在该共享变量上由于调用 wait 系列方法而被挂起的线程。

Thread类也提供了一个方法用于等待的方法:

  • join():如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才

    从thread.join()返回。

线程休眠

  • sleep(long millis) :Thread类中的静态方法,当一个执行中的线程A调用了Thread 的sleep方法后,线程A会暂时让出指定时间的执行权,但是线程A所拥有的监视器资源,比如锁还是持有不让出的。指定的睡眠时间到了后该函数会正常返回,接着参与 CPU 的调度,获取到 CPU 资源后就可以继续运行。

让出优先权

  • yield() :Thread类中的静态方法,当一个线程调用 yield 方法时,实际就是在暗示线程调度器当前线程请求让出自己的CPU ,但是线程调度器可以无条件忽略这个暗示。

线程中断

Java 中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。

  • void interrupt() :中断线程,例如,当线程A运行时,线程B可以调用线程interrupt() 方法来设置线程的中断标志为true 并立即返回。设置标志仅仅是设置标志, 线程A实际并没有被中断, 会继续往下执行。
  • boolean isInterrupted() 方法: 检测当前线程是否被中断。
  • boolean interrupted() 方法: 检测当前线程是否被中断,与 isInterrupted 不同的是,该方法如果发现当前线程被中断,则会清除中断标志。

5.线程有几种状态? 

简述Java内存模型(JMM)

Java内存模型定义了程序中各种变量的访问规则:

  • 所有变量都存储在主存,每个线程都有自己的工作内存。
  • 工作内存中保存了被该线程使用的变量的主存副本,线程对变量的所有操作都必须在工作空间进行,不能直接读写主内存数据。
  • 操作完成后,线程的工作内存通过缓存一致性协议将操作完的数据刷回主存。

简述as-if-serial

编译器会对原始的程序进行指令重排序和优化。但不管怎么重排序,其结果都必须和用户原始程序输出的预定结果保持一致。

简述原子性操作

一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行,这就是原子性操作。

#简述线程的可见性

可见性指当一个线程修改了共享变量时,其他线程能够立即得知修改。volatile、synchronized、final 关键字都能保证可见性。

#简述有序性

虽然多线程存在并发和指令优化等操作,但在本线程内观察该线程的所有执行操作是有序的

简述Java中volatile关键字作用

  • 保证变量对所有线程的可见性。当一个线程修改了变量值,新值对于其他线程来说是立即可以得知的。
  • 禁止指令重排。使用 volatile 变量进行写操作,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器进行重排序

synchronized底层实现原理

Java 对象底层都会关联一个 monitor,使用 synchronized 时 JVM 会根据使用环境找到对象的 monitor,根据 monitor 的状态进行加解锁的判断。如果成功加锁就成为该 monitor 的唯一持有者,monitor 在被释放前不能再被其他线程获取。

synchronized在JVM编译后会产生monitorenter 和 monitorexit 这两个字节码指令,获取和释放 monitor。这两个字节码指令都需要一个引用类型的参数指明要锁定和解锁的对象,对于同步普通方法,锁是当前实例对象;对于静态同步方法,锁是当前类的 Class 对象;对于同步方法块,锁是 synchronized 括号里的对象。

执行 monitorenter 指令时,首先尝试获取对象锁。如果这个对象没有被锁定,或当前线程已经持有锁,就把锁的计数器加 1,执行 monitorexit 指令时会将锁计数器减 1。一旦计数器为 0 锁随即就被释放。

#synchronized关键词使用方法

  • 直接修饰某个实例方法
  • 直接修饰某个静态方法
  • 修饰代码块

 6、什么是线程池?

 线程池: 简单理解,它就是一个管理线程的池子。

  • 它帮我们管理线程,避免增加创建线程和销毁线程的资源损耗。因为线程其实也是一个对象,创建一个对象,需要经过类加载过程,销毁一个对象,需要走GC垃圾回收流程,都是需要资源开销的。
  • 提高响应速度。 如果任务到达了,相对于从线程池拿线程,重新去创建一条线程执行,速度肯定慢很多。
  • 重复利用。 线程用完,再放回池子,可以达到重复利用的效果,节省资源。

线程池参数:

  • corePoolSize:常驻核心线程数。超过该值后如果线程空闲会被销毁。
  • maximumPoolSize:线程池能够容纳同时执行的线程最大数。
  • keepAliveTime:线程空闲时间,线程空闲时间达到该值后会被销毁,直到只剩下 corePoolSize 个线程为止,避免浪费内存资源。
  • workQueue:工作队列。
  • threadFactory:线程工厂,用来生产一组相同任务的线程。
  • handler:拒绝策略。

拒绝策略有以下几种:

  • AbortPolicy:丢弃任务并抛出异常
  • CallerRunsPolicy:重新尝试提交该任务
  • DiscardOldestPolicy 抛弃队列里等待最久的任务并把当前任务加入队列
  • DiscardPolicy 表示直接抛弃当前任务但不抛出异常。

简述线程池类型

  • newCachedThreadPool 可缓存线程池,可设置最小线程数和最大线程数,线程空闲1分钟后自动销毁。
  • newFixedThreadPool 指定工作线程数量线程池。
  • newSingleThreadExecutor 单线程Executor。
  • newScheduleThreadPool 支持定时任务的指定工作线程数量线程池。
  • newSingleThreadScheduledExecutor 支持定时任务的单线程Executor。

简述阻塞队列 

阻塞队列是生产者消费者的实现具体组件之一。当阻塞队列为空时,从队列中获取元素的操作将会被阻塞,当阻塞队列满了,往队列添加元素的操作将会被阻塞。具体实现有:

  • ArrayBlockingQueue:底层是由数组组成的有界阻塞队列。
  • LinkedBlockingQueue:底层是由链表组成的有界阻塞队列。
  • PriorityBlockingQueue:阻塞优先队列。
  • DelayQueue:创建元素时可以指定多久才能从队列中获取当前元素
  • SynchronousQueue:不存储元素的阻塞队列,每一个存储必须等待一个取出操作
  • LinkedTransferQueue:与LinkedBlockingQueue相比多一个transfer方法,即如果当前有消费者正等待接收元素,可以把生产者传入的元素立刻传输给消费者。
  • LinkedBlockingDeque:双向阻塞队列。

谈一谈ThreadLocal

ThreadLocal 是线程共享变量。ThreadLoacl 有一个静态内部类 ThreadLocalMap,其 Key 是 ThreadLocal 对象,值是 Entry 对象,ThreadLocalMap是每个线程私有的。

  • set 给ThreadLocalMap设置值。
  • get 获取ThreadLocalMap。
  • remove 删除ThreadLocalMap类型的对象。

存在的问题:对于线程池,由于线程池会重用 Thread 对象,因此与 Thread 绑定的 ThreadLocal 也会被重用,造成一系列问题。

比如说内存泄漏。由于 ThreadLocal 是弱引用,但 Entry 的 value 是强引用,因此当 ThreadLocal 被垃圾回收后,value 依旧不会被释放,产生内存泄漏。

简述ConcurrentHashMap

JDK7采用锁分段技术。首先将数据分成 Segment 数据段,然后给每一个数据段配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。

get 除读到空值不需要加锁。该方法先经过一次再散列,再用这个散列值通过散列运算定位到 Segment,最后通过散列算法定位到元素。put 须加锁,首先定位到 Segment,然后进行插入操作,第一步判断是否需要对 Segment 里的 HashEntry 数组进行扩容,第二步定位添加元素的位置,然后将其放入数组。

JDK8的改进

  • 取消分段锁机制,采用CAS算法进行值的设置,如果CAS失败再使用 synchronized 加锁添加元素
  • 引入红黑树结构,当某个槽内的元素个数超过8且 Node数组 容量大于 64 时,链表转为红黑树。
  • 使用了更加优化的方式统计集合内的元素数量

猜你喜欢

转载自blog.csdn.net/pachupingminku/article/details/132909937