java面试题:多线程与并发

多线程

线程,锁

Q:如何新建一个线程?
继承Thread,或者实现Runnable接口,或者通过Callable接口实现。
Q:Callable有什么区别?
Callable接口,有一个call()方法,可以返回值。
Q:讲一下Callable接口、Future接口、FutureTask类
Callable可以作为FutureTask的方法参数。
而FutureTask类间接实现了Future接口。
FutureTask进行多线程操作时,可以通过Future接口的get()获取返回结果,也就是通过FutureTask实现异步。
Q:在T1线程中A B C三个方法依次执行,假如B方法通过Future接口的get()方法获取异步结果,那么T1线程是否会阻塞
Future实现类的get()方法会导致主线程阻塞,直到Callable任务执行完成;

线程状态

Q:线程有哪些状态?
新建,就绪,运行,阻塞,停止
阻塞可以是sleep(),wait(),或者join()
线程 Thread 中 State 枚举中代表线程状态的值。

public enum State {    
NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED;
}

NEW: 表示刚创建的线程。
RUNNABLE:可运行不代表运行状态,可以理解为 Ready 就绪状态,等获取到 CPU 资源,才会变为 Running 运行状态。
BLOCKED:阻塞状态的线程会暂定执行,不占用 CPU 时间片资源,直到获取锁。
WAITING、TIMED_WAITING:两种状态都表示等待状态,不同的是 WAITING 是无限期等待,需要另一个事件进行唤醒; TIMED_WAITING 等到一个时限点时自我唤醒。
TERMINATED:当线程执行完毕后,会进入TERMINATED 终止、结束的状态。
Q:如何查看线程的运行状态?
使用Thread类的getState()方法可以获得线程的状态,该方法的返回值是Thread.state,他是线程状态的枚举。
分别是NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED。

线程通信

Q: java多线程之间,是如何进行通信的?
wait,notify,join,yield,intercept。
Q: sleep() 和 wait() 的区别?
所属的类不一样。Thread.sleep(1000); 而wait()是属于Object的。
sleep()不会释放锁,而wait()会释放对象锁。通过wait使得线程挂起,可以调用notify或者notifyAll来唤醒这个进程。wait和notify都属于Object的方法。
sleep()可以在任何地方使用。而wait,notify,notifyAll只能在同步控制方法或者同步控制块中使用。
sleep()必须捕获异常,而wait,notify,notifyAll的不需要捕获异常。
Q:线程等待唤醒的实现方法
(1)Object对象中的wait()方法可以让线程等待,使用Object中的notify()方法唤醒线程;

  • 必须都在同步代码块内使用;
  • 调用wait,notify的对象是加锁对象;
  • notify必须在wait后执行才能唤醒;
  • wait后能释放锁对象,线程处于wait状态;
    (2)使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程;
  • 必须在lock同步代码块内使用;
  • signal必须在await后执行才能唤醒;
    (3)LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程;
  • 不需要锁块;
  • unpark()可以在park()前唤醒;

死锁

Q:死锁是怎么回事?
死锁,就是两个(或多个)线程对彼此加锁的资源进行加锁,导致彼此等待而永远阻塞。
比如,如果线程1锁住了A,然后尝试对B进行加锁,同时线程2已经锁住了B,接着尝试对A进行加锁,这时死锁就发生了。线程1永远得不到B,线程2也永远得不到A,并且它们永远也不会知道发生了这样的事情。为了得到彼此的对象(A和B),它们将永远阻塞下去。这种情况就是一个死锁。
Q:如何避免死锁?如何解决死锁?
破坏“不可剥夺”条件:一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到 系统的资源列表中,可以被其他的进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行。
破坏”请求与保持条件“:第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源。第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源。
破坏“循环等待”条件:采用资源有序分配其基本思想是将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。

Q:在实践中,有没有使用过多线程?
可以使用多线程同时执行多个计算任务,提高程序运行的效率。
还可以通过异步来处理耗时操作。比如一个线程做为主线程,新建另一个线程进行下载任务,这样主线程就不需要等待耗时操作。
Q:有三个线程,如何使它们顺序执行?
使用join,可以让某个线程在另一线程之前执行。
Q:假如有Thread1、Thread2、Thread3、Thread4四条线程分别统计C、D、E、F四个盘的大小,所有线程都统计完毕交给Thread5线程去做汇总,应当如何实现?
使用ForkJoinPool 。可以进行fork()分而治之,将任务进行分解,然后合并所有的结果。
Q:如何中断线程?中断线程意味着什么?
interrupt() 方法只是改变中断状态而已,它不会中断一个正在运行的线程。
如果线程被wait, join和sleep三种方法之一阻塞,此时调用该线程的interrupt()方法,那么该线程将抛出一个 InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态。
如果线程没有被阻塞,这时调用 interrupt()将不起作用,直到执行到wait(),sleep(),join()时,才马上会抛出 InterruptedException。

