锁的概念及synchronized使用原理解析

Posted 踩踩踩从踩

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了锁的概念及synchronized使用原理解析相关的知识,希望对你有一定的参考价值。

前言

在之前的文章中,分析了线程安全常见的 可见性、原子性、有序性产生的原因和解决办法。

接着这篇文章会接着讲解线程相关的锁的概念,包括什么是自旋锁,重量级锁、轻量级锁、公平锁、乐观锁等等;以及从底层分析java在堆中如何存储对象,并解析synchronized怎么锁住对象,以及简单应用,基本概念等等。

锁的概念

java中将锁分为下面几种类型

  • 自旋锁:是指一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断判断锁是否能够被获取,直到获取成功锁才会推出循环。

表现形式:

  1. juc包中,atomic开头的,都会使用使用自旋锁,底层都使用了unsafe,自旋是为了修改  变量,直接使用CAS去进行操作
  2. 通过自旋实现一把锁,也是自旋锁 而不停的去抢锁,这种锁即使自旋锁也是悲观锁。
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关键字,没有必要的,就会把这个优化掉了

在JDK6 以后,默认已经开启了偏向锁这个优化,通过JVM 参数 -XX:-UseBiasedLocking 来禁用偏向锁 若偏向锁开启,只有一个线程抢锁,可获取到偏向锁

 开启偏向锁,但未升级,默认这里都是1

存入单个线程id, 如果多线程在过来 另外的线程id过来,会升级轻量级锁,需要这个状态的,是为了优化,单线程情况下防止频繁加锁,和释放锁,才有偏向锁。

代码执行完了,thread id也一直会存在。

锁升级的过程

 

偏向标记第一次有用,出现过争用后就没用了。 -XX :-UseBiasedLocking 禁用使用偏置锁定,
偏向锁,本质就是无锁,如果没有发生过任何多线程争抢锁的情况,JVM认为就是单线程,无需做同步 (jvm为了少干活:同步在JVM底层是有很多操作来实现的,如果是没有争用,就不需要去做同步操作) 
如果出现了锁升级就会关闭偏向锁。
默认情况下 JVM 锁会经历:未锁定 -> 偏向锁 -> 轻量级锁 -> 重量级锁 这四个状态

wait和notify机制

wait 方法导致当前线程等待,加入该对象的等待集合中,并且放弃当前持有的对象锁。
notify/notifyAll 方法唤醒一个或所有正在等待这个对象锁的线程。
注意:
  • 只能在synchronized关键字中使用,且调用waitnotify的对象与锁对象相同,否则会 抛出IllegalMonitorStateException异常。
  • wait() 方法调用后, 会破坏原子性 。 因为在调用过后wait过后,会释放锁,也会破坏其原子性,相当于变成了加两个synchronized.

以上是关于锁的概念及synchronized使用原理解析的主要内容,如果未能解决你的问题,请参考以下文章

锁的概念及Redis分布式锁的实现(转)

JDK源码Synchronized关键字原理,和锁的膨胀过程

从三个层面解析synchronized原理

heartbeat 裂脑的概念及原理

Docker的概念及剖析原理和特点

java反射机制的概念及原理