java并发三大特性,java并发经验

  java并发三大特性,java并发经验

  这篇文章给大家带来了一些关于java的知识,主要介绍了一些与java并发相关的问题,并总结了一些问题。来看看会有多少吧,希望对你有帮助。

  如何解决写爬虫IP受阻的问题?立即使用。

  

1.并行跟并发有什么区别?

  从操作系统的角度来看,线程是CPU分配的最小单位。

  并行意味着两个线程同时执行。这需要两个CPU分别执行两个线程。并发是指同一时间只执行一个线程,但两个线程都在一段时间内执行。并发的实现依赖于CPU切换线程,因为切换时间极短,所以用户基本察觉不到。

  就好像我们去食堂做饭一样。并行就是我们在多个窗口排队,几个阿姨同时做饭;同时我们挤在一个窗口,我阿姨给这个一勺,然后赶紧给那个一勺。

  

2.说说什么是进程和线程?

  要说线程,首先要说进程。

  进程:进程是代码在数据集上的运行活动,是系统中资源分配和调度的基本单位。线程:线程是一个进程的执行路径。一个进程中至少有一个线程,一个进程中的多个线程共享该进程的资源。操作系统分配资源的时候是给进程分配资源,但是CPU资源比较特殊,是分配给线程的。因为真正占用CPU的是线程,所以也有人说线程是CPU分配的基本单位。

  比如在Java中,当我们启动main函数时,实际上是启动了一个JVM进程,main函数所在的线程就是这个进程中的一个线程,也叫主线程。

  一个进程中有多个线程,它们共享进程的堆和方法区资源,但每个线程都有自己的程序计数器和堆栈。

  

3.说说线程有几种创建方式?

   Java创建线程主要有三种方式,分别是继承线程类、实现Runnable接口、实现Callable接口。

  继承Thread类,重写run()方法,调用start()方法启动线程公共类ThreadTest {

  /**

  *继承线程类。

  */

  公共静态类MyThread扩展线程{

  @覆盖

  公共无效运行(){

  System.out.println(这是子线程);

  }

  }

  公共静态void main(String[] args) {

  MyThread thread=new MyThread();

  thread . start();

  }}实现runnable接口并覆盖run()方法公共类Runnable任务实现runnable {

  公共无效运行(){

  System.out.println(Runnable!);

  }

  公共静态void main(String[] args) {

  runnable task task=new runnable task();

  新线程(任务)。start();

  }}以上两种都是没有返回值的,但是如果需要得到线程的执行结果怎么办?

  实现Callable接口并重写call()方法。这样就可以通过FutureTask得到任务执行的返回值。公共类调用者任务实现可调用字符串{

  公共字符串调用()引发异常{

  回复‘你好,我在跑步!’;

  }

  公共静态void main(String[] args) {

  //创建异步任务

  FutureTaskString task=new FutureTaskString(new caller task());

  //启动线程

  新线程(任务)。start();

  尝试{

  //等待执行完成,并获得返回的结果

  字符串result=task . get();

  System.out.println(结果);

  } catch (InterruptedException e) {

  e . printstacktrace();

  } catch (ExecutionException e) {

  e . printstacktrace();

  }

  } }

4.为什么调用start()方法时会执行run()方法,那怎么不直接调用run()方法?

   JVM执行start方法时,会先创建一个线程,创建的新线程会执行thread的run方法,起到多线程的效果。

  * *为什么不能直接调用run()方法?* *同样清楚的是,如果直接调用Thread的run()方法,那么run方法仍然在主线程中运行,相当于顺序执行,不会有多线程的效果。

  

5.线程有哪些常用的调度方法?

  线程等待与通知

  Object类中有一些函数可以用来等待和通知线程。

  Wait():当线程A调用共享变量的wait()方法时,线程A会被阻塞挂起,在返回之前会发生以下情况:

  (1)线程A调用共享对象的notify()或notifyAll()方法;

  (2)其他线程调用线程A的interrupt()方法,线程A抛出InterruptedException异常。

  Wait(长超时):这个方法比wait()方法多一个超时参数。不同的是,如果线程A调用了共享对象的wait(long timeout)方法,并且在指定的超时ms时间内没有被其他线程唤醒,这个方法还是会因为超时而返回。

  Wait(long timeout,int nanos),其内部调用是wait (long timeout)函数。

  以上是线程等待的方法,而唤醒线程主要是以下两种方法:

  Notify():线程A调用共享对象的notify()方法后,会唤醒一个在这个共享变量上调用wait series方法后被挂起的线程。可能有多个线程在等待一个共享变量,唤醒哪个等待的线程是随机的。Notfyall():与在共享变量上调用notify()函数会唤醒在共享变量上阻塞的线程不同,notifyAll()方法会唤醒由于调用wait series方法而在共享变量上挂起的所有线程。Thread类还提供了一种等待方法:

  Join():如果一个线程A执行了thread.join()语句,就意味着当前线程A一直等到线程终止。

  从thread.join()返回。

  线程休眠

  sleep(long millis):Thread类中的一个静态方法。当正在执行的线程A调用线程的sleep方法时,线程A将在指定的时间内暂时放弃执行权,但是线程A拥有的监视器资源(如锁)仍然被持有。当指定的睡眠时间到了,函数会正常返回,然后参与CPU调度。获得CPU资源后,可以继续运行。让出优先权

  yield():Thread类中的静态方法。当一个线程调用yield方法时,实际上暗示了线程调度器的当前线程请求放弃自己的CPU,但是线程调度器可以无条件忽略这个暗示。线程中断

  Java中的线程中断是线程间的一种协作方式。通过设置线程的中断标志,不能直接终止线程的执行,而是被中断的线程根据中断状态自行处理。

  Voidiinterrupt():中断线程。比如线程A在运行时,线程B可以调用千程中断()方法,将线程的中断标志设置为true,并立即返回。设置标志只是设置标志。线程A实际上没有被中断,并将继续执行。Boolean isInterrupted()方法:检测当前线程是否中断。Boolean interrupted()方法:检测当前线程是否被中断。与中断不同,如果发现当前线程被中断,该方法将清除中断标志。

