2021准备面试-多线程

多线程

拒绝白嫖,记得三连哦

1、什么是线程?

线程,有时被称为轻量进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。
线程是程序中一个单一的顺序控制流程。进程内有一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指令运行时的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。

2、什么是线程安全和线程不安全?

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
只有线程之间共享了数据,才会出现安全问题,对于共享的数据,要么通过代码去保证安全,要么其本身就能够保证安全

3、什么是自旋锁?

自旋锁(Spinlock)是一种广泛运用的底层同步机制。自旋锁是一个互斥设备,它只有两个值:“锁定”和“解锁”。它通常实现为某个整数值中的某个位。希望获得某个特定锁得代码测试相关的位。如果锁可用,则“锁定”被设置,而代码继续进入临界区;相反,如果锁被其他人获得,则代码进入忙循环(而不是休眠,这也是自旋锁和一般锁的区别)并重复检查这个锁,直到该锁可用为止,这就是自旋的过程

4、什么是CAS?

cas是比较并交换,属乐观锁,当线程并发程度不高时适用,尝试更新
CAS的缺点:
1.CPU开销较大

在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

2.不能保证代码块的原子性

CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。

3.ABA问题
如果一个值原来是A,变成了B,然后又变成了A,适用CAS检查会认为没有发生变化,实际上却变化了。
这是CAS机制最大的问题所在。

5、什么是乐观锁和悲观锁?

悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized独占锁就是悲观锁思想的实现。
乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
两种锁的使用场景
从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
乐观锁常见的两种实现方式

乐观锁一般会使用版本号机制或CAS算法实现。
版本号机制
一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

6、什么是AQS?

AQS是JDK1.5提供的一个基于FIFO等待队列实现的一个用于实现同步器的基础框架,这个基础框架的重要性可以这么说,JCU包里面几乎所有的有关锁、多线程并发以及线程同步器等重要组件的实现都是基于AQS这个框架。**AQS的核心思想是基于volatile int state这样的一个属性同时配合Unsafe工具对其原子性的操作来实现对当前锁的状态进行修改。**当state的值为0的时候,标识改Lock不被任何线程所占有。
AQS的等待队列

作为AQS的核心实现的一部分,举个例子来描述一下这个队列长什么样子,我们假设目前有三个线程Thread1、Thread2、Thread3同时去竞争锁,如果结果是Thread1获取了锁,Thread2和Thread3进入了等待队列,那么他们的样子如下:
2021准备面试-多线程_第1张图片
AQS的等待队列基于一个双向链表实现的,HEAD节点不关联线程,后面两个节点分别关联Thread2和Thread3,他们将会按照先后顺序被串联在这个队列上。这个时候如果后面再有线程进来的话将会被当做队列的TAIL。

7、什么是原子操作?在Java Concurrency API中有哪些原子类(atomic classes)?

原子操作是不可被中断的一个或一系列操作,在java中通过锁和循环cas的方式来实现原子操作
原子更新基本类型类
AtomicBoolean、AtomicInteger、AtomicLong
原子更新数组类
AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
原子更新引用类型类
AtomicReference、AtomicReferenceFieldUpdater、AtomicMarkableReference
原子更新字段类
AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicStampedReference

8、什么是Executors框架?

executor是用户级的调度器,管理若干线程来执行用户任务,Executor框架由3大部分组成:
1、任务-包括被执行任务需要实现的接口:Runnable接口或者Callable接口
2、任务的执行-包括任务执行机制的核心接口Executor,以及继承Executor的ExecutorService接口。Executor框架由两个关键类实现了ExecutorService接口(ThreadPoolExecutor和ScheduledThreadPoolExecutor)
3、异步计算的结果-包括Future和实现Future接口的FutureTask类

首先创建Runnable或者Callable接口的任务对象,然后调用ExecutorService执行,Runnable使用execute方法,Callable使用submit方法

9、什么是阻塞队列?如何使用阻塞队列来实现生产者-消费者模型?

阻塞队列 (BlockingQueue)是Java util.concurrent包下重要的数据结构,BlockingQueue提供了线程安全的队列访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。并发包下很多高级同步类的实现都是基于BlockingQueue实现的。
阻塞队列原理:
其实阻塞队列实现阻塞同步的方式很简单,使用的就是是lock锁的多条件(condition)阻塞控制。使用BlockingQueue封装了根据条件阻塞线程的过程,而我们就不用关心繁琐的await/signal操作了。
使用阻塞队列实现生产者-消费者很方便,使用阻塞队列的put和take方法,方法是阻塞的,当没有数据时会阻塞线程。

10、什么是Callable和Future?

Callable接口代表一段可以调用并返回结果的代码;Future接口表示异步任务,是还没有完成的任务给出的未来结果。所以说Callable用于产生结果,Future用于获取结果。

11、什么是FutureTask?

FutureTask一个可取消的异步计算,FutureTask 实现了Future的基本方法,提空 start cancel 操作,可以查询计算是否已经完成,并且可以获取计算的结果。结果只可以在计算完成之后获取,get方法会阻塞当计算没有完成的时候,一旦计算已经完成,那么计算就不能再次启动或是取消。

