并发编程面试题

1.进程和线程还有协程之间的关系

1.1 进程
程序运行起来,进程会占用系统资源“cpu”,进程是系统资最小的单位,只是个线程提供执行空间,由于一个进程会产生一个进程地址空间,所以说每一个进程都是独立的,一个进程死掉不会影响其他线程。

1.2 线程
轻量级进程,线程是最小的执行单位,CPU分配轮片对象,尝试原因:提高CPU抢占概率。

1.3 协程
coroutine,也教轻量级线程,与传统的线程和进程对吧,协成是最有优势的“轻量级”,他可以轻轻松松建立上万个而不导致系统资源衰竭,而进程和线程很难超过1w个

2.并发和并行之间的区别

并发:指统一时间内,宏观上处理多个任务
并行:指统一时间内,真正上处理多个任务

3.Java中多线程实现的方式

3.1 Thread
通过继承Thread实现
**
3.2 Runable**
继承Thread类,实现Runable接口,但是除以上两种方法外,还可以通过Callable创建一个带返回值的线程,可以通过线程池进行线程创建。

4.Callable和Future模式

4.1 Callble
在java多线程中,一般一两种方式,一种是接触Thread,还有一种就是实现Runable,这两种的缺点是在线程任务行过后无法获取执行结果,我们一般是采用共享变量和共享内存地址以及线程通信的方式过去任务结果

不过,Java中,也提供了使用Callable和Future来实现获取任务结果的操作。Callable用来执行任务,产生结果,而Future用来获得结果。

Callable接口的定义如下:

public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

4. 2 Future模式

Future模式的核心在于:去除了主函数的等待时间,并使得原本需要等待的时间段可以用于处理其他业务逻辑

Futrure模式:对于多线程,如果线程A要等待线程B的结果,那么线程A没必要等待B,直到B有结果,可以先拿到一个未来的Future,等B有结果是再取真实的结果。(就如同照片的例子,刚开始缓存的图片就是Future,在图片缓存完后在进行替换)

5.线程池创建的方式(一般不适用Excecutors.nexxxx创建,一般使用ThreadPoolExecutor)

5.1 newCachedThreadPool
newCachedThreadPool:可缓存线程池,先查看池中有没有以前建立的线程,如果有,就直接使用;如果没有,就创建一个新的线程加入池中,缓存型池通常用于执行一些生存期很短的异步型任务;

package com.zzc.newCachedThreadPool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class newCachedThreadPool {
    //无限大侠线程池,JVM自动回收
    public static void main( String[] args ){
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        for (int i=1;i<=10;i++){
            newCachedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
        }
    }
}

5.2 newFixedThreadPool
    newFixedThreadPool:创建一个可重用固定个数的线程池,以共享的无界队列方式来运行这些线程;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class NewFixedThreadPoolTest {
public static void main( String[] args ){
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3);
for (int i=0;i<10;i++){
newFixedThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
}
}
}

5.3 newScheduledThreadPool
    newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class NewScheduledThreadPoolTest {
    public static void main( String[] args ) {
        ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(3);
        for (int i=0;i<10;i++){
            newScheduledThreadPool.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            },3, TimeUnit.SECONDS);
        }
    }
}