6.线程有几种状态?

  在Java中,线程有六种状态:

  在它自己的生命周期中,

状态说明
NEW初始状态:线程被创建,但还没有调用start()方法
RUNNABLE运行状态:Java线程将操作系统中的就绪和运行两种状态笼统的称作“运行”
BLOCKED阻塞状态:表示线程阻塞于锁
WAITING等待状态:表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)
TIME_WAITING超时等待状态:该状态不同于 WAITIND,它是可以在指定的时间自行返回的
TERMINATED终止状态:表示当前线程已经执行完毕
线程不是固定在某个状态,而是随着代码的执行在不同的状态之间切换。Java线程状态会发生变化,如图所示:

  

7.什么是线程上下文切换?

  使用多线程的目的是为了充分利用CPU,但是我们知道并发实际上是一个CPU处理多个线程。

  为了让用户感觉到多个线程在同时执行,CPU资源采用时间片轮换的方式分配,即每个线程分配一个时间片,线程占用CPU在时间片内执行任务。当一个线程用完时间片时,它将准备好并为其他线程放弃CPU,这就是上下文切换。

  

8.守护线程了解吗?

   Java中的线程分为两类,即守护线程(daemon thread)和用户线程(user thread)。

  JVM启动时会调用主函数,主函数所在的钱程是一个用户线程。事实上,很多守护线程也是在JVM内部启动的,比如垃圾收集线程。

  那么守护线程和用户线程有什么区别呢?其中一个区别是,当最后一个非守护线程捆绑时,不管当前是否有守护线程,JVM都会正常退出,也就是说守护线程结束与否并不影响JVM的退出。换句话说,只要一个用户线程没有完成,JVM就不会正常退出。

  

9.线程间有哪些通信方式?

  volatile和synchronized关键字关键字volatile可以用来修改一个字段(成员变量),即通知程序任何对变量的访问都需要从共享内存中获取,对它的更改必须同步刷新回共享内存,这样可以保证所有线程对变量的访问可见。

  关键字synchronized可以以修改方法或同步块的形式使用。它主要保证多个线程在一个方法或同步块中同时只能有一个线程,它保证线程访问变量的可见性和排他性。

  等待/通知机制Java内置的等待/通知机制(wait()/notify())使一个线程能够修改一个对象的值,而另一个线程感知到这种变化,然后做出相应的动作。

  管道输入/输出流管道I/O流不同于普通的文件I/O流或网络I/O流,主要用于线程间的数据传输,传输介质是内存。

  管道I/O流主要包括以下四种具体实现:PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前两种是面向字节的,后两种是面向字符的。

  使用Thread.join()如果一个线程A执行了thread.join()语句,则意味着当前线程A等待线程终止后,才从thread.join()返回。Thread不仅提供了join()方法,还提供了两个具有超时特性的方法:join(long millis)和join(long millis,int nanos)。

  使用ThreadLocalThreadLocal,即线程变量,是以ThreadLocal对象为键,任意对象为值的存储结构。这个结构附加在线程上,意味着线程可以根据ThreadLocal对象查询绑定到这个线程的值。

  可以通过set(T)方法设置一个值,然后在当前线程下通过get()方法获取原来的设置值。

  

ThreadLocal

   ThreadLocal其实没有太多应用场景,不过是被吹了几千次的面试滑头。它涉及到多线程、数据结构和JVM。有很多要点要问,一定要赢。

  

10.ThreadLocal是什么?

   ThreadLocal,这是线程局部变量。如果创建一个ThreadLocal变量,每个访问该变量的线程都将拥有该变量的一个本地副本。多线程操作这个变量时,实际上是在自己的本地内存中操作变量,从而起到线程隔离的作用,避免线程安全问题。

  创建一个ThreadLoca变量localVariable,任何线程都可以并发访问localVariable。

  //创建一个ThreadLocal变量公共静态threadlocal字符串局部变量=new thread local();编写器线程可以在任何地方使用localVariable来编写变量。

  LocalVariable.set(我三);读取线程到处读取它所写的变量。

  localVariable.get()。

11.你在工作中用到过ThreadLocal吗?

  用于存储用户信息上下文。

  我们的系统应用程序是典型的MVC架构。每次登录的用户访问接口时,他都会在请求头中携带一个令牌。根据这个令牌,可以在控制层解析用户的基本信息。那么问题来了。如果服务层和持久层都使用用户信息,比如rpc调用,更新用户获取等等,怎么办?

  一种方法是显式定义与用户相关的参数,比如帐号和用户名.这样我们可能需要大面积修改代码,这就有些表面化了,那该怎么办呢?

  这时候我们可以使用ThreadLocal在控制层拦截将用户信息存储在ThreadLocal中的请求,这样就可以在任何地方取出存储在ThreadLocal中的用户数据。

  cookie、会话等的数据隔离。在很多其他场景下也可以通过ThreadLocal来实现。

  ThreadLocal也在我们的公共数据库连接池中使用:

  数据库连接池的连接由ThreadLoca管理,保证当前线程的操作是同一个连接。

