java同步机制的几种方式
出现线程安全问题:
如果存在多个线程对共享资源竞争,就可能发生线程安全问题。
一般解决线程安全问题,需要➕锁
1. synchronized同步方法
对于非static方法加上synchronized,是对当前对象加锁。而如果对static方法加上synchronized关键字,是对当前类对象-class对象加锁。
1. 如果一个线程访问一个对象的synchronized方法,那么其它线程不能访问该对象的synchronized方法。
2. 但是其它线程可以访问这个对象的非synchronized方法。
3. 当然,对于一个类的不同实例之间互不影响。
4. 如果一个线程执行一个对象的非static synchronized方法,另外一个线程执行这个对象所属类的static synchronized方法,不会互斥。因为一个是对象锁,一个是类锁。
5. 对于synchronized同步代码块或者同步方法,当出现异常的时候,虚拟机会自动释放当前线程占用的锁,因此不会出现由于异常导致出现死锁现象。
synchronized原理
对于同步代码块用javap -c com.demo.synchronzeddemo反编译成字节码的时候,会在同步块的入口和出口分看到monitorenter,monitorexit ,这两个指令。
其实对于每个对象都有一个监视器锁,monitor,当monitor被占用的时候就会处于锁定状态,线程执行monitorenter的时候会尝试获取monitor的所有权。
1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程为monitor的所有者;
2. 如果线程为该monitor的所有者,重新进入,则进入monitor的进入数+1;
3. 如果其它线程占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,则重新尝试获取monitor的所有权。(非公平锁)
执行monitorexit的线程必须是objectref所对应的monitor的持有者。
指令执行的时候,monitor的进入数-1,如果-1后为0,那么线程推出monitor,不再是这个monitor的所有者其它被阻塞的线程尝试获取monitor。
类似的,wait,notify方法也是依赖于monitor锁,所以只有在同步方法或者同步代码块中才能调用wait,notify,否则会抛出java.lang.illegalmonitorstateexception.
如果是同步方法,编译成普通方法的调用和返回指令,在jvm字节码层面没有任何特别的指令来实现同步方法,而是在class文件的方法表中将该方法的access_flags字段中的synchronized标志为1。如果设置了,那么执行线程将先获取monitor,成功以后才执行方法体。
synchronised实现了自旋锁,在线程进入ContentionList时,即第一步操作之前,线程在等待进入等待队列的时候,首先进行自旋尝试获得锁,如果不成功,进入等待队列。另外,自旋线程可能会抢占了ready线程的锁,自旋锁由每一个监视对象维护,每个监视对象一个。
首先用户自旋尝试获得锁,如果失败,加入ContentionList虚拟队列中。虚拟队列新加入在队列头部,每次从队列尾部选取。因为虚拟队列的尾部会发生线程并发访问,为了降低对于队尾的争用,建立了EntryList。Owner线程会在unlock的时候从contentionlist中迁移到entry list中,并指定entrylist中的某个线程为ready。owner线程并不是直接吧锁传递给ready,只是把竞争锁的一个权利交给它,此时还需要和新进来的线程(自旋)竞争。
而owner线程被wait方法阻塞,则转移到waitset队列中,如果某个时刻被notify唤醒,则再次转移到entrylist中。
volatile关键字解析
java内存模型的相关概念
java虚拟机定义了一种java内存模型来屏蔽硬件平台和操作系统的内存访问差异,以实现让java程序在各个平台都能达到一致的内存访问效果。
主要是定义了程序的变量访问规则,也就是定义了程序执行的次序。
java内存模型规定所有的变量都存在于主存,每个线程都有自己的工作内存。线程对于变量的所有操作都必须在自己的工作内存内,不能直接对主存操作。每个线程都不能访问别的线程的工作内存。
并发编程的三个概念:原子性,可见性,有序性
原子性:java内存模型保证了基本读取和赋值是原子操作。
可见性:volatile关键字保证了可见性
有序性:指令重排序不会影响单线程程序的运行,但是会影响多线程安全。
深入剖析volatile关键字:
volatile关键字的两层语义:
保证了不同线程对于这个变量进行操作时的可见性;
禁止指令重排序。
也就是说volatile保证了可见性和有序性。如果被volatile修饰的共享变量,一旦发生修改,会强制将修改的值立即写入主存,同时别的线程的工作内存中的该变量缓存失效,需要重新去主存读取。
同时,volatile保证了有序性,被volatile修饰的变量相对位置不可改变。
volatile不保证原子性
如果要保证原子性,需要synchronized或者lock锁,或者用atomic包下的一些原子操作。(利用cas)保证原子操作。
原因:i++,load,increment,store,storeload barrier,四步,内存屏障保证最后一步把写入的值刷新到缓存,但是,从load到store不是安全的,如果期间其它线程修改值,那么值会丢失。
volatile的原理和实现机制
加入volatile关键字以后,会多出一个lock前缀指令,汇编代码。实际上相当于一个内存屏障,cpu指令。
这个内存屏障,确保重排序的时候内存屏障之前的代码不会跑到屏障后面;同时强制将缓存的修改操作立即写入主存;如果是写操作,导致其它工作内存中对应的缓存行无效。
volatile的应用场景:
我的理解是,必须是满足原子性
1. 状态标记量;
2. 双重检验。代码示例:
class Singleton{
private volatile static Singleton instance = null;
private Singleton{
}
public static Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();}}}
return instance;}
}
1
2
3
4
5
6
7
8
9
10
11
12
lock锁
synchronized的缺陷
对于synchronized锁来说,只有执行完毕或者异常才会释放锁,否则一直堵塞,且没办法中断等待;
同时对于synchronized锁来说,无论是读还写,同一时段都只有一个线程可以做到,对于很多读线程,效率低下。
java.util.concurrent.locks包下的lock
Lock是一个接口,里面定义了5个方法,主要是获得锁和释放锁。
其中lock()的用法如右图所示。
tryLock()会返回,无论是否拿到锁都会立刻返回。
tryLock(long time, timeout unit)会等待一段时间。
lockInterruptibly(),当通过这个方法去获得锁的时候,如果线程正在等待锁,可以响应中断。
ReentrantLock
可重入锁,唯一一个实现了Lock接口的类。
ReadWriteLock
也是一个接口。主要:一个readLock(),一个readLock()。
读锁不互斥,读写互斥。
公平锁
ReentrantLock默认是非公平锁,但是可以在新建的时候戏而已参数(true)