一个FutureTask 可以用来包装一个 Callable 或是一个runnable对象。因为FurtureTask实现了Runnable方法,所以一个 FutureTask可以提交(submit)给一个Excutor执行(excution).

12、什么是同步容器和并发容器的实现?

同步容器:可以简单地理解为通过synchronized来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如Vector,Hashtable,以及Collections.synchronizedSet,synchronizedList等方法返回的容器。这些类实现线程安全的方式是:将他们的状态封装起来,并对每个公有的方法都进行同步,使得每次只有一个线程能访问容器的状态。
并发容器:针对多个线程设计的,用并发容器来代替同步容器,可以极大地提高伸缩性并降低风险。如ConcurrentHashMap,CopyOnWriteArrayList等。并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在ConcurrentHashMap中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问map,并且执行读操作的线程和写操作的线程也可以并发的访问map,同时允许一定数量的写操作线程并发地修改map,所以它可以在并发环境下实现更高的吞吐量。

13、什么是多线程?优缺点?

多线程(英语:multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理(Chip-level multithreading)或同时多线程(Simultaneous multithreading)处理器。 在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理(Multithreading)”。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程(台湾译作“执行绪”),进而提升整体处理性能。
优点:
1)更多的处理器核心
2)更快的响应时间
3)更好的编程模型
缺点:
1)程序更加复杂
2)容易出现更多的问题

14、什么是多线程的上下文切换?

时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,当前任务执行一个时间片后会切换到下一个任务,切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态,任务从保存到再加载的过程就是一次上下文的切换。

15、ThreadLocal的设计理念与作用?

ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

  • ThreadLocal 并不解决线程间共享数据的问题
  • ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
  • 每个线程持有一个 Map 并维护了 ThreadLocal 对象与具体实例的映射,该 Map 由于只被持有它的线程访问,故不存在线程安全以及锁的问题
  • ThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用,避免了 ThreadLocal 对象无法被回收的问题
  • ThreadLocalMap 的 set 方法通过调用 replaceStaleEntry 方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏
  • ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景

16、ThreadPool(线程池)用法与优势?

1.线程池创建(直接使用ThreadPoolExecutor创建)
int corePoolSize = 2;//核心线程数
int maximumPoolSize = 4;//最大线程数
int keepAliveTime = 60;//线程活动保持时间
TimeUnit timeUnit = TimeUnit.MILLISECONDS;//线程活动保持时间的单位
BlockingQueue<Runnable> taskQueue = new ArrayBlockingQueue<Runnable>(10);//任务队列,基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序
ThreadFactory threadFactory = new ThreadFactory(){
     
    @Override
    public Thread newThread(Runnable r) {
     
        return new Thread(r);
    }
};//用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字
ThreadPoolExecutor.CallerRunsPolicy handler = new ThreadPoolExecutor.CallerRunsPolicy();//饱和策略,只用调用者所在线程来运行任务(通常会是主线程)
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, timeUnit, taskQueue, threadFactory, handler);

2.参数说明

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

2)maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。

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

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

5)runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列。

ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。

LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。

SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。

PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

6)ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。

7)RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。

                    AbortPolicy:直接抛出异常。

                    CallerRunsPolicy:使用调用者所在线程来运行任务。

                    DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。

                    DiscardPolicy:不处理,丢弃掉。

                    自定义:当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。
合理利用线程池能够带来三个好处。
  • 第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。但是要做到合理的利用线程池,必须对其原理了如指掌。

参考资料http://blog.qianxuefeng.com/article/154

17、synchronized和ReentrantLock的区别?

两者的共同点:
1)协调多线程对共享对象、变量的访问
2)可重入,同一线程可以多次获得同一个锁
3)都保证了可见性和互斥性
两者的不同点:
1)ReentrantLock显示获得、释放锁,synchronized隐式获得释放锁
2)ReentrantLock可响应中断、可轮回,synchronized是不可以响应中断的,为处理锁的不可用性提供了更高的灵活性
3)ReentrantLock是API级别的,synchronized是JVM级别的
4)ReentrantLock可以实现公平锁
5)ReentrantLock通过Condition可以绑定多个条件
6)底层实现不一样, synchronized是同步阻塞,使用的是悲观并发策略,lock是同步非阻塞,采用的是乐观并发策略

18、Semaphore有什么作用?

semaphore是用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理使用公共资源

19、Lock接口(Lock interface)是什么?对比同步它有什么优势?

  1. **操作方面(锁控制):**Lock 是可以手动控制加锁与释放锁操作的。而synchronized自动释放锁。
  2. **性能方面:**使用到 Lock 接口 的实现类 ReadWriteLock 子类 ReentrantReadWriteLock 的 对象rwl,在多线程同时对静态资源进行修改时,可以使用 rwl.readLock().lock() 或者 rwl.writeLock().lock() 对静态资源做并发修改控制。读写锁可以实现读写互斥,但是读读不互斥,这个是synchroized实现不了的,synchroized对读读也互斥。
  3. **线程通信方面:**Lock对应的实现类对象lock 通过 lock.newCondition() 可以实例化Condition对象condition,condition 通过 condition.await() 实现synchronized + t.wait() 的效果,t代表线程,通过condition.signal()或者 condition.signalAll()实现线程唤醒,且lock 可以为读写线程创建两种不同操作(read or write)类型的Condition对象,使得线程间通信要比传统的wait(),notifiy()进行线程通信的效率要高很多。使得加锁,释放锁的操作更具选择性,精准性。

