本篇文章给大家带来了关于java的相关知识,其中主要介绍了java并发的相关问题,总结了一些问题,大家来看一下会多少,希望对大家有帮助。
本篇文章给大家带来了关于java的相关知识,其中主要介绍了java并发的相关问题,总结了一些问题,大家来看一下会多少,希望对大家有帮助。 推荐学习:《java教程》 1.并行跟并发有什么区别?从操作系统的角度来看,线程是CPU分配的最小单位。
就好像我们去食堂打饭,并行就是我们在多个窗口排队,几个阿姨同时打菜;并发就是我们挤在一个窗口,阿姨给这个打一勺,又手忙脚乱地给那个打一勺。 2.说说什么是进程和线程?要说线程,必须得先说说进程。
操作系统在分配资源时是把资源分配给进程的, 但是 CPU 资源比较特殊,它是被分配到线程的,因为真正要占用CPU运行的是线程,所以也说线程是 CPU分配的基本单位。 比如在Java中,当我们启动 main 函数其实就启动了一个JVM进程,而 main 函数在的线程就是这个进程中的一个线程,也称主线程。 一个进程中有多个线程,多个线程共用进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈。 3.说说线程有几种创建方式?Java中创建线程主要有三种方式,分别为继承Thread类、实现Runnable接口、实现Callable接口。
public class ThreadTest { /** * 继承Thread类 */ public static class MyThread extends Thread { @Override public void run() { System.out.println("This is child thread"); } } public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); }}
public class RunnableTask implements Runnable { public void run() { System.out.println("Runnable!"); } public static void main(String[] args) { RunnableTask task = new RunnableTask(); new Thread(task).start(); }} 上面两种都是没有返回值的,但是如果我们需要获取线程的执行结果,该怎么办呢?
public class CallerTask implements Callable<String> { public String call() throws Exception { return "Hello,i am running!"; } public static void main(String[] args) { //创建异步任务 FutureTask<String> task=new FutureTask<String>(new CallerTask()); //启动线程 new Thread(task).start(); try { //等待执行完成,并获取返回结果 String result=task.get(); System.out.println(result); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } }} 4.为什么调用start()方法时会执行run()方法,那怎么不直接调用run()方法?JVM执行start方法,会先创建一条线程,由创建出来的新线程去执行thread的run方法,这才起到多线程的效果。 **为什么我们不能直接调用run()方法?**也很清楚, 如果直接调用Thread的run()方法,那么run方法还是运行在主线程中,相当于顺序执行,就起不到多线程的效果。 5.线程有哪些常用的调度方法?线程等待与通知 在Object类中有一些函数可以用于线程的等待与通知。
上面是线程等待的方法,而唤醒线程主要是下面两个方法:
Thread类也提供了一个方法用于等待的方法:
线程休眠
让出优先权
线程中断 Java 中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。
6.线程有几种状态?在Java中,线程共有六种状态:
线程在自身的生命周期中, 并不是固定地处于某个状态,而是随着代码的执行在不同的状态之间进行切换,Java线程状态变化如图示: 7.什么是线程上下文切换?使用多线程的目的是为了充分利用CPU,但是我们知道,并发其实是一个CPU来应付多个线程。 为了让用户感觉多个线程是在同时执行的, CPU 资源的分配采用了时间片轮转也就是给每个线程分配一个时间片,线程在时间片内占用 CPU 执行任务。当线程使用完时间片后,就会处于就绪状态并让出 CPU 让其他线程占用,这就是上下文切换。 8.守护线程了解吗?Java中的线程分为两类,分别为 daemon 线程(守护线程)和 user 线程(用户线程)。 在JVM 启动时会调用 main 函数,main函数所在的钱程就是一个用户线程。其实在 JVM 内部同时还启动了很多守护线程, 比如垃圾回收线程。 那么守护线程和用户线程有什么区别呢?区别之一是当最后一个非守护线程束时, JVM会正常退出,而不管当前是否存在守护线程,也就是说守护线程是否结束并不影响 JVM退出。换而言之,只要有一个用户线程还没结束,正常情况下JVM就不会退出。 9.线程间有哪些通信方式?
关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。 关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。
可以通过Java内置的等待/通知机制(wait()/notify())实现一个线程修改一个对象的值,而另一个线程感知到了变化,然后进行相应的操作。
管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存。 管道输入/输出流主要包括了如下4种具体实现:PipedOutputStream、PipedInputStream、 PipedReader和PipedWriter,前两种面向字节,而后两种面向字符。
如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回。。线程Thread除了提供join()方法之外,还提供了join(long millis)和join(long millis,int nanos)两个具备超时特性的方法。
ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。 可以通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。
ThreadLocalThreadLocal其实应用场景不是很多,但却是被炸了千百遍的面试老油条,涉及到多线程、数据结构、JVM,可问的点比较多,一定要拿下。 10.ThreadLocal是什么?ThreadLocal,也就是线程本地变量。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。
创建了一个ThreadLoca变量localVariable,任何一个线程都能并发访问localVariable。 //创建一个ThreadLocal变量public static ThreadLocal<String> localVariable = new ThreadLocal<>();
线程可以在任何地方使用localVariable,写入变量。 localVariable.set("鄙人三某”);
线程在任何地方读取的都是它写入的变量。 localVariable.get(); 11.你在工作中用到过ThreadLocal吗?有用到过的,用来做用户信息上下文的存储。 我们的系统应用是一个典型的MVC架构,登录后的用户每次访问接口,都会在请求头中携带一个token,在控制层可以根据这个token,解析出用户的基本信息。那么问题来了,假如在服务层和持久层都要用到用户信息,比如rpc调用、更新用户获取等等,那应该怎么办呢? 一种办法是显式定义用户相关的参数,比如账号、用户名……这样一来,我们可能需要大面积地修改代码,多少有点瓜皮,那该怎么办呢? 这时候我们就可以用到ThreadLocal,在控制层拦截请求把用户信息存入ThreadLocal,这样我们在任何一个地方,都可以取出ThreadLocal中存的用户数据。 很多其它场景的cookie、session等等数据隔离也都可以通过ThreadLocal去实现。 我们常用的数据库连接池也用到了ThreadLocal:
12.ThreadLocal怎么实现的呢?我们看一下ThreadLocal的set(T)方法,发现先获取到当前线程,再获取 public void set(T value) { //获取当前线程 Thread t = Thread.currentThread(); //获取ThreadLocalMap ThreadLocalMap map = getMap(t); //讲当前元素存入map if (map != null) map.set(this, value); else createMap(t, value); } ThreadLocal实现的秘密都在这个 public class Thread implements Runnable { //ThreadLocal.ThreadLocalMap是Thread的属性 ThreadLocal.ThreadLocalMap threadLocals = null;} ThreadLocalMap既然被称为Map,那么毫无疑问它是<key,value>型的数据结构。我们都知道map的本质是一个个<key,value>形式的节点组成的数组,那ThreadLocalMap的节点是什么样的呢? static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; //节点类 Entry(ThreadLocal<?> k, Object v) { //key赋值 super(k); //value赋值 value = v; } } 这里的节点,key可以简单低视作ThreadLocal,value为代码中放入的值,当然实际上key并不是ThreadLocal本身,而是它的一个弱引用,可以看到Entry的key继承了 WeakReference(弱引用),再来看一下key怎么赋值的: public WeakReference(T referent) { super(referent); } key的赋值,使用的是WeakReference的赋值。
13.ThreadLocal 内存泄露是怎么回事?我们先来分析一下使用ThreadLocal时的内存,我们都知道,在JVM中,栈内存线程私有,存储了对象的引用,堆内存线程共享,存储了对象实例。 所以呢,栈中存储了ThreadLocal、Thread的引用,堆中存储了它们的具体实例。 ThreadLocalMap中使用的 key 为 ThreadLocal 的弱引用。
那么现在问题就来了,弱引用很容易被回收,如果ThreadLocal(ThreadLocalMap的Key)被垃圾回收器回收了,但是ThreadLocalMap生命周期和Thread是一样的,它这时候如果不被回收,就会出现这种情况:ThreadLocalMap的key没了,value还在,这就会造成了内存泄漏问题。
很简单,使用完ThreadLocal后,及时调用remove()方法释放内存空间。 ThreadLocal<String> localVariable = new ThreadLocal();try { localVariable.set("鄙人三某”); ……} finally { localVariable.remove();}
key设计成弱引用同样是为了防止内存泄漏。 假如key被设计成强引用,如果ThreadLocal Reference被销毁,此时它指向ThreadLoca的强引用就没有了,但是此时key还强引用指向ThreadLoca,就会导致ThreadLocal不能被回收,这时候就发生了内存泄漏的问题。 14.ThreadLocalMap的结构了解吗?ThreadLocalMap虽然被叫做Map,其实它是没有实现Map接口的,但是结构还是和HashMap比较类似的,主要关注的是两个要素:
private Entry[] table;
int i = key.threadLocalHashCode & (table.length - 1); 这里的threadLocalHashCode计算有点东西,每创建一个ThreadLocal对象,它就会新增 private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } 15.ThreadLocalMap怎么解决Hash冲突的?我们可能都知道HashMap使用了链表来解决冲突,也就是所谓的链地址法。 ThreadLocalMap没有使用链表,自然也不是用链地址法来解决冲突了,它用的是另外一种方式——开放定址法。开放定址法是什么意思呢?简单来说,就是这个坑被人占了,那就接着去找空着的坑。 如上图所示,如果我们插入一个value=27的数据,通过 hash计算后应该落入第 4 个槽位中,而槽位 4 已经有了 Entry数据,而且Entry数据的key和当前不相等。此时就会线性向后查找,一直找到 Entry为 null的槽位才会停止查找,把元素放到空的槽中。 在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该槽位Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置。 16.ThreadLocalMap扩容机制了解吗?在ThreadLocalMap.set()方法的最后,如果执行完启发式清理工作后,未清理到任何数据,且当前散列数组中 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); 再着看rehash()具体实现:这里会先去清理过期的Entry,然后还要根据条件判断 private void rehash() { //清理过期Entry expungeStaleEntries(); //扩容 if (size >= threshold - threshold / 4) resize();}//清理过期Entryprivate void expungeStaleEntries() { Entry[] tab = table; int len = tab.length; for (int j = 0; j < len; j++) { Entry e = tab[j]; if (e != null && e.get() == null) expungeStaleEntry(j); }} 接着看看具体的 具体代码: 17.父子线程怎么共享数据?父线程能用ThreadLocal来给子线程传值吗?毫无疑问,不能。那该怎么办? 这时候可以用到另外一个类—— 使用起来很简单,在主线程的InheritableThreadLocal实例设置值,在子线程中就可以拿到了。 public class InheritableThreadLocalTest { public static void main(String[] args) { final ThreadLocal threadLocal = new InheritableThreadLocal(); // 主线程 threadLocal.set("不擅技术"); //子线程 Thread t = new Thread() { @Override public void run() { super.run(); System.out.println("鄙人三某 ," + threadLocal.get()); } }; t.start(); }}
原理很简单,在Thread类里还有另外一个变量: ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; 在Thread.init的时候,如果父线程的 if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals) 18.说一下你对Java内存模型(JMM)的理解?Java内存模型(Java Memory Model,JMM),是一种抽象的模型,被定义出来屏蔽各种硬件和操作系统的内存访问差异。 JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在 Java内存模型的抽象图: 本地内存是JMM的 一个抽象概念,并不真实存在。它其实涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。 图里面的是一个双核 CPU 系统架构 ,每个核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辅运算。每个核都有自己的一级缓存,在有些架构里面还有一个所有 CPU 共享的二级缓存。 那么 Java 内存模型里面的工作内存,就对应这里的 Ll 缓存或者 L2 缓存或者 CPU 寄存器。 19.说说你对原子性、可见性、有序性的理解?原子性、有序性、可见性是并发编程中非常重要的基础概念,JMM的很多技术都是围绕着这三大特性展开。
int i = 2;int j = i;i++;i = i + 1;
20.那说说什么是指令重排?在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型。
从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序,如图: 我们比较熟悉的双重校验单例模式就是一个经典的指令重排的例子, JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。 21.指令重排有限制吗?happens-before了解吗?指令重排也是有一些限制的,有两个规则 happens-before的定义:
happens-before和我们息息相关的有六大规则:
22.as-if-serial又是什么?单线程的程序一定是顺序的吗?as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。 为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。为了具体说明,请看下面计算圆面积的代码示例。 double pi = 3.14; // Adouble r = 1.0; // B double area = pi * r * r; // C 上面3个操作的数据依赖关系: A和C之间存在数据依赖关系,同时B和C之间也存在数据依赖关系。因此在最终执行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的结果将会被改变)。但A和B之间没有数据依赖关系,编译器和处理器可以重排序A和B之间的执行顺序。 所以最终,程序可能会有两种执行顺序: as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器、runtime和处理器共同编织了这么一个“楚门的世界”:单线程程序是按程序的“顺序”来执行的。as- if-serial语义使单线程情况下,我们不需要担心重排序的问题,可见性的问题。 23.volatile实现原理了解吗?volatile有两个作用,保证可见性和有序性。
相比synchronized的加锁方式来解决共享变量的内存可见性问题,volatile就是更轻量的选择,它没有上下文切换的额外开销成本。 volatile可以确保对某个变量的更新对其他线程马上可见,一个变量被声明为volatile 时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存 当其它线程读取该共享变量 ,会从主内存重新获取最新值,而不是使用当前线程的本地内存中的值。 例如,我们声明一个 volatile 变量 volatile int x = 0,线程A修改x=1,修改完之后就会把新的值刷新回主内存,线程B读取x的时候,就会清空本地内存变量,然后再从主内存获取最新值。
重排序可以分为编译器重排序和处理器重排序,valatile保证有序性,就是通过分别限制这两种类型的重排序。 为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
24.synchronized用过吗?怎么使用?synchronized经常用的,用来保证代码的原子性。 synchronized主要有三种用法:
synchronized void method() { //业务代码}
synchronized void staic method() { //业务代码}
synchronized(this) { //业务代码} 25.synchronized的实现原理?
我们使用synchronized的时候,发现不用自己去lock和unlock,是因为JVM帮我们把这个事情做了。
monitorenter、monitorexit或者ACC_SYNCHRONIZED都是基于Monitor实现的。 实例对象结构里有对象头,对象头里面有一块结构叫Mark Word,Mark Word指针指向了monitor。 所谓的Monitor其实是一种同步工具,也可以说是一种同步机制。在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,可以叫做内部锁,或者Monitor锁。 ObjectMonitor的工作原理:
ObjectMonitor() { _header = NULL; _count = 0; // 记录线程获取锁的次数 _waiters = 0, _recursions = 0; //锁的重入次数 _object = NULL; _owner = NULL; // 指向持有ObjectMonitor对象的线程 _WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; } 可以类比一个去医院就诊的例子[18]:
这个过程就和Monitor机制比较相似:
所以我们就知道了,同步是锁住的什么东西:
26.除了原子性,synchronized可见性,有序性,可重入性怎么实现?
synchronized同步的代码块,具有排他性,一次只能被一个线程拥有,所以synchronized保证同一时刻,代码是单线程执行的。 因为as-if-serial语义的存在,单线程的程序能保证最终结果是有序的,但是不保证不会指令重排。 所以synchronized保证的有序是执行结果的有序性,而不是防止指令重排的有序性。
synchronized 是可重入锁,也就是说,允许一个线程二次请求自己持有对象锁的临界资源,这种情况称为可重入锁。 synchronized 锁对象的时候有个计数器,他会记录下线程获取锁的次数,在执行完对应的代码块之后,计数器就会-1,直到计数器清零,就释放锁了。 之所以,是可重入的。是因为 synchronized 锁对象有个计数器,会随着线程获取锁后 +1 计数,当线程执行完毕后 -1,直到清零释放锁。 27.锁升级?synchronized优化了解吗?了解锁升级,得先知道,不同锁的状态是什么样的。这个状态指的是什么呢? Java对象头里,有一块结构,叫 64 位虚拟机 Mark Word 是 64bit,我们来看看它的状态变化: Mark Word存储对象自身的运行数据,如哈希码、GC分代年龄、锁状态标志、偏向时间戳(Epoch) 等。
在JDK1.6之前,synchronized的实现直接调用ObjectMonitor的enter和exit,这种锁被称之为重量级锁。从JDK6开始,HotSpot虚拟机开发团队对Java中的锁进行优化,如增加了适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等优化策略,提升了synchronized的性能。
锁升级方向:无锁–>偏向锁—> 轻量级锁---->重量级锁,这个方向基本上是不可逆的。 我们看一下升级的过程: 偏向锁:偏向锁的获取:
偏向锁的撤销:
轻量级锁:轻量级锁的获取:
大体上省简的升级过程: 完整的升级过程: 28.说说synchronized和ReentrantLock的区别?可以从锁的实现、功能特点、性能等几个维度去回答这个问题:
下面的表格列出出了两种锁之间的区别: 29.AQS了解多少?AbstractQueuedSynchronizer 抽象同步队列,简称 AQS ,它是Java并发包的根基,并发包中的锁就是基于AQS实现的。
AQS 中的队列是 CLH 变体的虚拟双向队列,通过将每条请求共享资源的线程封装成一个节点来实现锁的分配: AQS 中的 CLH 变体等待队列拥有以下特性:
ps:AQS源码里面有很多细节可问,建议有时间好好看看AQS源码。 30.ReentrantLock实现原理?ReentrantLock 是可重入的独占锁,只能有一个线程可以获取该锁,其它获取该锁的线程会被阻塞而被放入该锁的阻塞队列里面。 看看ReentrantLock的加锁操作: // 创建非公平锁 ReentrantLock lock = new ReentrantLock(); // 获取锁操作 lock.lock(); try { // 执行代码逻辑 } catch (Exception ex) { // ... } finally { // 解锁操作 lock.unlock(); }
公平锁 FairSync
非公平锁 NonfairSync
默认创建的对象lock()的时候:
31.ReentrantLock怎么实现公平锁的?
public ReentrantLock() { sync = new NonfairSync();} 同时也可以在创建锁构造函数中传入具体参数创建公平锁 FairSync ReentrantLock lock = new ReentrantLock(true);--- ReentrantLock// true 代表公平锁,false 代表非公平锁public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync();} FairSync、NonfairSync 代表公平锁和非公平锁,两者都是 ReentrantLock 静态内部类,只不过实现不同锁语义。 非公平锁和公平锁的两处不同:
相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。 32.CAS呢?CAS了解多少?CAS叫做CompareAndSwap,?较并交换,主要是通过处理器的指令来保证操作的原?性的。 CAS 指令包含 3 个参数:共享变量的内存地址 A、预期的值 B 和共享变量的新值 C。 只有当内存中地址 A 处的值等于 B 时,才能将内存中地址 A 处的值更新为新值 C。作为一条 CPU 指令,CAS 指令本身是能够保证原子性的 。 33.CAS 有什么问题?如何解决?CAS的经典三大问题: ABA 问题并发环境下,假设初始条件是A,去修改数据时,发现是A就会执行修改。但是看到的虽然是A,中间可能发生了A变B,B又变回A的情况。此时A已经非彼A,数据即使成功修改,也可能有问题。
每次修改变量,都在这个变量的版本号上加1,这样,刚刚A->B->A,虽然A的值没变,但是它的版本号已经变了,再判断版本号就会发现此时的A已经被改过了。参考乐观锁的版本号,这种做法可以给数据带上了一种实效性的检验。 Java提供了AtomicStampReference类,它的compareAndSet方法首先检查当前的对象引用值是否等于预期引用,并且当前印戳(Stamp)标志是否等于预期标志,如果全部相等,则以原子方式将引用值和印戳标志的值更新为给定的更新值。 循环性能开销自旋CAS,如果一直循环执行,一直不成功,会给CPU带来非常大的执行开销。
在Java中,很多使用自旋CAS的地方,会有一个自旋次数的限制,超过一定次数,就停止自旋。 只能保证一个变量的原子操作CAS 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。
34.Java有哪些保证原子性的方法?如何保证多线程下i++ 结果正确?
35.原子操作类了解多少?当程序更新一个变量时,如果多线程同时更新这个变量,可能得到期望之外的值,比如变量i=1,A线程更新i+1,B线程也更新i+1,经过两个线程操作之后可能i不等于3,而是等于2。因为A和B线程在更新变量i的时候拿到的i都是1,这就是线程不安全的更新操作,一般我们会使用synchronized来解决这个问题,synchronized会保证多线程不会同时更新变量i。 其实除此之外,还有更轻量级的选择,Java从JDK 1.5开始提供了java.util.concurrent.atomic包,这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。 因为变量的类型有很多种,所以在Atomic包里一共提供了13个类,属于4种类型的原子更新方式,分别是原子更新基本类型、原子更新数组、原子更新引用和原子更新属性(字段)。 Atomic包里的类基本都是使用Unsafe实现的包装类。 使用原子的方式更新基本类型,Atomic包提供了以下3个类:
通过原子的方式更新数组里的某个元素,Atomic包提供了以下4个类:
原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic包提供了以下3个类:
如果需原子地更新某个类里的某个字段时,就需要使用原子更新字段类,Atomic包提供了以下3个类进行原子字段更新:
36.AtomicInteger 的原理?一句话概括:使用CAS实现。 以AtomicInteger的添加方法为例: public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } 通过 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)); return var5; } compareAndSwapInt 是一个native方法,基于CAS来操作int类型变量。其它的原子操作类基本都是大同小异。 37.线程死锁了解吗?该如何避免?死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。 那么为什么会产生死锁呢? 死锁的产生必须具备以下四个条件:
该如何避免死锁呢?答案是至少破坏死锁发生的一个条件。
38.那死锁问题怎么排查呢?可以使用jdk自带的命令行工具排查:
基本就可以看到死锁的信息。 还可以利用图形化工具,比如JConsole。出现线程死锁以后,点击JConsole线程面板的 39.CountDownLatch(倒计数器)了解吗?CountDownLatch,倒计数器,有两个常见的应用场景[18]: 场景1:协调子线程结束动作:等待所有子线程运行结束 CountDownLatch允许一个或多个线程等待其他线程完成操作。 例如,我们很多人喜欢玩的王者荣耀,开黑的时候,得等所有人都上线之后,才能开打。 CountDownLatch模仿这个场景(参考[18]): 创建大乔、兰陵王、安其拉、哪吒和铠等五个玩家,主线程必须在他们都完成确认后,才可以继续运行。 在这段代码中, public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(5); Thread 大乔 = new Thread(countDownLatch::countDown); Thread 兰陵王 = new Thread(countDownLatch::countDown); Thread 安其拉 = new Thread(countDownLatch::countDown); Thread 哪吒 = new Thread(countDownLatch::countDown); Thread 铠 = new Thread(() -> { try { // 稍等,上个卫生间,马上到... Thread.sleep(1500); countDownLatch.countDown(); } catch (InterruptedException ignored) {} }); 大乔.start(); 兰陵王.start(); 安其拉.start(); 哪吒.start(); 铠.start(); countDownLatch.await(); System.out.println("所有玩家已经就位!"); } 场景2. 协调子线程开始动作:统一各线程动作开始的时机 王者游戏中也有类似的场景,游戏开始时,各玩家的初始状态必须一致。不能有的玩家都出完装了,有的才降生。 所以大家得一块出生,在 在这个场景中,仍然用五个线程代表大乔、兰陵王、安其拉、哪吒和铠等五个玩家。需要注意的是,各玩家虽然都调用了 public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(1); Thread 大乔 = new Thread(() -> waitToFight(countDownLatch)); Thread 兰陵王 = new Thread(() -> waitToFight(countDownLatch)); Thread 安其拉 = new Thread(() -> waitToFight(countDownLatch)); Thread 哪吒 = new Thread(() -> waitToFight(countDownLatch)); Thread 铠 = new Thread(() -> waitToFight(countDownLatch)); 大乔.start(); 兰陵王.start(); 安其拉.start(); 哪吒.start(); 铠.start(); Thread.sleep(1000); countDownLatch.countDown(); System.out.println("敌方还有5秒达到战场,全军出击!"); } private static void waitToFight(CountDownLatch countDownLatch) { try { countDownLatch.await(); // 在此等待信号再继续 System.out.println("收到,发起进攻!"); } catch (InterruptedException e) { e.printStackTrace(); } } CountDownLatch的核心方法也不多:
40.CyclicBarrier(同步屏障)了解吗?CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一 组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。 它和CountDownLatch类似,都可以协调多线程的结束动作,在它们结束后都可以执行特定动作,但是为什么要有CyclicBarrier,自然是它有和CountDownLatch不同的地方。 不知道你听没听过一个新人UP主小约翰可汗,小约翰生平有两大恨——“想结衣结衣不依,迷爱理爱理不理。”我们来还原一下事情的经过:小约翰在亲政后认识了新垣结衣,于是决定第一次选妃,向结衣表白,等待回应。然而新垣结衣回应嫁给了星野源,小约翰伤心欲绝,发誓生平不娶,突然发现了铃木爱理,于是小约翰决定第二次选妃,求爱理搭理,等待回应。 我们拿代码模拟这一场景,发现CountDownLatch无能为力了,因为CountDownLatch的使用是一次性的,无法重复利用,而这里等待了两次。此时,我们用CyclicBarrier就可以实现,因为它可以重复利用。 运行结果: CyclicBarrier最最核心的方法,仍然是await():
上面的例子抽象一下,本质上它的流程就是这样就是这样: 41.CyclicBarrier和CountDownLatch有什么区别?两者最核心的区别[18]:
它们区别用一个表格整理:
42.Semaphore(信号量)了解吗?Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。 听起来似乎很抽象,现在汽车多了,开车出门在外的一个老大难问题就是停车 。停车场的车位是有限的,只能允许若干车辆停泊,如果停车场还有空位,那么显示牌显示的就是绿灯和剩余的车位,车辆就可以驶入;如果停车场没位了,那么显示牌显示的就是绿灯和数字0,车辆就得等待。如果满了的停车场有车离开,那么显示牌就又变绿,显示空车位数量,等待的车辆就能进停车场。 我们把这个例子类比一下,车辆就是线程,进入停车场就是线程在执行,离开停车场就是线程执行完毕,看见红灯就表示线程被阻塞,不能执行,Semaphore的本质就是协调多个线程对共享资源的获取。 我们再来看一个Semaphore的用途:它可以用于做流量控制,特别是公用资源有限的应用场景,比如数据库连接。 假如有一个需求,要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个线程并发地读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这时我们必须控制只有10个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。这个时候,就可以使用Semaphore来做流量控制,如下: public class SemaphoreTest { private static final int THREAD_COUNT = 30; private static ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_COUNT); private static Semaphore s = new Semaphore(10); public static void main(String[] args) { for (int i = 0; i < THREAD_COUNT; i++) { threadPool.execute(new Runnable() { @Override public void run() { try { s.acquire(); System.out.println("save data"); s.release(); } catch (InterruptedException e) { } } }); } threadPool.shutdown(); }} 在代码中,虽然有30个线程在执行,但是只允许10个并发执行。Semaphore的构造方法 43.Exchanger 了解吗?Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。 这两个线程通过 exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。 Exchanger可以用于遗传算法,遗传算法里需要选出两个人作为交配对象,这时候会交换两人的数据,并使用交叉规则得出2个交配结果。Exchanger也可以用于校对工作,比如我们需要将纸制银行流水通过人工的方式录入成电子银行流水,为了避免错误,采用AB岗两人进行录入,录入到Excel之后,系统需要加载这两个Excel,并对两个Excel数据进行校对,看看是否录入一致。 public class ExchangerTest { private static final Exchanger<String> exgr = new Exchanger<String>(); private static ExecutorService threadPool = Executors.newFixedThreadPool(2); public static void main(String[] args) { threadPool.execute(new Runnable() { @Override public void run() { try { String A = "银行流水A"; // A录入银行流水数据 exgr.exchange(A); } catch (InterruptedException e) { } } }); threadPool.execute(new Runnable() { @Override public void run() { try { String B = "银行流水B"; // B录入银行流水数据 String A = exgr.exchange("B"); System.out.println("A和B数据是否一致:" + A.equals(B) + ",A录入的是:" + A + ", |