金九银十快要来了,整理了50道多线程并发面试题,大家可以点赞、收藏起来,慢慢品!~
选择多线程的原因,就是因为快。举个例子:
如果要把1000块砖搬到楼顶,假设到楼顶有几个电梯,你觉得用一个电梯搬运快,还是同时用几个电梯同时搬运快呢?这个电梯就可以理解为线程。
所以,我们使用多线程就是因为: 在正确的场景下,设置恰当数目的线程,可以用来程提高序的运行速率。更专业点讲,就是充分地利用CPU和I/O的利用率,提升程序运行速率。
当然,有利就有弊,多线程场景下,我们要保证线程安全,就需要考虑加锁。加锁如果不恰当,就很很耗性能。
Java中创建线程主要有以下这几种方式:
public class ThreadTest { public static void main(String[] args) { Thread thread = new MyThread(); thread.start(); }}class MyThread extends Thread { @Override public void run() { System.out.println("关注公众号:捡田螺的小男孩"); }}
public class ThreadTest { public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); Thread thread = new Thread(myRunnable); thread.start(); }}class MyRunnable implements Runnable { @Override public void run() { System.out.println("关注公众号:捡田螺的小男孩"); }}//运行结果:关注公众号:捡田螺的小男孩
如果想要执行的线程有返回,可以使用Callable。
public class ThreadTest { public static void main(String[] args) throws ExecutionException, InterruptedException { MyThreadCallable mc = new MyThreadCallable(); FutureTask ft = new FutureTask<>(mc); Thread thread = new Thread(ft); thread.start(); System.out.println(ft.get()); }}class MyThreadCallable implements Callable { @Override public String call()throws Exception { return "关注公众号:捡田螺的小男孩"; }}//运行结果:关注公众号:捡田螺的小男孩
日常开发中,我们一般都是用线程池的方式执行异步任务。
public class ThreadTest { public static void main(String[] args) throws Exception { ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new ArrayBlockingQueue(20), new CustomizableThreadFactory("Tianluo-Thread-pool")); executorOne.execute(() -> { System.out.println("关注公众号:捡田螺的小男孩"); }); //关闭线程池 executorOne.shutdown(); }}
其实start和run的主要区别如下:
大家可以结合代码例子来看看哈~
public class ThreadTest { public static void main(String[] args){ Thread t=new Thread(){ public void run(){ pong(); } }; t.start(); t.run(); t.run(); System.out.println("好的,马上去关注:捡田螺的小男孩"+ Thread.currentThread().getName()); } static void pong(){ System.out.println("关注公众号:捡田螺的小男孩"+ Thread.currentThread().getName()); }}//输出关注公众号:捡田螺的小男孩main关注公众号:捡田螺的小男孩main好的,马上去关注:捡田螺的小男孩main关注公众号:捡田螺的小男孩Thread-0
举个例子:
你打开QQ,开了一个进程;打开了迅雷,也开了一个进程。
在QQ的这个进程里,传输文字开一个线程、传输语音开了一个线程、弹出对话框又开了一个线程。
所以运行某个软件,相当于开了一个进程。在这个软件运行的过程里(在这个进程里),多个工作支撑的完成QQ的运行,那么这“多个工作”分别有一个线程。
所以一个进程管着多个线程。
通俗的讲:“进程是爹妈,管着众多的线程儿子”...
大家可以看下它俩的API:
@FunctionalInterfacepublic interface Callable { /** * 支持泛型V,有返回值,允许抛出异常 */ V call() throws Exception;}@FunctionalInterfacepublic interface Runnable { /** * 没有返回值,不能继续上抛异常 */ public abstract void run();}
为了方便大家理解,写了一个demo,小伙伴们可以看看哈:
/* * @Author 关注公众号:捡田螺的小男孩 * @date 2022-07-11 */public class CallableRunnableTest { public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(5); Callable callable =new Callable() { @Override public String call() throws Exception { return "你好,callable,关注公众号:捡田螺的小男孩"; } }; //支持泛型 Future futureCallable = executorService.submit(callable); try { System.out.println("获取callable的返回结果:"+futureCallable.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } Runnable runnable = new Runnable() { @Override public void run() { System.out.println("你好呀,runnable,关注公众号:捡田螺的小男孩"); } }; Future<?> futureRunnable = executorService.submit(runnable); try { System.out.println("获取runnable的返回结果:"+futureRunnable.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } executorService.shutdown(); }}//运行结果获取callable的返回结果:你好,callable,关注公众号:捡田螺的小男孩你好呀,runnable,关注公众号:捡田螺的小男孩获取runnable的返回结果:null
volatile关键字是Java虚拟机提供的的最轻量级的同步机制。它作为一个修饰符,用来修饰变量。它保证变量对所有线程可见性,禁止指令重排,但是不保证原子性。
我们先来一起回忆下java内存模型(jmm):
volatile变量,保证新值能立即同步回主内存,以及每次使用前立即从主内存刷新,所以我们说volatile保证了多线程操作变量的可见性。
volatile保证可见性和禁止指令重排,都跟内存屏障有关。我们来看一段volatile使用的demo代码:
/** * 关注公众号:捡田螺的小男孩 **/public class Singleton { private volatile static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
编译后,对比有volatile关键字和没有volatile关键字时所生成的汇编代码,发现有volatile关键字修饰时,会多出一个lock addl $0x0,(%esp),即多出一个lock前缀指令,lock指令相当于一个内存屏障
lock指令相当于一个内存屏障,它保证以下这几点:
第2点和第3点就是保证volatile保证可见性的体现嘛,第1点就是禁止指令重排的体现。
内存屏障四大分类:(Load 代表读取指令,Store代表写入指令)
有些小伙伴,可能对这个还是有点疑惑,内存屏障这玩意太抽象了。我们照着代码看下吧:
内存屏障保证前面的指令先执行,所以这就保证了禁止了指令重排啦,同时内存屏障保证缓存写入内存和其他处理器缓存失效,这也就保证了可见性,哈哈~有关于volatile的底层实现,我们就讨论到这哈~
并发和并行最开始都是操作系统中的概念,表示的是CPU执行多个任务的方式。
(即 A B 顺序执行的话,A 一定会比 B 先完成,而并发执行则不一定。)
(即在任意时间点上,串行执行时必然只有一个任务在执行,而并行则不一定。)
知乎有个很有意思的回答,大家可以看下:
你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。所以我认为它们最关键的点就是:是否是同时。
来源:知乎
synchronized是Java中的关键字,是一种同步锁。synchronized关键字可以作用于方法或者代码块。
一般面试时。可以这么回答:
如果synchronized作用于代码块,反编译可以看到两个指令:monitorenter、monitorexit,JVM使用monitorenter和monitorexit两个指令实现同步;如果作用synchronized作用于方法,反编译可以看到ACCSYNCHRONIZED标记,JVM通过在方法访问标识符(flags)中加入ACCSYNCHRONIZED来实现同步功能。
monitor是什么呢?操作系统的管程(monitors)是概念原理,ObjectMonitor是它的原理实现。
在Java虚拟机(HotSpot)中,Monitor(管程)是由ObjectMonitor实现的,其主要数据结构如下:
ObjectMonitor() { _header = NULL; _count = 0; // 记录个数 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; }
ObjectMonitor中几个关键字段的含义如图所示:
Mark Word 是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。
重量级锁,指向互斥量的指针。其实synchronized是重量级锁,也就是说Synchronized的对象锁,Mark Word锁标识位为10,其中指针指向的是Monitor对象的起始地址。
线程有6个状态,分别是:New, Runnable, Blocked, Waiting, Timed_Waiting, Terminated。
转换关系图如下:
/** * 关注公众号:捡田螺的小男孩 */public class ThreadTest { public static void main(String[] args) { Thread thread = new Thread(); System.out.println(thread.getState()); }}//运行结果:NEW
public class ThreadTest { public static void main(String[] args) { Thread thread = new Thread(); thread.start(); System.out.println(thread.getState()); }}//运行结果:RUNNABLE
Thread t = new Thread(new Runnable { void run() { synchronized (lock) { // 阻塞于这里,变为Blocked状态 // dothings } }});t.getState(); //新建之前,还没开始调用start方法,处于New状态t.start(); //调用start方法,就会进入Runnable状态
Thread t = new Thread(new Runnable { void run() { synchronized (lock) { // Blocked // dothings while (!condition) { lock.wait(); // into Waiting } } }});t.getState(); // Newt.start(); // Runnable
Thread t = new Thread(new Runnable { void run() { Thread.sleep(1000); // Timed_waiting }});t.getState(); // Newt.start(); // Runnable
再来看个代码demo吧:
/** * 关注公众号:捡田螺的小男孩 */public class ThreadTest { private static Object object = new Object(); public static void main(String[] args) throws Exception { Thread thread = new Thread(new Runnable() { @Override public void run() { try { for(int i = 0; i< 1000; i++){ System.out.print(""); } Thread.sleep(500); synchronized (object){ object.wait(); } } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread thread1 = new Thread(new Runnable() { @Override public void run() { try { synchronized (object){ Thread.sleep(1000); } Thread.sleep(1000); synchronized (object){ object.notify(); } } catch (InterruptedException e) { e.printStackTrace(); } } }); System.out.println("1"+thread.getState()); thread.start(); thread1.start(); System.out.println("2"+thread.getState()); while (thread.isAlive()){ System.out.println("---"+thread.getState()); Thread.sleep(100); } System.out.println("3"+thread.getState()); }}运行结果:1NEW2RUNNABLE---RUNNABLE---TIMED_WAITING---TIMED_WAITING---TIMED_WAITING---TIMED_WAITING---BLOCKED---BLOCKED---BLOCKED---BLOCKED---BLOCKED---WAITING---WAITING---WAITING---WAITING---WAITING---WAITING---WAITING---WAITING---WAITING
suspend()不建议使用,因为suspend()方法在调用后,线程不会释放已经占有的资 源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。
CAS,全称是Compare and Swap,翻译过来就是比较并交换;
CAS涉及3个操作数,内存地址值V,预期原值A,新值B;如果内存位置的值V与预期原A值相匹配,就更新为新值B,否则不更新
CAS有什么缺陷?
并发环境下,假设初始条件是A,去修改数据时,发现是A就会执行修改。但是看到的虽然是A,中间可能发生了A变B,B又变回A的情况。此时A已经非彼A,数据即使成功修改,也可能有问题。
可以通过AtomicStampedReference 解决ABA问题,它,一个带有标记的原子引用类,通过控制变量值的版本来保证CAS的正确性。
自旋CAS,如果一直循环执行,一直不成功,会给CPU带来非常大的执行开销。很多时候,CAS思想体现,是有个自旋次数的,就是为了避开这个耗时问题~
CAS 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。可以通过这两个方式解决这个问题:1. 使用互斥锁来保证原子性; 2.将多个变量封装成对象,通过AtomicReference来保证原子性。
有兴趣的朋友可以看看我之前的这篇实战文章哈~CAS乐观锁解决并发问题的一次实践
CountDownLatch和CyclicBarrier都用于让线程等待,达到一定条件时再运行。主要区别是:
举个例子吧:
CountDownLatch:假设老师跟同学约定周末在公园门口集合,等人齐了再发门票。那么,发门票(这个主线程),需要等各位同学都到齐(多个其他线程都完成),才能执行。
CyclicBarrier:多名短跑运动员要开始田径比赛,只有等所有运动员准备好,裁判才会鸣枪开始,这时候所有的运动员才会疾步如飞。
CPU的缓存是以缓存行(cache line)为单位进行缓存的,当多个线程修改相互独立的变量,而这些变量又处于同一个缓存行时就会影响彼此的性能。这就是伪共享
现代计算机计算模型:
也正是因为缓存行的存在,就导致了伪共享问题,如图所示:
假设数据a、b被加载到同一个缓存行。
既然伪共享是因为相互独立的变量存储到相同的Cache line导致的,一个缓存行大小是64字节。那么,我们就可以使用空间换时间的方法,即数据填充的方式,把独立的变量分散到不同的Cache line~
来看个例子:
/** * 更多干货内容,关注公众号:捡田螺的小男孩 */public class FalseShareTest { public static void main(String[] args) throws InterruptedException { Rectangle rectangle = new Rectangle(); long beginTime = System.currentTimeMillis(); Thread thread1 = new Thread(() -> { for (int i = 0; i < 100000000; i++) { rectangle.a = rectangle.a + 1; } }); Thread thread2 = new Thread(() -> { for (int i = 0; i < 100000000; i++) { rectangle.b = rectangle.b + 1; } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("执行时间" + (System.currentTimeMillis() - beginTime)); }}class Rectangle { volatile long a; volatile long b;}//运行结果:执行时间2815
一个long类型是8字节,我们在变量a和b之间不上7个long类型变量呢,输出结果是啥呢?如下:
class Rectangle { volatile long a; long a1,a2,a3,a4,a5,a6,a7; volatile long b;}//运行结果执行时间1113
可以发现利用填充数据的方式,让读写的变量分割到不同缓存行,可以很好挺高性能~
Fork/Join框架是Java7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
Fork/Join框架需要理解两个点,「分而治之」和「工作窃取算法」。
分而治之
以上Fork/Join框架的定义,就是分而治之思想的体现啦
工作窃取算法
把大任务拆分成小任务,放到不同队列执行,交由不同的线程分别执行时。有的线程优先把自己负责的任务执行完了,其他线程还在慢慢悠悠处理自己的任务,这时候为了充分提高效率,就需要工作盗窃算法啦~
工作盗窃算法就是,「某个线程从其他队列中窃取任务进行执行的过程」。一般就是指做得快的线程(盗窃线程)抢慢的线程的任务来做,同时为了减少锁竞争,通常使用双端队列,即快线程和慢线程各在一端。
ThreadLocal的内存结构图
为了对ThreadLocal有个宏观的认识,我们先来看下ThreadLocal的内存结构图
从内存结构图,我们可以看到:
关键源码分析
对照着关键源码来看,更容易理解一点哈~
首先看下Thread类的源码,可以看到成员变量ThreadLocalMap的初始值是为null
public class Thread implements Runnable { //ThreadLocal.ThreadLocalMap是Thread的属性 ThreadLocal.ThreadLocalMap threadLocals = null;}
成员变量ThreadLocalMap的关键源码如下:
static class ThreadLocalMap { static class Entry extends WeakReference> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } //Entry数组 private Entry[] table; // ThreadLocalMap的构造器,ThreadLocal作为key ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); }}
ThreadLocal类中的关键set()方法:
public void set(T value) { Thread t = Thread.currentThread(); //获取当前线程t ThreadLocalMap map = getMap(t); //根据当前线程获取到ThreadLocalMap if (map != null) //如果获取的ThreadLocalMap对象不为空 map.set(this, value); //K,V设置到ThreadLocalMap中 else createMap(t, value); //创建一个新的ThreadLocalMap } ThreadLocalMap getMap(Thread t) { return t.threadLocals; //返回Thread对象的ThreadLocalMap属性 } void createMap(Thread t, T firstValue) { //调用ThreadLocalMap的构造函数 t.threadLocals = new ThreadLocalMap(this, firstValue); this表示当前类ThreadLocal }
ThreadLocal类中的关键get()方法
public T get() { Thread t = Thread.currentThread();//获取当前线程t ThreadLocalMap map = getMap(t);//根据当前线程获取到ThreadLocalMap if (map != null) { //如果获取的ThreadLocalMap对象不为空 //由this(即ThreadLoca对象)得到对应的Value,即ThreadLocal的泛型值 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); //初始化threadLocals成员变量的值 } private T setInitialValue() { T value = initialValue(); //初始化value的值 Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); //以当前线程为key,获取threadLocals成员变量,它是一个ThreadLocalMap if (map != null) map.set(this, value); //K,V设置到ThreadLocalMap中 else createMap(t, value); //实例化threadLocals成员变量 return value; }
所以怎么回答ThreadLocal的实现原理?如下,最好是能结合以上结构图一起说明哈~
大家可以看下我之前这篇文章哈: ThreadLocal的八个关键知识点
我们先来看看TreadLocal的引用示意图哈:
关于ThreadLocal内存泄漏,网上比较流行的说法是这样的:
ThreadLocalMap使用ThreadLocal的弱引用作为key,当ThreadLocal变量被手动设置为null,即一个ThreadLocal没有外部强引用来引用它,当系统GC时,ThreadLocal一定会被回收。这样的话,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如线程池的核心线程),这些key为null的Entry的value就会一直存在一条强引用链:Thread变量 -> Thread对象 -> ThreaLocalMap -> Entry -> value -> Object 永远无法回收,造成内存泄漏。
当ThreadLocal变量被手动设置为null后的引用链图:
实际上,ThreadLocalMap的设计中已经考虑到这种情况。所以也加上了一些防护措施:即在ThreadLocal的get,set,remove方法,都会清除线程ThreadLocalMap里所有key为null的value。
源代码中,是有体现的,如ThreadLocalMap的set方法:
private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; return; } //如果k等于null,则说明该索引位之前放的key(threadLocal对象)被回收了,这通常是因为外部将threadLocal变量置为null, //又因为entry对threadLocal持有的是弱引用,一轮GC过后,对象被回收。 //这种情况下,既然用户代码都已经将threadLocal置为null,那么也就没打算再通过该对象作为key去取到之前放入threadLocalMap的value, 因此ThreadLocalMap中会直接替换调这种不新鲜的entry。 if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; //触发一次Log2(N)复杂度的扫描,目的是清除过期Entry if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
如ThreadLocal的get方法:
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { //去ThreadLocalMap获取Entry,方法里面有key==null的清除逻辑 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue();}private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else //里面有key==null的清除逻辑 return getEntryAfterMiss(key, i, e); } private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; // Entry的key为null,则表明没有外部引用,且被GC回收,是一个过期Entry if (k == null) expungeStaleEntry(i); //删除过期的Entry else i = nextIndex(i, len); e = tab[i]; } return null; }
有些小伙伴可能有疑问,ThreadLocal的key既然是弱引用.会不会GC贸然把key回收掉,进而影响ThreadLocal的正常使用?
弱引用:具有弱引用的对象拥有更短暂的生命周期。如果一个对象只有弱引用存在了,则下次GC将会回收掉该对象(不管当前内存空间足够与否)
其实不会的,因为有ThreadLocal变量引用着它,是不会被GC回收的,除非手动把ThreadLocal变量设置为null,我们可以跑个demo来验证一下:
public class WeakReferenceTest { public static void main(String[] args) { Object object = new Object(); WeakReference
结论就是,小伙伴放下这个疑惑了,哈哈~
给大家来看下一个内存泄漏的例子,其实就是用线程池,一直往里面放对象
public class ThreadLocalTestDemo { private static ThreadLocal tianLuoThreadLocal = new ThreadLocal<>(); public static void main(String[] args) throws InterruptedException { ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>()); for (int i = 0; i < 10; ++i) { threadPoolExecutor.execute(new Runnable() { @Override public void run() { System.out.println("创建对象:"); TianLuoClass tianLuoClass = new TianLuoClass(); tianLuoThreadLocal.set(tianLuoClass); tianLuoClass = null; //将对象设置为 null,表示此对象不在使用了 // tianLuoThreadLocal.remove(); } }); Thread.sleep(1000); } } static class TianLuoClass { // 100M private byte[] bytes = new byte[100 * 1024 * 1024]; }}创建对象:创建对象:创建对象:创建对象:Exception in thread "pool-1-thread-4" java.lang.OutOfMemoryError: Java heap space at com.example.dto.ThreadLocalTestDemo$TianLuoClass.(ThreadLocalTestDemo.java:33) at com.example.dto.ThreadLocalTestDemo$1.run(ThreadLocalTestDemo.java:21) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)
运行结果出现了OOM,tianLuoThreadLocal.remove();加上后,则不会OOM。
创建对象:创建对象:创建对象:创建对象:创建对象:创建对象:创建对象:创建对象:......
我们这里没有手动设置tianLuoThreadLocal变量为null,但是还是会内存泄漏。因为我们使用了线程池,线程池有很长的生命周期,因此线程池会一直持有tianLuoClass对象的value值,即使设置tianLuoClass = null;引用还是存在的。这就好像,你把一个个对象object放到一个list列表里,然后再单独把object设置为null的道理是一样的,列表的对象还是存在的。
public static void main(String[] args) { List
所以内存泄漏就这样发生啦,最后内存是有限的,就抛出了OOM了。如果我们加上threadLocal.remove();,则不会内存泄漏。为什么呢?因为threadLocal.remove();会清除Entry,源码如下:
private void remove(ThreadLocal<?> key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { //清除entry e.clear(); expungeStaleEntry(i); return; } }}
通过阅读ThreadLocal的源码,我们是可以看到Entry的Key是设计为弱引用的(ThreadLocalMap使用ThreadLocal的弱引用作为Key的)。为什么要设计为弱引用呢?
我们先来回忆一下四种引用:
我们先来看看官方文档,为什么要设计为弱引用:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.为了应对非常大和长时间的用途,哈希表使用弱引用的 key。
我再把ThreadLocal的引用示意图搬过来:
下面我们分情况讨论:
因此可以发现,使用弱引用作为Entry的Key,可以多一层保障:弱引用ThreadLocal不会轻易内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
实际上,我们的内存泄漏的根本原因是,不再被使用的Entry,没有从线程的ThreadLocalMap中删除。一般删除不再使用的Entry有这两种方式:
我们知道ThreadLocal是线程隔离的,如果我们希望父子线程共享数据,如何做到呢?可以使用InheritableThreadLocal。先来看看demo:
public class InheritableThreadLocalTest { public static void main(String[] args) { ThreadLocal threadLocal = new ThreadLocal<>(); InheritableThreadLocal inheritableThreadLocal = new InheritableThreadLocal<>(); threadLocal.set("关注公众号:捡田螺的小男孩"); inheritableThreadLocal.set("关注公众号:程序员田螺"); Thread thread = new Thread(()->{ System.out.println("ThreadLocal value " + threadLocal.get()); System.out.println("InheritableThreadLocal value " + inheritableThreadLocal.get()); }); thread.start(); }}//运行结果ThreadLocal value nullInheritableThreadLocal value 关注公众号:程序员田螺
可以发现,在子线程中,是可以获取到父线程的 InheritableThreadLocal 类型变量的值,但是不能获取到 ThreadLocal 类型变量的值。
获取不到ThreadLocal 类型的值,我们可以好理解,因为它是线程隔离的嘛。InheritableThreadLocal 是如何做到的呢?原理是什么呢?
在Thread类中,除了成员变量threadLocals之外,还有另一个成员变量:inheritableThreadLocals。它们两类型是一样的:
public class Thread implements Runnable { ThreadLocalMap threadLocals = null; ThreadLocalMap inheritableThreadLocals = null; }
Thread类的init方法中,有一段初始化设置:
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { ...... if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); /* Stash the specified stack size in case the VM cares */ this.stackSize = stackSize; /* Set thread ID */ tid = nextThreadID(); } static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { return new ThreadLocalMap(parentMap); }
可以发现,当parent的inheritableThreadLocals不为null时,就会将parent的inheritableThreadLocals,赋值给前线程的inheritableThreadLocals。说白了,就是如果当前线程的inheritableThreadLocals不为null,就从父线程哪里拷贝过来一个过来,类似于另外一个ThreadLocal,但是数据从父线程那里来的。有兴趣的小伙伴们可以在去研究研究源码~
举个简单的例子,如下:
/** * 关注公众号:捡田螺的小男孩 * 非常多干货 */public class AtomicIntegerTest { private static AtomicInteger atomicInteger = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { testIAdd(); } private static void testIAdd() throws InterruptedException { //创建线程池 ExecutorService executorService = Executors.newFixedThreadPool(2); for (int i = 0; i < 1000; i++) { executorService.execute(() -> { for (int j = 0; j < 2; j++) { //自增并返回当前值 int andIncrement = atomicInteger.incrementAndGet(); System.out.println("线程:" + Thread.currentThread().getName() + " count=" + andIncrement); } }); } executorService.shutdown(); Thread.sleep(100); System.out.println("最终结果是 :" + atomicInteger.get()); } }
运行结果:
...线程:pool-1-thread-1 count=1997线程:pool-1-thread-1 count=1998线程:pool-1-thread-1 count=1999线程:pool-1-thread-2 count=315线程:pool-1-thread-2 count=2000最终结果是 :2000
死锁是指多个线程因竞争资源而造成的一种互相等待的僵局。如图感受一下:
死锁的四个必要条件:
如何预防死锁?
使用多线程可以提升程序性能。但是如果使用过多的线程,则适得其反。
过多的线程会影响程序的系统。
因此,我们平时尽量使用线程池来管理线程。同时还需要设置恰当的线程数。
在Java语言中,有一个先行发生原则(happens-before)。它包括八大规则,如下:
LockSupport是一个工具类。它的主要作用是挂起和唤醒线程。该工具类是创建锁和其他同步类的基础。它的主要方法是
public static void park(Object blocker); // 暂停指定线程public static void unpark(Thread thread); // 恢复指定的线程public static void park(); // 无期限暂停当前线程
看个代码的例子:
public class LockSupportTest { private static Object object = new Object(); static MyThread thread = new MyThread("线程田螺"); public static class MyThread extends Thread { public MyThread(String name) { super(name); } @Override public void run() { synchronized (object) { System.out.println("线程名字: " + Thread.currentThread()); try { Thread.sleep(2000L); } catch (InterruptedException e) { e.printStackTrace(); } LockSupport.park(); if (Thread.currentThread().isInterrupted()) { System.out.println("线程被中断了"); } System.out.println("继续执行"); } } } public static void main(String[] args) { thread.start(); LockSupport.unpark(thread); System.out.println("恢复线程调用"); }}//output恢复线程调用线程名字: Thread[线程田螺,5,main]继续执行
因为thread线程内部有休眠2秒的操作,所以unpark方法的操作肯定先于park方法的调用。为什么thread线程最终仍然可以结束,是因为park和unpark会对每个线程维持一个许可证(布尔值)
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
我们的服务器CPU核数为8核,一个任务线程cpu耗时为20ms,线程等待(网络IO、磁盘IO)耗时80ms,那最佳线程数目:( 80 + 20 )/20 * 8 = 40。也就是设置 40个线程数最佳。
有兴趣的小伙伴,也可以看这篇文章哈: 线程池到底设置多少线程比较合适?
线程池:一个管理线程的池子。线程池可以:
大家可以看看我之前这篇文章,很经典, 面试必备:Java线程池解析
线程池的执行原理如下:
为了形象描述线程池执行,打个比喻:
我们先来看看ThreadPoolExecutor的构造函数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
四种拒绝策略
几种工作阻塞队列
我们先来看一段代码:
ExecutorService threadPool = Executors.newFixedThreadPool(5); for (int i = 0; i < 5; i++) { threadPool.submit(() -> { System.out.println("current thread name" + Thread.currentThread().getName()); Object object = null; System.out.print("result## "+object.toString()); }); }
显然,这段代码会有异常,我们再来看看执行结果
虽然没有结果输出,但是没有抛出异常,所以我们无法感知任务出现了异常,所以需要添加try/catch。 如下图:
OK,线程的异常处理,我们可以直接try...catch捕获。
最近写了一篇线程池坑相关的,大家可以去看看哈: 细数线程池的10个坑
AQS,即AbstractQueuedSynchronizer,是构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。可以回答以下这几个关键点哈:
CLH 同步队列,全英文Craig, Landin, and Hagersten locks。是一个FIFO双向队列,其内部通过节点head和tail记录队首和队尾元素,队列元素的类型为Node。AQS依赖它来完成同步状态state的管理,当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。
我们都知道,synchronized控制同步的时候,可以配合Object的wait()、notify(),notifyAll() 系列方法可以实现等待/通知模式。而Lock呢?它提供了条件Condition接口,配合await(),signal(),signalAll() 等方法也可以实现等待/通知机制。ConditionObject实现了Condition接口,给AQS提供条件变量的支持
ConditionObject队列与CLH队列的爱恨情仇:
模板方法模式:在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
AQS的典型设计模式就是模板方法设计模式啦。AQS全家桶(ReentrantLock,Semaphore)的衍生实现,就体现出这个设计模式。如AQS提供tryAcquire,tryAcquireShared等模板方法,给子类实现自定义的同步器。
你要实现自定义锁的话,首先需要确定你要实现的是独占锁还是共享锁,定义原子变量state的含义,再定义一个内部类去继承AQS,重写对应的模板方法即可啦
Semaphore,我们也把它叫做信号量。可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。
我们可以把它简单的理解成我们停车场入口立着的那个显示屏,每当有一辆车进入停车场显示屏就会显示剩余车位减1,每有一辆车从停车场出去,显示屏上显示的剩余车辆就会加1,当显示屏上的剩余车位为0时,停车场入口的栏杆就不会再打开,车辆就无法进入停车场了,直到有一辆车从停车场出去为止。
我们就以停车场的例子,来实现demo。
假设停车场最多可以停20辆车,现在有100辆要进入停车场。
我们很容易写出以下代码;
public class SemaphoreTest { private static Semaphore semaphore=new Semaphore(20); public static void main(String[] args) { ExecutorService executorService= Executors.newFixedThreadPool(200); //模拟100辆车要来 for (int i = 0; i < 100; i++) { executorService.execute(()->{ System.out.println("===="+Thread.currentThread().getName()+"准备进入停车场=="); //车位判断 if (semaphore.availablePermits() == 0) { System.out.println("车辆不足,请耐心等待"); } try { //获取令牌尝试进入停车场 semaphore.acquire(); System.out.println("====" + Thread.currentThread().getName() + "成功进入停车场"); //模拟车辆在停车场停留的时间 Thread.sleep(new Random().nextInt(20000)); System.out.println("====" + Thread.currentThread().getName() + "驶出停车场"); //释放令牌,腾出停车场车位 semaphore.release(); } catch (InterruptedException e) { e.printStackTrace(); } }); //线程池关闭 executorService.shutdown(); } }}
我们来看下实现的原理是怎样的。
Semaphore semaphore=new Semaphore(20);
它会创建一个非公平的锁的同步阻塞队列,并且把初始令牌数量(20)赋值给同步队列的state,这个state就是AQS的哈。
//构造函数,创建一个非公平的锁的同步阻塞队列 public Semaphore(int permits) { sync = new NonfairSync(permits);} NonfairSync(int permits) { super(permits);}//把令牌数量赋值给同步队列的stateSync(int permits) { setState(permits);}
2.可用令牌数
这个availablePermits,获取的就是state值。刚开始为20,所以肯定不会为0嘛。
semaphore.availablePermits();public int availablePermits() { return sync.getPermits();}final int getPermits() { return getState();}
接着我们再看下获取令牌的API
semaphore.acquire();
获取1个令牌
public void acquire() throws InterruptedException { sync.acquireSharedInterruptibly(1); } public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); //尝试获取令牌,arg为获取令牌个数 if (tryAcquireShared(arg) < 0) // doAcquireSharedInterruptibly(arg); }
尝试获取令牌,使用了CAS算法。
final int nonfairTryAcquireShared(int acquires) { for (;;) { int available = getState(); int remaining = available - acquires; if (remaining < 0 || compareAndSetState(available, remaining)) return remaining; } }
可获取令牌的话,就创建节点,加入阻塞队列;重双向链表的head,tail节点关系,清空无效节点;挂起当前节点线程
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { //创建节点加入阻塞队列 final Node node = addWaiter(Node.SHARED); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head) { //返回锁的state int r = tryAcquireShared(arg); if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; // help GC failed = false; return; } } //重组双向链表,清空无效节点,挂起当前线程 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }
semaphore.release(); /** * 释放令牌 */public void release() { sync.releaseShared(1);} public final boolean releaseShared(int arg) { //释放共享锁 if (tryReleaseShared(arg)) { //唤醒所有共享节点线程 doReleaseShared(); return true; } return false; }
在JDK1.6之前,synchronized的实现直接调用ObjectMonitor的enter和exit,这种锁被称之为重量级锁。从JDK6开始,HotSpot虚拟机开发团队对Java中的锁进行优化,如增加了适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等优化策略,提升了synchronized的性能。
什么是CPU上下文?
CPU 寄存器,是CPU内置的容量小、但速度极快的内存。而程序计数器,则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。它们都是 CPU 在运行任何任务前,必须的依赖环境,因此叫做CPU上下文。
什么是CPU上下文切换?
它是指,先把前一个任务的CPU上下文(也就是CPU寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。
一般我们说的上下文切换,就是指内核(操作系统的核心)在CPU上对进程或者线程进行切换。进程从用户态到内核态的转变,需要通过系统调用来完成。系统调用的过程,会发生CPU上下文的切换。
所以大家有时候会听到这种说法,线程的上下文切换。 它指,CPU资源的分配采用了时间片轮转,即给每个线程分配一个时间片,线程在时间片内占用 CPU 执行任务。当线程使用完时间片后,就会处于就绪状态并让出 CPU 让其他线程占用,这就是线程的上下文切换。看个图,可能会更容易理解一点
锁只是个一个标记,存在对象头里面。
下面从面向对象和观察者模式角度来分析。
面向对象的角度:我们可以把wait和notify直接理解为get和set方法。wait和notify方法都是对对象的锁进行操作,那么自然这些方法应该属于对象。举例来说,门对象上有锁属性,开锁和关锁的方法应该属于门对象,而不应该属于人对象。
从观察者模式的角度:对象是被观察者,线程是观察者。被观察者的状态如果发生变化,理应有被观察者去轮询通知观察者,否则的话,观察者怎么知道notify方法应该在哪个时刻调用?n个观察者的notify又如何做到同时调用?
来源:知乎 https://www.zhihu.com/question/321674476
AtomicInteger的底层,是基于CAS实现的。我们可以看下AtomicInteger的添加方法。如下
public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } 通过Unsafe类的实例来进行添加操作 public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));//使用了CAS算法实现 return var5; }
注意:compareAndSwapInt是一个native方法哈,它是基于CAS来操作int类型的变量。并且,其它的原子操作类基本也大同小异。
我们知道有两种调度模型:分时调度和抢占式调度。
Java默认的线程调度算法是抢占式。即线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。
几种常用线程池:
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), threadFactory); }
使用场景
FixedThreadPool 适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue(), threadFactory); }
使用场景
当提交任务的速度大于处理任务的速度时,每次提交一个任务,就必然会创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。由于空闲 60 秒的线程会被终止,长时间保持空闲的 CachedThreadPool 不会占用任何资源。
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), threadFactory)); }
使用场景
适用于串行执行任务的场景,一个任务一个任务地执行。
public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue()); }
使用场景
周期性执行任务的场景,需要限制线程数量的场景
FutureTask是一种可以取消的异步的计算任务。它的计算是通过Callable实现的,可以把它理解为是可以返回结果的Runnable。
使用FutureTask的优点:
它实现了Runnable接口和Future接口,底层基于生产者消费者模式实现。
FutureTask用于在异步操作场景中,FutureTask作为生产者(执行FutureTask的线程)和消费者(获取FutureTask结果的线程)的桥梁,如果生产者先生产出了数据,那么消费者get时能会直接拿到结果;如果生产者还未产生数据,那么get时会一直阻塞或者超时阻塞,一直到生产者产生数据唤醒阻塞的消费者为止。
public void interrupt() { if (this != Thread.currentThread()) checkAccess(); synchronized (blockerLock) { Interruptible b = blocker; if (b != null) { interrupt0(); // Just to set the interrupt flag b.interrupt(this); return; } } interrupt0(); } public static boolean interrupted() { return currentThread().isInterrupted(true); } public boolean isInterrupted() { return isInterrupted(false); }
可以使用join方法解决这个问题。比如在线程A中,调用线程B的join方法表示的意思就是:A等待B线程执行完毕后(释放CPU执行权),在继续执行。
代码如下:
public class ThreadTest { public static void main(String[] args) { Thread spring = new Thread(new SeasonThreadTask("春天")); Thread summer = new Thread(new SeasonThreadTask("夏天")); Thread autumn = new Thread(new SeasonThreadTask("秋天")); try { //春天线程先启动 spring.start(); //主线程等待线程spring执行完,再往下执行 spring.join(); //夏天线程再启动 summer.start(); //主线程等待线程summer执行完,再往下执行 summer.join(); //秋天线程最后启动 autumn.start(); //主线程等待线程autumn执行完,再往下执行 autumn.join(); } catch (InterruptedException e) { e.printStackTrace(); } }}class SeasonThreadTask implements Runnable{ private String name; public SeasonThreadTask(String name){ this.name = name; } @Override public void run() { for (int i = 1; i <4; i++) { System.out.println(this.name + "来了: " + i + "次"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } }}运行结果:春天来了: 1次春天来了: 2次春天来了: 3次夏天来了: 1次夏天来了: 2次夏天来了: 3次秋天来了: 1次秋天来了: 2次秋天来了: 3次
并发度就是segment的个数,通常是2的N次方。默认是16
Thread.sleep(long)方法,使线程转到超时等待阻塞(TIMED_WAITING) 状态。long参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,线程自动转为就绪(Runnable)状态。
interrupt()表示中断线程。需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到wait()/sleep()/join()后,就会立刻抛出InterruptedException。可以用isInterrupted()来获取状态。
Object类中的wait()方法,会导致当前的线程等待,直到其他线程调用此对象的notify()方法或notifyAll()唤醒方法。
Thread.yield()方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
Object的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。
notifyAll(),则是唤醒在此对象监视器上等待的所有线程。
ReentrantLock,是可重入锁,是JDK5中添加在并发包下的一个高性能的工具。它支持同一个线程在未释放锁的情况下重复获取锁。
我们先来看下是ReentrantLock使用的模板:
//实例化对象 ReentrantLock lock = new ReentrantLock(); //获取锁操作 lock.lock(); try { // 执行业务代码逻辑 } catch (Exception ex) { //异常处理 } finally { // 解锁操作 lock.unlock(); }
ReentrantLock无参构造函数,默认创建的是非公平锁,如下:
public ReentrantLock() { sync = new NonfairSync();}
而通过fair参数指定使用公平锁(FairSync)还是非公平锁(NonfairSync)
public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync();}
什么是公平锁?
什么是非公平锁?
大家可以结合AQS + 公平锁/非公平锁 + CAS去讲ReentrantLock的原理哈。
等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B 调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而 执行后续操作。
如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才 从thread.join()返回。线程Thread除了提供join()方法之外,还提供了join(long millis)和join(long millis,int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时 时间里没有终止,那么将会从该超时方法中返回。
ThreadLocal,即线程本地变量(每个线程都有自己唯一的一个哦),是一个以ThreadLocal对象为键、任意对象为值的存储结构。底层是一个ThreadLocalMap来存储信息,key是弱引用,value是强引用,所以使用完毕后要及时清理(尤其使用线程池时)。
大家可以看下我之前这篇文章,ThreadLocal的八个关键知识点
这是因为,JDK开发者提供了线程池的实现类都是有坑的,如newFixedThreadPool和newCachedThreadPool都有内存泄漏的坑。
来源:https://mp.weixin.qq.com/s/7js2uDZNv2Pb55ihKPc1xg
作者:捡田螺的小男孩
留言与评论(共有 0 条评论) “” |