Java中的并发是Android面试时常考的一项,今天就来写一篇关于Java并发知识的整理。
一、为什么要使用多线程
为了更快的执行
如果你想要一个程序运行的更快,那么可以将其断开为多个片段,在单独的处理器上运行每个片段。并发是用于多处理器编程的基本工具。
并发通常是提高运行在单处理器上的程序的性能。听起来好像不太对,因为在运行并发程序的时候,我们需要增加上下文切换的开销,整体时间会比顺序执行程序要长。但是如果某一个程序执行的时候,阻塞发生了。那么此时并发程序的好处就会显现出来。顺序执行的程序会因阻塞,而全部陷入停滞。
二、基本的线程机制
并发编程使我们可以将程序划分为多个分离的,独立运行的任务。这些独立的任务每一个都由执行线程来驱动。一个线程就是在进程中的一个单一的顺序控制流。其底层机制是切分CPU时间,在使用线程时,CPU将轮流给每个任务分配其占用时间。
当一个任务分配时间结束,要暂停切换另一个任务时,会先保存当前任务状态。等下一次时间再次分配到此任务时,基于此状态加载。一个任务的保存与加载就是一次上下文切换。
三、使用线程的方式
Java中使用线程的方式有两种,一种是实现Runnable接口并编写里面的run方法。另一种是继承Tread类。
四、使用Executor
在java.util.concurrent包中,执行器Executor将为你管理Thread对象。
1、CachedThreadPool将为每一个任务都创建一个线程,其shutdown()方法可以防止在其调用之后加入的线程被执行。CachedThreadPool在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程。它是Executor首选。
2、FixedThreadPool可以使用有限的线程集来执行所提交的任务。
3、SingleThreadExecutor就像是线程数量为1的FixedThreadPool。如果在其提交了多个任务,这些任务将会排队。
五、线程优先级
线程的优先级将该线程的重要性传递给了调度器。尽管CPU处理现有线程集的顺序是不确定的,但是调度器将倾向于让优先级高的线程先执行。线程的优先级有三种级别可移植。分别是MAX_PRIORITY、NORM_PRIORITY、MIN_PROIORITY。
六、sleep和wait的区别
调用sleep方法,将使任务中止执行给定的时间。而wait方法实际上是Object的方法,wait方法需要使用notify或notifyAll方法来唤醒。
七、让步
如果一个线程的任务已经执行完了,可以通过yield方法来进行让步。当调用yield方法时,也是建议具有相同优先级的线程可以运行。
八、加入线程
一个线程可以在其他线程之上调用join方法。其效果是等待一段时间直到第二个线程结束才执行。调用时可以加一个超时参数,这样如果目标线程在指定时间内未返回,当时间结束后,join方法总能返回。
九、线程的几种状态
1、新建状态
当线程被创建时,它会短暂的处于这种状态,此时它已经被分配了必需的系统资源,并执行了初始化。此时它有资格获取CPU时间,之后可转为运行状态或阻塞状态。
2、就绪状态
这种状态下,只要调度器把时间片分给线程,线程就可以运行。
3、阻塞状态
线程能够运行,但是某些情况阻止它运行。阻塞状态下的线程,调度器不会分配给它CPU时间,直到它重新进入就绪状态,才会被重新分配。
4、死亡状态
处于死亡或终止状态的线程将不会被调度。
线程可以通过sleep或wati方法进入阻塞状态。
十、java内存模型导致线程不安全的原因
多个子线程,独立运行于自己的线程空间。但是当操作涉及到主内存的变量时,若一个线程对变量进行了写操作,而另一个线程此时正在使用该变量,则会导致数据的不一致性。
十一、volatile关键字
1、原子性
除了long和double之外的所有基本类型之上的简单操作拥有原子性。而使用volatile关键字定义的long和double类型数据,保证了原子性。
2、可视性
使用volatile关键字定义的变量,如果发生了修改,所有读操作都会看到这个修改。除此之外,synchronized和final关键字也可以实现有序性。
3、有序性
在java类里定义的变量和程序块,其运行顺序并非顺序的,只是根据程序逻辑上的依赖性保证其顺序的。当多线程运行时,使用volatile关键字可以有效的避免指令重排序所带来的影响。
如果在本线程内,所有操作都是有序的。但是在其他线程处观察此线程,所有操作都是无序的。其他线程看到的就是指令重排序。
十二、synchornized关键字
Java提供了synchronized关键字来防止资源冲突。当任务要执行被synchronized关键字保护的代码片段时,会检查锁是否可用。如果某一个任务处于对被synchronized关键字修饰的方法的调用中,在该方法返回前,其它线程要调用该方法都会被阻塞。
一个对象可以多次获得对象的锁。JVM负责跟踪对象被锁的次数。只有计数为0时,对象才会被释放。
Java的对象头中,mark word是一个复用的数据结构,主要用于表示对象当前的状态。其低位可以是01无锁定,00轻量级锁,10重量级锁,11被GC,01偏向锁(区分标志在第三位为1)。
monitorenter和monitorexit指令分别被插入到同步代码块的开始和结束位置。当线程执行到monitorenter位置是,将会尝试获取对象monitor的所有权,即尝试获得对象的锁。
十三、Lock与synchronized对比
java的concurrent类库中,提供了Lock类。Lock对象必须被显式的创建,锁定,与释放。它与synchronized关键字相比写起来不怎么优雅。但是处理一些问题是更加灵活。使用Lock时,你可以在try-final语句中做最后的处理工作。而synchronized关键字某些任务失败了只会抛出一个异常。而ReentrantLock允许你尝试获取但最终未获取到锁。这样如果别人已经获取了这个锁,你可以决定离开并去执行其他任务。
十四、原子类
Java SE5引入了AtomicInteger、AtomicLong、AtomicReference等特殊的原子性变量类。
十五、CAS概念
同步问题就会涉及到锁。独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所需要锁的线程挂起,等待持有锁的线程释放锁。而另一种更加有效的锁就是乐观锁,所谓乐观锁就是每次不去加锁而是假设没有冲突去完成某项操作。如果因为冲突失败就去重试,直到成功为止。乐观锁用到的概念就是CAS。Compare and Swap。
CAS包含三个操作数——V内存位置,E预期原值和N新值。如果内存位置的值与预期原值相匹配,那么处理器将自动将该位置的值更新为新值。否则,处理器不做任何操作。
十六、乐观锁,悲观锁,使用场景
乐观锁:每次去拿数据都认为别人不会修改,所以不会上锁。但更新时会对比别人有没有修改。
悲观锁:每次拿数据都认为别人修改了,会上锁。
两种锁的使用场景主要是看资源竞争激烈程度。
对于资源竞争较少的情况,推荐使用乐观锁。而资源竞争较激烈的场景,还是使用悲观锁。
在jdk1.6以后,synchronized底层实现主要依靠Lock-Free的队列,其基本思想是自旋后阻塞。
十七、线程池概念及原理
线程池就是上面提到过的Executor类。
线程池的优点在于可以降低资源消耗重复利用已创建的线程,减少线程创建与销毁的消耗。提高响应速度,任务到达时无需等待线程创建即可执行。提高线程管理性,比如合理控制线程的创建数量,同时限制数量会减少系统资源的占用。
工作原理
当一个任务提交到线程池后,有以下三个步骤:
1、线程池会判断核心线程池里的线程是否都在执行任务。如果有核心线程没在工作或者还未被创建,则创建一个线程执行任务。否则进入下一环节。
2、判断工作队列是否已经满了,如果不是,则进入队列等待。如果是则进入下一环节。
3、判断所有非核心线程是否处于工作状态。如果不是,创建一个工作线程来执行任务。如果是,则会使用饱和策略来处理。
Java为我们提供了四种饱和策略。
1、直接抛出异常
2、只调用所在的线程运行任务
3、丢弃队列里最近一个任务,并执行当前任务。
4、不处理,丢弃。
十八、JVM锁优化
1、减少锁持有时间
对一个方法加锁不如对方法中需要同步的几行代码加锁。
2、减小锁粒度
如ConcurrentHashMap使用segment概念
3、锁分离
根据同步操作的性质,把锁分为读锁和写锁
4、锁粗化
如果不连续的同步块需要频繁的加锁,那么就把这些同步块一次性一起加锁。
虚拟机中的加锁
偏向锁:锁对象偏向于当前获得它的线程。当接下来没有其它线程请求时,持有该锁的线程不再需要进行同步操作。当另一个线程申请该锁时,偏向模式才会结束。
轻量级锁:synchronized的底层实现是通过监视器monitor来控制的,而monitorenter与monitorexit两个原语是依赖操作系统互斥来实现的。互斥会导致挂起,消耗资源。轻量级锁使用CAS来进行补救。当偏向锁失败后,会进行轻量级锁的操作。当轻量级锁也失败后才会使用重量级锁(synchronized)来加锁。
自旋锁:当线程申请锁时,锁被占用,则让当前线程执行一个忙循环(自旋),看持有锁的线程是否释放。如果还未释放,则进入阻塞状态。
自适应自旋:如果一个线程的自旋时间与上一次自旋耗时相同,则认为这个线程自旋很少成功,便不会进行自旋操作。避免浪费CPU资源。
JVM锁的使用顺序为偏向锁、轻量级锁、自旋锁最后是重量级锁。
十九、死锁的概念
在并发编程里,某个任务等待另一个任务,而后者有等待另一个任务,这样一直下去。所有任务一直等待下去,形成一个循环。这被称为死锁。
死锁的四个必要条件:
1、互斥条件。任务使用的资源中至少有一个是不能共享的。
2、至少有一个任务它必须持有一个资源且正在等待一个当前被别的任务持有的资源。
3、资源不能被任务抢占,任务必须把资源释放当做普通事件。
4、必须有循环等待。
所以要防止死锁,破坏其中一个条件即可。