线程池

Q:线程池有没有了解过?为什么要用线程池?
为了避免频繁的创建和销毁线程,让创建的线程进行复用,就有了线程池的概念。
线程池里会维护一部分活跃线程,如果有需要,就去线程池里取线程使用,用完即归还到线程池里,免去了创建和销毁线程的开销,且线程池也会线程的数量有一定的限制。
Q:线程池的submit()和execute()方法区别?
submit()有返回值,而execute()没有。
Q:线程池的参数有哪些?

  • 参数如下:
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
        BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
  • corePoolSize:核心线程池里面的线程数量
  • maximumPoolSize:线程池允许的最大线程数,它表示在线程池中最多能创建多少个线程;
  • keepAliveTime:表示当线程池的个数大于核心数量corePoolSize时,线程的空闲时间达到指定的时间时会被销毁。
  • unit:参数keepAliveTime的时间单位
  • workQueue:一个阻塞队列,用来存储等待执行的任务。当请求的线程数大于corePoolSize时,线程会进入这个BlockingQueue阻塞队列。
  • handler:执行拒绝策略的对象。当阻塞队列workQueue的任务缓存区到达上限,并且活动的线程数大于maximumPoolSize的时候,线程池会执行拒绝策略。
  • threadFactory: 定义如何启动一个线程,可以设置线程的名称,并且可以确定是否是后台线程等。

Q:假设我们有一个线程池,核心线程数为10,最大线程数也为20,阻塞队列为100。现在来了100个任务,线程池里现在有几个线程运行?"
有两种情况:
1.核心线程数用完了,先进队列,到最大值,再起线程
JDK中的线程池,也就是ThreadPoolExecutor就是这种机制的。
核心线程数10用完了,剩下的90个任务进入了阻塞队列。因此现在线程池里有10个线程运行。
2.核心线程数用完了,先起线程,到最大值,再进队列
在dubbo中,有一种线程池叫EagerThreadPoolExecutor线程池。在Tomcat里面也有类似的线程池。
核心线程数10用完了,剩下的线程先达到最大值20,然后再剩下的才会进阻塞队列。因此现在线程池里有20个线程运行。
Q:拒绝策略有哪些?
拒绝策略有以下几种:
ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。 (默认)
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列中等待最久的任务,然后把当前任务加入队列。
ThreadPoolExecutor.CallerRunsPolicy:由调用任务的run()方法绕过线程池执行此线程。
Q:阻塞队列有哪些?
Q:阻塞队列的默认值是多少?
ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列
LinkedBlockingQueue:一个基于链表结构的阻塞队列。LinkedBlockingQueue如果不指定大小,就是一个无界队列。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
Q:线程池有哪些类型?有什么不同?
1.newCachedThreadPool:

new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);

newCachedThreadPool里面的corePoolSize核心线程数为0,最大线程数设置为最大的Integer.MAX_VALUE。
由于newCachedThreadPool是任意伸缩的线程池,如果最大线程数maximumPoolSize达到最大,那么会导致OOM异常。

2.newScheduledThreadPool:
newScheduledThreadExecutor可以定时或者周期性执行任务。
如果最大线程数maximumPoolSize达到最大,那么会导致OOM异常。

3.newFixedThreadPool:

ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());

newFixedThreadPool是 具有线程池提高程序效率和节省创建线程时所耗的开销的优点。
但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
由于使用的LinkedBlockingQueue()是一个无界队列,队列长度可达到Integer.MAX_VALUE,如果瞬间请求非常大,会有OOM的风险。

4.newSingleThreadExecutor:

new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));

newSingleThreadExecutor创建一个单线程的线程池,相当于串行地执行所有任务,能保证任务的提交顺序依次执行。

5.newWorksStealingPool:这个是在jdk8引入的,创建持有足够线程的线程池支持给定的并行度,并通过使用多个队列减少竞争。

Q:线程池的核心线程数,你们是怎么设置的?有什么依据?
CPU密集型:核心线程数 = CPU核数 + 1
CPU密集的意思是任务需要大量的运算,而没有IO阻塞,CPU一直全速运行。线程数一般只需要设置为CPU核心数的线程个数就可以了。
例如:一些业务复杂的计算和逻辑处理过程。

