一、sychronized介绍
并发时,多个线程需要操作同一个资源,容易导致错误数据的产生,为了解决这个问题,当存在多个线程操作共享数据时,需要保证同一时刻只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行。
java中sychronized关键字可以保证同一时刻只有一个线程可以执行某个方法或者代码块,同时sychronized可保证一个线程的变化被其他线程看到(保证可见性)。
二、sychronized 应用方式
1、作用于实例方法,当前实例加锁,进入同步代码块时需要获得当前实例的锁;
2、作用于静态方法,当前类加锁,进去同步代码前需要获取当前类的锁;
3、作用于代码块,这需要指定加锁对象,对所给的指定对象加锁,进入同步代码块前要获得指定对象的锁;
三、sychronized底层原理
java虚拟机中的同步Synchronization基于进入和退出管程Monitor对象实现。在java中,sychronized可以修饰同步方法,同步方法不是由monitorenter和moniterexit指令来实现同步,而是由方法调用指令读取运行时常量池中的ACC_SYNCHRONIZED标注来隐式调用的。
java对象头与monitor
在java中,对象在内存中的布局分为三块区域,对象头,实例数据和填充数据。
java对象
1、对象头
HotSpot虚拟机的对象头包括markword和klass,数组长度;
markword用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为MarkWord。
对象头的另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
数组长度,如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度。
2、实例数据
实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
3、对象填充
对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
hotSpot虚拟机中的markword结构
重量级锁就是sychronized的对象锁,锁标识为10,其中指针指向的是monitor对象的起始地址。每个对象都存在着一个monitor与之关联,对象与其monitor之间的关系有存在多重实现方式,如monitor可以与对象一起创建销毁或线程试图获取对象锁时自动生成,但当一个monitor被某个线程持有后,它便处于锁定状态。
在java虚拟机中(hotSpot),monitor是由ObjectMonitor实现的;
ObjectMonitor对象
ObjectMonitor中有两个队列,_WaitSet和_EntryList,用来保存ObjectWaiter列表,每个等待锁的线程都会封装成ObjectWaiter对象,_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问同一段同步代码时,首先会进入_EntryList集合,当线程获取到对象的monitor后进入_Owner区域并把monitor中的_owner变量设置为当前线程,同时monitor的计数器_count加1,若先写调用wait()方法,将释放当前持有的monitor,_owner变量恢复为null,count自减1,同时该线程进入_waitSet集合等待被唤醒。若当前线程执行完毕也将释放monitor,并复位变量的值,以便于其他线程获取monitor锁。
监视器
monitor对象存在每个java对象的对象头中(存储的指针的指向),synchronized锁便是用过这种方式获取锁的,这也是为什么java的任意对象都可以作为锁的原因,同时也是notify/notifyAll/wait方法存在object方法中的原因。
同步方法的实现原理
同步方法
同步代码块的实现原理
同步代码块
同步代码块是使用monitorenter和moniterexist指令实现的,会在同步块的区域通过监听器对象去获取锁和释放锁。
同步方法和静态同步方法是依赖在方法修饰符ACC_SYNCHRONIZED实现,当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行时将先获取monitor,获取成功够才能执行方法体,方法执行完成后再释放monitor,在方法执行期间,其他任何线程都无法在获得同一个monitor对象。
四、多线程面试
1、双重校验锁的单例模式
单例模式
volatile修饰singleton很有必要,voliatile防止了指令重排,singleton =new Singleton();其实分为了三步:
1.为singleton分配空间;2.初始化singleton;3.将singleton指向分配的内存地址;
由于jvm有指令重排的特性,执行顺序可能变成1->3->2,指令重排在单线程下可能没有问题,但是在多线程下可能会导致一个线程获得还没有初始化的实例。
2、JDK1.6后的sychronized关键字底层做了哪些优化?
引入了偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术减少所操作的开销。
锁主要存在四种状态,无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态,锁可以升级不能降级,这种策略是为了提高获得锁和释放锁的效率。
3、sychronized和ReenTrantLock的区别
两者都是可重入锁:自己可以再次获取自己的内部锁,此时锁的计数器count加1就行,多以要等到锁的计数器降为0才能释放锁。
sychronized依赖于jvm底层,ReenTrantLock依赖于API。
ReenTrantLock比sychronized增加了一些高级功能:等待可中断,可实现公平锁,可实现选择性通知:
(1)ReenTrantLock使用lock.lockInterruptibly()来实现等待可中断功能,也就是说正在等待的线程可以选择放弃等待;
(2)ReenTrantLock 可以指定公平锁还是非公平锁,而sychronized只能是非公平锁,公平锁就是先等待的线程先获得锁;实现方式是在ReentrantLock(boolean fair)构造方法来制定是否是公平的。
(3)sychronized和wait(),notify(),notifyAll()方法结合可以实现等待通知机制,ReenTrantLock类也可以实现,需要使用Condition接口和newCondition()方法。Condition具有很好的灵活性,可以在一个Lock对象中可以创建多个Condition实例(对象监视器),线程对象可以注册在指定的Condition中,从而可以选择性的进行线程通知,在调度线程上更加灵活。在使用notify()/notifyAll()方法进行通知时,被通知的线程是由JVM选择的,用ReenTrantLock类结合Condition实例可以实现选择性通知。而sychronized关键字就相当于整个Lock对象中只有一个Condition,多有的线程都注册在它一个身上。如果执行notifyAll()放的话就会通知所有处于等待状态的线程,而Condition的singalAll()方法只会唤醒注册在该Condition实例中的所有等待对象。
性能上已经不是选择标准:在高并发下,ReenTrantLock的效率比sychronized的效率高一些;
3、volatile关键字
在现在的java模型下,线程可以把变量保存在本地内存(比如寄存器)中,而不是直接在主内存中进行读写,这就可能造成一个线程在主内存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量的拷贝,造成数据不一致。
这个问题可以用volatile关键字解决,将变量声明为volatile,这就指示JVM这个变量不稳定,每次使用它都去主内存读取。
volatile关键字就是保证变量的可见性和防止指令的重排序。
4、sychronized和volatile的区别
sychronized关键字可以修饰方法,变量和代码块,而volatile只能修饰变量;
volatile是sychronized的轻量级实现,所以性能比sychronized好;但是JDK1.6后优化了锁,sychronized性能得到了提升,在实际任务中使用sychronized的时候更加多一些。
多线程访问volatile变量不会发生阻塞,而访问sychronized关键字会方式阻塞。
volatile关键字能保证数据的可见性,但是不能保证数据的原子性,sychronized保证了可见性和原子性。
volatile主要保证变量在线程间的可见性,而sychronized主要保证数据在多线程间的的同步性。
5、线程池
线程池提供了一种限制和管理资源,每个线程池还维护了一些基本统计信息,比如线程的数量。
线程池的优点:
降低资源消耗,通过重复利用已创建的线程降低线程的创建和销毁的消耗。
提高响应速度:当任务到达时,不需要等到创建线程就可以立即执行。
提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
6、实现Runnable和Callable接口的区别
如果想让线程池执行任务的话需要实现Runnable或者Callable接口,两个接口都可以被ThreadPoolExcutor和ScheduledPoolExcutor执行。但是Runnable接口不会返回结果,而Callable接口可以返回结果。
工具类Executors可以实现Runnable对象和Callable对象之间的相互转换。(Executors.callable(Runnable task)或Executors.callable(Runnable task,Object resule))。
7、执行execute()方法和submit()方法的区别
execute()方法用于提交不需要返回值的任务,所以午饭判断任务是否被线程池执行成功与否;
submit()方法用于提交需要返回值的任务,线程池会返回一个future类型的对象,通过这个对象来判断任务是否执行成功。并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
8、创建线程池
一般不建议使用Excutors去创建,而是通过ThreadPoolExcutor的方式,这样的处理方式更加明确线程池的运行规则,规避资源耗尽的风险。
ThreadPoolExcutorsAPI说明
一般通过构造方法可以创建
构造方法创建
使用Excutors返回线程池对象存在问题:
FixThreadPool和SingleThreadExutor:允许请求的队列长度为Integer.MAX_VALUE,可能堆积的请求过多,导致OOM,
CachedThreadPool和ScheduledThreadPool:允许创建的现场数量为Integer.MAX_VALUE,可能导致大量的线程被创建,导致OOM。
通过Executor的工具类Excutors实现:
使用工具类创建
9、Atomic原子类
Atomic是指一个操作不可中断,一旦开始,就不会被其他线程干扰。Atomic原子类就是具有原子/原子操作特性的类。
并发包java.util.concurrent的原子类都存放在java.util.concurrent.atomic下:
原子类
主要分为四类:
基本类型:AtomicBoolean,AtomicInteger,AtomicLong
数组类型:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
引用类型:AtomicReference,AtomicStampedRerence,AtomicMarkableReference
对象属性修改类型:AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicLongFieldUpdater
AtomicInteger的方法:
AtomicInteger API
AtomicInteger类主要是利用CAS +volatile和native方法来保证原子操作,从而避免sychronized的高开销,执行效率大为提升。
atomicInteger源码
CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
10、使用CAS实现单例模式
cas实现单例模式
这个实现里面有一个循环,是基于忙等待的算法,依赖底层硬件的实现,相当于锁它没有线程切换和阻塞额外消耗,可以支持较大的并行度。
CAS的一个重要缺点是,如果忙等待一直执行不成功,会对CPU造成较大的执行开销。如果多个线程同时执行到singleton = new Singleton()的时候,会创建大量的Singleton对象,很可能会造成内存溢出,所以不建议使用这种。
11、AQS
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。
12、happen-before规则
有些指令是可以重排的,有些指令是不可以重排的。规则如下:
程序顺序规则:一个线程内保证语义的串行性,比如第二条语句依赖第一条语句执行的结果,那么就不能执行第二条再执行第一条。
volatile原则:volatile防止了指令重排,volatile变量的写先于读,这保证了volatile变量的可见性。
锁规则:先解锁,后续步骤再加锁,加锁不能重排到解锁之前,这样加锁行为无法获得多。
传递性:A先于B,B先于C,则A先于C。
线程的start()先于它的每个动作。
线程的所有操作先于线程的终结。
线程的中断先于中断线程的代码。
对象的构造函数执行、结束先于finalize()方法。
13、ReadWritLock
(1)Java并发库中ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性
(2)ReetrantReadWriteLock读写锁的效率明显高于synchronized关键字
(3)ReetrantReadWriteLock读写锁的实现中,读锁使用共享模式;写锁使用独占模式,换句话说,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的
(4)ReetrantReadWriteLock读写锁的实现中,需要注意的,当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁
14、线程池的处理流程
(1)判断线程池里的核心线程是否都在执行任务,如果有核心线程空闲或者核心线程还没有创建,则创建一个新的工作线程来执行任务;如果核心线程都在执行任务,则进入下个流程。
(2)线程判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下一个流程。
(3)判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务,如果满了,则交给饱和策略来处理这个任务。