一、进程和线程的区别
进程:
进程是程序的一次执行过程,是程序在执行过程中的分配和管理资源的基本单位,每个进程都有自己的地址空间,线程至少有 5 种状态:初始态、执行态、等待态、就绪态、终止态。
线程:
线程是CPU调度和分派的基本单位,它可以和同一进程下的其他线程共享全部资源
联系:
线程是进程中的一部分,一个进程可以有多个线程,但线程只能存在于一个进程中。
区别:
- 根本区别:进程是操作系统资源调度的基本单位,线程是任务的调度执行的基本单位
- 开销方面:进程都有自己的独立数据空间,程序之间的切换开销大;线程也有自己的运行栈和程序计数器,线程间的切换开销较小。
- 共享空间:进程拥有各自独立的地址空间、资源,所以共享复杂,需要用IPC(Inter-Process Communication,进程间通信),但是同步简单。而线程共享所属进程的资源,因此共享简单,但是同步复杂,需要用加锁等措施。
设计线程的原因:
操作系统模型中,进程有两个功能:
1、任务的调度执行基本单位
2、资源的所有权
线程的出现就是将这两个功能分离开来了:thread 执行任务的调度和执行 ; process 资源所有权
这样的好处是:
操作系统中有两个重要概念:并发和隔离
并发:提高硬件利用率,进程的上下文切换比线程的上下文切换效率低,所以线程可以提高并发的效率
隔离:计算机的资源是共享的,当程序发生奔溃时,需要保证这些资源要被回收,进程的资源是独立的,奔溃时不会影响其他程 序的进行,线程资源是共享的,奔溃时整个进程也会奔溃
线程和并发有关系,进程和隔离有关系
二、如何做到线程同步
1. 同步方法
用synchronized关键字修饰方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
2. 同步代码块
用synchronized关键字修饰语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
3. Volatile
a.volatile关键字为域变量的访问提供了一种免锁机制
b.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新
c.因此每次使用该域就要重新计算,而不是使用寄存器中的值
d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量
因为volatile不能保证原子操作导致的,因此volatile不能代替 synchronized。此外volatile会组织编译器对代码优化。它的原理是每次要线程要访问volatile修饰 的变量时都是从内存中读取,而不是存缓存当中读取,因此每个线程访问到的变量值都是一样的。这样就保证了同步。
4. 使用重入锁实现线程同步
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
ReenreantLock类的常用方法有:
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用
1、ReentrantLock()还可以通过public ReentrantLock(boolean fair)构造方法创建公平锁,即,优先运行等待时间最长的线程,这样大幅度降低程序运行效率。
2、关于Lock对象和synchronized关键字的选择:
(1)、最好两个都不用,使用一种java.util.concurrent包提供的机制,能够帮助用户处理所有与锁相关的代码。 ??
(2)、如果synchronized关键字能够满足用户的需求,就用synchronized,他能简化代码。
(3)、如果需要使用更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally中释放锁。
5. ThreadLocal
ThreadLocal 类的常用方法
ThreadLocal() : 创建一个线程本地变量
get() : 返回此线程局部变量的当前线程副本中的值
initialValue() : 返回此线程局部变量的当前线程的"初始值"
set(T value) : 将此线程局部变量的当前线程副本中的值设置为value
ThreadLocal的原理:
如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变 量副本,而不会对其他线程产生影响。
即每个线程运行的都是一个副本,也就是说存钱和取钱是两个账户,只是名字相同而已,两个线程间的count没有关系。所以就会发生上面的效果。
ThreadLocal与同步机制
a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题
b.前者采用以”空间换时间”的方法,后者采用以”时间换空间”的方式
ThreadLocal并不能替代同步机制,两者面向的问题领域不同。
1:同步机制是为了同步多个线程对相同资源的并发访问,是为了多个线程之间进行通信的有效方式;
2:而threadLocal是隔离多个线程的数据共享,从根本上就不在多个线程之间共享变量,这样当然不需要对多个线程进行同步了。
三、Volatile具体实现原则
如果把加入volatile关键字的代码和未加入volatile关键字的代码都生成汇编代码,会发现加入volatile关键字的代码会多出一个lock前缀指令。
lock前缀指令实际相当于一个内存屏障,内存屏障提供了以下功能:
1 . 重排序时不能把后面的指令重排序到内存屏障之前的位置
2 . 使得本CPU的Cache写入内存
3 . 写入动作也会引起别的CPU或者别的内核无效化其Cache,相当于让新写入的值对别的线程可见。
为什么设计Volatile
1.Volatile是轻量级的synchronized
2.volatile在多处理器开发中保证了共享变量的可见性
3.也能保证在多线程并发情况中指令重排序的情况
四、synchronized和volatile的区别
- volatile不需要加锁,比synchronized更轻量级,不会阻塞线程;
- 从内存可见性角度,volatile读相当于加锁,volatile写相当于解锁;
- synchronized既能够保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性。
五、锁状态和锁升级
无锁
无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
特点:1.修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。
2.CAS原理及应用即是无锁的实现。
3.无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。
偏向锁
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
轻量级锁
是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
重量级锁
若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
总结
偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。
六、CAS机制,缺点,如何解决
CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。
CAS算法涉及到三个操作数:
- 需要读写的内存值 V。
- 进行比较的值 A。
- 要写入的新值 B。
当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。
缺点:
- ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。
- JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。
- 循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
- 只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
- Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。
七、CAS一定比synchronized效率高吗(乐观锁和悲观锁的比较)
CAS(乐观锁)与synchronized(悲观锁)的使用情景
对于资源竞争较少(线程冲突较轻)的情况, 使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源; 而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大, 从而浪费更多的CPU资源,效率低于synchronized。
乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
乐观锁适用于读比较少的情况下(多写场景),如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
八、线程池参数
没有线程池的情况下,多次创建,销毁线程开销比较大。如果在开辟的线程执行完当前任务后执行接下来任务,复用已创建的线程,降低开销、控制最大并发数。
线程池创建线程时,会将线程封装成工作线程 Worker,Worker 在执行完任务后还会循环获取工作队列中的任务来执行。
将任务派发给线程池时,会出现以下几种情况
1. 核心线程池未满,创建一个新的线程执行任务。
2. 如果核心线程池已满,工作队列未满,将线程存储在工作队列。
3. 如果工作队列已满,线程数小于最大线程数就创建一个新线程处理任务。
4. 如果超过大小线程数,按照拒绝策略来处理任务。
1. corePoolSize:常驻核心线程数。超过该值后如果线程空闲会被销毁。
2. maximumPoolSize:线程池能够容纳同时执行的线程最大数。
3. keepAliveTime:线程空闲时间,线程空闲时间达到该值后会被销毁,直到只剩下 corePoolSize 个
线程为止,避免浪费内存资源。
4. workQueue:工作队列。
5. threadFactory:线程工厂,用来生产一组相同任务的线程。
6. handler:拒绝策略。有以下几种拒绝策略:
AbortPolicy:丢弃任务并抛出异常
CallerRunsPolicy: 重新尝试提交该任务
DiscardOldestPolicy 抛弃队列里等待最久的任务并把当前任务加入队列
DiscardPolicy 表示直接抛弃当前任务但不抛出异常。
九、线程池创建方法
1. newFixedThreadPool,创建固定大小的线程池。
2. newSingleThreadExecutor,使用单线程线程池。
3. newCachedThreadPool,maximumPoolSize 设置为 Integer 最大值,工作完成后会回收工作线程
4. newScheduledThreadPool:支持定期及周期性任务执行,不回收工作线程。
5. newWorkStealingPool:一个拥有多个任务队列的线程池。