20、Hashtable的size()方法中明明只有一条语句”return count”,为什么还要做同步?

· 由于非同步方法可以多个线程同时访问,如果其他线程正在对hashtable进行添加或者删除操作,当已经添加或者删除后,还没有对size进行修改,这时获得的size值就不正确,所以需要进行同步。

21、ConcurrentHashMap的并发度是什么?

java8之前,concurrentHashMap是segment的概念,有多少segment其最大并发量就是多少,到java8,对concurrentHashmap进行了调整,摒弃了segment的概念,采用了hash表加红黑树作为底层实现,加锁的对象是一个节点,所以节点有多少,其并发度就是多少。

22、ReentrantReadWriteLock读写锁的使用?

ReentrantReadWriteLock会使用两把锁来解决问题,一个读锁,一个写锁线程进入读锁的前提条件:没有其他线程的写锁,没有写请求或者有写请求,但调用线程和持有锁的线程是同一个线程进入写锁的前提条件:没有其他线程的读锁没有其他线程的写锁
ReentrantReadWriteLock,首先要做的是与ReentrantLock划清界限。它和后者都是单独的实现,彼此之间没有继承或实现的关系。然后就是总结这个锁机制的特性了:(a).重入方面其内部的WriteLock可以获取ReadLock,但是反过来ReadLock想要获得WriteLock则永远都不要想。(b).WriteLock可以降级为ReadLock,顺序是:先获得WriteLock再获得ReadLock,然后释放WriteLock,这时候线程将保持Readlock的持有。反过来ReadLock想要升级为WriteLock则不可能,为什么?参看(a),呵呵.©.ReadLock可以被多个线程持有并且在作用时排斥任何的WriteLock,而WriteLock则是完全的互斥。这一特性最为重要,因为对于高读取频率而相对较低写入的数据结构,使用此类锁同步机制则可以提高并发量。(d).不管是ReadLock还是WriteLock都支持Interrupt,语义与ReentrantLock一致。(e).WriteLock支持Condition并且与ReentrantLock语义一致,而ReadLock则不能使用Condition,否则抛出UnsupportedOperationException异常。

23、CyclicBarrier和CountDownLatch的用法及区别?

CyclicBarrier和CountDownLatch 都位于java.util.concurrent 这个包下

CountDownLatch CyclicBarrier
减计数方式 加计数方式
计算为0时释放所有等待的线程 计数达到指定值时释放所有等待线程
计数为0时,无法重置 计数达到指定值时,计数置为0重新开始
调用countDown()方法计数减一,调用await()方法只进行阻塞,对计数没任何影响 调用await()方法计数加1,若加1后的值不等于构造方法的值,则线程阻塞
不可重复利用 可重复利用
一、CountDownLatch用法
CountDownLatch类只提供了一个构造器:

public CountDownLatch(int count) { }; //参数count为计数值
然后下面这3个方法是CountDownLatch类中最重要的方法:

public void await() throws InterruptedException { }; //调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { }; //和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public void countDown() { }; //将count值减1
CountDownLatch, 一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。

二、CyclicBarrier用法
CyclicBarrier会在所有线程任务结束之后,才会进行后续任务,具体可以看下面例子。

CyclicBarrier提供2个构造器:

public CyclicBarrier(int parties, Runnable barrierAction) {
}

public CyclicBarrier(int parties) {
}
参数parties指让多少个线程或者任务等待至barrier状态;参数barrierAction为当这些线程都达到barrier状态时会执行的内容。
CyclicBarrier中最重要的方法就是await方法

//挂起当前线程,直至所有线程都到达barrier状态再同时执行后续任务;
public int await() throws InterruptedException, BrokenBarrierException { };

//让这些线程等待至一定的时间,如果还有线程没有到达barrier状态就直接让到达barrier的线程执行后续任务
public int await(long timeout, TimeUnit unit)throws InterruptedException,BrokenBarrierException,TimeoutException { };

24、LockSupport工具?

1、LockSupport基本介绍与基本使用

LockSupport是JDK中比较底层的类,用来创建锁和其他同步工具类的基本线程阻塞。java锁和同步器框架的核心 AQS: AbstractQueuedSynchronizer,就是通过调用 LockSupport .park()和 LockSupport .unpark()实现线程的阻塞和唤醒 的。

LockSupport 很类似于二元信号量(只有1个许可证可供使用),如果这个许可还没有被占用,当前线程获取许可并继 续 执行;如果许可已经被占用,当前线 程阻塞,等待获取许可。