5.4 newSingleThreadExecutor
    newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class NewSingleThreadExecutorTest {
    public static void main( String[] args ){
        ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
        for (int i=0;i<10;i++){
            final int index=i;
            newSingleThreadExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"正在被执行,index:"+index);
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}

6.Java当中线程状态有哪些

6.1 初始状态(New):

线程对象创建出来后,没有调用start方法,线程处于初始状态;

6.2 运行状态:

1.就绪状态(Ready):调用了Start方法,等待CPU分配资源;

2.运行状态(RUNNING):CPU分配资源给该线程,该线程处于运行状态;

6.3 阻塞状态 (BLOCKED):

线程获取资源,如果资源获取成功则正常运行,如果资源获取失败,就处于阻塞状态,等待什么时候获取到资源再变为运行状态;

6.4 等待状态 (WAITING):

线程手动调用了wait()方法,或者join()方法,这些方法都是主动进入等待状态,等待状态会将CPU资源让渡,需要其他线程手动唤醒,notify(),notifyAll()唤起所有的等待线程;

6.5 超时等待状态 (TIMED_WAITING):

与等待状态相同,都是主动进入等待,也是需要其他线程唤醒,但是区别在与超时等待,如果超过了等待时间,则自动唤醒;

6.6 终止状态(DIED):

线程结束之后的状态;

7.多线程中的常用方法

7.1 start()方法

用于启动一个线程,使相应的线程进入排队等待状态。一旦轮到它使用CPU的资源的时候,它就可以脱离它的主线程而独立开始自己的生命周期了。注意即使相应的线程调用了start方法,但相关的线程也不一定会立刻执行,调用start方法的主要目的是使当前线程进入排队等待。不一定就立刻得到cpu的使用权限…

7.2 run()方法

Thread类和Runnable接口中的run方法的作用相同,都是系统自动调用而用户不得调用的。

7.3 sleep()方法

是Java中Thread类中的方法,会使当前线程暂停执行让出cpu的使用权限。但是监控状态依然存在,即如果当前线程进入了同步锁的话,sleep方法并不会释放锁,即使当前线程让出了cpu的使用权限,但其它被同步锁挡在外面的线程也无法获得执行。待到sleep方法中指定的时间后,sleep方法将会继续获得cpu的使用权限而后继续执行之前sleep的线程。

7.4 wait()方法

是Object类的方法,wait方法指的是一个已经进入同步锁的线程内,让自己暂时让出同步锁,以便其它正在等待此同步锁的线程能够获得机会执行。,只有其它方法调用了notify或者notifyAll(需要注意的是调用notify或者notifyAll方法并不释放锁,只是告诉调用wait方法的其它 线程可以参与锁的竞争了…)方法后,才能够唤醒相关的线程。此外注意wait方法必须在同步关键字修饰的方法中才能调用。

7.5 notify()方法

唤醒在此对象监视器上等待的单个线程,使其进入“就绪状态”。

7.6 notifyAll()方法

唤醒在此对象监视器上等待的所有线程,使其进入“就绪状态”。

8.线程状态流程图在这里插入图片描述

新建(new):
 新创建了一个线程对象。

可运行(runnable):
  线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。

运行(running):
  可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。

阻塞(block):
  阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:

等待阻塞:

运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。

同步阻塞:

运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。

其他阻塞:

运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。

死亡(dead):
  线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

9.volatile关键字有什么用途,和Synchronize有什么区别

volatile用途:
    volatile是一个轻量级的Synchronize,保证了共享变量的可见性,能够防止脏读,被volatile关键字修饰的变量,如果值发生了改变,其他线程立刻可见
区别:

1)volatile只能作用于变量,使用范围较小。synchronized可以用在变量、方法、类、同步代码块等,使用范围比较广。

(2)volatile只能保证可见性和有序性,不能保证原子性。而可见性、有序性、原子性synchronized都可以包证。

(3)volatile不会造成线程阻塞。synchronized可能会造成线程阻塞。

什么是脏读,为什么发生脏读

脏读是指当一个事务正在访问数据,并且对数据进行了修改。而这种修改还没有提交到数据库中,这时,另外一个事务也访问了这个数据,然后使用了这个数据。

10.指令重排和先行发生原则

10.1 程序次序原则:

在一个线程内,按照代码的顺序,书写在前面的代码优先于书写后面的代码;

10.2 管程锁定规则:

一个unlock操作先行发生于后面对同一个锁的lock操作,注意是同一个锁;

10.3 volatile原则:

对于一个volatile变量的写操作先行发生于后面对变量的读操作;

10.4 线程启动原则:

Thread对象的start()方法优先于此线程的每一个动作;

10.5 线程终止原则:

线程中所有的操作都优先发生于此线程的每一个动作;

10.6 对象中断原则:

对象的interrupt()方法的调用优先发生于被中断线程的代码监测中断事件的发生;先中断再检测;

10.7 对象终结原则:

一个对象的初始化(构造函数执行完毕)完成优先发生于它的finalize()方法的开始;

10.8 传递性

如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;

11.并发编程线程安全三要素

11.1 原子性(Synchronized, Lock)

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

在Java中,基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

11.2 有序性(Volatile,Synchronized, Lock)

即程序执行的顺序按照代码的先后顺序执行。

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

11.3 可见性(Volatile,Synchronized,Lock)

指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取共享变量时,它会去内存中读取新值。

普通的共享变量不能保证可见性,因为普通共享变量被修改后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

12.进程和线程间调度算法:

先来先服务调度算法
短作业优先调度算法
高响应调度法
时间片轮训算法
优先级调度算法

Java中采用抢占式

13.Java开发中用过哪些锁:

13.2 乐观锁
    乐观锁顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS(Compare and Swap 比较并交换)实现的

乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升;

乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

13.2 悲观锁
    悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。比如Java里面的同步原语synchronized关键字的实现就是悲观锁。

