游戏电视苹果数码历史美丽
投稿投诉
美丽时装
彩妆资讯
历史明星
乐活安卓
数码常识
驾车健康
苹果问答
网络发型
电视车载
室内电影
游戏科学
音乐整形

2w字40张图带你参透并发编程

  1hr并发历史
  在计算机最早期的时候,没有操作系统,执行程序只需要一种方式,那就是从头到尾依次执行。任何资源都会为这个程序服务,在计算机使用某些资源时,其他资源就会空闲,就会存在浪费资源的情况。这里说的浪费资源指的是资源空闲,没有充分使用的情况。
  操作系统的出现为我们的程序带来了并发性,操作系统使我们的程序能够同时运行多个程序,一个程序就是一个进程,也就相当于同时运行多个进程。
  操作系统是一个并发系统,并发性是操作系统非常重要的特征,操作系统具有同时处理和调度多个程序的能力,比如多个IO设备同时在输入输出;设备IO和CPU计算同时进行;内存中同时有多个系统和用户程序被启动交替、穿插地执行。操作系统在协调和分配进程的同时,操作系统也会为不同进程分配不同的资源。
  操作系统实现多个程序同时运行解决了单个程序无法做到的问题,主要有下面三点资源利用率,我们上面说到,单个进程存在资源浪费的情况,举个例子,当你在为某个文件夹赋予权限的时候,输入程序无法接受外部的输入字符,只有等到权限赋予完毕后才能接受外部输入。总的来讲,就是在等待程序时无法执行其他工作。如果在等待程序时可以运行另一个程序,那么将会大大提高资源的利用率。(资源并不会觉得累)因为它不会划水公平性,不同的用户和程序都能够使用计算机上的资源。一种高效的运行方式是为不同的程序划分时间片来使用资源,但是有一点需要注意,操作系统可以决定不同进程的优先级。虽然每个进程都有能够公平享有资源的权利,但是当有一个进程释放资源后的同时有一个优先级更高的进程抢夺资源,就会造成优先级低的进程无法获得资源,进而导致进程饥饿。便利性,单个进程是是不用通信的,通信的本质就是信息交换,及时进行信息交换能够避免信息孤岛,做重复性的工作;任何并发能做的事情,单进程也能够实现,只不过这种方式效率很低,它是一种顺序性的。
  但是,顺序编程(也称为串行编程)也不是一无是处的,串行编程的优势在于其直观性和简单性,客观来讲,串行编程更适合我们人脑的思考方式,但是我们并不会满足于顺序编程,wewantitmore!!!。资源利用率、公平性和便利性促使着进程出现的同时,也促使着线程的出现。
  如果你还不是很理解进程和线程的区别的话,那么我就以我多年操作系统的经验(吹牛逼,实则半年)来为你解释一下:进程是一个应用程序,而线程是应用程序中的一条顺序流。
  进程中会有多个线程来完成一些任务,这些任务有可能相同有可能不同。每个线程都有自己的执行顺序。
  每个线程都有自己的栈空间,这是线程私有的,还有一些其他线程内部的和线程共享的资源,如下所示。在计算机中,一般堆栈指的就是栈,而堆指的才是堆
  线程会共享进程范围内的资源,例如内存和文件句柄,但是每个线程也有自己私有的内容,比如程序计数器、栈以及局部变量。下面汇总了进程和线程共享资源的区别
  线程是一种轻量级的进程,轻量级体现在线程的创建和销毁要比进程的开销小很多。注意:任何比较都是相对的。
  在大多数现代操作系统中,都以线程为基本的调度单位,所以我们的视角着重放在对线程的探究。
  2hr线程
  什么是多线程
  多线程意味着你能够在同一个应用程序中运行多个线程,我们知道,指令是在CPU中执行的,多线程应用程序就像是具有多个CPU在同时执行应用程序的代码。
  其实这是一种假象,线程数量并不等于CPU数量,单个CPU将在多个线程之间共享CPU的时间片,在给定的时间片内执行每个线程之间的切换,每个线程也可以由不同的CPU执行,如下图所示
  并发和并行的关系
  并发意味着应用程序会执行多个的任务,但是如果计算机只有一个CPU的话,那么应用程序无法同时执行多个的任务,但是应用程序又需要执行多个任务,所以计算机在开始执行下一个任务之前,它并没有完成当前的任务,只是把状态暂存,进行任务切换,CPU在多个任务之间进行切换,直到任务完成。如下图所示
  并行是指应用程序将其任务分解为较小的子任务,这些子任务可以并行处理,例如在多个CPU上同时进行。
  优势和劣势
  合理使用线程是一门艺术,合理编写一道准确无误的多线程程序更是一门艺术,如果线程使用得当,能够有效的降低程序的开发和维护成本。
  Java很好的在用户空间实现了开发工具包,并在内核空间提供系统调用来支持多线程编程,Java支持了丰富的类库java。util。concurrent和跨平台的内存模型,同时也提高了开发人员的门槛,并发一直以来是一个高阶的主题,但是现在,并发也成为了主流开发人员的必备素质。
  虽然线程带来的好处很多,但是编写正确的多线程(并发)程序是一件极困难的事情,并发程序的Bug往往会诡异地出现又诡异的消失,在当你认为没有问题的时候它就出现了,难以定位是并发程序的一个特征,所以在此基础上你需要有扎实的并发基本功。那么,并发为什么会出现呢?并发为什么会出现
  计算机世界的快速发展离不开CPU、内存和IO设备的高速发展,但是这三者一直存在速度差异性问题,我们可以从存储器的层次结构可以看出
  CPU内部是寄存器的构造,寄存器的访问速度要高于高速缓存,高速缓存的访问速度要高于内存,最慢的是磁盘访问。
  程序是在内存中执行的,程序里大部分语句都要访问内存,有些还需要访问IO设备,根据漏桶理论来说,程序整体的性能取决于最慢的操作也就是磁盘访问速度。
  因为CPU速度太快了,所以为了发挥CPU的速度优势,平衡这三者的速度差异,计算机体系机构、操作系统、编译程序都做出了贡献,主要体现为:CPU使用缓存来中和和内存的访问速度差异操作系统提供进程和线程调度,让CPU在执行指令的同时分时复用线程,让内存和磁盘不断交互,不同的CPU时间片能够执行不同的任务,从而均衡这三者的差异编译程序提供优化指令的执行顺序,让缓存能够合理的使用
  我们在享受这些便利的同时,多线程也为我们带来了挑战,下面我们就来探讨一下并发问题为什么会出现以及多线程的源头是什么线程带来的安全性问题
  线程安全性是非常复杂的,在没有采用同步机制的情况下,多个线程中的执行操作往往是不可预测的,这也是多线程带来的挑战之一,下面我们给出一段代码,来看看安全性问题体现在哪publicclassTSynchronizedimplementsRunnable{staticinti0;publicvoidincrease(){i;}Overridepublicvoidrun(){for(inti0;i1000;i){increase();}}publicstaticvoidmain(String〔〕args)throwsInterruptedException{TSynchronizedtSynchronizednewTSynchronized();ThreadaThreadnewThread(tSynchronized);ThreadbThreadnewThread(tSynchronized);aThread。start();bThread。start();System。out。println(ii);}}
  这段程序输出后会发现,i的值每次都不一样,这不符合我们的预测,那么为什么会出现这种情况呢?我们先来分析一下程序的运行过程。
  TSynchronized实现了Runnable接口,并定义了一个静态变量i,然后在increase方法中每次都增加i的值,在其实现的run方法中进行循环调用,共执行1000次。可见性问题
  在单核CPU时代,所有的线程共用一个CPU,CPU缓存和内存的一致性问题容易解决,CPU和内存之间
  如果用图来表示的话我想会是下面这样
  在多核时代,因为有多核的存在,每个核都能够独立的运行一个线程,每颗CPU都有自己的缓存,这时CPU缓存与内存的数据一致性就没那么容易解决了,当多个线程在不同的CPU上执行时,这些线程操作的是不同的CPU缓存
  因为i是静态变量,没有经过任何线程安全措施的保护,多个线程会并发修改i的值,所以我们认为i不是线程安全的,导致这种结果的出现是由于aThread和bThread中读取的i值彼此不可见,所以这是由于可见性导致的线程安全问题。原子性问题
  看起来很普通的一段程序却因为两个线程aThread和bThread交替执行产生了不同的结果。但是根源不是因为创建了两个线程导致的,多线程只是产生线程安全性的必要条件,最终的根源出现在i这个操作上。
  这个操作怎么了?这不就是一个给i递增的操作吗?也就是iii1,这怎么就会产生问题了?
  因为i不是一个原子性操作,仔细想一下,i其实有三个步骤,读取i的值,执行i1操作,然后把i1得出的值重新赋给i(将结果写入内存)。
  当两个线程开始运行后,每个线程都会把i的值读入到CPU缓存中,然后执行1操作,再把1之后的值写入内存。因为线程间都有各自的虚拟机栈和程序计数器,他们彼此之间没有数据交换,所以当aThread执行1操作后,会把数据写入到内存,同时bThread执行1操作后,也会把数据写入到内存,因为CPU时间片的执行周期是不确定的,所以会出现当aThread还没有把数据写入内存时,bThread就会读取内存中的数据,然后执行1操作,再写回内存,从而覆盖i的值,导致aThread所做的努力白费。
  为什么上面的线程切换会出现问题呢?
  我们先来考虑一下正常情况下(即不会出现线程安全性问题的情况下)两条线程的执行顺序
  可以看到,当aThread在执行完整个i的操作后,操作系统对线程进行切换,由aThreadbThread,这是最理想的操作,一旦操作系统在任意读取增加写入阶段产生线程切换,都会产生线程安全问题。例如如下图所示
  最开始的时候,内存中i0,aThread读取内存中的值并把它读取到自己的寄存器中,执行1操作,此时发生线程切换,bThread开始执行,读取内存中的值并把它读取到自己的寄存器中,此时发生线程切换,线程切换至aThread开始运行,aThread把自己寄存器的值写回到内存中,此时又发生线程切换,由aThreadbThread,线程bThread把自己寄存器的值1然后写回内存,写完后内存中的值不是2,而是1,内存中的i值被覆盖了。
  我们上面提到原子性这个概念,那么什么是原子性呢?并发编程的原子性操作是完全独立于任何其他进程运行的操作,原子操作多用于现代操作系统和并行处理系统中。
  原子操作通常在内核中使用,因为内核是操作系统的主要组件。但是,大多数计算机硬件,编译器和库也提供原子性操作。
  在加载和存储中,计算机硬件对存储器字进行读取和写入。为了对值进行匹配、增加或者减小操作,一般通过原子操作进行。在原子操作期间,处理器可以在同一数据传输期间完成读取和写入。这样,其他输入输出机制或处理器无法执行存储器读取或写入任务,直到原子操作完成为止。
  简单来讲,就是原子操作要么全部执行,要么全部不执行。数据库事务的原子性也是基于这个概念演进的。有序性问题
  在并发编程中还有带来让人非常头疼的有序性问题,有序性顾名思义就是顺序性,在计算机中指的就是指令的先后执行顺序。一个非常显而易见的例子就是JVM中的类加载
  这是一个JVM加载类的过程图,也称为类的生命周期,类从加载到JVM到卸载一共会经历五个阶段加载、连接、初始化、使用、卸载。这五个过程的执行顺序是一定的,但是在连接阶段,也会分为三个过程,即验证、准备、解析阶段,这三个阶段的执行顺序不是确定的,通常交叉进行,在一个阶段的执行过程中会激活另一个阶段。
  有序性问题一般是编译器带来的,编译器有的时候确实是好心办坏事,它为了优化系统性能,往往更换指令的执行顺序。活跃性问题
  多线程还会带来活跃性问题,如何定义活跃性问题呢?活跃性问题关注的是某件事情是否会发生。
  如果一组线程中的每个线程都在等待一个事件的发生,而这个事件只能由该组中正在等待的线程触发,这种情况会导致死锁。
  简单一点来表述一下,就是每个线程都在等待其他线程释放资源,而其他资源也在等待每个线程释放资源,这样没有线程抢先释放自己的资源,这种情况会产生死锁,所有线程都会无限的等待下去。
  死锁的必要条件
  造成死锁的原因有四个,破坏其中一个即可破坏死锁互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程释放。请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持占有。不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。循环等待:指在发生死锁时,必然存在一个进程对应的环形链。
  换句话说,死锁线程集合中的每个线程都在等待另一个死锁线程占有的资源。但是由于所有线程都不能运行,它们之中任何一个资源都无法释放资源,所以没有一个线程可以被唤醒。
  如果说死锁很痴情的话,那么活锁用一则成语来表示就是弄巧成拙。
  某些情况下,当线程意识到它不能获取所需要的下一个锁时,就会尝试礼貌的释放已经获得的锁,然后等待非常短的时间再次尝试获取。可以想像一下这个场景:当两个人在狭路相逢的时候,都想给对方让路,相同的步调会导致双方都无法前进。
  现在假想有一对并行的线程用到了两个资源。它们分别尝试获取另一个锁失败后,两个线程都会释放自己持有的锁,再次进行尝试,这个过程会一直进行重复。很明显,这个过程中没有线程阻塞,但是线程仍然不会向下执行,这种状况我们称之为活锁(livelock)。
  如果我们期望的事情一直不会发生,就会产生活跃性问题,比如单线程中的无限循环while(true){。。。}for(;;){}
  在多线程中,比如aThread和bThread都需要某种资源,aThread一直占用资源不释放,bThread一直得不到执行,就会造成活跃性问题,bThread线程会产生饥饿,我们后面会说。性能问题
  与活跃性问题密切相关的是性能问题,如果说活跃性问题关注的是最终的结果,那么性能问题关注的就是造成结果的过程,性能问题有很多方面:比如服务时间过长,吞吐率过低,资源消耗过高,在多线程中这样的问题同样存在。
  在多线程中,有一个非常重要的性能因素那就是我们上面提到的线程切换,也称为上下文切换(ContextSwitch),这种操作开销很大。在计算机世界中,老外都喜欢用context上下文这个词,这个词涵盖的内容很多,包括上下文切换的资源,寄存器的状态、程序计数器等。contextswitch一般指的就是这些上下文切换的资源、寄存器状态、程序计数器的变化等。
  在上下文切换中,会保存和恢复上下文,丢失局部性,把大量的时间消耗在线程切换上而不是线程运行上。
  为什么线程切换会开销如此之大呢?线程间的切换会涉及到以下几个步骤
  将CPU从一个线程切换到另一线程涉及挂起当前线程,保存其状态,例如寄存器,然后恢复到要切换的线程的状态,加载新的程序计数器,此时线程切换实际上就已经完成了;此时,CPU不在执行线程切换代码,进而执行新的和线程关联的代码。引起线程切换的几种方式
  线程间的切换一般是操作系统层面需要考虑的问题,那么引起线程上下文切换有哪几种方式呢?或者说线程切换有哪几种诱因呢?主要有下面几种引起上下文切换的方式当前正在执行的任务完成,系统的CPU正常调度下一个需要运行的线程当前正在执行的任务遇到IO等阻塞操作,线程调度器挂起此任务,继续调度下一个任务。多个任务并发抢占锁资源,当前任务没有获得锁资源,被线程调度器挂起,继续调度下一个任务。用户的代码挂起当前任务,比如线程执行sleep方法,让出CPU。使用硬件中断的方式引起上下文切换
  3hr线程安全性
  在Java中,要实现线程安全性,必须要正确的使用线程和锁,但是这些只是满足线程安全的一种方式,要编写正确无误的线程安全的代码,其核心就是对状态访问操作进行管理。最重要的就是最共享(Shared)的和可变(Mutable)的状态。只有共享和可变的变量才会出现问题,私有变量不会出现问题,参考程序计数器。
  对象的状态可以理解为存储在实例变量或者静态变量中的数据,共享意味着某个变量可以被多个线程同时访问、可变意味着变量在生命周期内会发生变化。一个变量是否是线程安全的,取决于它是否被多个线程访问。要使变量能够被安全访问,必须通过同步机制来对变量进行修饰。
  如果不采用同步机制的话,那么就要避免多线程对共享变量的访问,主要有下面两种方式不要在多线程之间共享变量将共享变量置为不可变的
  我们说了这么多次线程安全性,那么什么是线程安全性呢?什么是线程安全性
  多个线程可以同时安全调用的代码称为线程安全的,如果一段代码是安全的,那么这段代码就不存在竞态条件。仅仅当多个线程共享资源时,才会出现竞态条件。
  根据上面的探讨,我们可以得出一个简单的结论:当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。
  单线程就是一个线程数量为1的多线程,单线程一定是线程安全的。读取某个变量的值不会产生安全性问题,因为不管读取多少次,这个变量的值都不会被修改。原子性
  我们上面提到了原子性的概念,你可以把原子性操作想象成为一个不可分割的整体,它的结果只有两种,要么全部执行,要么全部回滚。你可以把原子性认为是婚姻关系的一种,男人和女人只会产生两种结果,好好的和说散就散,一般男人的一生都可以把他看成是原子性的一种,当然我们不排除时间管理(线程切换)的个例,我们知道线程切换必然会伴随着安全性问题,男人要出去浪也会造成两种结果,这两种结果分别对应安全性的两个结果:线程安全(好好的)和线程不安全(说散就散)。竞态条件
  有了上面的线程切换的功底,那么竞态条件也就好定义了,它指的就是两个或多个线程同时对一共享数据进行修改,从而影响程序运行的正确性时,这种就被称为竞态条件(racecondition),线程切换是导致竞态条件出现的诱导因素,我们通过一个示例来说明,来看一段代码publicclassRaceCondition{privateSignletonsinglenull;publicSignletonnewSingleton(){if(singlenull){singlenewSignleton();}returnsingle;}}
  在上面的代码中,涉及到一个竞态条件,那就是判断single的时候,如果single判断为空,此时发生了线程切换,另外一个线程执行,判断single的时候,也是空,执行new操作,然后线程切换回之前的线程,再执行new操作,那么内存中就会有两个Singleton对象。加锁机制
  在Java中,有很多种方式来对共享和可变的资源进行加锁和保护。Java提供一种内置的机制对资源进行保护:synchronized关键字,它有三种保护机制对方法进行加锁,确保多个线程中只有一个线程执行方法;对某个对象实例(在我们上面的探讨中,变量可以使用对象来替换)进行加锁,确保多个线程中只有一个线程对对象实例进行访问;对类对象进行加锁,确保多个线程只有一个线程能够访问类中的资源。
  synchronized关键字对资源进行保护的代码块俗称同步代码块(SynchronizedBlock),例如synchronized(lock){线程安全的代码}
  每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁(InstrinsicLock)或者监视器锁(MonitorLock)。线程在进入同步代码之前会自动获得锁,并且在退出同步代码时自动释放锁,而无论是通过正常执行路径退出还是通过异常路径退出,获得内置锁的唯一途径就是进入这个由锁保护的同步代码块或方法。
  synchronized的另一种隐含的语义就是互斥,互斥意味着独占,最多只有一个线程持有锁,当线程A尝试获得一个由线程B持有的锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果线程B不释放锁的话,那么线程A将会一直等待下去。
  线程A获得线程B持有的锁时,线程A必须等待或者阻塞,但是获取锁的线程B可以重入,重入的意思可以用一段代码表示publicclassRetreent{publicsynchronizedvoiddoSomething(){doSomethingElse();System。out。println(doSomething。。。。。。);}publicsynchronizedvoiddoSomethingElse(){System。out。println(doSomethingElse。。。。。。);}
  获取doSomething()方法锁的线程可以执行doSomethingElse()方法,执行完毕后可以重新执行doSomething()方法中的内容。锁重入也支持子类和父类之间的重入,具体的我们后面会进行介绍。
  volatile是一种轻量级的synchronized,也就是一种轻量级的加锁方式,volatile通过保证共享变量的可见性来从侧面对对象进行加锁。可见性的意思就是当一个线程修改一个共享变量时,另外一个线程能够看见这个修改的值。volatile的执行成本要比synchronized低很多,因为volatile不会引起线程的上下文切换。
  我们还可以使用原子类来保证线程安全,原子类其实就是rt。jar下面以atomic开头的类
  除此之外,我们还可以使用java。util。concurrent工具包下的线程安全的集合类来确保线程安全,具体的实现类和其原理我们后面会说。
  可以使用不同的并发模型来实现并发系统,并发模型说的是系统中的线程如何协作完成并发任务。不同的并发模型以不同的方式拆分任务,线程可以以不同的方式进行通信和协作。
  4hr竞态条件和关键区域
  竞态条件是在关键代码区域发生的一种特殊条件。关键区域是由多个线程同时执行的代码部分,关键区域中的代码执行顺序会对造成不一样的结果。如果多个线程执行一段关键代码,而这段关键代码会因为执行顺序不同而造成不同的结果时,那么这段代码就会包含竞争条件。
  5hr并发模型和分布式系统很相似
  并发模型其实和分布式系统模型非常相似,在并发模型中是线程彼此进行通信,而在分布式系统模型中是进程彼此进行通信。然而本质上,进程和线程也非常相似。这也就是为什么并发模型和分布式模型非常相似的原因。
  分布式系统通常要比并发系统面临更多的挑战和问题比如进程通信、网络可能出现异常,或者远程机器挂掉等等。但是一个并发模型同样面临着比如CPU故障、网卡出现问题、硬盘出现问题等。
  因为并发模型和分布式模型很相似,因此他们可以相互借鉴,例如用于线程分配的模型就类似于分布式系统环境中的负载均衡模型。
  其实说白了,分布式模型的思想就是借鉴并发模型的基础上推演发展来的。
  6hr认识两个状态
  并发模型的一个重要的方面是,线程是否应该共享状态,是具有共享状态还是独立状态。共享状态也就意味着在不同线程之间共享某些状态
  状态其实就是数据,比如一个或者多个对象。当线程要共享数据时,就会造成竞态条件或者死锁等问题。当然,这些问题只是可能会出现,具体实现方式取决于你是否安全的使用和访问共享对象。
  独立的状态表明状态不会在多个线程之间共享,如果线程之间需要通信的话,他们可以访问不可变的对象来实现,这是最有效的避免并发问题的一种方式,如下图所示
  使用独立状态让我们的设计更加简单,因为只有一个线程能够访问对象,即使交换对象,也是不可变的对象。
  7hr并发模型并行Worker
  第一个并发模型是并行worker模型,客户端会把任务交给代理人(Delegator),然后由代理人把工作分配给不同的工人(worker)。如下图所示
  并行worker的核心思想是,它主要有两个进程即代理人和工人,Delegator负责接收来自客户端的任务并把任务下发,交给具体的Worker进行处理,Worker处理完成后把结果返回给Delegator,在Delegator接收到Worker处理的结果后对其进行汇总,然后交给客户端。
  并行Worker模型是Java并发模型中非常常见的一种模型。许多java。util。concurrent包下的并发工具都使用了这种模型。并行Worker的优点
  并行Worker模型的一个非常明显的特点就是很容易理解,为了提高系统的并行度你可以增加多个Worker完成任务。
  并行Worker模型的另外一个好处就是,它会将一个任务拆分成多个小任务,并发执行,Delegator在接受到Worker的处理结果后就会返回给Client,整个WorkerDelegatorClient的过程是异步的。并行Worker的缺点
  同样的,并行Worker模式同样会有一些隐藏的缺点
  共享状态会变得很复杂
  实际的并行Worker要比我们图中画出的更复杂,主要是并行Worker通常会访问内存或共享数据库中的某些共享数据。
  这些共享状态可能会使用一些工作队列来保存业务数据、数据缓存、数据库的连接池等。在线程通信中,线程需要确保共享状态是否能够让其他线程共享,而不是仅仅停留在CPU缓存中让自己可用,当然这些都是程序员在设计时就需要考虑的问题。线程需要避免竞态条件,死锁和许多其他共享状态造成的并发问题。
  多线程在访问共享数据时,会丢失并发性,因为操作系统要保证只有一个线程能够访问数据,这会导致共享数据的争用和抢占。未抢占到资源的线程会阻塞。
  现代的非阻塞并发算法可以减少争用提高性能,但是非阻塞算法比较难以实现。
  可持久化的数据结构(Persistentdatastructures)是另外一个选择。可持久化的数据结构在修改后始终会保留先前版本。因此,如果多个线程同时修改一个可持久化的数据结构,并且一个线程对其进行了修改,则修改的线程会获得对新数据结构的引用。
  虽然可持久化的数据结构是一个新的解决方法,但是这种方法实行起来却有一些问题,比如,一个持久列表会将新元素添加到列表的开头,并返回所添加的新元素的引用,但是其他线程仍然只持有列表中先前的第一个元素的引用,他们看不到新添加的元素。
  持久化的数据结构比如链表(LinkedList)在硬件性能上表现不佳。列表中的每个元素都是一个对象,这些对象散布在计算机内存中。现代CPU的顺序访问往往要快的多,因此使用数组等顺序访问的数据结构则能够获得更高的性能。CPU高速缓存可以将一个大的矩阵块加载到高速缓存中,并让CPU在加载后直接访问CPU高速缓存中的数据。对于链表,将元素分散在整个RAM上,这实际上是不可能的。
  无状态的worker
  共享状态可以由其他线程所修改,因此,worker必须在每次操作共享状态时重新读取,以确保在副本上能够正确工作。不在线程内部保持状态的worker成为无状态的worker。
  作业顺序是不确定的
  并行工作模型的另一个缺点是作业的顺序不确定,无法保证首先执行或最后执行哪些作业。任务A在任务B之前分配给worker,但是任务B可能在任务A之前执行。流水线
  第二种并发模型就是我们经常在生产车间遇到的流水线并发模型,下面是流水线设计模型的流程图
  这种组织架构就像是工厂中装配线中的worker,每个worker只完成全部工作的一部分,完成一部分后,worker会将工作转发给下一个worker。
  每道程序都在自己的线程中运行,彼此之间不会共享状态,这种模型也被称为无共享并发模型。
  使用流水线并发模型通常被设计为非阻塞IO,也就是说,当没有给worker分配任务时,worker会做其他工作。非阻塞IO意味着当worker开始IO操作,例如从网络中读取文件,worker不会等待IO调用完成。因为IO操作很慢,所以等待IO非常耗费时间。在等待IO的同时,CPU可以做其他事情,IO操作完成后的结果将传递给下一个worker。下面是非阻塞IO的流程图
  在实际情况中,任务通常不会按着一条装配线流动,由于大多数程序需要做很多事情,因此需要根据完成的不同工作在不同的worker之间流动,如下图所示
  任务还可能需要多个worker共同参与完成
  响应式事件驱动系统
  使用流水线模型的系统有时也被称为响应式或者事件驱动系统,这种模型会根据外部的事件作出响应,事件可能是某个HTTP请求或者某个文件完成加载到内存中。Actor模型
  在Actor模型中,每一个Actor其实就是一个Worker,每一个Actor都能够处理任务。
  简单来说,Actor模型是一个并发模型,它定义了一系列系统组件应该如何动作和交互的通用规则,最著名的使用这套规则的编程语言是Erlang。一个参与者Actor对接收到的消息做出响应,然后可以创建出更多的Actor或发送更多的消息,同时准备接收下一条消息。
  Channels模型
  在Channel模型中,worker通常不会直接通信,与此相对的,他们通常将事件发送到不同的通道(Channel)上,然后其他worker可以在这些通道上获取消息,下面是Channel的模型图
  有的时候worker不需要明确知道接下来的worker是谁,他们只需要将作者写入通道中,监听Channel的worker可以订阅或者取消订阅,这种方式降低了worker和worker之间的耦合性。流水线设计的优点
  与并行设计模型相比,流水线模型具有一些优势,具体优势如下
  不会存在共享状态
  因为流水线设计能够保证worker在处理完成后再传递给下一个worker,所以worker与worker之间不需要共享任何状态,也就无需考虑并发问题。你甚至可以在实现上把每个worker看成是单线程的一种。
  有状态worker
  因为worker知道没有其他线程修改自身的数据,所以流水线设计中的worker是有状态的,有状态的意思是他们可以将需要操作的数据保留在内存中,有状态通常比无状态更快。
  更好的硬件整合
  因为你可以把流水线看成是单线程的,而单线程的工作优势在于它能够和硬件的工作方式相同。因为有状态的worker通常在CPU中缓存数据,这样可以更快地访问缓存的数据。
  使任务更加有效的进行
  可以对流水线并发模型中的任务进行排序,一般用来日志的写入和恢复。流水线设计的缺点
  流水线并发模型的缺点是任务会涉及多个worker,因此可能会分散在项目代码的多个类中。因此很难确定每个worker都在执行哪个任务。流水线的代码编写也比较困难,设计许多嵌套回调处理程序的代码通常被称为回调地狱。回调地狱很难追踪debug。
  8hr函数性并行
  函数性并行模型是最近才提出的一种并发模型,它的基本思路是使用函数调用来实现。消息的传递就相当于是函数的调用。传递给函数的参数都会被拷贝,因此在函数之外的任何实体都无法操纵函数内的数据。这使得函数执行类似于原子操作。每个函数调用都可以独立于任何其他函数调用执行。
  当每个函数调用独立执行时,每个函数都可以在单独的CPU上执行。这也就是说,函数式并行并行相当于是各个CPU单独执行各自的任务。
  JDK1。7中的ForkAndJoinPool类就实现了函数性并行的功能。Java8提出了stream的概念,使用并行流也能够实现大量集合的迭代。
  函数性并行的难点是要知道函数的调用流程以及哪些CPU执行了哪些函数,跨CPU函数调用会带来额外的开销。
  我们之前说过,线程就是进程中的一条顺序流,在Java中,每一条Java线程就像是JVM的一条顺序流,就像是虚拟CPU一样来执行代码。Java中的main()方法是一条特殊的线程,JVM创建的main线程是一条主执行线程,在Java中,方法都是由main方法发起的。在main方法中,你照样可以创建其他的线程(执行顺序流),这些线程可以和main方法共同执行应用代码。
  Java线程也是一种对象,它和其他对象一样。Java中的Thread表示线程,Thread是java。lang。Thread类或其子类的实例。那么下面我们就来一起探讨一下在Java中如何创建和启动线程。
  9hr创建并启动线程
  在Java中,创建线程的方式主要有三种通过继承Thread类来创建线程通过实现Runnable接口来创建线程通过Callable和Future来创建线程
  下面我们分别探讨一下这几种创建方式继承Thread类来创建线程
  第一种方式是继承Thread类来创建线程,如下示例publicclassTJavaThreadextendsThread{staticintcount;Overridepublicsynchronizedvoidrun(){for(inti0;i10000;i){count;}}publicstaticvoidmain(String〔〕args)throwsInterruptedException{TJavaThreadtJavaThreadnewTJavaThread();tJavaThread。start();tJavaThread。join();System。out。println(countcount);}}
  线程的主要创建步骤如下定义一个线程类使其继承Thread类,并重写其中的run方法,run方法内部就是线程要完成的任务,因此run方法也被称为执行体创建了Thread的子类,上面代码中的子类是TJavaThread启动方法需要注意,并不是直接调用run方法来启动线程,而是使用start方法来启动线程。当然run方法可以调用,这样的话就会变成普通方法调用,而不是新创建一个线程来调用了。publicstaticvoidmain(String〔〕args)throwsInterruptedException{TJavaThreadtJavaThreadnewTJavaThread();tJavaThread。run();System。out。println(countcount);}
  这样的话,整个main方法只有一条执行线程也就是main线程,由两条执行线程变为一条执行线程
  Thread构造器只需要一个Runnable对象,调用Thread对象的start()方法为该线程执行必须的初始化操作,然后调用Runnable的run方法,以便在这个线程中启动任务。我们上面使用了线程的join方法,它用来等待线程的执行结束,如果我们不加join方法,它就不会等待tJavaThread的执行完毕,输出的结果可能就不是10000
  可以看到,在run方法还没有结束前,run就被返回了。也就是说,程序不会等到run方法执行完毕就会执行下面的指令。
  使用继承方式创建线程的优势:编写比较简单;可以使用this关键字直接指向当前线程,而无需使用Thread。currentThread()来获取当前线程。
  使用继承方式创建线程的劣势:在Java中,只允许单继承(拒绝肛精说使用内部类可以实现多继承)的原则,所以使用继承的方式,子类就不能再继承其他类。使用Runnable接口来创建线程
  相对的,还可以使用Runnable接口来创建线程,如下示例publicclassTJavaThreadUseImplementsimplementsRunnable{staticintcount;Overridepublicsynchronizedvoidrun(){for(inti0;i10000;i){count;}}publicstaticvoidmain(String〔〕args)throwsInterruptedException{newThread(newTJavaThreadUseImplements())。start();System。out。println(countcount);}}
  线程的主要创建步骤如下首先定义Runnable接口,并重写Runnable接口的run方法,run方法的方法体同样是该线程的线程执行体。创建线程实例,可以使用上面代码这种简单的方式创建,也可以通过new出线程的实例来创建,如下所示TJavaThreadUseImplementstJavaThreadUseImplementsnewTJavaThreadUseImplements();newThread(tJavaThreadUseImplements)。start();再调用线程对象的start方法来启动该线程。
  线程在使用实现Runnable的同时也能实现其他接口,非常适合多个相同线程来处理同一份资源的情况,体现了面向对象的思想。
  使用Runnable实现的劣势是编程稍微繁琐,如果要访问当前线程,则必须使用Thread。currentThread()方法。使用Callable接口来创建线程
  Runnable接口执行的是独立的任务,Runnable接口不会产生任何返回值,如果你希望在任务完成后能够返回一个值的话,那么你可以实现Callable接口而不是Runnable接口。JavaSE5引入了Callable接口,它的示例如下publicclassCallableTaskimplementsCallable{staticintcount;publicCallableTask(intcount){this。countcount;}OverridepublicObjectcall(){returncount;}publicstaticvoidmain(String〔〕args)throwsExecutionException,InterruptedException{FutureTaskIntegertasknewFutureTask((CallableInteger)(){for(inti0;i1000;i){count;}returncount;});ThreadthreadnewThread(task);thread。start();Integertotaltask。get();System。out。println(totaltotal);}}
  我想,使用Callable接口的好处你已经知道了吧,既能够实现多个接口,也能够得到执行结果的返回值。Callable和Runnable接口还是有一些区别的,主要区别如下Callable执行的任务有返回值,而Runnable执行的任务没有返回值Callable(重写)的方法是call方法,而Runnable(重写)的方法是run方法。call方法可以抛出异常,而Runnable方法不能抛出异常使用线程池来创建线程
  首先先来认识一下顶级接口Executor,Executor虽然不是传统线程创建的方式之一,但是它却成为了创建线程的替代者,使用线程池的好处如下利用线程池能够复用线程、控制最大并发数。实现任务线程队列缓存策略和拒绝机制。实现某些与时间相关的功能,如定时执行、周期执行等。隔离线程环境。比如,交易服务和搜索服务在同一台服务器上,分别开启两个线程池,交易线程的资源消耗明显要大;因此,通过配置独立的线程池,将较慢的交易服务与搜索服务隔开,避免服务线程互相影响。
  你可以使用如下操作来替换线程创建newThread(new(RunnableTask()))。start()替换为ExecutorexecutornewExecutorSubClass()线程池实现类;executor。execute(newRunnableTask1());executor。execute(newRunnableTask2());
  ExecutorService是Executor的默认实现,也是Executor的扩展接口,ThreadPoolExecutor类提供了线程池的扩展实现。Executors类为这些Executor提供了方便的工厂方法。下面是使用ExecutorService创建线程的几种方式CachedThreadPool
  从而简化了并发编程。Executor在客户端和任务之间提供了一个间接层;与客户端直接执行任务不同,这个中介对象将执行任务。Executor允许你管理异步任务的执行,而无须显示地管理线程的生命周期。publicstaticvoidmain(String〔〕args){ExecutorServiceserviceExecutors。newCachedThreadPool();for(inti0;i5;i){service。execute(newTestThread());}service。shutdown();}
  CachedThreadPool会为每个任务都创建一个线程。注意:ExecutorService对象是使用静态的Executors创建的,这个方法可以确定Executor类型。对shutDown的调用可以防止新任务提交给ExecutorService,这个线程在Executor中所有任务完成后退出。
  FixedThreadPool
  FixedThreadPool使你可以使用有限的线程集来启动多线程publicstaticvoidmain(String〔〕args){ExecutorServiceserviceExecutors。newFixedThreadPool(5);for(inti0;i5;i){service。execute(newTestThread());}service。shutdown();}
  有了FixedThreadPool使你可以一次性的预先执行高昂的线程分配,因此也就可以限制线程的数量。这可以节省时间,因为你不必为每个任务都固定的付出创建线程的开销。SingleThreadExecutor
  SingleThreadExecutor就是线程数量为1的FixedThreadPool,如果向SingleThreadPool一次性提交了多个任务,那么这些任务将会排队,每个任务都会在下一个任务开始前结束,所有的任务都将使用相同的线程。SingleThreadPool会序列化所有提交给他的任务,并会维护它自己(隐藏)的悬挂队列。publicstaticvoidmain(String〔〕args){ExecutorServiceserviceExecutors。newSingleThreadExecutor();for(inti0;i5;i){service。execute(newTestThread());}service。shutdown();}
  从输出的结果就可以看到,任务都是挨着执行的。我为任务分配了五个线程,但是这五个线程不像是我们之前看到的有换进换出的效果,它每次都会先执行完自己的那个线程,然后余下的线程继续走完这条线程的执行路径。你可以用SingleThreadExecutor来确保任意时刻都只有唯一一个任务在运行。休眠
  影响任务行为的一种简单方式就是使线程休眠,选定给定的休眠时间,调用它的sleep()方法,一般使用的TimeUnit这个时间类替换Thread。sleep()方法,示例如下:publicclassSuperclassThreadextendsTestThread{Overridepublicvoidrun(){System。out。println(Thread。currentThread()starting。。。);try{for(inti0;i5;i){if(i3){System。out。println(Thread。currentThread()sleeping。。。);TimeUnit。MILLISECONDS。sleep(1000);}}}catch(InterruptedExceptione){e。printStackTrace();}System。out。println(Thread。currentThread()wakeupandend。。。);}publicstaticvoidmain(String〔〕args){ExecutorServiceexecutorsExecutors。newCachedThreadPool();for(inti0;i5;i){executors。execute(newSuperclassThread());}executors。shutdown();}}关于TimeUnit中的sleep()方法和Thread。sleep()方法的比较,请参考下面这篇博客
  (https:www。cnblogs。comxiadongqingp9925567。html)
  优先级
  上面提到线程调度器对每个线程的执行都是不可预知的,随机执行的,那么有没有办法告诉线程调度器哪个任务想要优先被执行呢?你可以通过设置线程的优先级状态,告诉线程调度器哪个线程的执行优先级比较高,请给这个骑手马上派单,线程调度器倾向于让优先级较高的线程优先执行,然而,这并不意味着优先级低的线程得不到执行,也就是说,优先级不会导致死锁的问题。优先级较低的线程只是执行频率较低。publicclassSimplePrioritiesimplementsRunnable{privateintpriority;publicSimplePriorities(intpriority){this。prioritypriority;}Overridepublicvoidrun(){Thread。currentThread()。setPriority(priority);for(inti0;i100;i){System。out。println(this);if(i100){Thread。yield();}}}OverridepublicStringtoString(){returnThread。currentThread()priority;}publicstaticvoidmain(String〔〕args){ExecutorServiceserviceExecutors。newCachedThreadPool();for(inti0;i5;i){service。execute(newSimplePriorities(Thread。MAXPRIORITY));}service。execute(newSimplePriorities(Thread。MINPRIORITY));}}
  toString()方法被覆盖,以便通过使用Thread。toString()方法来打印线程的名称。你可以改写线程的默认输出,这里采用了Thread〔pool1thread1,10,main〕这种形式的输出。
  通过输出,你可以看到,最后一个线程的优先级最低,其余的线程优先级最高。注意,优先级是在run开头设置的,在构造器中设置它们不会有任何好处,因为这个时候线程还没有执行任务。
  尽管JDK有10个优先级,但是一般只有MAXPRIORITY,NORMPRIORITY,MINPRIORITY三种级别。作出让步
  我们上面提过,如果知道一个线程已经在run()方法中运行的差不多了,那么它就可以给线程调度器一个提示:我已经完成了任务中最重要的部分,可以让给别的线程使用CPU了。这个暗示将通过yield()方法作出。有一个很重要的点就是,Thread。yield()是建议执行切换CPU,而不是强制执行CPU切换。
  对于任何重要的控制或者在调用应用时,都不能依赖于yield()方法,实际上,yield()方法经常被滥用。后台线程
  后台(daemon)线程,是指运行时在后台提供的一种服务线程,这种线程不是属于必须的。当所有非后台线程结束时,程序也就停止了,同时会终止所有的后台线程。反过来说,只要有任何非后台线程还在运行,程序就不会终止。publicclassSimpleDaemonsimplementsRunnable{Overridepublicvoidrun(){while(true){try{TimeUnit。MILLISECONDS。sleep(100);System。out。println(Thread。currentThread()this);}catch(InterruptedExceptione){System。out。println(sleep()interrupted);}}}publicstaticvoidmain(String〔〕args)throwsInterruptedException{for(inti0;i10;i){ThreaddaemonnewThread(newSimpleDaemons());daemon。setDaemon(true);daemon。start();}System。out。println(AllDaemonsstarted);TimeUnit。MILLISECONDS。sleep(175);}}
  在每次的循环中会创建10个线程,并把每个线程设置为后台线程,然后开始运行,for循环会进行十次,然后输出信息,随后主线程睡眠一段时间后停止运行。在每次run循环中,都会打印当前线程的信息,主线程运行完毕,程序就执行完毕了。因为daemon是后台线程,无法影响主线程的执行。
  但是当你把daemon。setDaemon(true)去掉时,while(true)会进行无限循环,那么主线程一直在执行最重要的任务,所以会一直循环下去无法停止。ThreadFactory
  按需要创建线程的对象。使用线程工厂替换了Thread或者Runnable接口的硬连接,使程序能够使用特殊的线程子类,优先级等。一般的创建方式为classSimpleThreadFactoryimplementsThreadFactory{publicThreadnewThread(Runnabler){returnnewThread(r);}}Executors。defaultThreadFactory方法提供了一个更有用的简单实现,它在返回之前将创建的线程上下文设置为已知值
  ThreadFactory是一个接口,它只有一个方法就是创建线程的方法publicinterfaceThreadFactory{构建一个新的线程。实现类可能初始化优先级,名称,后台线程状态和线程组等ThreadnewThread(Runnabler);}
  下面来看一个ThreadFactory的例子publicclassDaemonThreadFactoryimplementsThreadFactory{OverridepublicThreadnewThread(Runnabler){ThreadtnewThread(r);t。setDaemon(true);returnt;}}publicclassDaemonFromFactoryimplementsRunnable{Overridepublicvoidrun(){while(true){try{TimeUnit。MILLISECONDS。sleep(100);System。out。println(Thread。currentThread()this);}catch(InterruptedExceptione){System。out。println(Interrupted);}}}publicstaticvoidmain(String〔〕args)throwsInterruptedException{ExecutorServiceserviceExecutors。newCachedThreadPool(newDaemonThreadFactory());for(inti0;i10;i){service。execute(newDaemonFromFactory());}System。out。println(Alldaemonsstarted);TimeUnit。MILLISECONDS。sleep(500);}}
  Executors。newCachedThreadPool可以接受一个线程池对象,创建一个根据需要创建新线程的线程池,但会在它们可用时重用先前构造的线程,并在需要时使用提供的ThreadFactory创建新线程。publicstaticExecutorServicenewCachedThreadPool(ThreadFactorythreadFactory){returnnewThreadPoolExecutor(0,Integer。MAXVALUE,60L,TimeUnit。SECONDS,newSynchronousQueueRunnable(),threadFactory);}加入一个线程
  一个线程可以在其他线程上调用join()方法,其效果是等待一段时间直到第二个线程结束才正常执行。如果某个线程在另一个线程t上调用t。join()方法,此线程将被挂起,直到目标线程t结束才回复(可以用t。isAlive()返回为真假判断)。
  也可以在调用join时带上一个超时参数,来设置到期时间,时间到期,join方法自动返回。
  对join的调用也可以被中断,做法是在线程上调用interrupted方法,这时需要用到try。。。catch子句publicclassTestJoinMethodextendsThread{Overridepublicvoidrun(){for(inti0;i5;i){try{TimeUnit。MILLISECONDS。sleep(1000);}catch(InterruptedExceptione){System。out。println(Interruptedsleep);}System。out。println(Thread。currentThread()i);}}publicstaticvoidmain(String〔〕args)throwsInterruptedException{TestJoinMethodjoin1newTestJoinMethod();TestJoinMethodjoin2newTestJoinMethod();TestJoinMethodjoin3newTestJoinMethod();join1。start();join1。join();join2。start();join3。start();}}
  join()方法等待线程死亡。换句话说,它会导致当前运行的线程停止执行,直到它加入的线程完成其任务。线程异常捕获
  由于线程的本质,使你不能捕获从线程中逃逸的异常,一旦异常逃出任务的run方法,它就会向外传播到控制台,除非你采取特殊的步骤捕获这种错误的异常,在Java5之前,你可以通过线程组来捕获,但是在Java5之后,就需要用Executor来解决问题,因为线程组不是一次好的尝试。
  下面的任务会在run方法的执行期间抛出一个异常,并且这个异常会抛到run方法的外面,而且main方法无法对它进行捕获publicclassExceptionThreadimplementsRunnable{Overridepublicvoidrun(){thrownewRuntimeException();}publicstaticvoidmain(String〔〕args){try{ExecutorServiceserviceExecutors。newCachedThreadPool();service。execute(newExceptionThread());}catch(Exceptione){System。out。println(eeeee);}}}
  为了解决这个问题,我们需要修改Executor产生线程的方式,Java5提供了一个新的接口Thread。UncaughtExceptionHandler,它允许你在每个Thread上都附着一个异常处理器。Thread。UncaughtExceptionHandler。uncaughtException()会在线程因未捕获临近死亡时被调用。publicclassExceptionThread2implementsRunnable{Overridepublicvoidrun(){ThreadtThread。currentThread();System。out。println(run()byt);System。out。println(eht。getUncaughtExceptionHandler());手动抛出异常thrownewRuntimeException();}}实现Thread。UncaughtExceptionHandler接口,创建异常处理器publicclassMyUncaughtExceptionHandlerimplementsThread。UncaughtExceptionHandler{OverridepublicvoiduncaughtException(Threadt,Throwablee){System。out。println(caughte);}}publicclassHandlerThreadFactoryimplementsThreadFactory{OverridepublicThreadnewThread(Runnabler){System。out。println(thiscreatingnewThread);ThreadtnewThread(r);System。out。println(createdt);t。setUncaughtExceptionHandler(newMyUncaughtExceptionHandler());System。out。println(ext。getUncaughtExceptionHandler());returnt;}}publicclassCaptureUncaughtException{publicstaticvoidmain(String〔〕args){ExecutorServiceserviceExecutors。newCachedThreadPool(newHandlerThreadFactory());service。execute(newExceptionThread2());}}
  在程序中添加了额外的追踪机制,用来验证工厂创建的线程会传递给UncaughtExceptionHandler,你可以看到,未捕获的异常是通过uncaughtException来捕获的。
  关注我,带你更好的学习编程。

我们还能在春晚上看到宫廷玉液酒,一百八一杯的经典小品吗娱兔迎春《打工奇遇》带给我们的喜剧盛宴和新鲜出炉的段子乐呵了广大群众20多年。赵丽蓉、巩汉林、陈佩斯、赵本山、小沈阳等一众小品界大咖在除夕夜带给了电视机前的观众们多少欢乐。《吃……腾讯程序员大神毛星云跳楼身亡毛星云,1991年出生,腾讯游戏开发大神,税后年薪250万。就是这么优秀的一个90后,拥有光鲜的履历和高薪,可以称之为完美的人生赢家,却选择跳楼结束了年仅30岁的生命,真是令人……赢了,力压陈芋汐夺第一,全红婵笑了在东京奥运会夺得金牌后,喜欢和支持全红婵的粉丝们或许不会想到,全红婵迎来了职业生涯比生长发育更为艰难的挑战,那就是与队友陈芋汐的竞争。在连续多次输给陈芋汐后,全红婵终于迎来了胜……无线充电器选哪个?苹果重掀无线充电方向,但国产厂商掀起热潮在智能手机行业中,似乎有一个规律,那就是苹果引领一个方向之后,把热度推向市场高潮的还是中国厂商,不论是AirPods带动的无线降噪耳机,还是AirPower重掀的无线充电器,都……卡西欧G2000大泥王防泥防震手表值得购买吗男朋友生日,选来选去,最终还是决定这个卡西欧GSHOCKGWG2000大泥王防泥防震手表,平时他也比较喜好登山,收到货就拿给他,很喜欢。表带质量:整体佩戴柔软舒适,表带弹……短道速滑王者王濛她曾12次打破世界纪录,却被开除出国家队王濛,中国获得冬奥金牌和奖牌最多的运动员,第一个卫冕冬奥金牌的运动员,第一个在同一届冬奥会上获得三枚金牌的运动员。在短道速滑500米项目,她是独孤求败的存在,先后12次打……农药正在破害土壤健康图片来源:QianLing铲起一抔健康的土壤,你可能会拥有比地球上人口更多的生物。就像地下城市里永远不睡觉的居民一样,数以万计的无脊椎动物、线虫、细菌和真菌等地下物种不断……不忍了,曝刘国梁正式对国乒名将出台惩罚措施,或受丑闻影响随着第56届世乒赛的开赛时间的临近,近日,网上曝出中国国乒参加成都世乒赛的大名单。这份大名单一经曝光,立即引起了不小的争议。女队方面倒是没有什么争议,毕竟女队最强的五人组全部入……原神3。0版本UP池一常驻3复刻,玩家省原石了2。8版本临近末期,随着3。0的版本前瞻,玩家们心心念的卡池也是浮出水面。这期浅谈下3。0版本的卡池。由于之前种种原因所推迟更迭版本的情况,在3。03。2版本都会补回来,……风靡全国的王者荣耀,为何KPL关注度不到LPL零头?王者荣耀和英雄联盟一直以上都是大家对比的对象,两边玩家也是互相看对方不顺眼,尤其是当英雄联盟手游出来之后,LOL玩家的优越感就更加强。被刺痛的王者荣耀玩家也是拿起DOTA来反击……天呐!谢娜!变美了冲着我家阿娇的颜,今天终于点进了浪姐3,意外的发现那个陪着我们从小到大的娜娜居然变美了!这届浪姐3中娜姐的造型真的是赢过了很多姐姐呢,要给造型师加鸡腿,真的完全摒弃了过去……X射线成像偏振探测器任务推进太空探索的边界长期以来,人类一直对隐藏在广袤无垠的外层空间中的秘密感到着迷。17世纪光学望远镜的发明使人类能够看到星星在夜空中仅以闪烁的点的形式出现。由于接下来四个世纪的科学创新,人类现在可……
热议中国男篮惨败澳洲!贾磊给杜锋打满分苏群承认差距太大北京时间7月3日,中国男篮出战世预赛赛事,4天内第二次对阵澳大利亚男篮,此役前三节,中国男篮和对手形成僵局,三节打完4545平手,看到赢球希望。但末节中国男篮突然崩盘,被对手连……患有胃炎后,身体或有5个症状表现,需留心观察,及时治疗胃炎是一种胃黏膜炎症,也是一种很常见的消化系统疾病。胃炎包括急性胃炎,以及慢性胃炎,另外,还有一类特殊类型的胃炎。导致患上胃炎的原因较多,一般认为与胃黏膜损伤、药物损伤、……雪里蕻最好吃腌法,奶奶用了60年的秘方,一不小心被我泄露,真导语:奶奶用了60年的秘方,一不小心被我泄露,腌雪里蕻不能直接抹盐,想要味道香还入味,不霉不臭,这1步不能少!一餐一饭,心有所栖。柴米油盐,爱有所依。静看一树花开,慢享三……4年前,让你们别生二胎,非要生女儿愤怒回嘴,父母无言反驳2023育儿季文文爸一直想不通一个问题:那些经济条件一般,一胎已经上高中,甚至上大学了,还执意生二胎的父母,究竟是为啥?随着二胎、三胎政策的全面放开,越来越多的高龄……湖人官宣沃格尔下课,纳斯成选帅头号目标湖人队今天官方宣布,弗兰克沃格尔将不再担任球队主教练。湖人总经理罗勃佩林卡发表声明表示:弗兰克是一位出色的教练,也是个出色的人。我们将永远感谢他带领我们获得201920赛……4换1!奇才报价戴维斯,筹码十分豪华北京时间12月18日,NBA常规赛正在如火如荼的进行之中,就在刚刚结束的一场焦点之战中,由缺少保罗乔治、雷吉杰克逊、祖巴茨和诺曼鲍威尔的洛杉矶快船主场迎战缺少比尔的华盛顿奇才。……南宁这个地方的桃花惊艳绽放,撞不撞桃花运你来定气温回暖,春意盎然连日来种植在广西东盟经开区大帽山公园的桃花竞相绽放分外美丽吸引到众多市民前来观赏在公园里粉里透红的桃花一朵紧挨一朵……你玩过如此真实的武侠养成吗?种田玩家的福音来了相信有不少的玩家都特别喜欢经营养成的游戏,但是你玩过有关于古代的一些养成游戏吗?市面上绝大多数的一些有关古代的武侠类游戏中,其绝大多数都是把游戏的重心放在了游戏剧情里,以及在武……广东名酒玉冰烧,在本地一家独大,却为何在全国排不上号?中国上千年酿酒历史,造就的美酒,数不胜数!但如今一说到好酒,很多人第一反应便是茅台、五粮液,毕竟全国知名,就算不喝酒的人,也没有多少不知道的。在这种态势下,反倒忽视……西媒皇马明夏欲加强进攻线,恩昆库是首选但基本无望直播吧11月14日讯西班牙媒体Relevo报道,皇马想在下赛季补强锋线,莱比锡的恩昆库是首选。报道称,皇马体育部门已经开始了锋线补强对象的选择,目前他们仍未找到合适的人选……现役食之无味弃之可惜的十大球员,开拓者着手重建,18届成重灾NBA是世界上最大的篮球殿堂,世界上最优秀的篮球运动员都汇聚于此,但即便如此,NBA球员同样分成了三六九等,有的球员拿着超级顶薪大合同光芒万丈、有的球员拿着底薪也算是实现了人生……夫妻性福生活当中面对压力我们该怎么办呢?医生坦言可以这样说起生活的话题,对于夫妻之间来说总是少不了关于压力的问题,工作上、家庭上、经济上尤其是在有了孩子基础上,经济上的压力就会加重一些。这些方面随着时间的推移,夫妻生活就会越来越趋于……
友情链接:易事利快生活快传网聚热点七猫云快好知快百科中准网快好找文好找中准网快软网