全部操作:

  • park()/park(Object)
  • 等待通行准许。
  • parkNanos(long)/parkNanos(Object, long)
  • 在指定运行时间(即相对时间)内,等待通行准许。
  • parkUntil(long)/parkUntil(Object, long)
  • 在指定到期时间(即绝对时间)内,等待通行准许。
  • unpark(Thread)
  • 发放通行准许或提前发放。(注:不管提前发放多少次,只用于一次性使用。)
  • getBlocker(Thread)
  • 进入等待通行准许时,所提供的对象。

主要用途:

当前线程需要唤醒另一个线程,但是只确定它会进入阻塞,但不确定它是否已经进入阻塞,因此不管是否已经进入阻塞,还是准备进入阻塞,都将发放一个通行准许

正确用法:

把LockSupport视为一个sleep()来用,只是sleep()是定时唤醒,LockSupport既可以定时唤醒,也可以由其它线程唤醒

public static void main(String[] args)
{
     
     LockSupport.park();
     System.out.println("block.");
}

运行该代码,可以发现主线程一直处于阻塞状态。因为 许可默认是被占用的 ,调用park()时获取不到许可,所以进入阻塞状态。

如下代码:先释放许可,再获取许可,主线程能够正常终止。LockSupport许可的获取和释放,一般来说是对应的,如果多次unpark,只有一次park也不会出现什么问题,结果是许可处于可用状态。

public static void main(String[] args)
{
     
     Thread thread = Thread.currentThread();
     LockSupport.unpark(thread);//释放许可
     LockSupport.park();// 获取许可
     System.out.println("b");
}

LockSupport是不可重入 的,如果一个线程连续2次调用 LockSupport .park(),那么该线程一定会一直阻塞下去。

public static void main(String[] args) throws Exception
{
     
  Thread thread = Thread.currentThread();
  
  LockSupport.unpark(thread);
  
  System.out.println("a");
  LockSupport.park();
  System.out.println("b");
  LockSupport.park();
  System.out.println("c");
}

这段代码打印出a和b,不会打印c,因为第二次调用park的时候,线程无法获取许可出现死锁。

25、Condition接口及其实现原理?

  • 在java.util.concurrent包中,有两个很特殊的工具类,Condition和ReentrantLock,使用过的人都知道,ReentrantLock(重入锁)是jdk的concurrent包提供的一种独占锁的实现
  • 我们知道在线程的同步时可以使一个线程阻塞而等待一个信号,同时放弃锁使其他线程可以能竞争到锁
  • 在synchronized中我们可以使用Object的wait()和notify方法实现这种等待和唤醒
  • 但是在Lock中怎么实现这种wait和notify呢
  • 答案是Condition,学习Condition主要是为了方便以后学习blockqueue和concurrenthashmap的源码,同时也进一步理解ReentrantLock。

ReentrantLock和Condition的使用方式通常是这样的:

public static void main(String[] args) {
     
    final ReentrantLock reentrantLock = new ReentrantLock();
    final Condition condition = reentrantLock.newCondition();
 
    Thread thread = new Thread((Runnable) () -> {
     
            try {
     
                reentrantLock.lock();
                System.out.println("我要等一个新信号" + this);
                condition.wait();
            }
            catch (InterruptedException e) {
     
                e.printStackTrace();
            }
            System.out.println("拿到一个信号!!" + this);
            reentrantLock.unlock();
    }, "waitThread1");
 
    thread.start();
     
    Thread thread1 = new Thread((Runnable) () -> {
     
            reentrantLock.lock();
            System.out.println("我拿到锁了");
            try {
     
                Thread.sleep(3000);
            }
            catch (InterruptedException e) {
     
                e.printStackTrace();
            }
            condition.signalAll();
            System.out.println("我发了一个信号!!");
            reentrantLock.unlock();
    }, "signalThread");
     
    thread1.start();
}

运行后,结果如下:

我要等一个新信号lock.ReentrantLockTest$1@a62fc3
我拿到锁了
我发了一个信号!!
拿到一个信号!!

可以看到

Condition的执行方式,是当在线程1中调用await方法后,线程1将释放锁,并且将自己沉睡,等待唤醒,

线程2获取到锁后,开始做事,完毕后,调用Condition的signal方法,唤醒线程1,线程1恢复执行。

以上说明Condition是一个多线程间协调通信的工具类,使得某个,或者某些线程一起等待某个条件(Condition),只有当该条件具备( signal 或者 signalAll方法被带调用)时 ,这些等待线程才会被唤醒,从而重新争夺锁。