悲观锁适合写操作非常多的场景;

悲观锁在Java中的使用,就是利用各种锁;

13.3 独享锁
    独享锁是指该锁一次只能被一个线程所持有。

独享锁通过AQS来实现的,通过实现不同的方法,来实现独享锁。

对于Synchronized而言,当然是独享锁。

13.4 共享锁
    共享锁是指该锁可被多个线程所持有。

读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。

共享锁也是通过AQS来实现的,通过实现不同的方法,来实现共享锁。

13.5 互斥锁
    互斥锁在Java中的具体实现就是ReentrantLock。

13.6 读写锁
    读写锁在Java中的具体实现就是ReadWriteLock。

13.7 可重入锁    
    重入锁也叫作递归锁,指的是同一个线程外层函数获取到一把锁后,内层函数同样具有这把锁的控制权限;
    synchronized和ReentrantLock就是重入锁对应的实现;
    synchronized重量级的锁 ;
    ReentrantLock轻量级的锁;

13.8 公平锁
    公平锁是指多个线程按照申请锁的顺序来获取锁。

对于Java ReetrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。

13.9 非公平锁
    非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。

对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

13.10 分段锁
    分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。

我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7和JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。

当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在哪一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。

但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。

分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

13.11 偏向锁  
    偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。

13.12 轻量级锁
    轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

13.13 重量级锁
    重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让他申请的线程进入阻塞,性能降低。

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

14.sync关键字理解

使用了synchronized关键字可以轻松地解决多线程共享数据同步问题。

synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果再细的分类,synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。

synchronized取得的锁都是对象;每个对象只有一个锁(lock)与之相关联;实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

synchronized的4种用法:

1. 方法声明时使用,线程获得的是成员锁;

2. 对某一代码块使用,synchronized后跟括号,括号里是变量,线程获得的是成员锁;

3. synchronized后面括号里是一对象,此时,线程获得的是对象锁;

4. synchronized后面括号里是类,此时,线程获得的是对象锁;

15.CAS无锁机制

CAS:
 Compare and Swap,即比较交换;

jdk1.5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronized同步锁的一种乐观锁。jdk1.5之前java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是悲观锁;

本身无锁,采用乐观锁的思想,在数据操作时对比数据是否一致,如果一致代表之前没有线程操作该数据,那么就会更新数据,如果不一致代表有县城更新则重试;

CAS当中包含三个参数CAS(V,E,N),V标识要更新的变量,E标识预期值,N标识新值

运行过程:
1.线程访问时,先会将主内存中的数据同步到线程的工作内存当中;

2.假设线程A和线程B都有对数据进行更改,那么假如线程A先获取到执行权限;

3.线程A先会对比工作内存当中的数据和主内存当中的数据是否一致,如果一致(V==E)则进行更新,不一致则刷新数据,重新循环判断;

4.这时更新完毕后,线程B也要进行数据更新,主内存数据和工作内存数据做对比,如果一致则进行更新,不一致则将主内存数据重新更新到工作内存,然后循环再次对比两个内存中的数据,直到一致为止; 
    
CAS无锁机制存在一个问题

ABA问题,如果将原来A的值改为了B,然后又改回了A,虽然最终结果没有发生改变,但是在过程中是对该数据进行了修改操作

解决该问题:在Java中并发包下有一个原子类:AtomicStampedReference,在该类当中通过版本控制判断值到底是否被修改

解释:如果对值进行了更改则版本号+1,那么在CAS当中不仅仅对比变量的值,还要对比版本号,如果值和版本号都相等则代表没有被修改,如果有一方不相等代表进行过更改

那么就从主内存中重新刷新数据到工作内存然后循环对比,直到成功为止~
16.AQS

17.ReentrantLock底层实现
公平和非公平

18.ReentrantLock和sync之间的区别

19.ReentrantReadWriteLock

20.BlockingQueue阻塞队列的实现方式

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列,这两个附加的操作是:

在队列为空时,获取元素的线程会等待队列变为非空;

当队列满时,存储元素的线程会等待队列可用;

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器拿元素;

在java中,BlockingQueue的接口位于java.util.concurrent包中,阻塞队列是线程安全的;

在新增呢的concurrent包中,BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题,通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利;

常用的队列主要由以下两种:

1.先进先出(FIFO):
    先插入的队列的元素也最先出队列,类似于排队的功能,从某种程度上来说这种队列也体现了一种公平性;

2.后进后出(LIFO):
    后插入队列的元素最先出队列,这种队列优先处理最近发生的事件;
21.ConcurrentLinkedQueue

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值