12.ThreadLocal怎么实现的呢?

  我们来看看ThreadLocal的set(T)方法,发现我们先获取当前线程,再获取ThreadLocalmap,然后将元素存储在这个map中。

  公共空集(T值){

  //获取当前线程

  thread t=thread . currentthread();

  //获取ThreadLocalMap

  ThreadLocalMap map=get map(t);

  //谈论当前存储在map中的元素

  如果(图!=空)

  map.set(this,value);

  其他

  createMap(t,value);

  } thread local实现的秘密都在这个ThreadLocalMap里。您可以定义ThreadLocal类型的成员变量threadLocals。Thread类中的ThreadLocalMap。

  公共类线程实现Runnable {

  //ThreadLocal。ThreadLocalMap是Thread的一个属性。

  ThreadLocalThreadLocalMap thread locals=null;}ThreadLocalMap叫Map,所以毫无疑问是一个key,value数据结构。我们都知道map的本质是一个以键和值的形式存在的节点数组。ThreadLocalMap的节点是什么样的?

  静态类入口扩展WeakReferenceThreadLocal?{

  /**与此ThreadLocal关联的值。*/

  对象值;

  //节点类

  Entry(ThreadLocal?k,对象v) {

  //键分配

  超(k);

  //值赋值

  值=v;

  }

  }这里的节点,key可以简单看成ThreadLocal,value就是放在代码里的值。当然,其实关键不是ThreadLocal本身,而是它的一个弱引用。可以看出,Entry的key继承了WeakReference。让我们来看看如何分配密钥:

  公共弱引用(T referent) {

  super(指物);

  }键,使用WeakReference的赋值。

  Thread类有一个ThreadLocal类型的实例变量threadLocals。ThreadLocalMap,并且每个线程都有自己的ThreadLocalMap。ThreadLocalMap在内部维护一个条目数组,每个条目代表一个完整的对象,key是ThreadLocal的弱引用,value是ThreadLocal的泛型值。当每个线程在ThreadLocal中设置一个值时,它会将其存储在自己的ThreadLocalMap中。读取时,它也以某个ThreadLocal为参考,在自己的map中寻找对应的key,从而实现线程隔离。ThreadLocal本身并不存储值,它只是作为线程访问ThreadLocalMap中的值的一个键。

13.ThreadLocal 内存泄露是怎么回事?

  我们先来分析一下使用ThreadLocal时的内存。众所周知,在JVM中,栈内存线程是私有的,存储对象的引用,堆内存线程是共享的,存储对象实例。

  因此,对ThreadLocal和Thread的引用存储在堆栈中,它们的具体实例存储在堆中。

  ThreadLocalMap中使用的键是ThreadLocal的弱引用。

  那么问题就来了。弱引用很容易被回收。如果Thread local(ThreadLocalMap的键)被垃圾收集器回收,但是ThreadLocalMap的生命周期和Thread是一样的,此时不回收就会出现这样的情况:Thread local Map的键没了,值还在,会是造成了内存泄漏问题

  很简单。使用ThreadLocal后,及时调用remove()方法释放内存空间。

  ThreadLocalString local variable=new thread local();尝试{

  LocalVariable.set(我三);

  ……最后{

  local variable . remove();}键被设计为弱引用,以防止内存泄漏。

  如果键被设计成一个强引用,如果ThreadLocal引用被破坏,那么它对ThreadLoca的强引用就没有了。但如果此时键仍然强引用ThreadLocal,thread local就无法回收,这时就会发生内存泄漏。

  

14.ThreadLocalMap的结构了解吗?

   ThreadLocalMap被称为Map,但它不实现Map接口。但其结构类似于HashMap,主要集中在两个元素上:元素数组和hash方法。

  元素数组

  一个表数组,存储Entry类型的元素。Entry是以threal弱引用为键,以Object为值的结构。

  私有条目[]表;哈希方法

  hash方法就是如何将对应的键映射到表数组对应的下标。ThreadLocalMap使用散列余数方法,取出键的threadLocalHashCode,然后从表数组的长度中减去一(相当于余数)。

  int I=key . threadlocalhashcode(table . length-1);这里的threadLocalHashCode计算是有意义的。每创建一个ThreadLocal对象,就会增加0x61c88647。这个值很特别。就是斐波那契数,也叫黄金分割数。hash的增量是这个数,收益是hash分布非常均匀

  私有静态final int HASH _ INCREMENT=0x61c 88647;

  private static int nextHashCode(){

  返回nexthashcode . getandadd(HASH _ INCREMENT);

  }

15.ThreadLocalMap怎么解决Hash冲突的?

  我们可能都知道HashMap使用链表来解决冲突,也就是所谓的链地址法。

  ThreadLocalMap不使用链表,自然也不使用链地址的方法来解决冲突。它用的是另一种方式,3354开放定址法。开放地址法是什么意思?简单来说,这个坑被占了,那就去找那个空坑吧。

  如上图所示,如果我们插入一个value=27的数据,那么经过哈希计算后应该会落入第四个槽,而槽4已经有了Entry数据,Entry数据的key不等于当前的。此时,它将向后线性搜索,并停止搜索,直到找到空条目的槽,并将该元素放入空槽中。

  在get时,它还会根据ThreadLocal对象的hash值在table中定位位置,然后判断slot Entry对象中的key是否与get的key一致,如果不一致,则确定下一个位置。

  

16.ThreadLocalMap扩容机制了解吗?

  在ThreadLocalMap.set()方法结束时,如果启发式清理后没有清理任何数据,并且当前哈希数组中的条目数已经达到列表的扩展阈值(len*2/3),则执行rehash()逻辑:

  如果(!cleanSomeSlots(i,sz) sz=阈值)

  rehash();我们来看rehash()的具体实现:这里先清理过期的条目,然后根据条件判断size=threshold-threshold/4,即size=threshold* 3/4,决定是否需要扩容。

  私有void rehash() {

  //清除过期的条目

  expungeStaleEntries();

  //扩展

  if(大小=阈值-阈值/4)

  resize();}//清理过期条目private void expungestaleentries(){

  entry[]tab=table;

  int len=tab.length

  for(int j=0;j lenj ) {

  条目e=tab[j];

  如果(e!=null e.get()==null)

  expungeStaleEntry(j);

  }}}然后看具体的resize()方法。扩展后的newTab的大小是旧数组的两倍,然后遍历oldTable数组。hash方法重新计算位置,打开地址解决冲突,然后将其放入新的newTab中。遍历完成后,旧选项卡中的所有条目数据都已放入newTab,然后表引用指向newTab。

  特定代码:

  