Condition自己也维护了一个队列,该队列的作用是维护一个等待signal信号的队列,两个队列的作用是不同,事实上,每个线程也仅仅会同时存在以上两个队列中的一个,流程是这样的

  • 线程1调用reentrantLock.lock时,线程被加入到AQS的等待队列中。
  • 线程1调用await方法被调用时,该线程从AQS中移除,对应操作是锁的释放。
  • 接着马上被加入到Condition的等待队列中,以为着该线程需要signal信号。
  • 线程2,因为线程1释放锁的关系,被唤醒,并判断可以获取锁,于是线程2获取锁,并被加入到AQS的等待队列中。
  • 线程2调用signal方法,这个时候Condition的等待队列中只有线程1一个节点,于是它被取出来,并被加入到AQS的等待队列中。 注意,这个时候,线程1 并没有被唤醒。
  • signal方法执行完毕,线程2调用reentrantLock.unLock()方法,释放锁。这个时候因为AQS中只有线程1,于是,AQS释放锁后按从头到尾的顺序唤醒线程时,线程1被唤醒,于是线程1回复执行。
  • 直到释放所整个过程执行完毕。
  • 可以看到,整个协作过程是靠结点在AQS的等待队列和Condition的等待队列中来回移动实现的,Condition作为一个条件类,很好的自己维护了一个等待信号的队列,并在适时的时候将结点加入到AQS的等待队列中来实现的唤醒操作。

26、Fork/Join框架的理解?

  1. Fork/Join是什么
    Oracle的官方给出的定义是:Fork/Join框架是一个实现了ExecutorService接口的多线程处理器。它可以把一个大的任务划分为若干个小的任务并发执行,充分利用可用的资源,进而提高应用的执行效率。

我们再通过Fork和Join这两个单词来理解下Fork/Join框架,Fork就是把一个大任务切分为若干子任务并行的执行,Join就是合并这些子任务的执行结果,最后得到这个大任务的结果。

比如计算1+2+。。+10000,可以分割成10个子任务,每个子任务分别对1000个数进行求和,最终汇总这10个子任务的结果。

工作窃取算法
工作窃取算法是指线程从其他任务队列中窃取任务执行(可能你会很诧异,这个算法有什么用。待会你就知道了)。考虑下面这种场景:有一个很大的计算任务,为了减少线程的竞争,会将这些大任务切分为小任务并分在不同的队列等待执行,然后为每个任务队列创建一个线程执行队列的任务。那么问题来了,有的线程可能很快就执行完了,而其他线程还有任务没执行完,执行完的线程与其空闲下来不如帮助其他线程执行任务,这样也能加快执行进程。所以,执行完的空闲线程从其他队列的尾部窃取任务执行,而被窃取任务的线程则从队列的头部取任务执行(这里使用了双端队列,既不影响被窃取任务的执行过程又能加快执行进度)。

从以上的介绍中,能够发现工作窃取算法的优点是充分利用线程提高并行执行的进度。当然缺点是在某些情况下仍然存在竞争,比如双端队列只有任务需要执行的时候

使用Fork/Join框架分为两步:
分割任务:首先需要创建一个ForkJoin任务,执行该类的fork方法可以对任务不断切割,直到分割的子任务足够小

合并任务执行结果:子任务执行的结果同一放在一个队列中,通过启动一个线程从队列中取执行结果。

Fork/Join实现了ExecutorService,所以它的任务也需要放在线程池中执行。它的不同在于它使用了工作窃取算法,空闲的线程可以从满负荷的线程中窃取任务来帮忙执行。
下面是计算1+2+3+4为例演示如何使用使用Fork/Join框架:


```c

```java
package com.rhwayfun.concurrency.r0406;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;

/**

   - Created by rhwayfun on 16-4-6.
*/
public class CountTask extends RecursiveTask{
     

//阈值
private static final int THRESHOLD = 2;
//起始值
private int start;
//结束值
private int end;

public CountTask(int start, int end) {
     
this.start = start;
this.end = end;
}

@Override
protected Integer compute() {
     
	boolean compute = (end - start) <= THRESHOLD;
	int res = 0;
	if (compute){
     
		for (int i = start; i <= end; i++){
     
		res += i;
		}
	}else {
     
		//如果长度大于阈值,则分割为小任务
		int mid = (start + end) / 2;
		CountTask task1 = new CountTask(start,mid);
		CountTask task2 = new CountTask(mid + 1, end);
		//计算小任务的值
		task1.fork();
		task2.fork();
		//得到两个小任务的值
		int task1Res = task1.join();
		int task2Res = task2.join();
		res = task1Res + task2Res;
	}
return res;
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
     
	ForkJoinPool pool = new ForkJoinPool();
	CountTask task = new CountTask(1,5);
	ForkJoinTask submit = pool.submit(task);
	System.out.println("Final result:" + submit.get());
	}
}

代码执行结果为:

15
代码中使用了FokJoinTask,其与一般任务的区别在于它需要实现compute方法,在方法需要判断任务是否在阈值区间内,如果不是则需要把任务切分到足够小,直到能够进行计算。

每个被切分的子任务又会重新进入compute方法,再继续判断是否需要继续切分,如果不需要则直接得到子任务执行的结果,如果需要的话则继续切分,如此循环,直到调用join方法得到最终的结果。
**
可以发现Fork/Join框架的需要把提交给ForkJoinPool,ForkJoinPool由ForkJoinTask数组和ForkJoinWorkerThread数组组成,前者负责将存放程序提交给ForkJoinPool的任务,后者则负责执行这些任务。关键在于在于fork方法与join方法**

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

sleep()

方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。

因为sleep() 是static静态的方法,他不能改变对象的机锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。

