并发呜呜呜

1、并行和并发有什么区别?

  1. 并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生;

  2. 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件;

  3. 在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如 Hadoop 分布式集群。所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。

2、进程和线程的区别?

1 . 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位。

2 . 并发性:进程可以并发执行,而且同一个进程内的多个线程也可以并发执行。

3 . 拥有资源:进程是拥有资源的基本单位,线程不拥有资源,但线程可以共享其隶属进程的系统资源。进程之间不能共享地址空间,而线程是共享所在进程的地址空间的。

4 . 系统开销:在创建或撤销进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤销线程时的开销。

3、守护线程是什么?

守护线程(即 Daemon thread),是个服务线程,准确地来说就是服务其他的线程。

4、创建线程的几种方式?

  1. 继承 Thread 类创建线程;
class MyThread extends Thread{  //创建线程类
    public void run(){
        System.out.println("Thread body");
    }
}
public class Test {
    public static void main(String[] args){
        MyThread thread = new MyThread();
        thread.start();//开启线程
    }
}
  1. 实现 Runnable 接口创建线程;
class MyThread implements Runnable{ //创建线程类
    public void run(){
        System.out.println("Thread body");
    }
}
public class Test {
    public static void main(String[] args){
        MyThread thread = new MyThread();
        Thread t = new Thread(thread);
        t.start();//开启线程
    }
}
  1. 通过 Callable 和 Future 创建线程;
    Callable 接口实际是属于Executor框架中的功能类,Callable接口与Runnable接口的功能类似,但提供了比Runnable更强大的功能。
    (1)Callable可以在任务结束后提供一个返回值,Runnable无法提供这个功能。
    (2)Callable中的call()方法可以抛出异常,而Runnable的run()方法不能抛出异常。
    (3)运行Callable可以拿到一个Future对象,Future对象表示异步计算的结果,它提供了检查计算是否完成的方法。由于线程属于异步计算模型,因此无法从别的线程中得到函数的返回值,在这种情况下,就可以使用Future来监视目标线程调用call()方法的情况,当调用Future的get()方法以获取结果时,当前线程就会堵塞,直到call()方法结束返回结果。