17.父子线程怎么共享数据?

  父线程可以使用ThreadLocal将值传递给子线程吗?毫无疑问,没有。那我该怎么办?

  此时,可以使用另一个类——InheritableThreadLocal。

  很好用。在主线程的InheritableThreadLocal实例中设置值,可以在子线程中获取。

  公共类InheritableThreadLocalTest {

  公共静态void main(String[] args) {

  final thread local thread local=new inheritable thread local();

  //主线程

  ThreadLocal.set(不擅长技术);

  //子线程

  线程t=新线程(){

  @覆盖

  公共无效运行(){

  super . run();

  System.out.println(我是第三个, thread local . get());

  }

  };

  t . start();

  }}原理很简单。Thread类中还有另一个变量:

  ThreadLocalThreadLocalMap inheritable thread locals=null;在Thread.init,如果父线程的inheritableThreadLocals不为空,则将其分配给当前线程(子线程)的inheritableThreadLocals。

  if(inheritThreadLocals parent . inheritablethreadlocals!=空)

  this.inheritableThreadLocals=

  ThreadLocal创建继承的映射(父级。InheritableThreadLocals)

18.说一下你对Java内存模型(JMM)的理解?

   Java内存模型(JMM),一个抽象模型,定义用来屏蔽各种硬件和操作系统的内存访问差异。

  JMM定义了线程和主存之间的抽象关系:线程之间的共享变量存储在主存中,每个线程都有一个私有的本地内存,内存中存储着共享变量的副本,用于读/写。

  Java内存模型的抽象图;

  本地记忆是JMM的一个抽象概念,但它并不真正存在。实际上,它涵盖了缓存、写缓冲区、寄存器和其他硬件以及编译器优化。

  该图显示了双核CPU系统架构。每个内核都有自己的控制器和运算器,其中控制器包含一组寄存器和运算控制器,运算器执行算术逻辑运算。每个内核都有自己的一级缓存,在某些架构中,所有CPU共享一个二级缓存。那么Java内存模型中的工作内存对应于这里的L1缓存或者L2缓存或者CPU寄存器。

  

19.说说你对原子性、可见性、有序性的理解?

  原子性、有序性和可见性是并发编程中非常重要的基本概念,JMM的很多技术都围绕着这三个特性。

  原子性:原子性是指一个操作是不可分割的、不间断的,要么全部执行且执行的过程不会被任何因素打断,要么根本不执行。可见性:可见性是指当一个线程修改了一个共享变量的值时,其他线程可以立即知道这个修改。有序性:有序性是指一个线程的执行代码,从前到后依次执行。在单线程下,程序可以看作是有序的,但并发时可能会发生指令重排。int I=2;int j=I;我;I=I 1;第一句是基本类型赋值,是原子操作。第二句先读取I的值,然后赋值给j,两步操作不能保证原子性。第三和第四句其实是等价的。先读取I的值,再读取1,最后赋给I,三步操作不能保证原子性。原子性:JMM只能保证基本的原子性。如果你想保证代码块的原子性,你需要使用synchronized。可见性:Java使用volatile关键字来确保可见性。此外,final和synchronized还可以确保可见性。有序性:无论是synchronized还是volatile都可以保证多线程之间操作的顺序。

20.那说说什么是指令重排?

  在执行程序时,编译器和处理器通常会对指令进行重新排序以提高性能。重新排序分为三种类型。

  编译器优化的重新排序。编译器可以重新安排语句的执行顺序,而不改变单线程程序的语义。指令级并行的重新排序。现代处理器使用指令级并行(ILP)技术来重叠和执行多条指令。如果没有数据依赖性,处理器可以改变对应于语句的机器指令的执行顺序。系统内存的重新排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看起来是无序的。从Java源代码到实际执行的指令序列,会分别经历以下三种重新排序,如图所示:

  大家熟悉的双重检查singleton模式是指令重排的经典例子,singleton instance=newsingleton();对应的JVM指令分为三步:分配内存空间——初始化对象——对象指向分配的内存空间,但编译器的指令重新排序后,第二步和第三步可能会重新排序。

  JMM属于语言级内存模型,通过禁止某些类型的编译器重排序和处理器重排序,确保在不同的编译器和不同的处理器平台上,为程序员提供一致的内存可见性保证。

  

