文章目录
1.锁的种类
- 自旋锁 ,自旋,jvm默认是10次吧,有jvm自己控制。for去争取锁
- 阻塞锁 被阻塞的线程,不会争夺锁。
- 可重入锁 多次进入改锁的域
- 读写锁
- 互斥锁 锁本身就是互斥的
- 悲观锁 不相信,这里会是安全的,必须全部上锁
- 乐观锁 相信,这里是安全的。
- 公平锁 有优先级的锁
- 非公平锁 无优先级的锁
- 偏向锁 无竞争不锁,有竞争挂起,转为轻量锁
- 对象锁 锁住对象
- 线程锁
- 锁粗化 多锁变成一个,自己处理
- 轻量级锁 CAS 实现
- 锁消除 偏向锁就是锁消除的一种
- 锁膨胀 jvm实现,锁粗化
- 信号量 使用阻塞锁 实现的一种策略
- 排它锁:X锁,若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。这就保证了其他事务在T释放A上的锁之前不能再读取和修改A。
2.synchronized
⑴原理
-
代码块同步
代码块同步是使用monitorenter和monitorexit指令实现的,monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。
根据虚拟机规范的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1;相应地,在执行monitorexit指令时会将锁计数器减1,当计数器被减到0时,锁就释放了。如果获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。 -
方法同步
相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。其实本质上和代码块同步没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
⑵注意事项
- synchronized取得的都是对象锁
- A线程持有object对象的Lock锁,B线程可以调用object对象的非synchronized方法,但不可以调用object任何的synchronized方法
- synchronized是可重入锁
- 出现异常,锁自动释放
- static方法加synchronized是属于类的,非static方法是属于对象的
3.ReentrantLock
⑴概述
首先我们看两点Synchronized的局限性:
- 当线程尝试获取锁的时候,如果获取不到锁会一直阻塞
- 如果获取锁的线程进入休眠或者阻塞,除非当前线程异常,否则其他线程尝试获取锁必须一直等待
JDK1.5之后发布,加入了Doug Lea实现的concurrent包。包内提供了Lock类,用来提供更多扩展的加锁功能。Lock弥补了synchronized的局限,提供了更加细粒度的加锁功能。
⑵锁的基本概念
- 可重入锁
如果当前线程已经获得了某个监视器对象所持有的锁,那么该线程在该方法中调用另外一个同步方法也同样持有该锁。也可以这样说:同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁。 - 可中断锁
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。 - 公平锁
先到先得。
⑶ReentrantLock和Synchronized对比
- 相同点
- 都是可重入锁
- 不同点
- synchronized是关键字,reentrantLock是类
- synchronized是通过JVM的字节码实现,reentrantLock通过CAS实现
- synchronized的加锁和释放锁是自动的,ReetrantLock需要手动加锁和释放锁
- synchronized是不可中断的,ReetrantLock可中断的
- ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁
- ReetrantLock的tryLock可以设置超时机制
⑷ReentrantLock的用法
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition(); // 和synchronized的wait/notify一个意思
一定要在finally里面释放锁
4.ConcurrentHashMap
⑴实现原理
- JDK1.8之前
使用的是分段锁的概念,把一个大的MAP拆分成几个类似hashtable结构(Segment),使用可重入锁ReentrantLock,put和get的时候,都是现根据key.hashCode()算出放到哪个Segment中,只有在同一个分段内才存在竞态关系。试想,原来只能一个线程进入,现在却能同时16个(默认是分16段)写线程进入(写线程才需要锁定,而读线程并不锁定,只有在求size等操作时才需要锁定整个表),并发性的提升是显而易见的。
ConcurrentHashMap中的HashEntry相对于HashMap中的Entry有一定的差异性:HashEntry中的value以及next都被volatile修饰,这样在多线程读写过程中能够保持它们的可见性 - JDK1.8舍弃了segment,并且大量使用了synchronized,以及CAS无锁操作以保证ConcurrentHashMap操作的线程安全性。另外,底层数据结构改变为采用数组+链表/红黑树的数据形式。
⑵与hashtable比较
- ⑴ HashTable的线程安全使用的是一个单独的全部Map范围的锁,ConcurrentHashMap抛弃了HashTable的单锁机制,使用了锁分离技术,使得多个修改操作能够并发进行,只有进行SIZE()操作时ConcurrentHashMap会锁住整张表。
- ⑵ HashTable的put和get方法都是同步方法, 而ConcurrentHashMap的get方法多数情况都不用锁,put方法需要锁。
- ⑶但是ConcurrentHashMap不能替代HashTable,因为两者的迭代器的一致性不同的,hash table的迭代器是强一致性的,而concurrenthashmap是弱一致的。 ConcurrentHashMap的get,clear,iterator 都是弱一致性的。
5.ThreadLocal
1.原理
每个线程对象都有一个ThreadLocalMap类(类似于Map结构)的成员变量,KEY就是设置的当前的ThreadLocal这个对象,值就是对应的VALUE。
2.注意事项
- 每次使用完ThreadLocal,都调用它的remove()方法,清除数据,尤其是线程池线程会出现复用很容易出现业务问题。
3.ThreadLocalMap为什么设计成弱引用
设计成弱引用的目的是为了更好地对ThreadLocal进行回收,当我们在代码中将ThreadLocal的强引用置为null后,这时候Entry中的ThreadLocal理应被回收了,但是如果Entry的key被设置成强引用则该ThreadLocal就不能被回收,这就是将其设置成弱引用的目的。
4.InheritableThreadLocal
用InheritableThreadLocal可以让子线程从父线程取得值,也可以进行更改,但是需要注意:当子线程取得值的同时,主线程将
InheritableThreadLocal中的值进行更改,那么子线程取到的值还是旧值。
6.volatile
⑴目的
保证变量内存可见性,防止局部重排序
⑵原理
观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
①它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
②它会强制将对缓存的修改操作立即写入主存;
③如果是写操作,它会导致其他CPU中对应的缓存行无效
⑶volatile和synchronized的对比
- volatile只能修饰变量,性能好。synchronized可以修饰代码块,方法。
- volatile不会出现阻塞,synchronized会出现阻塞
- volatile保持数据可见性,不支持原子性,synchronized保证原子性,也间接保证可见性
- volatile解决的是变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程访问资源的同步性
⑷volatile的非原子性
先看四条语句:
x = 10; //语句1 原子性
y = x; //语句2 非原子性
x++; //语句3 非原子性
x = x + 1; //语句4 非原子性
只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。
我们看一个例子:
public class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
这里的输出并非是10000而是一个小于10000的数。
假如某个时刻变量inc的值为10,
线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;
然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。
然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。
那么两个线程分别进行了一次自增操作后,inc只增加了1。
⑸指令重排序
JVM可以对它们在不改变数据依赖关系的情况下进行任意排序以提高程序性能(遵循as-if-serial语义,即不管怎么重排序,单线程程序的执行结果不能被改变),而这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不会被编译器和处理器考虑
先看下面的代码:
线程A:
content = initContent(); //(1)
isInit = true; //(2)
线程B
while (isInit) {