什么是线程和进程
何为进程
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建、运行到消亡的过程。
在Java中,当我们启动main函数其实就是启动了一个JVM进程,而main函数所在的线程就是这个进程中的一个线程,也被称为主线程。
何为线程
线程与进程相似,但是线程是一个比进程更小的执行单位。一个进程在执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆何方法区资源,但是每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比进程小的多,也正是因为如此,线程也被称为轻量级进程。
简要描述线程与进程的关系,区别以及优缺点
一个进程可以有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程都有自己的程序计数器、虚拟机栈和本地方法栈。
总结:线程是进程划分的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能互相影响。线程执行开销小,但不利于资源的管理和保护,而进程正相反。
简述线程、程序、进程的基本概念
程序是含有指令和数据的文件,被存储在磁盘或者其他的数据存储设备中,也就是说程序是静态的代码。
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建到运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令一个指令的运行。同时,进程还占有着一些系统资源,比如内存、CPU、文件,输入输出设备等。换句话说,进程是程序被载入操作系统运行的过程。
线程与进程相似,是CPU调度的最小单位,但是线程相比起进程是一个更小的运行单位,一个进程在运行过程中可以产生多个线程。与进程不同的是,同类型的线程共享一块内存区域和系统资源。所以当创建一个线程或是在各个线程之间切换工作时,负担要比进程小的多。所以线程也被称为轻量级进程。
线程和进程的区别:线程是进程划分成的更小的运行代为。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能互相影响。从另一个角度来说,进程属于操作系统范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
并行和并发的区别
- 并行:单位时间内,多个任务同时执行
- 并发:同一时间段:多个任务都在执行(单位时间内只有一个任务在执行)
为什么要使用多线程
先从总体上来说:
- 从计算机底层来说:线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远小于进程,另外,多核CPU时代意味着多个线程可以并行运行,这减少了线程上下文切换的开销。
- 从当代互联网发展的趋势来说:现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正式开发高并发系统的基础,利用好多线程机制可以大大提升整个系统的并发能力以及性能
深入到计算机底层
- 单核时代:在单核时代多线程主要是为了提高单进程利用CPU和IO系统的效率。假设只运行了一个Java进程的情况,当我们请求IO的时候,如果Java进程中只有一个线程,此时线程被IO阻塞则整个进程被阻塞。CPU和IO设备只有一个在运行,那么可以简单地说系统整体效率只有50%。当使用多线程的时候,一个线程被IO阻塞,其他线程还可以继续使用CPU。从而提高了Java进程利用资源的整体效率
- 多核时代:多核时代多线程主要时为了提高进程利用多核CPU的能力。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,不论系统有几个CPU核心,都只会有一个CPU核心被利用到。而创建多个线程,这些线程可以映射到底层多个CPU上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著的提高。
使用多线程可能带来什么问题
并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏,死锁,线程不安全等。
线程的生命周期和状态
- NEW:初始状态,线程被构建,但是还没有调用start()方法
- RUNNABLE:运行状态,Java线程将操作系统中的就绪和运行两种状态笼统的称为运行中
- BLOCKED:阻塞状态,表示线程阻塞于锁
- WAITING:等待状态,表示线程进入等待状态状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作。
- TIME_WAITING:超时等待状态,该状态不同于Waiting,他是可以在指定时间内自行返回的。
- TERMINATED:终止状态,表示当前线程已经执行完毕。
线程创建后,它将处于NEW(新建状态),调用start()方法后开始运行,线程这时候处于READY(可运行)状态。可运行状态的线程获得拉CPU时间片后就处于RUNNING(运行)状态。
在操作系统层面线程有READY和RUNNING状态,而在JVM层面只能看到RUNABLE状态,所以Java系统一般将这两个状态称为RUNNABLE(运行中状态)。
为什么JVM没有区分这两种状态呢?
现在的时分多任务操作系统架构通常都是用所谓的时间分片方式进行抢占式轮转调度。这个时间分片通常是很小的,一个线程一次最多只能在CPU上运行10-20ms时间,也即大概只有0.01秒这一量级,时间片用后就要被切换下来放入调度队列的末尾等待再次调度(回到ready状态)。线程切换的如此之快,区分这两种状态就没什么意义了。
Blocked/Waiting/Time_Waiting状态
Blocked和Waiting和Time_Waiting状态三个都是线程在没有运行结束时被暂停了的状态。
Blocked:线程在请求资源锁时,资源锁被其他线程占有。这个线程就会进入Blocked状态
Waiting:线程进入等待状态,因为调用了wait,join或者park方法。这个状态的特点是需要其他线程触发某个操作,比如说其他因为wait方法进入Waiting状态的线程,被其他方法的notify方法唤醒。比如因为join方法暂停的线程,其他方法运行结束,本线程唤醒。
Time_Waiting:线程进入超时等待状态,这个状态的特点是线程会在一定时间后自己唤醒,一般通过带参wait,join,sleep等方法进入这个状态
IO不会导致线程进入以上三种状态,例如Scanner后依然是Runnable状态。JVM底层状态类似于Runnable中的ready。
说到这三种状态要说以下对象监视器(ObjectMonitor),每个对象都有一个ObjectMonitor对象,这个对象是对象锁的关键。
我们知道多个线程争抢锁,成功的线程会进入Runnable状态,而失败的会进入Blocked状态。而实际上,这个线程就会进入ObjectMonitor对象的EntryList(用来存放Blocked状态的线程,底层是一个双向链表),当持有锁的线程释放锁时,就会从这个EntryList中获得线程(这个统一获取锁的入口。一般是cxq 或者waitSet数据复制过来进行统一排队)。
而调用wait/join等方法的线程会释放锁然后进入Waiting状态,这种状态需要持有锁的对象调用Notify方法来讲Waiting状态解除。实际上就是,调用了wait方法后,线程释放锁,然后线程进入ObjectMonitor中的waitSet中,waitSet中的线程不会主动抢锁。当持有锁的线程调用了notify方法后,waitset中的线程会被移动到EntryLIst中,才会开始抢锁。
什么是上下文切换
线程在执行过程中会有自己的运行条件和状态(也称上下文),比如程序计数器,栈信息等。当出现如下情况的时候,线程会从占用CPU状态中退出。
- 主动让出CPU,比如调用了sleep(),wait()等。
- 时间片用完,因为操作系统要防止一个线程或者进程长时间占用CPU导致其他线程或者进程饿死
- 调用了阻塞类型的系统中断,比如请求IO,线程被阻塞
- 被终止或者结束运行
这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,等待线程下次占用CPU的时候恢复现场。并加载下一个将要占用CPU的线程上下文。这就是所谓的上下文切换。
上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息。这将会占用CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果平凡切换就会造成整体效率低下。
什么是线程死锁?如何避免死锁
认识线程死锁
线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部在等待某个资源被释放。由于线程被无限期的阻塞,因此程序不可能正常终止。
即产生死锁必须具有以下四个条件:
- 互斥条件:该资源在任意时刻只能由一个线程占用
- 请求与保持条件:一个线程因为请求资源而阻塞时,对以获得的资源保持不释放
- 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕之后才释放资源。
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
如何预防和避免线程死锁
如何预防死锁?破坏死锁产生的必要条件即可:
- 请求与保持条件:线程在创建时,一次性申请全部所需的资源,即不会出现因为请求资源而阻塞的情况
- 不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到可以主动释放它占用的资源。
- 循环等待条件:靠按序申请来预防。按某一顺序申请资源,释放则反序释放。
如何避免死锁?
避免死锁就是在分配资源时,借助算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。
说说sleep()方法和wait()方法区别和共同点?
相同点:
- 两者都可以暂停线程的执行。
不同点:
- sleep()方法没有释放锁,而wait()方法释放了锁。
- sleep()通常被用于暂停执行,wait()通常被用于线程间交互/通信
- wait()方法被调用后,线程不会自动苏醒,需要别的线程调用的同一个对象上的notify()或者notifyAll()方法。sleep()方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout) 超时后线程会自动苏醒
为什么我们直接调用start()方法时会执行run()方法,为什么我们不直接调用run()方法?
new一个Thread,线程就进入了新建状态。调用start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了,start()会执行线程的相应准备工作,然后自动执行run()方法的内容,这是真正的多线程工作。但是直接执行run方法,会把run方法当成一个main线程下的普通方法去执行,并不会在某个线程中执行它,所以这不是多线程。
调用start()方法可启动线程并使线程进入就绪状态,直接执行run()方法的话不会以多线程的方式执行。
synchronized关键字
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
在java早期版本中,synchronized属于重量级锁,效率低下。
因为监视器锁monitor是依赖底层的操作系统的MutexLock来实现的,Java的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统来帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。
JDK1.6之后Java官方对synchronized 做出了较大优化,线程synchornized锁效率已经很不错了。JDK1.6对锁的实现引入了大量优化,如:自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
三种使用方式:
- 修饰实例方法 synchronized void method(){} ,作用于当前对象实例加锁,进入同步代码之前要获得当前对象实例的锁。
- 修饰静态方法 synchronized static void staticMethod(){} 给当前的类加索,会作用于类的所有实例,进入同步代码之前要获得当前class的锁。因为静态成员不属于任何一个实例对象,是类成员。所以一个线程A调用一个实例的非静态方法,而线程B需要调用这个实例所属类的静态方法,是允许的,不会发生互斥现象,因为访问静态方法占用的锁是当前类的锁,而访问非静态方法占用的锁是当前实例对象锁
- 修饰代码块 sunchronized(this){} 这种方式指定加锁对象,对给定对象/类加锁
构造方法可以使用synchronized吗
构造方法不可以使用synchronized关键字修饰,构造方法本身就属于线程安全的,不存在同步的构造方法一说。
构造方法不需要同步,因为它只能发生在一个线程里面,在构造器返回之前,没有其他线程可以使用该对象。
因为synchronized
保证同一对象上的操作不会由多个线程执行。当构造函数被调用时,你仍然没有对象。两个线程访问同一个对象的构造函数在逻辑上是不可能的。
Synchronized关键字底层原理
synchronize底层原理属于JVM层面
synchronized同步语句块的实现使用的是monitorenter和monitorexit指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指向同步代码块的结束位置。
当指向monitorenter指令时,线程试图获取锁也就是对象监视器的持有权。
在Java虚拟机中,Monitor是基于C++实现的,由ObjectMonitor实现的。每个对象中都内置了一个ObjectMonitor对象。
另外wait/notify 等方法也依赖于monitor对象,这就是为什么再有在同步的块或者方法中才能调用wait/notify等方法。
在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为0则表示锁可以被获取,获取后将锁计数器设为1,也就是该锁被持有。
对象锁的拥有者线程才可以执行montorexit来释放锁,在执行monitorexit指令后,将锁计数器设为0,表明锁被释放,其他线程可以尝试获取锁。
如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
synchronized修饰方法的情况
sunchronized修饰的方法并没有monitorenter指令和monitorexit指令,取而代之的是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法。JVM通过该ACC_SYNCHRONIZED访问标志来连别一个方法是否声明为同步方法,从而执行相应的同步调用。
方法块和方法中synchronized的本质都是ObjectMonitor(对象监视器)对象的获取。
说说JDK1.6之后的synchronized关键字底层做了哪些优化
JDK1.6对锁的实现引入了大量的优化,如偏向锁,锁消除、锁粗化、轻量级锁、自旋锁、适应性自旋锁等操作来减少锁操作的开销。
锁主要存在四种状态,依次是:无所状态,偏向锁状态、轻量级锁状态、重量级锁状态。他们会随着竞争的激励进行锁升级,升级之后的锁不可以降级,这种策略是为了提高获得锁和释放锁的效率。
synchronized和ReentrantLock的区别
两者都是可重入锁
可重入锁指的是字节可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有被释放,挡器再次想要获得这个对象的锁的时候还是可以获取的。如果不是可重入锁的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都会自增1,所以要等到锁的计数器下降为0时才能释放锁。
synchronized依赖于JVM而ReentrantLock依赖于API
synchronized是依赖于JVM实现的,ReentrantLock是JDK层面实现的也就是API层面。
ReentrantLock比synchronized增加了一些高级功能
- 等待可中断:ReentrantLock提供了一种能够钟断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制,也就是线程可以放弃等待。
- 可实现公平锁:可以指定ReentrantLock是公平锁或者非公平锁,而synchronized只能是非公平锁,ReentrantLock也默认是非公平的,但是可以通过构造器选择。
- 可实现选择性通知(锁可以绑定多个条件)synchronized关键字可于wait()和notify()等方法可以实现等待。通知机制。ReentrantLock也可以实现,借助Condition接口于newCondition()方法。
两者的共同点:
- 都是用来协调多线程对共享变量的访问
- 都是可重入锁,同一个线程可以多次获得同一个锁
- 都保证了可见性和互斥性
两者的不同点:
- ReentrantLock显式的获得、释放锁,synchronized隐式的获得释放锁
- ReentrantLock可响应中断(使用LockInterruptibly方法获得的锁可以随时中断,即使是没有获得锁的时候,synchronized只有在获得了锁以后才支持中断线程)、可轮回,synchronized是不可以响应中断的,为处理锁的不可用性提供了更高的灵活性
- ReentrantLock是API级别的,synchronized是JVM级别的
- ReentrantLock可以实现公平锁
- ReentrantLock可以通过Condition绑定多个条件(使用这种方法可以指定线程唤醒,Object的notify不能指定线程唤醒)。
- 底层实现不同,synchronized底层是同步阻塞,使用的是悲观并发策略,lock是同步非阻塞,底层使用CAS+AQS同步队列实现,采用乐观并发策略
- Lock是一个接口,而synchronized是Java中的关键字,synchronzied是内置的语言实现
- synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,没有主动通过unlock取释放锁,所以很可能会造成死锁线程。因此Lock需要在finally块中释放锁。
- 通过Lock可以知道有没有成功获取锁,而synchronized无法办到
- Lock可以提高多个线程进行读操作的效率,就是实现读写锁等。
synchronized锁粒度、模拟死锁场景
synchronized具有原子性、可见性、有序性
粒度:对象锁、类锁
三大性质总结
三大性质简介
在并发编程中分析线程安全问题往往需要切入点,那就是两大核心:JMM抽象内存模型以及happens-before规则,三大性质:原子性,有序性和可见性。
原子性
原子性是指一个操作是不可中断的,要么全部执行成功,要么全部执行失败。即是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。
int a=10;
a++;
int b=a;
a=a+1;
上面这四个语句只有第一个语句是原子操作,将10赋值给线程工作内存的变量a,而语句2(a++),实际上包含三个操作,1、在主内存中找到a,读取a的值到本地内存,2、修改本地内存中a的值将其加1,3、将修改后的值协会主内存。这三个操作无法构成原子操作。
java内存模型中定义了8种原子操作,是不可再分的
- lock(锁定):作用于主内存的变量,它把一个变量标识为一个线程独占的状态。
- unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便后面的load动作使用
- load(载入):作用于工作内存中的变量,它把read操作从主内存中得到的变量值放入工作内存中的变量副本
- use(使用):作用于工作内存中的变量,它把工作内存中的一个变量传给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作;
- assign(赋值):作用于工作内存中的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
- store(存储):作用于工作内存的变量,它把工作内存中的一个变量的值传送给主内存中以便随后write操作使用。
- write(操作):作用于主内存的变量,它把store操作从工作内存中得到的变量放入主内存的变量中。
把一个变量从主内存中复制到工作内存中就需要执行read,load操作,将工作内存同步到主内存中就需要执行store,write操作。注意的是:Java内存模型只是要求上述两个操作时属性执行的并不是连续执行的。也就是说read和store之后,load,write之前可以插入其他指令。比如对主内存中的a,b进行访问就可以出现这样的操作顺序:read a,read b,load b,load a;
由原子性变量操作read,load,used,assign,store,write,可以大致认为基本类型的访问读写具备原子性
有序性
有序性指的是程序按照代码的先后顺序执行
synchronized,synchronized语义表示锁在同一时刻只能由一个线程进行获取,当锁被占用后,其他线程只能等待。因此synchronized语义就要求线程在访问读写共享变量时只能串行执行,所以synchronized具有有序性。
volatile在java内存模型中说过,为了性能优化,编译器和处理器都会进行指令重排序;也就是说java程序天然的有序性可以总结为:如果在本线程内观察,所有的操作都是有序的;如果在一个线程内观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。
synchronized和volatile有序性比较
- synchronized的有序性:是持有相同锁的两个同步块只能串行的进入,即被加锁的内容要按照顺序被多个线程执行,但是其内部的同步代码还是会发生重排序,使块与块之间有序可见。
- volatile的有序性:是通过插入内存屏障来保证指令按照顺序执行。不会存在后面的指令跑到前面的指令之前来执行。是保证编译器优化的时候不会让指令乱序。
可见性
可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改。通过之前对synchronzied内存语义进行分析,当线程获取锁时会从主内存中获取共享变量的最新值,释放锁的时候会将共享变量同步到主内存中,从而,synchronized具有可见性。同样在volatile中,会通过在指令中添加Lock指令,以实现内存的可见性。
synchronized具有原子性,有序性和可见性
volatile具有有序性和可见性
volatile关键字
CPU缓存模型
为什么需要CPU高速缓存
类比我们开发网站后台系统使用的redis是为了解决程序处理速度和访问常规关系型数据库速度不对等的问题。CPU缓存则是为了解决CPU处理速度和内存处理速度不对等的问题。
内存可以看作外存的高速缓存,程序允许的时候我们把外存的数据复制到内存,由于内存的处理速度远远高于外存,这样就提高了处理速度。
CPU缓存 缓存的是内存数据用于解决CPU处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。
CPU缓存的工作方式
先复制一份数据到CPU缓存中,当CPU需要用到的时候就可以直接从CPU缓存中读取数据,当运算完成后,再将运算得到的数据写回主内存中。但是这样存在内存缓存不一致的问题,也就是,两个线程同时对cpu缓存中的一个数据的两份复制进行修改,只有一份修改会写回主内存。这样就出现了可见性问题。
CPU为了解决内存缓存不一致的问题可以通过定制缓存一致协议或者其他手段来解决
JMM(Java内存模型)
Java内存模型抽象了线程和主内存之间的关系,就比如说线程之间的共享变量必须存储再主内存中。Java内存模型主要的目的是为了屏蔽系统和硬件的差异。
在JDK1.2以前,Java的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的Java内存模型下,线程可以把变量保存在本地内存中。而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用原来值的拷贝。造成数据的不一致。
- 主内存:所有线程创建的实例对象都放在主内存中。
- 本地内存:每个线程都由一个私有的本地内存来存储共享变量的副本,并且每个线程只能访问自己的本地内存,无法访问其他线程的本地内存。
volatile原理
volatile变量进行写操作时,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在的缓存行的数据写回到系统内存.以这种方式实现可见性.
Lock前缀指令实际上相当于一个内存屏障(也称为内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作就已经全部完成(happens-before原则)
volatile的适用场景
- 状态标志,如:初始化或请求停机
- 一次性安全发布,如单例模式
- 独立观察,如定期更新某个值
- "volatile bean" 模式
- 开销较低的"读-写锁"策略,如计数器
1、状态标志
线程1执行dowork的过程中,可能有另外的线程2调用了shutdown,所以Boolean变量必须是volatile。
而如果使用synchronized块编写循环要比使用volatile状态编写麻烦的多。由于volatile简化了编码,并且状态标志并不依赖与程序内其他状态,因此此处非常适合使用volatile
这种类型的状态标记的一个公共特性是:通常只有一种状态转换;shutdownRequested标志从false转换为true,然后程序停止。这种模式可以扩展到来回转换的状态标志,但是只有在转换周期不被察觉的情况下才能拓展。此外还需要某些原子状态转换机制。例如原子变量
2、一次性安全发布,例如单例模式
在缺乏同步的情况下,可能会遇到某个对象引用的更新值和该对象状态的旧值同时存在。
单例模式使用volatile主要是使用volatile有序性的特点。
instance=new Singleton();其实分为三步
- 分配内存
- 在内存上实例化对象
- 引用对象指向实例对象
第二步和第三步在只有一个线程的情况下,发生指令重排也不影响。但是在多线程情况下,很可能出现。在线程一执行完1,3步骤后切换到了线程二,此时线程二就是使用实际上并未实例化完成的实例。会出现线程安全问题。
3、独立观察
定期发布观察结果供程序内部使用。
4、volatile bean 模式
volatile bean 模式的基本原理是:很多框架为易变数据的持有者(例如HttpSession)提供了容器,但是放入这些容器中的对象必须是线程安全的。
在volatile bean模式中,JavaBean的所有数据成员都是volatile类型的,并且getter和setter方法必须非常普通,即不包含约束
5、开销较低的 读-写锁策略
如果读操作远远超过写操作,可以使用内部锁和volatile变量来减少公共代码路径的开销。
即写时用synchronized修饰。读时不用,用volatile将写后的结果立即展示(可见性)。
正确使用Volatile变量
Java语言中的volatile变量可以被看做一种程度较轻的synchronized;与synchronized块相比,volatile变量所需的编码较少,但是它锁实现的功能也仅是synchronized的一部分。
锁提供了两种特性,互斥性和可见性。互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。可见性要更复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的,如果没有同步机制提供的这种可见性保证。线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题
正确使用volatile变量的条件
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中
这两个条件本质上就是保证对被volatile修饰的变量进行的操作是原子操作
性能考虑
使用volatile变量的主要原因是其简易性:在某些情形下,使用volatile变量要比使用相应的锁见到的多。使用volatile变量次要原因是其性能:在某些情况下,volatile变量同步机制的性能要优于锁。在的大多数处理器架构上。volatile读操作开销非常低,集合和非volatile读操作一样。而volatile写操作的开销要比非volatile写操作多很多,因为要保证可见性需要实现内存栅栏。即便如此volatile的总开销仍然要比锁获取低。
volatile操作不会像锁一样造成阻塞,因此,在能够安全使用volatile的情况下,volatile可以提供一些优于锁的可伸缩特性,如果读操作的次数要远远超过写操作,与锁相比volatile变量通常能够减少同步的性能开销。
Java内存区域和JMM有什么区别
Java内存区域和内存模型是完全不一样的两个东西:
- JVM内存结构和Java虚拟机运行时数据区相关,定义了JVM在运行时如何分区存储程序数据,就比如说堆主要用于存放对象实例。
- Java内存模型和Java的并发编程相关,抽象了线程和主内存的关系就比如说线程之间的共享变量必须存储在主内存中,规定了从Java源代码到CPU可指向指令这个转化的过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序的可移植性的。
happens-before原则是什么
- 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将堆第二个操作可见,并且第一个操作的执行顺序排在第二个操作之前。
- 两个操作之间存在happends-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,于按happends-before关系来执行的结果一致,那么JMM也允许这样的重排序。
happens-before原则表达的意义其实并不是一个操作发生在另一个操作的前面,虽然这从程序员角度上来说也并无大碍,更准确的说,它更想表达的意义是前一个操作的结果对于另一个操作时可见的,无论这两个操作是否在一个线程内。
happens-before常见规则有哪些?谈谈你的理解
- 程序顺序规则:一个线程内,按照代码顺序,书写在前面的操作happens-before于书写在后面的操作
- 解锁规则:解锁happens-before于加锁
- volatile变量规则:对一个volatile变量的写操作happens-before于后面堆这个volatile变量的读操作。说白了就是堆volatile变量的写操作的结果对于发生于其后的任何操作都是可见的。
- 传递规则:如果A happens-before B,且B happens-before C 那么A happens-before C
- 线程启动规则:Thread 对象的start()方法happens-before 于此线程的每一个动作。
- 线程中断规则:对现场interrupt方法的调用先行发生于被中断线程的代码检测到中断事件的发生
如何保证变量的可见性
在Java中,volatile关键字可以保证变量的可见性,如果我们将变量声明为volatile,这就指示JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
volatile关键字并非Java语言特有的,在C语言里也有,它最原始的意义就是禁用CPU缓存。如果我们将一个变量使用volatile 修饰,就是指示贬义词,这个变量是共享且不稳定的,每次使用它都到主存中读取。
volatile可以保存可见性,但是不能保存原子性,synchronized两者都可以保证。
*volatile不可以在方法中使用,我们在方法中声明一个实例,此时这个实例是当前线程创建的,也就是调用两次这个方法会创建两个不懂的对象,修改不同的对象不存在可见性问题,volatile没有意义。
如何禁止指令重排序
在Java中,volatile关键字除了可以保证可见性以外,还有一个重要的作用就是防止JVM的指令重排序。如果我们将变量声明为volatile,在对这个变量进行读写操作的时候,会通过特定的内存屏障的方式来禁止指令重排序。
解决缓存一致性问题的方法
缓存不一致问题是基于JMM出现了,也就是多个线程的本地内存共享一个主内存。假设两个线程同时进行i++操作。两个线程的本地内存中的i变量都是主内存中i变量的复制,i=0;第一个线程将本地内存i+1,然后将i=1写回主内存,但是此时线程二的本地内存中i依然是0,这个写回主内存的值也是i=1,结果是主内存中的i=1,但是实际上i++运行了两次。i应该等于2.这就是缓存不一致问题。
解决这个问题一般有两种方法
1)通过在总线加Lock锁的方式。
2)通过缓存一致性协议
这两种方法都是硬件层面上提供的方式
在早期的CPU中,使用在总线上加LOCK锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的。如果对总线加LOCK锁的话,也就是说阻塞了其他CPU对其他步建的访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。
也就是只让一个线程来操作共享变量,其他线程等待,线程结束后将共享变量写回,其他线程使用更新的共享变量。这种方式有一个问题,就是效率低下。
缓存一致性协议,它的核心思想是:当CPU写数据时,发现操作的的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行设置为无效状态,因此当其他CPU读取这个变量时,发现自己缓存行中缓存的该变量时无效的,那么它就会从内存重新读取。
volatile可以保证原子性吗
volatile关键字能保证变量的可见性,但是不能保证对变量操作是原子性的。
volatile关键字保证可见性的方法是当一个线程修改了被volatile关键字修饰的参数时,其他线程的这个参数的缓存会被设置为无效。所以其他线程要先去从主内存中拿到该变量的复制,他会等到这个变量更新后在使用这个变量的复制。以这种方式实现可见性。
不能保证原子性是因为对变量的操作可能不是原子操作。例如i++,i++实际是三个原子操作,将主内存的i复制一份到工作内存,将i+1,将i写回主内存。我们知道一个线程中被修饰了volatile关键字的变量在被修改时,其他线程的这个变量的缓存就会被设置为无效状态。现在假设有1个线程把i++的第一个原子操作运行完了,这个时候CPU切换给了另一个线程。这个线程将i的值+1,并将其写回内存。这个时候线程1的缓存被置为无效,但是i的值已经固定了。所以这个时候就会出现线程安全问题了。
synchronized和volatile的区别
synchronized 关键字和volatile关键字是两个互补的存在,而不是对立的存在
- volatile关键字是线程同步的轻量级实现,所以volatile的性能肯定比synchronized好,但是volatile关键字只能用于变量而synchronized可以修饰方法以及代码块。
- volatile关键字可以保证数据的可见性,但是不能保证原子性,synchronized两者都可以保证
- volatile关键字主要用于解决变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性。
synchronized、volatile区别
- volatile主要应用在多个线程对实例更改的场合,刷新主内存共享变量的值从而使得各个线程可以获得最新的值,线程读取变量的值需要从主存中读取;synchronized只是锁定一个变量,只有拿到锁线程可以访问该变量,其他线程被阻塞住。另外,synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU操作都会直接刷到主存中(释放锁前),从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作的原子性。
- volatile仅能使用在变量级别;synchronized则可以使用在变量方法,和类级别的。volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞,比如多个线程争抢synchronized锁对象时,会出现阻塞。
- volatile仅能实现变量修改的可见性,不能保证原子性,而synchronized则可以保证变量修改的可见性和原子性,因为线程获得锁才能进入临界区,从而保证临界区的所有语句全部都得到执行。
- volatile标记的变量不会被编译器优化,可以禁止进行指令重排;synchronized标记的变量可以被编译器优化。
ThreadLocal
ThreadLocal
通常情况下我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有字节的专属本地变量该如何解决呢?
ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal信息的比喻成存放数据的盒子,盒子中可以存储线程的私有数据
ThreadLocal原理了解吗
每个Thread对象都有两个成员变量 ,他们的类型都是ThreadLocal.ThreadLocalMap。他们的默认值都是null。当你常见一个ThreadLocal对象时,调用TreadLocal的get/set方法时,这两个对象才会初始化。
最终的变量是存在了线程对象中,即存在了线程对象的成员变量ThreadLocalMap中,键即使ThreadLocal对象(实际上是ThreadLocal对象的弱引用),值即是你指定的值。
*ThreadLocalMap底层是一个Entry数组(他的基本结构类似HashMap,只是ThreadLocalMap底层只有数组,没有链表),Entry继承自WeakReference
ThreadLocal的内存泄漏问题是怎么导致的
ThreadLocalMap中使用的key为ThreadLocal的弱引用,而value是强引用。所以如果ThreadLocal没有被外部引用的情况下,在垃圾回收时,key会被清理掉。而value不会被清理掉。
这样就会出现key为null的Entry。假如我们不做任何措施的话,value永远无法被GC回收,这是就产生了内存泄漏。
ThreadLocal什么时候会出现OOM的情况,为什么
Thread对象内部维持着ThreadLocalMap对象,只要我们的线程不关闭这个ThreadLocalMap对象就会一直存在。
ThreadLocal在没有使用线程池的情况下,正常情况不会存在内存泄漏,但是如果使用了线程池的话,就依赖于线程池的实现,如果线程池不销毁的话,那么就存在内存泄漏。
线程池一直不销毁,TreadLocalMap会越来越大,之前执行使用的ThreadLocal对象不会在被使用,但是又不会被GC清理。内存泄漏会越来越大,导致出现OOM
ThreadLocal.set()
假设我们调用ThreadLocal的一个实例threadLocal的方法set()
ThreadLocal<String> threadLocal =new ThreadLocal();
threadLocal.set("zsh");
我们知道实际上ThreadLocal实际上是起到一个作为key的作用。set的基本过程是
- 通过当前的Thread对象(当前线程)来获取Thread对象的ThreadLocalMap成员变量。
- 如果没有则进行ThreadLocalMap对象的初始化
- 如果有则将ThreadLocal作为Key,“zsh”作为值存在Thread对象的ThreadLocalMap对象中。
ThreadLocalMap的哈希算法
ThreadLocalMap的哈希算法和HashMap一致,ThreadLocalMap的数组长度也默认为2的幂,所以哈希算法为 obj.hashCode&(n-1); n为数组长度
因为ThreadLocalMap底层没有链表的数据结构,当ThreadLocalMap出现哈希冲突时,使用开放定址法(线性探测)来解决哈希冲突。
为什么要使用ThreadLocalMap来保存局部对象
一个线程拥有的局部变量可能有很多,以ThreadLocalMap这种实现方法,无论线程局部变量有多少个,都只需要一个ThreadLocalMap来保存。当ThreadLocalMap中的数据过多,底层数组会进行扩容。
ThreadLocal的内存回收
ThreadLocal层面的内存回收,当线程死亡时,Thread对象的成员变量threadLocals,ThreadLocalMap的实例会被回收。
ThreadLocalMap层面的内存回收,如果线程存活的时间很长,并且该线程保存的线程局部变量有很多(也就是Entry对象很多),那么就涉及到在线程的生命周期内如果回收ThreadLocalMap的内存了,不然的话,ThreadLocalMap的实例越来越大,占用的内存越来越多,对于已经不需要的的线程局部变量,就应该清理掉其对应的Entry对象。
我们的ThreadLocal对象在set进ThreadLocalMap时,会创建一个Entry节点,这个节点继承自WeakReference,调用Entry构造方法时,会调用WeakReference的构造方法,让ThreadLocal成为一个弱引用对象。所以当ThreadLocal对象的强引用释放时,ThreadLocal对象就会被GC清理,且在set后,会进行槽位的清理以及是否需要扩容的判断,槽位的清理即判断entry对象的引用是否存在。
ThreadLocal特点
- ThreadLocal只是操作Thread中的ThreadLocalMap对象的集合;
- ThreadLocalMap变量属于线程的内部属性,不同的线程拥有完全不同的ThreadLocalMap变量
- 线程中的ThreadLocalMap变量的值实在ThreadLocal对象进行set或者get操作时创建的
- 使用当前线程的ThreadLocalMap的关键在于使用当前线程的ThreadLocal的实例作为key来存储value
- ThreadLocal模式实现了两个方面的数据访问隔离,1、各线程之间ThreadLocalMap不同,各ThreadLocal对象对应的Vlaue隔离
- 一个线程中的所有局部变量其实全部存储在一个ThreadLocalMap中
- 线程死亡时,线程局部变量会自动回收内存
- 局部变量是通过一个Entry节点保存在map中的,该Entry继承自weakReferernce,会软引用ThreadLocal对象
- 当线程拥有的局部变量超过了ThreadLocalMap的2/3,会涉及到ThreadLocalmap中entry的回收
对于多线程资源共享的问题,同步机制采用了“以时间换空间”的做法。而ThreadLocal采用了“以空间换时间”的做法。前者提供一份变量,让不同的线程排队访问,后者为每个线程提供了一份变量,因此可以同时访问互不影响。
线程池
为什么要用线程池?
线程池提供了一种限制和管理资源的方式。每个线程池还维护一些基本统计信息,例如已完成任务的数量。
线程池的好处
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和消耗造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
- 提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行同一的分配,调优和监控。
实现Runnable和Callable的区别
Runnable接口不会返回结果或者抛出检查异常,但是Callable接口可以。所以如果任务不需要返回结果或者抛出异常,推荐使用Runnable接口。
执行execute()方法和submit()方法的区别是什么。
execute()是Executor接口提供的方法,用于提交没有返回值的任务,无法判断任务是否被线程池成功执行。
submit()方法是ExecutorService接口提供的方法,用于提交需要返回值的任务,线程池会返回一个Future类型的对象,通过这个Future对象可以判断任务是否成功执行。
shutdown()和shutdownNow()的区别
- shutdown()关闭线程池,线程的状态变为SHUTDOWN,线程池此时不在接收新任务了,但是要将任务队列中已经接收的任务完成。
- shutdownNow()关闭线程池,线程的状态变成STOP,线程池会终止当前正在运行的任务,并停止处理排队的任务,并返回正在等待执行的队列
isTerminated()和isShutDown()的区别
- isShutDown(),当调用shutdown()方法后返回true
- isTerminateed(),当调用shutdown()方法后,并且所有提交的任务完成后返回true
如何创建线程池
- 通过构造函数
- 使用Executors工具类
- FixedThreadPool:这个方法返回一个固定线程数量的线程池。该线程池中的线程始终不变(核心线程数和最大线程数一样)。这个线程池的任务队列长度设置为Integer.MAX_VALUE,可能会堆积大量请求,导致OOM
- SingleThreadPool:这个方法返回一个只有一个线程的线程池,这个线程池的任务队列长度设置为Integer.MAX_VALUE,可能会堆积大量请求,导致OOM
- CachedThreadPool:该方法返回一个可根据实际情况调整线程数量的线程池。当所有已创建的线程都有任务时,来了任务,会创建新的线程来处理任务。这种线程池,最大线程数设置为Integer.MAX_VALUE可能会因为创建大量线程从而导致OOM。
newFixedThreadPool创建一个定长线程池,可控制线程最大并发数,超过的线程会在队列中等待
newCachedThreadPool,核心线程数为0,最大线程数为Integer.MAX_VALUE,任务队列为SynchronousQueue,已知当任务队列满时才会创建临时线程,又因为SynchronousQueue,实际上没有队列,所以在第一个任务来时就创建临时线程来处理任务。
newScheduled ThreadPool创建一个定长线程池,支持定其及周期性执行任务。任务队列使用DelayedWorkQueue,这个队列是一个无界队列,它能按一定的顺序对工作队列中的元素进行排列。
ThreadPoolExecutor 的参数
- corePoolSize:核心线程数定义了最小可以同时运行的线程数
- maximumPoolSize:线程池最大可以创建的线程数
- keepAliveTime:临时线程的存活时间。
- unit:存活时间的单位
- workQueue:任务队列,用于在线程都在工作时,存放任务
- threadFactory:线程工程,用于线程的创建
- handler:饱和策略,任务队列满时,接收的任务的处理方式。
Handler饱和策略
- ThreadPoolExecutor.AbortPolicy:直接抛出RejectedExecutionException来拒绝新任务的处理
- ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,也就是直接在创建线程池的线程中运行接收到的任务,如果执行程序已经关闭,则会丢弃该任务,因此这种策略会降低对于新任务的提交速度。
- ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃任务队列中最早的未处理任务。
线程池原理
线程池接收任务,会优先使用核心线程,当Executor接收任务,Executor会创建核心线程来处理任务。任务执行完毕的线程池会回到线程池,接收下一个任务。当所有的核心线程都在处理任务时,此时Executor接收的线程会被存到任务队列中,此时工作完成的核心线程会从任务队列接收任务。当核心线程都在处理任务且此时任务队列已满,会创建临时线程处理任务。当线程池中的线程已经到达最大值时,任务队列的任务也满了,此时接收的任务会进行饱和策略处理。
Executor框架的两级调度模型
在HotSpotVM的模型中,JAVA线程被一对一的映射为本地操作系统线程,Java线程启动时会创建一个本地操作系统线程,当Java线程终止时,对应的操作系统线程也被销毁回收,而操作系统会调度所有线程并将他们分配给可用的CPU。
在上层,Java程序会将应用分解为多个任务,然后使用应用级的调度器将这些任务映射成固定数量的线程;在底层,操作系统内核将这些线程映射到硬件处理器上。
Java线程既是工作单元,也是执行机制。而Executor框架中,我们将工作单位与执行机制分离开来。Runnable和Callable是工作端元(也就是俗称的任务),而执行机制由Executor来提供。这样依赖Executor是基于生产者消费者模式的,提交任务的操作相当于生产者,执行任务的线程相当于消费者。
Callable、Future、FutureTask详解
Callable与Future是在Java后续版本中引入进来的。Callable类似于Runnable接口,实现Callable接口的类与实现Runnable接口的类都是可以被线程执行的任务。
三者之间的关系:
Callable是Runnable封装的异步运算任务
Future用来保存Callable异步运算的结果
FutureTask封装Future的实体类
1、Callable与Runnable的区别
- Callable接口中的抽象方法是call,Runnable接口中的抽象方法是run
- call方法有返回值,run方法是没有返回值的
- call方法可以抛出异常,run方法不能抛出异常
2、Future
Future表示异步计算的结果,提供了以下方法,主要是判断任务是否完成、中断任务、获取任务执行结果。
3、FutureTask
FutureTask实现了Future和Runnable接口,是可以取消的异步计算,此类提供了对Future接口的基本实现,仅在计算完成时才能获取结果,如果计算尚未完成,则阻塞get方法。
FutureTask不仅实现了Future接口,还实现了Runnable接口,所以不仅可以将FutureTask当成一个任务交给Executor来执行,还可以通过Thread来创建一个线程。
如何合理配置线程池大小
1)线程池核心参数:corePoolSize,maximumPoolSize,workQueue,rejectExecutionHandler
2)线程池大小分配,线程池究竟设置的多大要看你的线程池执行什么类型的任务了,CPU密集型,IO密集象,混合型,任务类型不同,设置的方式也不一样.
2.1)CPU密集型:尽量使用较小的线程池,一般线程数量设置为Cpu核心数+1,+1是为了防止线程因为某种原因阻塞导致的资源浪费
2.2)IO密集型:方法1:可以使用较大的线程池,一般CPU核心数*2,方法2:(线程等待时间与线程CPU时间之比+1)*CPU数目.
2.3)混合型:可以将任务分为CPU密集型和IO密集型,然后分别使用不同的线程池去处理,按情况而定.
实际上合理配置线程池大小本身就是一个伪命题,因为实际运行情况的变数非常多,给线程池配置一个固定值想让线程池时刻处于最优状态显然是不现实的.实际上ThreadPoolExecutor中提供了一系列方法可以动态化配置线程池的核心配置.我们可以根据实际情况进行动态配置.
终止线程4种方式
正常运行结束
程序运行结束,线程自动结束
使用退出标志退出线程
一般run()方法执行完,线程就会正常结束,然而,常常有线程是伺服线程。它们需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如:最直接的方法就是设一个boolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出。
Interrupt方法结束线程
使用interrupt方法来中断线程有两种情况:
1)线程处于阻塞状态:如使用sleep方法,wait方法,socket种的receiver,accept等方法,会使线程处于阻塞状态。当调用线程interrupt方法时,会抛出interruptException异常。阻塞中的哪个方法抛出这个异常,通过代码捕获该异常,然后break跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用了interrupt方法线程就会结束,实际上是错的,一定要先捕获InterruptedException异常之后通过break来跳出循环,才能结束run方法
2)线程未处于阻塞状态:使用isInterrupted()判断线程的中断标志来退出循环。当使用interrupt方法时,中断标志就会置未true,和使用自定义的标志来控制循环是一样的道理。
stop方法终止线程(线程不安全)
线程的生命周期,什么时候会出现僵死进程
僵死进程是指子进程退出时,父进程并未对其发出的SIGCHLD信号进行适当处理,导致子进程停留在僵死状态等待父进程为其收尸,这个状态下的子进程就是僵死进程。
僵死进程和孤儿进程
在Unix/Linux系统中,正常情况下,子进程是父进程创建的,且两者的运行是相互独立的,父进程永远无法预测子进程到底什么时候结束。当一个进程调用exit()命令结束自己的生命时,其实它并没有真正的被销毁,内核只是释放了该进程的所有资源,包括打开的文件、占用的内存等,但是留下一个被称为僵尸进程的数据结构,这个结果保留了一定的信息(包括进程ID,退出状态,运行时间),这些信息知道父进程通过wait()/waitpid()来取时才释放。这样设计的目的主要是保证只要父进程想要知道子进程结束时的状态信息,就可以得到。
- 僵死进程:一个进程通过fork()创建子进程,如果子进程退出,而父进程并没有调用wait()/waitpid()方法获取子进程的状态信息。那么子进程的进程描述符依然保存在系统中,这种进程被称之为僵死进程。
- 孤儿进程:一个父进程退出,而它的一个或者多个子进程还在运行,那么这些子进程这个时候就成为了孤儿进程,这些进程会被init进程(进程ID1)所收养,由init进程对他们完成状态手机工作。
任务队列BlockingQueue详解
无界队列
队列大小无限制,常用的为无界的LinkedBlockingQueue,使用该队列作为阻塞队列时要尤其当心,当任务耗时较长时可能会导致大量新任务在队列中堆积导致OOM,
有界队列
常用的有两种,一类时遵循FIFO原则的队列如ArrayBlockingQueue与优解的LinkedBlockingQueue,另一类时优先级队列如PriorityBlockingQueue。使用有界队列时队列大小需要和线程池大小互相配合,线程池较小的有界队列较大时可以减少内存消耗,降低cpu使用率和上下文切换,但是可能会限制系统吞吐量
同步移交
如果不希望任务在队列中等待而是希望将任务直接移交给工作线程,可以使用synchronousQueue作为等待队列。SynchronousQueue不是一个真正的队列,而时一种线程之间移交的机制。要将一个元素放入synchronousQueue中,必须有另一个线程正在等待接收这一个元素。只有在使用无界线程池或则有饱和策略才建议使用该队列。
说说线程安全问题,什么是线程安全,如何实现线程安全
线程安全:如果线程执行过程中,不会产生资源共享的冲突,则线程安全。
线程不安全:如果有多个线程同时在操作主内存中的变量,则线程不安全。
实现线程安全的三种方法
1)互斥同步
- 临界区
- 信号量
- 互斥量
2)非阻塞同步
CAS
3)无同步方案
- 可重入代码
- 使用ThreadLocal类来包装共享变量
- 线程本地存储
Java多线程安全机制
在开始讨论Java多线程安全机制之前,首先从内存模型来了解一下什么是多线程的安全性。
我们知道在JMM中存在主内存和工作内存之分,主内存存放的是线程共享的变量(实例字段,静态字段和构成数组的元素),线程的工作内存是线程四所有的空间,存放的是线程私有的变量(方法参数和局部变量)。线程在工作的时候如果要操作主内存上的共享变量,为了获得更好的执行性能并不是直接去修改主内存而是会在线程私有的工作内存中创建一份变量的拷贝。在工作内存上对变量的拷贝修改之后在把修改的值刷回到主内存的变量中,JVM提供了八个原子操作来完成这一过程:lock,unlock,read,load,use,assign,store,write。
如果只有一个线程当然不会有什么问题,但是如果有多个线程在同时操作主内存的变量,因为JVM操作的非连续性和线程抢占CPU执行的机制就会带来冲突,也就是多线程的安全问题。线程安全的定义就是:如果线程执行过程中不会产生共享资源的冲突就是线程安全的。
Java里一般用以下几种机制保证线程安全:
1、互斥同步锁(悲观锁)
1)Synchronized
2)ReentrantLock
互斥同步锁也被叫做阻塞同步锁,特征是会对没有获取锁的线程进行阻塞。
要理解互斥同步锁,首先要明白什么是互斥什么是同步。简单的说互斥就是非你即我,同步就是顺序访问。互斥同步锁就是以互斥的手段达到顺序访问的目的。操作系统提供了很多互斥机制比如信号量,互斥量,临界区资源等来控制在某一时刻只能由一个或者一组线程访问同一个资源。
Java里面的互斥同步锁就是Synchronized和ReentrantLock,前者是语言即JDK层面实现的互斥同步锁,在JDK1.6之后Synchronized进行了大幅优化,即使在竞争激励的情况下也能保持一个和ReentrantLock相差不多的性能,所以在JDK1.6之后不会再因为性能问题放弃Synchronized。ReentrantLock是API层面实现的互斥同步锁,需要程序自己打开并在finally关闭锁,和synchronized相比更加的灵活,体现在三方面:等待可中断,公平锁,以及绑定多个条件,另外sunchronized是JVM实现的,可以通过监控工具来监控锁的状态,遇到异常JVM会自动释放掉锁,而ReentrantLock必须由程序主动的释放锁.
互斥同步锁都是可重入锁,好处是可以保证不会死锁.但是因为涉及到核心态和用户态的切换,因此比较消耗性能.
2.非阻塞同步锁
1)原子类(CAS)
非阻塞同步锁也叫乐观锁,相比悲观锁来说,他会先进行资源在工作内存中的更新,然后根据与主存中旧值的对比来确定在此期间是否有其他线程对共享资源进行了更新,如果旧值与期望值相同,就认为没有跟新,可以把新值协会内存,否则就一直重试直到成功.它的实现方式依赖于处理器的机器指令:CAS;
JUC包中提供的几种Atomic类以及每个类上的原子操作就是乐观锁机制即用volatile和cas实现
不激烈的情况下,性能比synchronized略逊,而激烈的时候也能维持常态.激烈的时候,Atomic的性能会优于ReentrantLock一倍左右.但是有其有一个缺点,就是只能同步一个值,一段代码中只能出现一个Atomic的变量,多于一个同步无效.因为他不能在多个Atomic之间同步.
3.无同步方案
1)可重入代码
在执行的任何时刻都是中断-重入执行而不会产生冲突.特点就是不会依赖堆上的共享资源.可重入代码又称为“纯代码”,是一种允许多个进程访问的代码,因此,可重入代码是一种不允许任何进程对它进行修改的代码 。为了能修改,访问纯代码的进程,把执行中可能改变的部分拷贝到该数据区,只需对该数据区中的内容进行修改,并不去改变共享的代码,这时的可共享代码即成为可重入码。
2)ThreadLocal/Volatile
线程本地的变量,每个线程获取一份变量的拷贝,单独进行处理.
3)线程本地存储
如果一个共享资源一定要被多线程共享,可以尽量让一个线程完成所有的处理操作,比如生产者消费者模式中,一般会让一个消费者完成对队列上资源的消费.电影的应用是基于请求-应答模式的Web服务器的设计.
Atomic原子类
在这里,我们这里Atomic是指一个操作是不可中断的。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰
所谓的原子类说简单点就是育有原子/原子操作特征的类。
JUC包中的原子类有哪些
基本类型
- AtomicInteger:整形原子类
- AtomicLong:长整型原子类
- AtomicBoolean:布尔型原子类
数组类型
- AtomicIntegerArray:整性数组原子类
- AtomicLongArray:长整型数组原子类
- AtomicReferenceArray:引用类型数组原子类
引用类型
- AtomicReference:引用类型原子类
- AtomicStampeReference:原子更新带有版本号的引用类型。该类将整形数值与引用关联起来,可用于解决原子的更新数据和数据版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题
- AtomicMarkableReference:原子更新带有标记为的引用类型
对象的属性修改类型
- AtomicIntegerFieldUpdater:原子更新整型字段的更新器
- AtomicLongFieldUpdater:原子类型更新长整型字段的更新器
- AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器
AtomicInteger常用方法
- int get();
- int getAndSet(int newValue);
- int getAndIncrement();
- int getAndDecrement();
- int getAndAdd(int delta);
- boolean compareAndSet(int expect,int update);//如果输入值等于预期值,则以原子的方式更新该值为输入值。
- void lazySet(int newValue);//使用这个方法,最终会变为newValue,但是可能导致其他线程在之后一小段时间里依然读到原来的值。
ABA问题
描述:第一个线程取到了变量x的值A,然后继续其他的操作,这段时间内,第二个线程也取到了变量x的值A,然后把变量x的值改为B,然后进行其他的操作,最后又把值从B改回A,第一个线程看来这个数据是没有变的。所以CAS操作成功,但是实际上变量x是改变过了的,这件事情不能无视。
AtomicInteger线程安全原理
AtomicInteger类主要利用CAS(compare and swap)+volatile和native方法来保证原子操作,从特人避免synchronized的高开销,执行效率大为提升。
CAS的原理解释拿期望的值和原本的值做比较,如果相同则更新成一个新的值,UnSafe类的ObjectFieldOffset()方法是一个本地方法,这个方法是用来拿到原来的值的内存地址。另外value是一个volatile变量,在内存中可见,因此JVM可以保证任何时刻总能拿到改变量的最新值。
AQS
AQS简单介绍
AQS全称为AbstractQueuedSynchronizer,即抽象队列同步器。AQS是一个抽象类,主要用来构建锁和同步器。
AQS为构建锁和同步器提供了一些通用功能的实现,因此使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask(JDK1.7)等等皆是基于AQS的。
AQS原理
AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的。
CLH队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(node)来实现锁的分配。
AQS使用一个int成员变量state来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的改变。
AQS对资源的共享方式
AQS定义两种资源共享方式
1)Exclusive(独占)
只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁,ReentrantLock同时支持两种锁,下面以ReentrantLock对这两种锁的定义做介绍:
- 公平锁:按照线程在队列中的排毒顺序,先到者先拿到锁
- 非公平锁:当线程要获取锁是,先通过两次CAS操作去抢锁,如果没抢到,当前线程再加入刀队列中等待唤醒。
公平锁和非公平锁的不同
- 非公平锁在调用了lock以后,首先就会调用CAS进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
- 非公平锁在CAS失败后,和公平锁一样都会进入到tryAcquire方法,在tryAcquire方法中,如果发现锁这个时候被释放了(state==0),非公平锁会直接CAS抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,然后进入等待队列。
相对来说,非公平锁会有更好的性能,因为它的吞吐量更大。当然,非公平锁让获取锁的时间变的更加不确定,导致在阻塞队列中的线程长期处于饥饿状态/
2)Share(共享)
多个线程可以同时执行,如Semphore,CountDownLatch,CyclicBarrier,ReadWriteLock。
ReentrantReadWriteLock,可以看做是两种锁的组合,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读写。
AQS是多线程同步器,它是JUC报中多个组件的底层实现,比如说像Lock,CountDownLatch,Semaphore都用到了AQS,从本质上来说AQS提供两种锁的机制,分别时互斥锁和共享锁,所谓互斥锁就是存在多个线程取争抢一个共享资源的时候,同一时刻值运行一个线程取访问这样的一个共享资源,也就是多个线程只有一个线程去获得这样一个锁的资源,比如Lock接口中的ReentrantLock,它的实现就是用到了AQS中一个互斥锁的功能。共享锁也被称为读锁,就是同一时刻允许多个线程同时获得这样一个锁的资源,比如CountDownLatch以及Semphore,都用到了AQS中共享锁的功能。那么AQS作为互斥锁来说呢,它的整个设计体系中,需要解决三个核心的问题,1、互斥变量的设计,以及如何保证多线程更新互斥变量时线程的安全性。2、未竞争到锁的线程的等待,以及竞争到锁的线程,释放之后的唤醒。3、锁竞争的公平性和非公平性。AQS中采用了一个int类型的互斥变量state用来记录锁竞争的状态,0表示当前没有任何线程竞争锁资源,也就是无锁状态,如果大于等于1表示以及由线程正在持有锁资源。一个线程来获取锁资源时,首先判断state是否等于0,也就是无锁状态,如果是,则把这个state更新为1,而这个过程中,如果有多个线程同时去做这样的一个操作,就会导致线程安全问题,因此AQS采用CAS机制,去保证state互斥变量更新的一个原子性。未获取锁的线程通过unsafe类中的park方法进行阻塞,把阻塞的线程按照FIFO的原则加入到双向链表中(AQS底层是一个Node节点的双向链表),当获得资源的线程释放锁之后,会从这样一个双向链表的头部去唤醒下一个等待的线程,再去竞争锁。最后关于锁竞争的公平性问题,AQS的处理方式是在竞争锁资源的时候公平锁要去判断双向队列中是否有阻塞的线程,如果有则优先唤醒。非公平锁则是,不去判断双向链表中有没有阻塞的线程,直接通过CAS操作抢锁。
怎么提高并发量,请列举你所知道的方案
一个小型的网站,可以使用最简单的html静态页面就实现了,配合一些图片达到美化效果,所有的页面 均存放在一个目录下,这样的网站对系统架构、性能的要求都很简单。随着互联网业务的不断丰富,网 站相关的技术经过这些年的发展,已经细分到很细的方方面面,尤其对于大型网站来说,所采用的技术更是涉及面非常广,从硬件到软件、编程语言、数据库、WebServer、防火墙等各个领域都有了很高的要求,已经不是原来简单的html静态网站所能比拟的。
1、HTML静态化
2、图片服务器分离
对于Web服务器来说,不论是什么容器,图片是最消耗资源得。于是有必要将图片与页面分离。
3、数据库集群、库表散列
大型网站都有复杂的应用,这些应用都必须使用数据库,那么在面对大量访问的时候,数据库的瓶颈很快就显现出来,这时一台数据库将很快无法满足应用,于是我们需要使用数据库集群或者库表散列
4、缓存
5、镜像
6、负载均衡
进程通讯的方式
通信方法 | 无法介于内核态与用户态的原因 |
---|---|
管道(不包括命名管道) | 局限于父子进程间的通信 |
消息队列 | 在硬软钟断中无法无阻塞的接收数据 |
信号量 | 无法介于内核态和用户态使用 |
内存共享 | 需要信号量辅助,而信号量又无法使用 |
套接字 | 在硬、软钟断中无法无阻塞的接收数据 |
线程间通信,wait和notify
wait和notify的理解与使用
- wait(),notify(),notifyAll()都不属于Thread类,而是属于Object基础类,Object是所有类的基类,所以所有类都有这三个方法的功能。因为每个对象都有锁,锁是每个对象的基础。
- 当需要调用以上方法的时候,一定要对竞争资源进行加锁,如果不加锁的话,则会报illegalMonitorStateException异常。即当我们针对一个obj对象使用以上方法时,obj对象应该只被一个线程操作。
- 当想要调用wait()进行线程等待是,必须要取得这个对象的控制器(MonitorObject),一般是放到synchronized(obj)代码中.
- 在while循环里而不是if语句下使用wait,这样,会在线程暂停恢复后都检查wait的条件,并在条件实际上并未改变的情况下处理唤醒通知。(在while中使用wait方法,而不是在if中,为什么这么说,wait线程被唤醒后从哪开始执行是关键,在if中wait的线程,唤醒后会继续执行,可能会出异常,而使用while后被唤醒,会进行条件的判断)
- 调用obj.wait()释放了obj的锁,否则其他线程也无法获得obj的锁,也就无法synchronized(obj){obj.notify()}代码断内唤醒A。
- notify()方法只会通知等待队列中的第一个相关线程(不会通知优先级比较高的线程)
- notifyAll()通知所有等待该资源的线程(也不会按照线程的优先级来执行)
- 假设有三个线程执行了obj.wait(),那么obj.notifyAll()则能全部唤醒thread1,thread2,thread2,但是要继续执行obj.wait()的下一条语句,必须获得obj锁,因此,thread1,2,3只有一个线程有机会执行,其他两个线程要等待另一个线程释放锁才能运行。
- 当调用obj.notify()/notifyAll()后,调用线程依旧持有obj锁,因此thread1,2,3,虽然被唤醒但是任然无法获得锁,要等待调用notify方法的线程的同步代码执行完毕,释放锁后,thread1,2,3才有可能获得锁然后运行。
Java如何等待子线程执行结束
工作中往往会遇到异步去执行某段逻辑,然后先去处理其他事情,处理完后再把那段逻辑的处理结果进行汇总的场景。这种场景就需要使用多线程。
主线程等待子线程完毕可以分为主动式和被动式
主动式指主线程主动去检测某个标志位判断子线程是否以叫完成,被动式指主线程被动的等待子线程的结束。
1)Thread.join()方法,调用这个方法的目的就是等待当前线程结束。(拓展,join底层其实就是使用了wait方法,但是没有使用notify方法,为什么线程会自动唤醒呢,其实是run方法运行完成后,系统自动调用notifyAll方法)
2)使用Callable接口实现,通过调用Future接口的get方法可以获取到线程运行的结果。get会阻塞到Callable线程运行结束。
3)使用阻塞队列,put和take操作都会阻塞,直到线程结束。
JAVA后台线程
定义:守护线程--也称“服务线程”,他是一个后台线程,它有一个特性,即为用户线程提供公共服务,在没有用户线程可服务是会自动离开。
优先级:守护线程的优先级比较低,用于为系统中的其他对象和线程提供服务。
设置:通过setDaemon(true)来设置线程为守护线程;将一个用户线程设置为守护线程的方式是在线程对象创建之前用线程对象的setDaemon方法
在Daemon线程中产生的新线程也是Daemon的
用户线程则是JVM级别的,以Tomcat为例,如果你在Web应用中启动了一个线程,这个线程的生命周期并不会和Web应用程序保持同步。也就是说,即使你停止了Web应用,这个线程依旧是活跃的。
例子:垃圾回收线程就是一个经典的守护线程,当我们的程序不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾线程会自动离开,它始终在级别低的状态中运行,用于实时监控和管理系统中的可回收资源。
生命周期:守护进程是运行在后台的一种特殊进程。它独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。也就是守护线程不依赖于终端。而是依赖于系统,与系统同生共死。当JVM中所有的线程都是守护线程的时候,JVM就可以退出了;如果还有非守护线程,JVM就不会退出。
JAVA锁
乐观锁(非阻塞同步锁)
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断以下在此期间别人有没有去更新这个数据,采取在写是先读出当前版本号然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写。
java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
悲观锁
悲观锁就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会阻塞直到拿到锁。java中的悲观锁就是synchornized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如ReentrantLock
自旋锁
自旋锁原理非常简单,如果持有锁的线程能在很短事件内释放资源,那么那些等待竞争的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要进行自旋,等待有锁的线程释放锁后即可立即获取锁。这样就避免了用户线程和内存的切换和消耗。
线程自选是需要消耗CPU的,说白了就是让CPU在做无用功,如果一直获取不到锁,那线程也不能一直占用CPU做无用功,所以需要设定一个自旋等待的最大时间。
如果持有锁的线程执行时间超过自旋等待的最大时间仍然没有释放锁,就会导致其他争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
自旋锁的优缺点
自旋锁尽可能减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的操作会小于线程阻塞挂起在唤醒操作的消耗,这些操作会导致线程发生两次上下文切换。
但是如果锁的竞争激励,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁之前一直都是占用CPU做无用功,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要CPU的线程又不能获得CPU,造成CPU资源的浪费。
Synchronized同步锁
synchronized它可以把任意一个非Null的对象当作锁,他属于独占式的悲观锁,同时属于可重入锁
synchronized核心组件
1)WaitSet:那些调用了wait等方法的线程被防止到这个队列
2)ContentionList:竞争队列,所有请求锁的线程首先被放在这个竞争队列中
3)EntryList:ContentionList 中那些有资格成为候选资源的线程被移动到EntryList中
4)OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程成为OnDeck
5)Owner:当前已经获取到锁资源的线程被称为Owner
6)!Owner:当前释放锁的线程
synchronized实现
1.JVM每次从队列尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,COntentionList会被大量并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList中作为候选竞争线程。
2.Owner线程会在unlock时,将Contentionlist中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(一般是最先进去的线程)
3.Owner线程并不直接把锁传递给OnDeck线程,而是把锁的竞争权利交给OnDeck,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM中也把这种选择欣慰称为竞争切换
4.OnDeck线程获取到锁资源后会编程Owner,而没有得到锁资源的任然停留在EntryList中。如果Owner线程被wait方法阻塞,则转移到waitSet队列中,直到某个时刻通过notify方法唤醒会重新进入EntryList
5.处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统在完成的
6.synchronized是非公平锁,synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已进入队列的线程是不公平的,还有一个不公平的地方就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源
7.每个对象都有个monitor对象,加锁就是在竞争monitor对象,代码块加锁是在前后分别加上monitorenter和monitorexit指令来实现的,方法加锁是通过一个标记为来判断的
8.synchronzied是一个重量级操作,需要调用操作系统相关接口,性能较低,有可能给线程加锁消耗的时间比有用操作消耗的时间更多。
9.java1.6synchronzied进行了很多优化,如适应型自旋、锁消除、锁粗化、轻量级锁及偏向锁等、效率有本质上的提高。在之后退出的java1.7和1.8中,均对synchronzied进行了优化,引入了偏向锁和轻量级锁
10.锁可以从偏向锁升级到轻量级锁,在升级到重量级锁
Java线程阻塞的代价
Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在用户太与内核态之间切换,这种切换会消耗大量的系统资源,因为用户态和内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递许多变量,参数给内核,内核也需要保护好用户态在切换时的一些寄存器值,变量等,以便内核态调用结束后切换回用户态继续工作。
- 如果线程状态切换时一个高频操作时,这将回消耗很多CPU处理时间;
- 如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略是非常糟糕的
偏向锁
Java偏向锁,是Java6引入的第一个多线程优化。
偏向锁,顾名思义,他会偏向于第一个访问锁的线程,如果在运行的过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加上一个偏向锁。如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程 就会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁
偏向锁的获取过程
- 访问markword中的偏向锁标识是否设置为1,锁标志位是否为01,确认为可偏向状态。
- 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤三
- 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功将MarkWord中线程ID设置为当前线程ID,然后执行5,如果竞争失败执行4
- 如果CAS获取偏向锁失败,则表示有竞争,当到达全局安全点是获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码(撤销偏向锁的时候会导致stw)
- 执行同步代码
偏向锁的适用场景
始终只有一个线程在执行同步块。在他没有执行完释放之前,没有其他线程取执行同步块,在锁无竞争的情况下适用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销会导致stw操作。在有锁的竞争时,偏向锁会做很多额外操作,尤其时撤销偏向锁的时候会导致线程进入安全点,安全点会导致stw,导致性能下降,这种情况应当禁用。
轻量级锁
轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级成为轻量级锁;
轻量级锁的加锁过程:
- 在代码进入同步块的时候,如果同步对象锁状态为无锁状态,虚拟机首先在当前线程的栈帧中建立一个名为LockRecord的空间,用于存储对象目前的markword的拷贝,官方称之为DisplacedMarkword。
- 拷贝对象投中的markword到LockRecord中
- 拷贝成功后,虚拟机将适用CAS操作尝试将对象的MarkWord更新为指向LockRecord的指针,并将Lockrecord里的owner指针指向objectmarkword。如果成功则执行步骤4否则执行步骤5
- 这个更新动作如果成功了,那么这个线程就拥有该对象的锁,并且对象Markword的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态。
- 如果这个更新操作失败了,虚拟机首先会检查对象的Mark word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志位变为“10”,markword中存储的就是指向重量级锁的指针,后面等待的线程也要进入阻塞状态。而当前线程便尝试适用自旋来获取锁。
ReentrantLock
ReentrantLock继承接口Lock并实现了接口中定义的方法,他是一种可重入的锁,除了能完成synchronized锁能完成的所有工作外,还提供了诸如相应可中断,可轮询请求,定时锁等避免多线程死锁的方法。
Lock接口的主要方法
- void lock()执行此方法是,如果锁处于空闲状态,当前线程将获取到锁,相反,如果锁已经被其他线程持有,将禁用当前线程,直到当前线程获取到锁
- boolean tryLock() 如果锁可用,则获取锁,并立即返回true,否则返回false。该方法和lock()的区别在于,tryLock只是试图获取锁,如果锁不可用,不会导致当前线程被禁用,当前线程仍然继续往下执行代码,而Lock方法则是一定要获取到锁,如果锁不可用,就一直等待,在未获得锁之前,当前线程并不继续向下执行
- void unlock() 执行此方法,当前线程将释放持有的锁,锁只能由持有扯释放,如果线程并不持有锁,却执行该方法,可能导致异常的发生
- Condition newCondition() 条件对象,获取等待通知的组件。该组件和当前锁绑定,当前线程只有获取了锁,才能调用该组件的awit()方法,而调用后,当前线程将缩放锁。
+ReentrantLock方法
- getHoldCount()查询当前线程保持次所的次数,(重入实现)
- getQueueLength() 返回正在等待此锁的线程估计数
- getWaitQueueLength(Condition condition) 返回等待次所相关的给定条件的线程估计数
- hasWaiters(Condition condition) 查询是都有线程等待与此锁有关的给定条件
- hasQueuedThread(Thread thread) 查询给定线程是否等待获取此锁
- hasQueuedThreads() 是否有线程等待此锁
- isFair() 是否是公平锁
- isHeldByCurrentThread() 当前线程是否保持锁定,线程的执行lock方法的前后分别是false和true
- isLock()此锁是否有任意线程占用
- lockInterruptibly()如果当前线程未被中断,获取锁
Condition类
- Condition类的await()方法和Object中的wait方法等效
- Condition类的signal()方法和Object中的notify方法等效
- Condition类的signalAll()方法和Object中的signalAll方法等效
- ReentrantLock类可以唤醒指定条件的线程,而object的是随机唤醒