wait()

wait()是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程

28、线程的五个状态(五种状态,创建、就绪、运行、阻塞和死亡)?

线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。

  • 第一是创建状态。在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。
  • 第二是就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
  • 第三是运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。
  • 第四是阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。
  • 第五是死亡状态。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪

29、start()方法和run()方法的区别?

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

start()方法启动一个线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码;
这时此线程是处于就绪状态, 并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。然后CPU再调度其它线程。

run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。
如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。

30、Runnable接口和Callable接口的区别?

有点深的问题了,也看出一个Java程序员学习知识的广度。

  • Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
  • Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。

这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。而Callable+Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,真的是非常有用。

31、volatile关键字的作用?

volatile关键字的作用主要有两个:

(1)多线程主要围绕可见性和原子性两个特性而展开,使用volatile关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到volatile变量,一定是最新的数据

(2)代码底层执行不像我们看到的高级语言—-Java程序这么简单,它的执行是Java代码–>字节码–>根据字节码执行对应的C/C++代码–>C/C++代码被编译成汇编语言–>和硬件电路交互,现实中,为了获取更好的性能JVM可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用volatile则会对禁止语义重排序,当然这也一定程度上降低了代码执行效率

从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性,详细的可以参见java.util.concurrent.atomic包下的类,比如AtomicInteger

32、Java中如何获取到线程dump文件?

死循环、死锁、阻塞、页面打开慢等问题,打线程dump是最好的解决问题的途径。所谓线程dump也就是线程堆栈,获取到线程堆栈有两步:

(1)获取到线程的pid,可以通过使用jps命令,在Linux环境下还可以使用ps -ef | grep java

(2)打印线程堆栈,可以通过使用jstack pid命令,在Linux环境下还可以使用kill -3 pid

另外提一点,Thread类提供了一个getStackTrace()方法也可以用于获取线程堆栈。这是一个实例方法,因此此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈,

33、线程和进程有什么区别?

  • 进程是系统进行资源分配的基本单位,有独立的内存地址空间
  • 线程是CPU独立运行和独立调度的基本单位,没有单独地址空间,有独立的栈,局部变量,寄存器, 程序计数器等。
  • 创建进程的开销大,包括创建虚拟地址空间等需要大量系统资源
  • 创建线程开销小,基本上只有一个内核对象和一个堆栈。
  • 一个进程无法直接访问另一个进程的资源;同一进程内的多个线程共享进程的资源。
  • 进程切换开销大,线程切换开销小;进程间通信开销大,线程间通信开销小。
  • 线程属于进程,不能独立执行。每个进程至少要有一个线程,成为主线程

34、线程实现的方式有几种(四种)?

  1. 继承Thread类,重写run方法
  2. 实现Runnable接口,重写run方法,实现Runnable接口的实现类的实例对象作为Thread构造函数的target
  3. 实现Callable接口通过FutureTask包装器来创建Thread线程
  4. 通过线程池创建线程

前面两种可以归结为一类:无返回值,原因很简单,通过重写run方法,run方式的返回值是void,所以没有办法返回结果

后面两种可以归结成一类:有返回值,通过Callable接口,就要实现call方法,这个方法的返回值是Object,所以返回的结果可以放在Object对象中

线程实现方式3:通过Callable和FutureTask创建线程

  1. 创建Callable接口的实现类 ,并实现Call方法
  2. 创建Callable实现类的实现,使用FutureTask类包装Callable对象,该FutureTask对象封装了Callable对象的Call方法的返回值
  3. 使用FutureTask对象作为Thread对象的target创建并启动线程
  4. 调用FutureTask对象的get()来获取子线程执行结束的返回值
public class ThreadDemo03 {
     

    public static void main(String[] args) {
     
        // TODO Auto-generated method stub

        Callable<Object> oneCallable = new Tickets<Object>();
        FutureTask<Object> oneTask = new FutureTask<Object>(oneCallable);

        Thread t = new Thread(oneTask);

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

        t.start();

    }

}

class Tickets<Object> implements Callable<Object>{
     

    //重写call方法
    @Override
    public Object call() throws Exception {
     
        // TODO Auto-generated method stub
        System.out.println(Thread.currentThread().getName()+"-->我是通过实现Callable接口通过FutureTask包装器来实现的线程");
        return null;
    }   
}

程序运行结果:

main
Thread-0–>我是通过实现Callable接口通过FutureTask包装器来实现的线程

线程实现方式4:通过线程池创建线程

public class ThreadDemo05{
     

    private static int POOL_NUM = 10;     //线程池数量

    public static void main(String[] args) throws InterruptedException {
     
        // TODO Auto-generated method stub
        ExecutorService executorService = Executors.newFixedThreadPool(5);  
        for(int i = 0; i<POOL_NUM; i++)  
        {
       
            RunnableThread thread = new RunnableThread();

            //Thread.sleep(1000);
            executorService.execute(thread);  
        }
        //关闭线程池
        executorService.shutdown(); 
    }   

}

class RunnableThread implements Runnable  
{
          