public class CallableAndFuture {
    //创建线程类
    public static class CallableTest implements Callable<String> {
        public String call() throws Exception{
            return "Hello World!";
        }
    }
    public static void main(String args[]){
        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        //启动线程
        Future<String> future = threadPool.submit(new CallableTest());
        try{
            System.out.println("waiting thread to finish");
            System.out.println(future.get());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

  1. 通过线程池创建线程。

5、实现runnable接口启动线程相比继承Thread类启动线程的优点

1.适合多个相同程序代码的线程去处理同一资源的情况;
2.可以避免由于java的单继承特性带来的局限性;
3.增强了程序的健壮性,代码能够被多个线程共享,代码与数据是独立的,所以,在开发中建议用Runnable接口实现多线程。

6、Runnable 和 Callable 有什么区别?

  1. Runnable 接口中的 run() 方法的返回值是 void,它做的事情只是纯粹地去执行 run() 方法中的代码而已;

  2. Callable 接口中的 call() 方法是有返回值的,是一个泛型,和 Future、FutureTask 配合可以用来获取异步执行的结果。

7、sleep() 和 wait() 的区别?

1 . Sleep()方法使正在执行的线程主动让出cpu (cpu就可以去执行其他任务),在sleep()指定时间后cpu 再回到该线程继续往下执行(只让出了cpu,没有释放同步资源);wait()让出同步资源锁,只有调用了notify() 或 notifyAll() 方法,才会使之前调用wait的线程有权利重新参与线程的调用。

2 . sleep()方法可以在任何地方使用,而wait()方法只能在同步方法或异步块中使用。

7.yield()礼让线程

  1. 礼让线程,让当前正在执行的线程暂停,但不阻塞
  2. 将线程从运行态转化为就绪状态
  3. 让CPU重新调度,礼让不一定成功,看CPU心情

8、线程的 run() 和 start() 有什么区别?

  1. 每个线程都是通过某个特定 Thread 对象所对应的方法 run() 来完成其操作的,方法 run() 称为线程体。通过调用 Thread 类的 start() 方法来启动一个线程;
  2. start() 方法来启动一个线程,真正实现了多线程运行。这时无需等待 run() 方法体代码执行完毕,可以直接继续执行下面的代码;
  3. run() 方法是在本线程里的,只是线程里的一个函数,如果直接调用 run(),其实就相当于是调用了一个普通函数而已;

9、在 Java 程序中怎么保证多线程的运行安全?

线程安全在三个方面体现:
1 . 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作 (atomic, synchronized, Lock);

在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。

Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

2 . 可见性:一个线程对主内存的修改可以及时地被其他线程看到 (synchronized, Lock, volatile);

3 . 有序性:即程序执行的顺序按照代码的先后顺序执行 (happens-before原则)

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

下面就来具体介绍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()方法的开始;

10、jion()方法,想象为插队

thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。

比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。

11、ThreadLocal特性

ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

做个不恰当的比喻,从表面上看ThreadLocal相当于维护了一个map,key就是当前的线程,value就是需要存储的对象。
这里的这个比喻是不恰当的,实际上是ThreadLocal的静态内部类ThreadLocalMap为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标就是value存储的对应位置。

ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,不同的点是
1 . Synchronized是通过线程等待,牺牲时间来解决访问冲突;
2 . ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。

当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。

12、说一说自己对于synchronized关键字的了解?

1 . synchronized 关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
2 . 另外,在Java早期版本中,synchronized 属于重量级锁,效率低下,(逻辑是这样的,每个对象都有一个监视器,获得了锁相当于获得了对象的监视器。)因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 JDK6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

13、synchronized和volatile区别?

1 . volatile是变量修饰符,而synchronized则作用于一段代码或方法。
2 . volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
3 . volatile保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存中和公共内存中的数据做同步。
4 . volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

13、synchronized 和Lock(ReenTrantLock)的区别?

共同点:都是独占锁,都可重入。

1 . 资源竞争激励的情况下,lock性能会比synchronize好,竞争不激励的情况下,synchronize比lock性能好。
2 . synchronize是在JVM层面实现的,系统会监控锁的释放与否。lock是代码实现的,需要手动释放,在finally块中释放。可以采用非阻塞的方式获取锁。
3 . 锁的细粒度和灵活度:Lock优于synchronized
4 . synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以响应中断
5 . ReentrantLock可以实现公平锁(new ReentrantLock(true);)

13、讲一下 synchronized 关键字的底层原理?

synchronized关键字底层原理属于JVM层面。

synchronized同步语句块的情况
public class SynchronizedDemo {
    public void method(){
        synchronized (this){
            System.out.println("djjya");
        }
    }
}

通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件,然后执行 javap -c -s -v -l SynchronizedDemo.class。
在这里插入图片描述
从上面我们可以看出:synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor的持有权。monitor 对象存在于每个 Java 对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么 Java 中任意对象可以作为锁的原因。当计数器为 0 则可以成功获取,获取后将锁计数器设为 1 也就是加 1。相应的在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

synchronized修饰方法的情况
public class SynchronizedDemo {
    public synchronized void method() {
        System.out.println("manong qiuzhi xiaozhushou");
    }
}

在这里插入图片描述
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

14、interrupt()方法

interrupt()方法只是改变中断状态而已,它不会中断一个正在运行的线程。它实际上完成的是,给受阻塞的线程发出一个中断信号,这样受阻线程就得以退出阻塞的状态。更确切的说,如果线程被 Object.wait,Thread.join和Thread.sleep() 三种方法之一阻塞,此时调用该线程的interrupt()方法,那么该线程将抛出一个InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态。如果线程没有被阻塞,这时调用interrupt()将不起作用,直到执行到wait(), sleep(), join()时,才马上会抛出InterruptedException.

14、Condition

Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,阻塞队列实际上是使用了Condition来模拟线程间协作。

Condition是个接口,基本的方法就是await()和signal()方法;
Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()
调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用
  Conditon中的await()对应Object的wait();

Condition中的signal()对应Object的notify();

Condition中的signalAll()对应Object的notifyAll()

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

15、乐观锁和悲观锁

1 . 乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和 CAS 算法实现。

应用:多读场景
乐观锁的两种实现方式:
(1)版本号机制: 一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数,当数据被修改时,version 值会加 1。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

(2)CAS 算法

2 . 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如:行锁、表锁、读锁、写锁等,都是在做操作之前先上锁。Java 中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现。
应用:多写场景

16、CAS算法

CAS 算法: 即 compare and swap(比较与交换),即不使用锁的情况下实现多线程之间的变量同步,CAS是一个原子操作,要么成功要么失败。

CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A(expect),要修改的新值B(update)。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

CAS优点:资源竞争不大的场景系统开销小

CAS缺点
(1)如果 CAS 长时间操作失败,即长时间自旋,会导致 CPU 开销大 , 但可以使用pause指令
(2)存在ABA问题,可以引入版本号来解决
(3)无法保证代码块的原子性,CAS只能保证单个变量的原子性操作,如果要保证多个变量的原子性操作就要使用悲观锁了。

17、 AQS

AQS(AbstractQueuedSynchronizer)抽象的队列同步器,AQS是一个同步器框架,Java中很多类底层都是使用AQS实现的,比如 : ReentranLock、CountDownLatch、ReentrantReadWriteLock,这些 java 同步类的内部会使用一个 Sync 内部类,而这个 Sync 继承了 AbstractQueuedSynchronizer 类.

AQS定义了3种资源共享方式:
1 . 独占锁,保证只有一条线程执行,比如ReentrantLock

2 . 共享锁,允许多个线程同时执行,比如CountDownLatch

3 . 同时实现独占和共享,比如ReentrantReadWriteLock, 允许多个线程同时执行读操作,只允许一个线程执行写操作。

AQS是JUC的核心,无论是信号量、CDL还是可重入锁、都有AQS的影子。这些类的同步过程如下:
在这里插入图片描述
tryAcquire和tryRelease过程很好理解,就是CAS地修改AQS的state值,关键是doAcquire和doRelease如何管理众多线程的状态,又如何决定哪个线程可以获得锁。答案就是,AQS在其内部管理了一个FIFO双向链表,所有的线程都会被添加到这个链表中进行管理,该链表就是CLH同步队列

17.1 AQS依赖CLH来完成同步状态管理

CLH是一种自旋锁,FIFO队列可以保证对进程的某种公平性和对避免饥饿的保证。

CLH同步队列是一个FIFO双向队列,AQS依赖它来完成同步状态的管理,当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。

在CLH同步队列中,一个节点表示一个线程,它保存着线程的引用(thread)、状态、前驱节点、后继节点。

需要把握的几个重点:

1 . CLH锁的节点对象只有一个active属性,关于其含义前面已经详细讨论过
2 . CLH锁的节点属性active的改变是由其自身触发的
3 . CLH锁是在前驱节点的active属性上进行自旋(唤醒线程的方式)

链接: link.

18、 堵塞队列

当阻塞队列是空,从队列中获取元素的操作会被阻塞;
当阻塞队列是满,往队列中添加元素的操作会被阻塞;

好处 :我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切阻塞队列都包办了。

常见的阻塞队列:
ArrayBlockingQueue由数组构成的有界阻塞队列.

LinkedBlockingQueue由链表构成的有界阻塞队列(默认值为Integer.MAX_VALUE)

阻塞队列应用场景:
生产模式,即生产者和消费者模式
线程池
消息中间件

19、 线程池

线程池提供了一种限制和管理资源(包括执行一个任务)的方式。每个线程池还维护一些基本统计信息。

线程池的好处

  • 提高线程的利用率
  • 提高程序的响应速度
  • 便于统一管理线程对象
  • 可以控制最大并发数

创建线程池的参数有哪些?
1 . corePoolSize(线程池中线程个数的基本大小) : 当提交一个任务到线程池时,如果当前poolSize < corePoolSize时, 线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前并启动所有基本线程。

2 . maximumPoolSize(线程池线程最大数量) :

3 . keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以,如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。

4 . TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微秒)。

5 . workQueue(任务队列):用于保存等待执行的任务的阻塞队列。

new ThreadPoolExecutor(corePoolSize:3, maximumPoolSize:5, keepAliveTime:1L, unit:TimeUnit.Seconds, 等待队列 , 线程工厂(Executors.defaultThreadFactory()), 拒绝策略(比如抛异常的方法))

在这里插入图片描述
饱和策略:
1 . 直接抛出异常
2 . 不处理,丢弃掉
3 . 丢弃队列里最近的一个任务,并执行当前任务
4 . 只用调用者所在线程来运行任务

如何创建线程池:
1 . 通过ThreadPoolExecutor的构造方法实现

我们可以创建三种类型的 ThreadPoolExecutor:

(1)FixedThreadPool:该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。

(2) SingleThreadExecutor:方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先进先出的顺序执行队列中的任务。

(3)CachedThreadPool:该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。

2 . 通过Executor框架的工具类Executors来实现
Executors创建线程池对象的弊端如下:
(1)FixedThreadPool 和 SingleThreadExecutor :允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。
(2)CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。

线程池中的线程数一般怎么设置?需要考虑哪些问题?
1 . 线程池中线程执行任务的性质: 计算密集型的任务比较占 cpu,所以一般线程数设置的大小 等于或者略微大于 cpu 的核数;但 IO 型任务主要时间消耗在 IO 等待上,cpu 压力并不大,所以线程数一般设置较大。
2 . cpu使用率:
3 . 内存使用率: 线程数过多和队列的大小都会影响此项数据,队列的大小应该通过前期计算线程池任务的条数,来合理的设置队列的大小,不宜过小,让其不会溢出,因为溢出会走拒绝策略,多少会影响性能,也会增加复杂度。
4 . 下游系统抗并发能力:多线程给下游系统造成的并发等于你设置的线程数,比如访问的是下游系统的接口,你就得考虑下游系统是否能抗的住这么多并发量,不能把下游系统打挂了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值