IO密集型:核心线程数 = CPU核数 * 2
IO密集型,就是程序中存在大量的 I/O 操作占用时间,导致线程空余时间很多,所以通常就需要开CPU核心数两倍的线程。当线程进行 I/O 操作 CPU 空闲时,启用其他线程继续使用 CPU,以提高 CPU 的使用率。
例如:数据库交互,文件上传下载,网络传输等。
大部分业务都是IO密集型的。

详情见: https://blog.csdn.net/weixin_44777693/article/details/95246059

Q:怎么手动实现一个线程池?

并发基础

关键词:线程安全、synchronized同步锁、Lock锁、volatile可见性、AtomicInteger原子操作类、CAS、AQS、ThreadLocal、CountDownLatch、Semaphore、ReentrantLock、Carrier
Q:线程安全是什么?
多个线程操作同一共享变量时,需要保证数据的安全性。
Q:如何保证线程安全?
1.加锁,synchronized同步锁或者ReentrantLock可重入锁。
2.使用AtomicInteger原子操作类,代替基本数据类型。(容易遗漏)
3.如果进行的是原子操作,可以使用volatile关键字修饰。
4.使用ThreadLocal对各个线程进行隔离

synchronized和Lock

Q:同步有哪些?
synchronized关键字和Lock锁。
Q:synchronized的底层实现?
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
详情参见: https://www.cnblogs.com/paddix/p/5367116.html

Q: synchronized修饰静态方法和普通方法、静态代码块的区别?
synchronized可以用来修饰代码块和方法。

  • synchronized修饰普通方法时,是对象锁,this对象。一个对象的对象锁是唯一的,只有一个线程可以拿到,因此当有线程在执行synchronized的方法时,其他线程需要进入阻塞队列等待。
    静态方法是所有对象公有的。
  • synchronized修饰静态方法时,是类锁。一个类的类锁是唯一的,只有一个线程可以拿到,因此当有对象在线程执行synchronized的方法时,其他线程的对象需要进入阻塞队列等待。
  • 类锁是所有对象一把锁, 对象锁是一个对象一把锁,多个对象多把锁。
  • synchronized修饰静态代码块,synchronized (this) {}是对象锁,synchronized (A.class) {} 是类锁。

详情见: https://blog.csdn.net/TesuZer/article/details/80874195

Q:什么是自旋?
在Java中,自旋是指在多线程编程中,当线程尝试获得某个锁时,如果该锁已经被其他线程占用,线程会一直循环检查该锁是否被释放,直到获取到该锁为止。这个循环等待的过程被称为自旋。

Q:讲一下锁的四种状态。
锁状态一种有四种,从级别由低到高依次是:无锁、偏向锁,轻量级锁,重量级锁,锁状态只能升级,不能降级。
1.无锁
无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
2.偏向锁
偏向锁是JDK6中引入的一项锁优化,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。
3.轻量级锁
是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
4.重量级锁
指的是原始的Synchronized的实现,重量级锁的特点:其他线程试图获取锁时,都会被阻塞,只有持有锁的线程释放锁之后才会唤醒这些线程。

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块场景
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到索竞争的线程,使用自旋会消耗CPU 追求响应速度,同步块执行速度非常快
重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢 追求吞吐量,同步块执行速度较慢

详情见: https://blog.csdn.net/u013994536/article/details/124233254
https://blog.csdn.net/ChenRui_yz/article/details/122448611

Q:讲一下锁升级过程。
无锁:当有一个线程访问同步块时升级成偏向锁。
偏向锁:当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁。
轻量级锁:自旋十次失败(锁膨胀)升级为重量级锁。

Q:讲一下Java对象头。以及synchronzied存储在java对象头的哪里?
Java对象头主要包括两部分数据:
1)类型指针(Klass Pointer)
是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;
2)标记字段(Mark Word)
用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等,它是实现轻量级锁和偏向锁的关键.
所以,很明显synchronized使用的锁对象是存储在Java对象头里的标记字段里。
参考资料:https://blog.csdn.net/ChenRui_yz/article/details/122448611

Q:锁有哪些?
可重入锁ReentrantLock。可一个线程获得某个对象的方法的锁时,想要访问该对象的其他方法时无需再加锁。
ReentrantLock还可以设置为公平锁还是非公平锁。公平锁,会保证各个线程尽可能公平地拿到锁,不会导致某个线程一直排队等待的情况。

