锁的概念及synchronized使用原理解析
Posted 踩踩踩从踩
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了锁的概念及synchronized使用原理解析相关的知识,希望对你有一定的参考价值。
前言
在之前的文章中,分析了线程安全常见的 可见性、原子性、有序性产生的原因和解决办法。
接着这篇文章会接着讲解线程相关的锁的概念,包括什么是自旋锁,重量级锁、轻量级锁、公平锁、乐观锁等等;以及从底层分析java在堆中如何存储对象,并解析synchronized怎么锁住对象,以及简单应用,基本概念等等。
锁的概念
java中将锁分为下面几种类型
- 自旋锁:是指一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断判断锁是否能够被获取,直到获取成功锁才会推出循环。
表现形式:
- juc包中,atomic开头的,都会使用使用自旋锁,底层都使用了unsafe,自旋是为了修改 变量,直接使用CAS去进行操作
- 通过自旋实现一把锁,也是自旋锁 而不停的去抢锁,这种锁即使自旋锁也是悲观锁。
public class SpinLock {
AtomicReference<Thread> owner = new AtomicReference<Thread>();
public void lock() {
Thread th = Thread.currentThread();
while (!owner.compareAndSet(null, th)) {
// 不断抢
}
}
public void unlock() {
Thread current = Thread.currentThread();
owner.compareAndSet(current, null);
}
}
- 乐观锁:假定没有冲突,在修改数据时如果发现数据和之前获取数据不一致,则读取到最新数据,修改后重新重试修改。
atomicInteger假定是不会有冲突的,只有多线程情况下旧值不一致,才会重新修改,这里如果一直get就基本是没有加锁的速率,是很快的。
- 悲观锁:假定数据发生并发冲突,同步所有对数据的相关操作,从读数据就开始上锁。
最典型的是synchronized和ReentrantLock就是悲观锁;
- 独享锁(写):给资源加上写锁,线程可以修改资源,其他线程不能再加锁;写锁 jdk中提供的writereadLock
- 共享锁(读):给资源加上读锁,线程只能读不能修改,其他线程也可以加读锁,不能加上写锁;(多读) 这里例如我们的信号量 semaphore 使用共享锁,常用于限制可以访问某些资源(物理或逻辑的)线程数目
- 可重入锁、不可重入锁:线程拿到锁之后,可以自由同一把锁所同步的其他代码。
reentrantlock 就是多次拿锁,但是要多次释放
private final static ReentrantLock lc = new ReentrantLock();
public static void add() throws InterruptedException {
lc.lock();
i++;
System.out.println("zai");
Thread.sleep(1000L);
add();
lc.unlock();
}
- 公平锁和非公平锁:争抢锁的顺序,如果是按照先来后到的,则为公平锁
同步关键字synchronized
首先它是jdk里面提供锁关键字
应用场景
- 使用在实例方法和静态方法上, 标记在实例方法时,相当于synchronized (this){} 当前实例对象;而标记在静态方法上相当于 synchronized(Test.class){} 类对象;
这里类对象和实例对象作用域是不一样,类对象作用在所有实例对象上,而实例对象只作用在单个实例对象上。
public synchronized void add() throws InterruptedException {
System.out.println( Thread.currentThread().getName() + " done ...");
i++;
Thread.currentThread().sleep(60000L);
}
public static synchronized void add() throws InterruptedException {
System.out.println( Thread.currentThread().getName() + " done ...");
i++;
Thread.currentThread().sleep(60000L);
}
- 用于代码块时,显示指定锁对象 锁的对象起来。
synchronized (Counter.class){}
- 锁的作用域:对象锁、类锁、分布式锁
- 如果时多个进程 ,锁是无法锁住的
特点
可重入、独享锁、悲观锁、非公平锁;
特殊化:锁消除 (开启锁消除的参数:-XX:+DoEscapeAnalysis -XX:+EliminateLocks),、锁粗化 JDK做了锁粗化的优化, 但我们自己可从代码层面优化
这都是编译器上做的锁上的优化。
锁消除
例如下面的StringBuffer ,源码这里是在方法上添加了synchronized 关键字的,而在日常应用中,大量运行append代码时,单线程中也使用了append方法,是不用锁住的,在jit编译 运行多次时,会将去锁化。
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
锁粗化
假设执行下面代码,是不断的添加数据,频繁添加数据,并且降低效率,然后在jit编译的时候,将锁粗化,进行优化
for(itn i=0;i<100000;i++){
synchronized (Counter.class){
j++;
}
}
//粗化后
synchronized (Counter.class){
for(itn i=0;i<100000;i++){
j++;
}
}
synchronized关键字,不仅实现同步,java内存模型中,synchronized关键字下面的代码会保证可见性,不可缓存数据 ,happens-before原则。
java中堆的对象信息
synchronized对象被锁住,会保存在对象头上面;对象头上面会保存锁的状态等等。
堆内存中对象如何存储
对象在堆中存的时属性值实例字段;通过一段代码去看
public class Demo5_main {
public static void main(String args[]){
int a = 1;
Teacher t= new Teacher();
t.stu = new Student();
}
}
class Teacher{
String name = "t";
int age = 40;
boolean gender = true;
Student stu;
}
class Student{
String name = "e";
int age = 18;
boolean gender = false;
}
由上面的图示,大概能看出整个 代码在运行时,在jvm数据区的分布情况
继续在看看对象头的信息
加锁的状态就是存到对象头中,具体的地方就在方法区中的。
在markword中存入的状态 未锁定的话就存
抢锁对应的对象头标志变化
轻量级锁
- 当有一个线程抢锁并抢到锁时,根据状态进行变化 ,将状态修改为00 ,然后存入线程地址
- 如果线程2来cas自旋抢锁,一直抢不到;轻量级锁中的自旋有一定的次数限制,超过了次数限制,轻量级锁升级为重量级锁。重量级锁,存到监视器中
重量级锁
- 线程1获取到锁,并执行wait方法时,就会将锁释放,并由其他等待线程抢锁,并将线程1引用放到 wait set中; 执行到notify,抢一下锁,如果锁已经被占用,则放到entrylist中,再次等待在此抢锁;
这里wait 和线程执行完毕推出,都会释放锁,区别点在于,一个会在放到entrylist中,而另一个不会。
这里也是个集合可以存入很多线程
synchronized中才能拿到owner,这就是为什么wait方法需要synchronized关键字包裹着,以及才能释放锁
偏向锁
偏向锁是单线程下 添加synchronized关键字,没有必要的,就会把这个优化掉了
开启偏向锁,但未升级,默认这里都是1
存入单个线程id, 如果多线程在过来 另外的线程id过来,会升级轻量级锁,需要这个状态的,是为了优化,单线程情况下防止频繁加锁,和释放锁,才有偏向锁。
代码执行完了,thread id也一直会存在。
锁升级的过程
wait和notify机制
- 只能在synchronized关键字中使用,且调用wait、notify的对象与锁对象相同,否则会 抛出IllegalMonitorStateException异常。
-
wait() 方法调用后, 会破坏原子性 。 因为在调用过后wait过后,会释放锁,也会破坏其原子性,相当于变成了加两个synchronized.
以上是关于锁的概念及synchronized使用原理解析的主要内容,如果未能解决你的问题,请参考以下文章