21.指令重排有限制吗?happens-before了解吗?

  指令重排也有一些限制。有两个规则来限制它,发生之前和好像连续。

  发生之前的定义:

  如果一个操作发生在另一个操作之前,第一个操作的执行结果对第二个操作可见,第一个操作的执行顺序排在第二个操作之前。两个操作之间存在先发生后关系并不意味着Java平台的特定实现必须按照先发生后关系指定的顺序执行。如果重新排序后的执行结果与根据发生前关系的执行结果一致,则该重新排序不违法。有六条规则与之前发生的事情密切相关:

  程序顺序规则:线程中的每个操作都发生在该线程中任何后续操作之前。监视器锁规则:开锁,发生在稍后上锁之前。volatile变量规则:易失性域的写入发生在对易失性域的任何后续读取之前。传递性:如果A发生-在B之前,B发生-在C之前,那么A发生-在C之前start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么线程A的ThreadB.start()操作发生-在线程B的任何操作之前join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任何操作发生-before从线程A中的ThreadB.join()操作成功返回010-010编译器、运行时和处理器必须符合as-if-serial语义。

  为了符合as-if-serial语义,编译器和处理器不会对具有数据依赖性的操作进行重新排序,因为这种重新排序会改变执行结果。然而,如果操作之间没有数据依赖性,这些操作可以被编译器和处理器重新排序。具体解释请参见下面计算圆面积的代码示例。

  双圆周率=3.14;//a double 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语义使我们有可能担心重新排序和可见性。

  

22.as-if-serial又是什么?单线程的程序一定是顺序的吗?

   volatile有两个功能,单线程程序的执行结果不能被改变可见性都有保障。

  与解决共享变量内存可见性问题的同步锁方法相比,volatile是一个更轻便的选择,它没有上下文切换的额外开销成本。

  Volatile可以确保其他线程可以立即看到变量的更新。当变量被声明为volatile时,线程在写入变量时不会将值缓存在寄存器或其他地方,而是将值刷新回主存。当其他线程读取共享变量时,它们将从主内存中检索最新的值,而不是使用当前线程的本地内存中的值。

  比如我们声明一个易变变量volatile int x=0,线程A修改x=1,修改后会把新值刷新回主存。当线程B读取x时,它将清除本地内存变量,然后从主内存中获取最新值。

  重排序可以分为编译器重排序和处理器重排序。valatile保证了排序,即通过分别限制这两种类型的重新排序。

  为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入一个内存屏障,禁止某些类型的处理器重新排序。

  在每个易失性写操作之前插入StoreStore屏障,在每个易失性写操作之后插入StoreLoad屏障,在每个易失性读操作之后插入LoadLoad屏障,在每个易失性读操作之后插入LoadStore屏障。

  