Q:ReentrantReadWriteLock是什么?读的时候可以写吗?读的时候可以读吗?写的时候可以写吗?
可重入的读写锁。读写锁分为读锁和写锁。
“读读共存,写写不共存,读写不共存”。
https://www.cnblogs.com/expiator/p/9374598.html

Q:synchronized是公平锁,还是非公平锁?
非公平锁。
Synchronized是Java中内置的锁机制,它的锁模式是非公平锁。
Synchronized的实现方式是基于对象监视器(monitor)的。
在Java中,每个对象都有一个与之关联的monitor,当一个线程需要获取某个对象的锁时,它会首先尝试获取这个对象关联的monitor。如果monitor已经被其他线程占用,那么这个线程就会进入monitor的等待队列中,等待其他线程释放锁。
在Synchronized中,当一个线程释放锁时,JVM会从等待队列中随机选择一个线程来获取锁,而不是按照申请锁的顺序来获取锁,因此Synchronized是一种非公平锁。这种实现方式的优点是可以减少线程上下文切换的开销,提高系统的吞吐量,但是容易出现饥饿现象,即某些线程可能会一直获取不到锁。

Q:ReentrantLock,Synchronized,有什么区别?
实现:synchronized是java语言的关键字,是原生语法层面的互斥,需要jvm实现。通过JVM加锁解锁。ReentrantLock 实现了 Lock接口,api层面的加锁解锁
灵活性:synchronized锁的范围是整个方法或synchronized块部分。ReentrantLock 可以跨方法,lock(),然后unLock(),灵活性更大。
是否公平锁:synchronized是非公平锁。ReentrantLock 两者都可以,默认公平锁,构造器可以传入boolean值,true为公平锁,false为非公平锁。
是否自动释放锁: synchronized执行完了会自己释放锁,ReentrantLock 需要手动释放锁。
Synchronized在线程发生异常时会自动释放锁,因此不会发生异常死锁。Lock异常时不会自动释放锁,所以需要在finally中实现释放锁。

Lock lock = new ReentrantLock();
lock.lock();
try {
  //...
} finally {
    lock.unlock();
}

详情见: https://blog.csdn.net/qq_40551367/article/details/89414446/

Q:讲一下并发工具类是怎么使用的?
CountDownLatch、Semaphore、Carrier这些并发工具类都是基于AQS实现的。线程池的内部也有继承自AQS的类。
CountDownLatch是闭锁(阀门)。CountDownLatch允许一个或者多个线程去等待其他线程完成操作。
Semaphore是信号量。
Carrier是 栅栏。

AQS

Q:简单讲下AQS。以及AQS的实现原理。
AbstractQueueSynchronizer。抽象队列同步器。
状态变量state,加上CLH双端队列。
在AQS内部会保存一个状态变量state,通过CAS修改该变量的值,修改成功的线程表示获取到该锁,没有修改成功,或者发现状态state已经是加锁状态,则添加到等待队列中,通过CAS和LockSupport.park()的方式,维护state变量,并挂起等待被唤醒。
线程获取锁失败,就会将线程封装成一个Node节点,插入队列尾。当有线程释放锁时,会尝试把队列头的next节点里面的线程占用锁。
详情见:https://blog.csdn.net/qq_37419449/article/details/120040856

Q:非公平锁和非公平锁,是怎么实现的?有哪些优缺点?
ReentrantLock是基于AQS实现的。内部有一个线程队列。

  • 公平锁:多个线程按顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
    优点:所有的线程都能得到资源,不会饿死在队列中。
    缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
  • 非公平锁:多个线程去获取锁的时候,会直接去尝试获取锁,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
    优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
    缺点:这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。

Q:AQS是怎么实现锁重入的?
state就用来表示加锁的次数。0标识无锁,一个线程拿到锁后,state就会加1。之后同一个线程再次获取锁,就会用CAS给state加一。通过state次数和记录线程来实现锁重入。

Q:AQS有几种工作模式?
AQS提供了两种工作模式:独占(exclusive)模式和共享(shared)模式。
独占模式:同一时间只有一个线程能拿到锁执行,锁的状态只有0和1两种情况。
共享模式:同一时间有多个线程可以拿到锁协同工作,锁的状态大于或等于0。

CAS

Q:CAS是什么?
CAS就是Compare And Swap,比较和替换。
比如说,比较当前状态是否为开启状态,如果不是开启状态,就将其改为开启状态。
需要读写的内存值: V,进行比较的预估值: A,拟写入的更新值: B。
当且仅当 V == A 时, V = B。

