volatile
volatile 是 JVM 提供的 最轻量级的同步机制,中文意思是不稳定的,易变的,用 volatile
修饰变量是为了保证变量在多线程中的可见性,它表达的含义是:告诉编译器,对这个变量的读写,需要基于主内存保证多CPU的缓存一致性。
volatile 变量的两个特性:解决可见性和有序性
1、解决可见性
保证变量对所有线程的可见性:当一条线程修改了 volatile 变量的值,新值对于其他线程来说是可以立即得知的。而普通变量不能做到这一点
线程写 volatile 变量的过程:
- 改变线程工作内存中 volatile 变量副本的值
- 将改变后的副本的值立即从工作内存刷新到主内存
线程读 volatile 变量的过程:
- 从主内存中读取 volatile 变量的最新值到线程的工作内存中
- 从工作内存中读取 volatile 变量的副本
注意:
1、volatile并不能保证并发操作的原子性,即不保证线程安全
2、volatile修饰引用类型,它只能保证引用本身的可见性,不能保证所引用对象内部属性的可见性
2、解决有序性
禁止进行指令重排序,具体一点解释,禁止重排序的规则如下:
- 写
volatile
变量时,可以确保volatile
写之前的操作不会被编译器重排序到volatile
写之后。 - 读
volatile
变量时,可以确保volatile
读之后的操作不会被编译器重排序到volatile
读之前。
synchronized
如果某一个资源被多个线程共享,为了避免因为资源抢占导致资源数据错乱,我们需要对线程进行同步,那么synchronized就是实现线程同步的关键字
synchronized的作用是保证在同一时刻, 被修饰的代码块或方法只会有一个线程执行,以达到保证并发安全的效果。
其用法如下:
从语法上讲,Synchronized可以把任何一个非null对象作为"锁",在HotSpot JVM实现中,锁有个专门的名字:对象监视器(Object Monitor)。
// (1)修饰静态方法
public synchronized static void helloStatic(){System.out.println("hello world static");
}// (2)修饰成员函数
public synchronized void hello(){System.out.println("hello world");
}// (3)直接定义代码块
public void test(){SynchronizedTest test = new SynchronizedTest(); synchronized (test){System.out.println("hello world");}
}
synchronized其底层说白了就是锁,所以呢,我们先从锁讲起
我们知道发生原子性的根源是CPU在执行完任意指令后都有可能发生线程切换。如果能够禁用线程切换的话那这个问题也就迎刃而解了。操作系统做线程切换是依赖 CPU 中断的,所以禁止 CPU 发生中断就能够禁止线程切换。
1、加锁和解锁操作在哪里体现的?
synchronized 的加锁和解锁是隐式实现的,可以查看字节码
2、synchronized 的锁对象是什么,也就是说锁定的是哪个对象?
- 如果修饰的是代码块,锁对象是我们自己指定的,指定哪个对象就锁定哪个对象。
- 如果修饰的是非静态方法,锁定的是当前实例对象
this
。 - 如果修饰的是静态方法,锁定的是当前类的 Class 对象。
Linux内核提供的锁机制
1、Mutex(互斥量):pthread_mutex_t,通过对该结构的操作,来判断资源是否可以访问,Mutex属于sleep-waiting类型的锁,例如在多核机器上有两个线程A,B,如果此时锁被A持有,那么B就会被阻塞,在等待队列中等待。
2、Spin lock(自旋锁):pthread_spinlock_t,属于busy-waiting类型的锁,它不会引起调用者睡眠等待,如果获取不到锁则进入忙等待,它会不停的尝试去获取锁,俗称自旋,获取锁的性能相对较高,但是费CPU,所以自旋锁不应该被长时间的持有。
3、 Condition Variable(条件变量):pthread_cond_t,条件变量是利用线程间共享的全局变量,进行同步的一种机制
4、Read/Write Lock(读写锁):pthread_rwlock_t,读写锁是用来解决读多写少问题的,读操作可以共享,写操作是排他的。
5、semaphore:sem_t,信号量(semaphore)机制,也可用于互斥锁的实现
synchronized底层
public static void synClass() {Object obj = new Object();synchronized (obj) {}
}
我们看看这段代码的字节码文件
0 new #2 <java/lang/Object>3 dup4 invokespecial #1 <java/lang/Object.<init> : ()V>7 astore_18 aload_19 dup
10 astore_2
11 monitorenter
12 aload_2
13 monitorexit
14 goto 22 (+8)
17 astore_3
18 aload_2
19 monitorexit
20 aload_3
21 athrow
22 return
其中跟synchronized关键字相关的就是这样的字节码
monitorenter
........
monitorexit
monitorenter主要是获取监视器锁,monitorexit主要是释放监视器锁,所以我们的synchronized就是去获取这个监控锁对象,他会在我们的对象头中(markword)
markword图示如下:
加锁过程
前面我们看到了synchronized在字节码层面是对应monitorenter
合monitorexit
,而真正实现互斥的锁其实依赖操作系统底层的Mutex Lock
来实现,首先要明确一点,这个锁是一个重量级的锁
整体锁升级的过程大致可以分为两条路径,如下:
1、偏向锁未启动,加锁默认加轻量级锁
轻量级锁:线程在自己的线程栈生成Lock Record
,使用CAS的方式将markword设置为指向自己线程LOCK Record
的指针,设置成功者得锁。竞争会让锁膨胀为重量级锁。
2、偏向锁启动
偏向锁,偏向的是第一个来获取锁的线程。所谓偏向锁,指的是获取锁的线程在markword中写自己的线程ID的过程,偏向锁升级为轻量级锁时首先要撤销偏向锁,如何设置轻量级锁。
偏向锁默认是打开的,但是启动有一个时延,默认4s,之所以要延迟,是因为JVM虚拟机自己有一些默认的启动线程,里面有好多sync代码,这些代码启动时就肯定会有竞争,如果直接使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低。
我们平时使用synchronized注意点总结如下:
-
降低锁的等级
能用对象级别的,尽量别用类锁,能用实例变量的不要用静态变量
-
减少锁的时间 不需要同步执行的代码,能不放在同步块里面执行就不要放在同步快内,可以让锁尽快释放
-
减少锁的粒度 共享资源数决定锁的数量。有一组资源定义一把锁,而不是多组资源共用一把锁,增加并行度,从而降低锁竞争,典型如分段锁
-
减少加减锁的次数 假如有一个循环,循环内的操作需要加锁,我们应该把锁放到循环外面,否则每次进出循环,都要加锁
-
读写锁 业务细分,读操作加读锁,可以并发读,写操作使用写锁
-
善用volatile
volatile的控制比synchronized更轻量化,在某些变量上不涉及多步打包操作和原子性问题,可以加以运用。
如ConcurrentHashMap的get操作,使用的volatile而不是加锁