    @Override
    public void run()  
    {
       
        System.out.println("通过线程池方式创建的线程:" + Thread.currentThread().getName() + " ");  

    }  
}

程序运行结果:

通过线程池方式创建的线程:pool-1-thread-3 
通过线程池方式创建的线程:pool-1-thread-4 
通过线程池方式创建的线程:pool-1-thread-1 
通过线程池方式创建的线程:pool-1-thread-5 
通过线程池方式创建的线程:pool-1-thread-2 
通过线程池方式创建的线程:pool-1-thread-5 
通过线程池方式创建的线程:pool-1-thread-1 
通过线程池方式创建的线程:pool-1-thread-4 
通过线程池方式创建的线程:pool-1-thread-3 
通过线程池方式创建的线程:pool-1-thread-2

ExecutorService、Callable都是属于Executor框架。返回结果的线程是在JDK1.5中引入的新特征,还有Future接口也是属于这个框架,有了这种特征得到返回值就很方便了。
通过分析可以知道,他同样也是实现了Callable接口,实现了Call方法,所以有返回值。这也就是正好符合了前面所说的两种分类

执行Callable任务后,可以获取一个Future的对象在该对象上调用get就可以获取到Callable任务返回的Object了。get方法是阻塞的,即:线程无返回结果,get方法会一直等待

再介绍Executors类:

newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

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

newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

  1. 高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?
    1. 这是我在并发编程网上看到的一个问题,把这个问题放在最后一个,希望每个人都能看到并且思考一下,因为这个问题非常好、非常实际、非常专业。关于这个问题,个人看法是:

(1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换

(2)并发不高、任务执行时间长的业务要区分开看:

a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务

b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换

(3)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。

2. 如果你提交任务时,线程池队列已满,这时会发生什么?

  1. 如果你使用的LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务;如果你使用的是有界队列比方说ArrayBlockingQueue的话,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy。
  2. 锁的等级:方法锁、对象锁、类锁?
    1. 方法锁(synchronized修饰方法时)

通过在方法声明中加入 synchronized关键字来声明 synchronized 方法。

synchronized 方法控制对类成员变量的访问:
每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态,从而有效避免了类成员变量的访问冲突。

对象锁(synchronized修饰方法或代码块)

当一个对象中有synchronized method或synchronized block的时候调用此对象的同步方法或进入其同步区域时,就必须先获得对象锁。如果此对象的对象锁已被其他调用者占用,则需要等待此锁被释放。(方法锁也是对象锁)

java的所有对象都含有1个互斥锁,这个锁由JVM自动获取和释放。线程进入synchronized方法的时候获取该对象的锁,当然如果已经有线程获取了这个对象的锁,那么当前线程会等待;synchronized方法正常返回或者抛异常而终止,JVM会自动释放对象锁。这里也体现了用synchronized来加锁的1个好处,方法抛异常的时候,锁仍然可以由JVM来自动释放

类锁(synchronized 修饰静态的方法或代码块)

由于一个class不论被实例化多少次,其中的静态方法和静态变量在内存中都只有一份。所以,一旦一个静态的方法被申明为synchronized。此类所有的实例化对象在调用此方法,共用同一把锁,我们称之为类锁。

对象锁是用来控制实例方法之间的同步,类锁是用来控制静态方法(或静态变量互斥体)之间的同步

如果同步块内的线程抛出异常会发生什么?

  1. 这个问题坑了很多Java程序员,若你能想到锁是否释放这条线索来回答还有点希望答对。无论你的同步块是正常还是异常退出的,里面的线程都会释放锁,所以对比锁接口我更喜欢同步块,因为它不用我花费精力去释放锁,该功能可以在finally block里释放锁实现。

35、并发编程(concurrency)并行编程(parallellism)有什么区别?

并发(concurrency)和并行(parallellism)是:

  1. 解释一:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
  2. 解释二:并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
  3. 解释三:在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群

所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。

36、如何保证多线程下 i++ 结果正确?

根据volatile特性来用1000个线程不断的累加数字,每次累加1个,到最后值确不是1000.

volatile只能保证你数据的可见性(获取到的是最新的数据,不能保证原子性,说白了,volatile跟原子性没关系

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

public class Counter {
     

    public static AtomicInteger count = new AtomicInteger();//原子操作
    public static CountDownLatch latch= new CountDownLatch(1000);//线程协作处理
    public static volatile int countNum = 0;//volatile    只能保证可见性,不能保证原子性
    public static int synNum = 0;//同步处理计算

    public static void inc() {
     

        try {
     
            Thread.sleep(1);
        } catch (InterruptedException e) {
     
        }
        countNum++;
        int c = count.addAndGet(1);
        add();
        System.out.println(Thread.currentThread().getName() + "------>" + c);
    }

    public static synchronized void add(){
     
        synNum++;
    }

    public static void main(String[] args) {
     

        //同时启动1000个线程,去进行i++计算,看看实际结果

        for (int i = 0; i < 1000; i++) {
     
            new Thread(new Runnable() {
     
                @Override
                public void run() {
     
                    Counter.inc();
                    latch.countDown();
                }
            },"thread" + i).start();
        }
        try {
     
            latch.await();
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());

        System.out.println("运行结果:Counter.count=" + count.get() + ",,," + countNum + ",,," + synNum);
    }

count.get()是AtomicInteger的值;

count是用volatile修饰的变量的值;

synNum是用synchronized修饰的值;

用synchronized和AtomicInteger能保证是你想要的数据,volatile并不能保证

第一次运行结果

main
运行结果:Counter.count=1000,991,1000

第二次运行结果:

main
运行结果:Counter.count=1000,998,1000

第三次运行结果:

main
运行结果:Counter.count=1000,993,1000

可见,就算用了volatile,也不能保证数据是你想要的数据,volatile只能保证你数据的可见性(获取到的是最新的数据,不能保证原子性,说白了,volatile跟原子性没关系)

要保证原子性,对数据的累加,可以用AtomicInteger类;

也可以用synchronized来保证数据的一致性

37、一个线程如果出现了运行时异常会怎么样?

如果这个异常没有被捕获的话,这个线程就停止执行了。另外重要的一点是:如果这个线程持有某个某个对象的监视器,那么这个对象监视器会被立即释放

38、如何在两个线程之间共享数据?

通过在线程之间共享对象就可以了,然后通过wait/notify/notifyAll、await/signal/signalAll进行唤起和等待,比方说阻塞队列BlockingQueue就是为线程之间共享数据而设计的

39、生产者消费者模型的作用是什么?

这个问题很理论,但是很重要:

(1)通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用

(2)解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要收到相互的制约

40、怎么唤醒一个阻塞的线程?

如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。

41、Java中用到的线程调度算法是什么

抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

42、单例模式的线程安全性?

老生常谈的问题了,首先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法,我总结一下:

(1)饿汉式单例模式的写法:线程安全

(2)懒汉式单例模式的写法:非线程安全

(3)双检锁单例模式的写法:线程安全

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

这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。

如果说上面的说法让你感到困惑,那么我举个例子,假设Thread2中new了Thread1,main函数中new了Thread2,那么:

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

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

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

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

public class SynObj{

public synchronized void showA(){
     
    System.out.println("showA..");
    try {
     
        Thread.sleep(3000);
    } catch (InterruptedException e) {
     
        e.printStackTrace();
    }
}

public void showB(){
     
    synchronized (this) {
     
        System.out.println("showB..");
    }
}

}

45、如何检测死锁?怎么预防死锁?

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


通俗地讲就是两个或多个进程被无限期地阻塞、相互等待的一种状态


死锁产生的原因?


1.因竞争资源发生死锁 现象:系统中供多个进程共享的资源的数目不足以满足全部进程的需要时,就会引起对诸资源的竞争而发生死锁现象


2.进程推进顺序不当发生死锁


死锁的四个必要条件:

  1. 互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源
  2. 请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放
  3. 不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
  4. 环路等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系


这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之
一不满足,就不会发生死锁。


检测死锁


有两个容器,一个用于保存线程正在请求的锁,一个用于保存线程已经持有的锁。每次加锁之前都会做如下检测:

  1. 检测当前正在请求的锁是否已经被其它线程持有,如果有,则把那些线程找出来
  2. 遍历第一步中返回的线程,检查自己持有的锁是否正被其中任何一个线程请求,如果第二步返回真,表示出现了死锁


死锁的解除与预防:


理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和
解除死锁。


所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确
定资源的合理分配算法,避免进程永久占据系统资源。


此外,也要防止进程在处于等待状态的情况下占用资源。因此,对资源的分配要给予合理的规划。

46、什么是Java内存模型?

Java内存模型定义了多线程之间共享变量的可见性以及如何在需要的时候对共享变量进行同步
JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

47、什么是守护线程?有什么用?

所谓守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。

48、ThreadLocal是怎么实现的?

通过线程获得对应的ThreadLocalMap对象,ThreadLocalMap里面存储的是一个entry(继承了WeakReference),成员变量存储value,set方法通过遍历entry数组,如果已存在则更新value,如果不存在就新建entry,获取是getEntry,通过线程hash值取余得到在数组中的位置拿到最终结果

49、两个线程如何串行执行?

可以通过join,让一个线程等待另一个线程执行结束后再执行

50、上下文切换是什么含义?

cpu通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是切换前会保存上一次任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。

51、可以运行时kill掉一个线程吗?

不可以,操作系统调度的最小单位是进程,不能直接对线程执行kill命令

52、什么是条件锁、读写锁、可重入锁?

53、线程池ThreadPoolExecutor的实现原理?

54、线程方法中的异常如何处理,父线程可以捕获到吗

通过实现Thread.UncaughtExceptionHandler接口并实现uncaughtException(Thread t, Throwable e)方法,将其通过Thread.setUncaughtExceptionHandler(handler)方法设置到线程上面,就可以对线程的异常进行处理,这样父线程就可以得到子线程的异常了

55、如何实现一个线程安全的数据结构?

参考资料:https://segmentfault.com/a/1190000013896476

你可能感兴趣的:(面试,面试,多线程)