Q:CAS原理是什么?CAS是怎么实现的?
比较和替换。
CAS其实是通过Unsafe类的compareAndSwap实现的,Unsafe可以用来直接访问系统内存资源并进行自主管理。

Q:CAS有什么缺点?
CAS的缺点是存在ABA问题。
Q:什么是ABA?
就是一个变量V,如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其他线程修改过了吗?如果在这段期间它的值曾经被改成了B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。
Q:怎么解决ABA问题?
java并发包中提供了一个带有标记的原子引用类"AtomicStampedReference",它可以通过控制变量值的版本来保证CAS的正确性。

volatile

Q:volatile关键字有什么用?
(1)可以保证线程的可见性,线程修改共享变量时会被其他线程发现。
(2)还可以禁止指令重排序。

Q:重排序有哪几种类型?
指令做重排序。重排序分三种类型:
(1)编译器优化的重排序
编译器在不改变单线程程序语义的前提下(代码中不包含synchronized关键字),可以重新安排语句的执行顺序。
(2)指令级并行的重排序
现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
(3)内存系统的重排序。
由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行.
说明:1属于编译器重排序,2和3属于处理器重排序。这些重排序都可能会导致多线程程序出现内存可见性问题。

Q:volatile是怎么实现的?
这个涉及到Java内存模型。JMM。
Java内存模型(JMM)规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,
工作内存中被volatile修饰的变量,在被修改后可以立即同步到主内存,被其修饰的变量在每次使用之前都从主内存读取刷新。因此,可以使用volatile来保证多线程操作时变量的可见性。
例如,线程A修改一个普通变量的值,然后向主内存进行回写,另外一个线程B在线程A回写完成了之后再从主内存进行读取操作,新变量值就会对线程B可见。
详情见: https://www.cnblogs.com/xrq730/p/7048693.html
https://blog.csdn.net/zezezuiaiya/article/details/81456060

Q:volatile能保证线程安全吗?
不能。volatile只有进行原子操作时,才是线程安全的
Q:使用volatile操作i++,是否线程安全?如果不安全,应该怎么处理?
线程不安全。因为i++不是原子操作。
可以使用AtomicInteger原子操作类进行操作。
Q:volatile和全局变量有什么区别?
Q:AtomicInteger是怎么实现的?
CAS乐观锁机制。比较和替换实现。

ThreadLocal

Q:讲一下ThreadLocal
每个线程都有一个自己的副本变量。

Q:讲一下ThreadLocal的底层实现。
Q:ThreadLocal是如何为每个线程创建变量的副本的?
Q:ThreadLocal是如何做到在不同线程set()、get()的值不被其它线程访问的;
https://www.cnblogs.com/expiator/p/12191625.html

各线程对共享的ThreadLocal实例进行操作,实际上是以该实例为键对内部持有的ThreadLocalMap对象进行操作。
首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。
初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。
然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

Q:ThreadLocal中的Map,是怎么解决hash冲突的?采用了哪种散列算法?
ThreadLocalMap 采用开放定址法。
开放定址法不会创建链表,当关键字散列到的数组单元已经被另外一个关键字占用的时候,就会尝试在数组中寻找其他的单元,直到找到一个空的单元。探测数组空单元的方式有很多,这里介绍一种最简单的 -- 线性探测法。线性探测法就是从冲突的数组单元开始,依次往后搜索空单元,如果到数组尾部,再从头开始搜索(环形查找)

Q:ThreadLocal可能会导致哪些问题?怎么解决?为什么会出现这些问题?
内存泄露。
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
使用ThreadLocal时,一般建议将其声明为static final的,避免频繁创建ThreadLocal实例。
尽量避免存储大对象,如果非要存,那么尽量在访问完成后及时调用remove()删除掉。
ThreadLocal为什么会导致内存泄露呢?
参考资料:https://blog.csdn.net/Not_Look_Back/article/details/123275572

Q:使用ThreadLocal时,如果复用了线程,可能会导致哪些问题?
使用线程池或有复用线程时,复用同一个线程时,每次请求结束后 ThreadLoca的值没有清空,导致第二次使用时ThreadLocal的值还是上次遗留一下的值。
解决方案:1、保证每次都用新的值覆盖线程变量;2、保证在每个请求结束后清空线程变量。

更详细的多线程资料参见 :
想进大厂需要懂的50个多线程面试题

posted on 2018-12-28 23:00  乐之者v  阅读(756)  评论(0编辑  收藏  举报

导航