23.volatile实现原理了解吗?

   synchronized常用于保证代码的原子性。

  同步有三种主要用途:

  有序性锁定当前对象实例,您必须获取修饰实例方法:同步的void方法(){

  //业务代码}当前对象实例的锁:即锁定当前类将应用于该类的所有对象实例,并且在输入同步代码之前必须获得当前类的锁。因为静态成员不属于任何实例对象,所以它是一个类成员(静态表示它是类的静态资源,不管新建多少个对象,都只有几个)。

  如果线程A调整一个实例对象的静态同步方法,线程B需要调整这个实例对象所属类的静态同步方法,这是允许的,不会造成互斥,因为访问静态同步方法占用的锁是当前类的锁,访问静态同步方法占用的锁是当前实例对象的锁。

  同步void staic方法(){

  //业务代码}修饰静态方法:指定锁定对象,锁定给定的对象/类。Synchronized(thisobject)表示在进入同步代码库之前获取给定对象的锁。同步(类。class)的意思是在输入同步码之前获得当前修饰代码块同步(this)的锁{

  //业务代码}

24.synchronized用过吗?怎么使用?

  当我们使用synchronized的时候,我们发现我们不用自己锁定和解锁,因为JVM帮助我们做到了这一点。

  当修饰同步代码块时,JVM使用两条指令monitorenter和monitorexit来实现同步。monitorenter指令指向同步代码块的起始位置,monitorexit指令指向同步代码块的结束位置。

  编译一个同步的修改代码块代码,javap-c-s-v-l synchronized demo . class,可以看到对应的字节码指令。

  synchronized修改一个同步方法时,JVM使用ACC_SYNCHRONIZED标记来实现同步,表示该方法是一个同步方法。

  也可以写一段代码反编译一下看看。

  Monitorenter、monitorexit或ACC_SYNCHRONIZED都是class

  实例结构中有一个对象头,对象头中有一个叫Mark Word的结构。标记字的指针指向基于Monitor实现

  所谓显示器,其实就是monitor的一种,也可以说是同步工具的一种。在Java虚拟机(HotSpot)中,监视器是同步机制,可称为内部锁或监视器锁。

  ObjectMonitor的工作原理:

  监视器有两个队列:_WaitSet和_EntryList,用于保存ObjectMonitor对象的列表。_owner,获取Monitor对象的线程进入_owner区域时_count 1。如果线程调用wait()方法,此时监视器对象将被释放,_owner将返回null,_count-1。同时,等待线程进入_WaitSet,等待被唤醒。对象监视器(){

  _ header=NULL

  _ count=0;//记录线程获取锁的次数

  _waiters=0,

  _ recursion=0;//锁的重新进入次数

  _ object=NULL

  _ owner=NULL//指向保存ObjectMonitor对象的线程

  _ WaitSet=NULL//处于等待状态的线程将被添加到_WaitSet

  _ WaitSetLock=0;

  _ Responsible=NULL

  _ succ=NULL

  _ cxq=NULL

  FreeNext=NULL

  _ EntryList=NULL//处于等待锁块状态的线程将被添加到列表中。

  _ spin freq=0;

  _ spin clock=0;

  OwnerIsThread=0;

  }可以比喻成去医院的例子[18]:

  一是患者在ObjectMonitor实现前台或门诊大厅自助挂号机;

  然后挂号后患者找到对应的进行挂号:

  一次只能有一名患者就诊;如果此时门诊有空,直接去门诊;如果此时门诊还有其他患者,那么当前患者输入诊室就诊,等待叫号;治疗结束后,候诊室的候诊室走出就诊室将进入诊室。

  这个过程类似于监控机制:

  下一位候诊患者:所有要进入的线程必须在门诊大厅注册才有资格;入口Entry Set:咨询室** _所有者**,只能有一个线程,线程在咨询结束后离开就诊室:咨询室忙时,输入候诊室,咨询室空闲时,从* *等待集* *中调用新线程。

  所以我们知道同步是被锁定的:

  Monitorenter,当判断同步标识符为ACC_SYNCHRONIZED的线程先进入这个方法,会先拥有监视器的所有者,那么计数器为1。Monitorexit,当执行完毕退出时,计数器-1,返回0后,由其他线程进入获得。

25.synchronized的实现原理?

  线程被锁之前,工作内存中共享变量的值会被清空,所以使用共享变量时需要从主内存中重新读取最新的值。一个线程被锁定后,其他线程就不能获得主存中的共享变量。在解锁线程之前,必须将共享变量的最新值刷新到主内存中。Synchronized Synchronized代码块是排他的,一次只能由一个线程拥有,所以synchronized确保代码同时由一个线程执行。

  由于as-if-serial语义的存在,单线程程序可以保证最终的结果是有序的,但不能保证指令不会被重排。

  所以synchronized保证的顺序是执行结果的顺序,而不是防止指令重排的顺序。

  Synchronized是一个可重入的锁,也就是说,允许一个线程请求它持有对象锁两次的关键资源。这种情况称为重入锁。

  当synchronized锁定一个对象时,有一个计数器,记录线程获取锁的次数。执行完相应的代码块后,计数器将为-1,锁将被释放,直到计数器清零。

  原因是可以重新输入。因为synchronized lock对象有一个计数器,当线程获得锁时,它将计数1,当线程完成执行直到锁被清除和释放时,它将计数-1。

  

26.除了原子性,synchronized可见性,有序性,可重入性怎么实现?

  要解锁升级,首先要知道不同锁的状态是什么。这种状态是什么意思?

  在Java对象头中,有一个结构叫做Mark Word mark field,它会随着锁的状态而变化。

  Mark Word,64位虚拟机,64位。让我们来看看它的状态变化:

  标记字存储对象本身的运行数据,如等待区(Wait Set)

  在JDK1.6之前,synchronized的实现直接调用ObjectMonitor的进入和退出,这个锁被称为哈希码、GC分代年龄、锁状态标志、偏向时间戳(Epoch)。从JDK6开始,HotSpot虚拟机开发团队对Java中的锁进行了优化,比如增加了自适应自旋和锁。

  消除、锁粗化、轻量级锁和偏向锁等优化策略,提升了synchronized的性能。

  偏向锁:在无竞争的情况下,只是在Mark Word里存储当前线程指针,CAS操作都不做。

  轻量级锁:在没有多线程竞争时,相对重量级锁,减少操作系统互斥量带来的性能消耗。但是,如果存在锁竞争,除了互斥量本身开销,还额外有CAS操作的开销。

  自旋锁:减少不必要的CPU上下文切换。在轻量级锁升级为重量级锁时,就使用了自旋加锁的方式

  锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。

  锁消除:虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。

  锁升级方向:无锁–>偏向锁—> 轻量级锁---->重量级锁,这个方向基本上是不可逆的。

  我们看一下升级的过程:

  

偏向锁:

偏向锁的获取:

  判断是否为可偏向状态–MarkWord中锁标志是否为‘01’,是否偏向锁是否为‘1’如果是可偏向状态,则查看线程ID是否为当前线程,如果是,则进入步骤’5’,否则进入步骤‘3’通过CAS操作竞争锁,如果竞争成功,则将MarkWord中线程ID设置为当前线程ID,然后执行‘5’;竞争失败,则执行‘4’CAS获取偏向锁失败表示有竞争。当达到safepoint时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块执行同步代码偏向锁的撤销:

  偏向锁不会主动释放(撤销),只有遇到其他线程竞争时才会执行撤销,由于撤销需要知道当前持有该偏向锁的线程栈状态,因此要等到safepoint时执行,此时持有该偏向锁的线程(T)有‘2’,‘3’两种情况;撤销----T线程已经退出同步代码块,或者已经不再存活,则直接撤销偏向锁,变成无锁状态----该状态达到阈值20则执行批量重偏向升级----T线程还在同步代码块中,则将T线程的偏向锁升级为轻量级锁,当前线程执行轻量级锁状态下的锁获取步骤----该状态达到阈值40则执行批量撤销

轻量级锁:

轻量级锁的获取:

  进行加锁操作时,jvm会判断是否已经时重量级锁,如果不是,则会在当前线程栈帧中划出一块空间,作为该锁的锁记录,并且将锁对象MarkWord复制到该锁记录中复制成功之后,jvm使用CAS操作将对象头MarkWord更新为指向锁记录的指针,并将锁记录里的owner指针指向对象头的MarkWord。如果成功,则执行‘3’,否则执行‘4’更新成功,则当前线程持有该对象锁,并且对象MarkWord锁标志设置为‘00’,即表示此对象处于轻量级锁状态更新失败,jvm先检查对象MarkWord是否指向当前线程栈帧中的锁记录,如果是则执行‘5’,否则执行‘4’表示锁重入;然后当前线程栈帧中增加一个锁记录第一部分(Displaced Mark Word)为null,并指向Mark Word的锁对象,起到一个重入计数器的作用。表示该锁对象已经被其他线程抢占,则进行自旋等待(默认10次),等待次数达到阈值仍未获取到锁,则升级为重量级锁大体上省简的升级过程:

  完整的升级过程:

  

28.说说synchronized和ReentrantLock的区别?

可以从锁的实现、功能特点、性能等几个维度去回答这个问题:

  锁的实现: synchronized是Java语言的关键字,基于JVM实现。而ReentrantLock是基于JDK的API层面实现的(一般是lock()和unlock()方法配合try/finally 语句块来完成。)性能: 在JDK1.6锁优化以前,synchronized的性能比ReenTrantLock差很多。但是JDK6开始,增加了适应性自旋、锁消除等,两者性能就差不多了。功能特点: ReentrantLock 比 synchronized 增加了一些高级功能,如等待可中断、可实现公平锁、可实现选择性通知。ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。synchronized与wait()和notify()/notifyAll()方法结合实现等待/通知机制,ReentrantLock类借助Condition接口与newCondition()方法实现。ReentrantLock需要手工声明来加锁和释放锁,一般跟finally配合释放锁。而synchronized不用手动释放锁。下面的表格列出出了两种锁之间的区别:

  

29.AQS了解多少?

AbstractQueuedSynchronizer 抽象同步队列,简称 AQS ,它是Java并发包的根基,并发包中的锁就是基于AQS实现的。

  AQS是基于一个FIFO的双向队列,其内部定义了一个节点类Node,Node 节点内部的 SHARED 用来标记该线程是获取共享资源时被阻挂起后放入AQS 队列的, EXCLUSIVE 用来标记线程是 取独占资源时被挂起后放入AQS 队列AQS 使用一个 volatile 修饰的 int 类型的成员变量 state 来表示同步状态,修改同步状态成功即为获得锁,volatile 保证了变量在多线程之间的可见性,修改 State 值时通过 CAS 机制来保证修改的原子性获取state的方式分为两种,独占方式和共享方式,一个线程使用独占方式获取了资源,其它线程就会在获取失败后被阻塞。一个线程使用共享方式获取了资源,另外一个线程还可以通过CAS的方式进行获取。如果共享资源被占用,需要一定的阻塞等待唤醒机制来保证锁的分配,AQS 中会将竞争共享资源失败的线程添加到一个变体的 CLH 队列中。先简单了解一下CLH:Craig、Landin and Hagersten 队列,是 单向链表实现的队列。申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现 前驱节点释放了锁就结束自旋

  AQS 中的队列是 CLH 变体的虚拟双向队列,通过将每条请求共享资源的线程封装成一个节点来实现锁的分配:

  AQS 中的 CLH 变体等待队列拥有以下特性:

  AQS 中队列是个双向链表,也是 FIFO 先进先出的特性通过 Head、Tail 头尾两个节点来组成队列结构,通过 volatile 修饰保证可见性Head 指向节点为已获得锁的节点,是一个虚拟节点,节点本身不持有具体线程获取不到同步状态,会将节点进行自旋获取锁,自旋一定次数失败后会将线程阻塞,相对于 CLH 队列性能较好ps:AQS源码里面有很多细节可问,建议有时间好好看看AQS源码。

  

30.ReentrantLock实现原理?

ReentrantLock 是可重入的独占锁,只能有一个线程可以获取该锁,其它获取该锁的线程会被阻塞而被放入该锁的阻塞队列里面。

  看看ReentrantLock的加锁操作:

   // 创建非公平锁

   ReentrantLock lock = new ReentrantLock();

   // 获取锁操作

   lock.lock();

   try {

   // 执行代码逻辑

   } catch (Exception ex) {

   // ...

   } finally {

   // 解锁操作

   lock.unlock();

   }new ReentrantLock()构造函数默认创建的是非公平锁 NonfairSync。

  公平锁 FairSync

  公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU 唤醒阻塞线程的开销比非公平锁大非公平锁 NonfairSync

  非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU 不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁默认创建的对象lock()的时候:

  如果锁当前没有被其它线程占用,并且当前线程之前没有获取过该锁,则当前线程会获取到该锁,然后设置当前锁的拥有者为当前线程,并设置 AQS 的状态值为1 ,然后直接返回。如果当前线程之前己经获取过该锁,则这次只是简单地把 AQS 的状态值加1后返回。如果该锁己经被其他线程持有,非公平锁会尝试去获取锁,获取失败的话,则调用该方法线程会被放入 AQS 队列阻塞挂起。

  

31.ReentrantLock怎么实现公平锁的?

new ReentrantLock()构造函数默认创建的是非公平锁 NonfairSync

  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 静态内部类,只不过实现不同锁语义。

  非公平锁和公平锁的两处不同:

  非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。

  相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。

  

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 目前无法直接保证操作的原子性的。

  可以考虑改用锁来保证操作的原子性可以考虑合并多个变量,将多个变量封装成一个对象,通过AtomicReference来保证原子性。

34.Java有哪些保证原子性的方法?如何保证多线程下i++ 结果正确?

  使用循环原子类,例如AtomicInteger,实现i++原子操作使用juc包下的锁,如ReentrantLock ,对i++操作加锁lock.lock()来实现原子性使用synchronized,对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个类:

  AtomicBoolean:原子更新布尔类型。

  AtomicInteger:原子更新整型。

  AtomicLong:原子更新长整型。

  通过原子的方式更新数组里的某个元素,Atomic包提供了以下4个类:

  AtomicIntegerArray:原子更新整型数组里的元素。

  AtomicLongArray:原子更新长整型数组里的元素。

  AtomicReferenceArray:原子更新引用类型数组里的元素。

  AtomicIntegerArray类主要是提供原子的方式更新数组里的整型

  原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic包提供了以下3个类:

  AtomicReference:原子更新引用类型。

  AtomicReferenceFieldUpdater:原子更新引用类型里的字段。

  AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,boolean initialMark)。

  如果需原子地更新某个类里的某个字段时,就需要使用原子更新字段类,Atomic包提供了以下3个类进行原子字段更新:

  AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。AtomicLongFieldUpdater:原子更新长整型字段的更新器。AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的 ABA问题。

36.AtomicInteger 的原理?

一句话概括:使用CAS实现

  以AtomicInteger的添加方法为例:

   public final int getAndIncrement() {

   return unsafe.getAndAddInt(this, valueOffset, 1);

   }通过Unsafe类的实例来进行添加操作,来看看具体的CAS操作:

   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.线程死锁了解吗?该如何避免?

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。

  那么为什么会产生死锁呢? 死锁的产生必须具备以下四个条件:

  互斥条件:指线程对己经获取到的资源进行它性使用,即该资源同时只由一个线程占用。如果此时还有其它线程请求获取获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。请求并持有条件:指一个 线程己经持有了至少一个资源,但又提出了新的资源请求,而新资源己被其它线程占有,所以当前线程会被阻塞,但阻塞 的同时并不释放自己已经获取的资源。不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其它线程抢占,只有在自己使用完毕后才由自己释放该资源。环路等待条件:指在发生死锁时,必然存在一个线程——资源的环形链,即线程集合 {T0,T1,T2,…… ,Tn} 中 T0 正在等待一 T1 占用的资源,Tl1正在等待 T2用的资源,…… Tn 在等待己被 T0占用的资源。该如何避免死锁呢?答案是至少破坏死锁发生的一个条件

  其中,互斥这个条件我们没有办法破坏,因为用锁为的就是互斥。不过其他三个条件都是有办法破坏掉的,到底如何做呢?

  对于“请求并持有”这个条件,可以一次性请求所有的资源。

  对于“不可剥夺”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。

  对于“环路等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后就不存在环路了。

  

38.那死锁问题怎么排查呢?

可以使用jdk自带的命令行工具排查:

  使用jps查找运行的Java进程:jps -l使用jstack查看线程堆栈信息:jstack -l 进程id基本就可以看到死锁的信息。

  还可以利用图形化工具,比如JConsole。出现线程死锁以后,点击JConsole线程面板的检测到死锁按钮,将会看到线程的死锁信息。

  

39.CountDownLatch(倒计数器)了解吗?

CountDownLatch,倒计数器,有两个常见的应用场景[18]:

  场景1:协调子线程结束动作:等待所有子线程运行结束

  CountDownLatch允许一个或多个线程等待其他线程完成操作。

  例如,我们很多人喜欢玩的王者荣耀,开黑的时候,得等所有人都上线之后,才能开打。

  CountDownLatch模仿这个场景(参考[18]):

  创建大乔、兰陵王、安其拉、哪吒和铠等五个玩家,主线程必须在他们都完成确认后,才可以继续运行。

  在这段代码中,new CountDownLatch(5)用户创建初始的latch数量,各玩家通过countDownLatch.countDown()完成状态确认,主线程通过countDownLatch.await()等待。

   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. 协调子线程开始动作:统一各线程动作开始的时机

  王者游戏中也有类似的场景,游戏开始时,各玩家的初始状态必须一致。不能有的玩家都出完装了,有的才降生。

  所以大家得一块出生,在

  在这个场景中,仍然用五个线程代表大乔、兰陵王、安其拉、哪吒和铠等五个玩家。需要注意的是,各玩家虽然都调用了start()线程,但是它们在运行时都在等待countDownLatch的信号,在信号未收到前,它们不会往下执行。

   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("收。

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: