关于 Synchronized和Lock 详解

常见问题

1.死锁的产生必须具备以四个条件。

●互斥条件:指线程对已经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
●请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。
●不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自已释放该资源
●环路等待条件:指在发生死锁时,必然存在一个线程一资源的环形链,即线程集合{T0, T1, T2,…,. Tn}中的T0正在等待一个T1占用的资源,T1正在等待T2占用的资源,…T. 正在等待已被T0占用的资源。

2.如何避免死锁

要想避免死锁,只需要破坏掉至 少一个构造死锁的必要条件即可 ,目前只有请求并持有和环路等待条件是可被破坏。造成死锁的原因其实和申请资源的顺序有很大关系 。

3.synchronized 和lock 区别

  1. synchronize是在JVM层面实现的,系统会监控锁的释放与否。
  2. Lock是JDK代码实现的,需要手动释放,在finally块中释放。可以采用非阻塞的方式获取锁。
  3. 性能:资源竞争激励的情况下,lock性能会比synchronize好,竞争不激励的情况下,synchronize比lock性能好,
  4. Synchronized的编程更简洁,lock的功能更多更灵活,缺点是一定要在finally里面 unlock()资源才行。
  5. synchronize可以用在代码块上,方法上。lock只能写在代码里,不能直接修改方法。
  6. lock 可中断锁,支持读写锁,用于写多读少的情况。
  7. lock 支持condition,很精准的调度工具。一个lock可以有多个condition。

1.synchronized

synchronized 块是 Java 提供的一种原子性内置锁,也是排它锁。Java 中的每个对象都可以把它当作同步锁来使用。 线程的执行代码在进入 synchronized 代码块前会自动获取内部锁,这时候其他线程访问同步代码块 会被阻塞挂起。
另外,由于 Java 中的线程是与操作系统的原生线程一一对应的,所以当阻塞 一个线程时,需要从用户态切换到内核态执行阻 操作,这是很耗时的操作,而 synchronized使用就会导致上下文切换。

进入 synchronized 块的内存语义是把在 synchronized块内使用到的变量从线程的工作内存 中清除,这样在 synchronized 块内使用到该变 时就不会从线程的工作内存中获取,而是直接从主内存中获取。

1.1 各个同步分类的特点

同步静态方法锁的是class对象,该class对象是共享的。
同步方法锁的是实例对象
同步代码块也是使用实例对象锁。

1.2 对象锁和类(class)锁的区别

类声明后,我们可以 new 出来很多的实例对象。这时候,每个实例在 JVM 中都有自己的引用地址和堆内存空间
1.对象锁
使用对象锁的情况,只有使用同一实例的线程才会受锁的影响,多个实例调用同一方法也不会受影响。

	//锁住非静态变量
    Object object = new Object();
    private void test1() {
        synchronized (object) {
            System.out.println("hello");
        }
    }
	//锁住当前实例对象
  	private void test1() {
        synchronized (this) {
            System.out.println("hello");
        }
    }
	//锁为当前实例对象
	private synchronized void test1() {
        System.out.println("hello");
    }

2.类(class)锁
class是有jvm加载,在整个程序里具有唯一性。是所有线程可以共享的锁,所以同一时刻,只能有一个线程使用加了锁的方法或方法体,不管是不是同一个实例。

  • 锁为静态的变量
  • 锁为静态的方法
  • 锁为class对象
//静态方法
private synchronized static void test1() {
    System.out.println("hello");
}

//静态变量
static Object object = new Object();

private  static void test1() {
    synchronized(object){
        System.out.println("hello");
    }
}

synchronized(Demo1.class){
    System.out.println("hello");
}

1.3 synchronized底层实现的不同

根据锁的使用形式不同,内部的原理也有所差异。

反编译class文件

javap -v cn.xxx.Demo
  1. synchronized 修饰代码块是使用monitorenter和monitorexit来实现锁的获取和释放,默认情况下会有两个monitorexit
  public void test1() {
        synchronized(this){
            System.out.println("hello");
        }
    }
 public void test1(); 方法名
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC 修饰符
    Code:
      stack=2, locals=3, args_size=1 //方法栈深度 局部变量表 参数个数1(代表this)
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter  //开始进入监视
         4: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #13                 // String hello
         9: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1
        13: monitorexit //退出监视
        14: goto          22
        17: astore_2
        18: aload_1
        19: monitorexit // 再次退出,避免因为异常情况无法释放锁
        20: aload_2
        21: athrow
        22: return

  1. synchronized修饰普通方法通过字节码flag标识,如果flag里面包含了Acc_synchroized,那么jvm执行的时候,就知道当前方法是一个同步方法,在调用的时候,就会首先获取该对象的monitor对象,其他线程则无法获取该monitor对象,并陷入等待,直到该方法执行完并释放monotor锁以后其他线程才可以获取到锁。

  2. synchronized修饰静态方法通过字节码flag标识,如果flag里面包含了Acc_static,Acc_sysnchroized,那么jvm执行的时候,就知道当前方法是一个静态同步方法,在调用的时候,就会首先获取该class对象的monitor对象,其他线程则无法获取该monitor对象,并陷入等待,直到该方法执行完并释放该class对象的monitor锁以后其他线程才可以获取到锁。

1.4 锁的底层实现Monitor

jvm中synchronized同步代码都是通过进入和退出的监视对象(管程)每当我们创建一个Java对象的时候,JVM都会给我们创建一个对应的monitor对象,它随着对象的创建而创建,随对象的销毁而销毁,有c++来实现。
Monitor锁本身不能实现互斥,借助操作系统的mutex_lock来实现,如果获得该锁代表线程持有了锁。

问题: synchronized是依赖Monitor实现的,Monitor是依赖底层的操作系统来实现的,也就是说想要获取到锁必须要跨越JVM进入到操作系统内核,此时就会发生用户态和内核态之间的切换,此操作会增加我们的性能开销。同时EntryList集合和WaitSet集合中的线程会进入阻塞状态,阻塞的线程会进入内核调度状态,因为阻塞状态是通过Linux的pthread_ mutex _lock来实现的,所以线程要想阻塞就会发生用户态和内核态的切换,此时会严重影响性能。这也是老版本的synchronized在高并发环境下性能不佳的原因,因为会频发的发生用户态和内核态之间的切换
解决办法:解决synchronized的性能问题的关键在于减少用户态和内核态之前的切换自旋(Spin),
互斥锁的属性:

  1. PTHREAD_ METEX TIMED NP:这个是系统的缺省值也是普通的锁,当一个线程获取到锁以后,其他的线程陷入等待队列,并且在锁解除的时候优先执行优先级高的线程,这样能保证线程的公平性
  2. PTHREAD_ MUTEX_ RECURSIVE NP:嵌套锁,允许一个线程对同一 个锁获取多次,获取了多少次,释放锁的时候就要执行多少次unlock操作,如果是不同的线程,则在加锁时从新竞争。
  3. PTHEAD. MUTEXT ERRORCHECK_ NP: 检错锁,如果同一个线程多次获取同一个锁,则会返回DEADLK,否则他的行为根PTHREAD_ .METEX _TIMED. _NP行为是一致的,该 属性保证了不允许重复枷锁的情况下,不会出现简单的思
    索操作。
  4. PTHEAD_ MUTEXT_ ADAPTIVE_ NP:适应锁,解锁后重新竞争锁,不考虑优先级

1.5 锁的优化

在jdk1.5之前,实现的所有的同步都是通过synchronized来实现的,因为只有一种方式去实现线程的同步,同时Java的原子操作也是通过他来实现的,synchronized锁的获取和释放都是通过JVM帮助我们隐式实现的.
jdk1.5的时候引入了并发包的L ock锁,Lock锁是基于java来实现的,因此获取锁和释放锁都是通过java来完成的,而synchronized是使用Monitor锁来实现的,Monitor是通过操作系统的mutex lock来实现的,所以一定会牵涉到用户态和内核态之间的切换,这会导致额外的开销,如果并发足够激烈的话,synchronized的性能表现非常差。
jdk1.6开始,synchronized引入了很多的锁的优化。比如偏向锁、轻量级锁、重量级锁等,从而减少锁竞争的时候带来的用户态和内核态之间切换所带来的性能开销,这种优化主要是通过对象头来实现的。

1.偏向锁:

他是针对一个线程来说的,主要的作用是优化同一个线程多次获取锁的情况,如果一个线程第一 -次访问synchronized同步代码的时候,他会把biased. _lock修改位1,并且把自己的Threadld写入对象头,这样就可以有效的避免用户态和内核态之间的切换。
如果此时还有其他线程来执行synchronized代码块的话,偏向锁就会被撤销,如果当前持有的线程还没有执行完同步代码块,那么此时会进行锁升级,如果已经执行完了那么会撤销偏向锁,重新进行锁的获取。

2.轻量级锁:

他的实现使用到了自旋,所以他只适合少量并发,你执行一会我执行一 会,因为大量的并发会让自旋失去意义,当然自旋也会有他的优化版本那就是适用性自旋他会根据前一次自旋成功的次数来决定当前这一次自旋的次数,如果之前几乎都是失败的,他就不会自旋。

3.自旋锁

互斥同步进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间如果在这段时间内能获得锁,就可以避免进入阻塞状态
自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。
在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。

4.锁消除

锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除。对于一些看起来没有加锁的代码,其实隐式的加了很多锁。

5.锁粗化

如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗
上一节的示例代码中连续的 append() 方法就属于这类情况。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。

2.Lock

通过查看Lock的源码可知,Lock是一个接口,接口的实现类ReentrantLock, ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock

2.1 ReentrantLock

可重入的独占锁,同时只能有一个线程可以获取锁,其他获取锁的线程会被阻塞而被放入到锁的AQS阻塞队列里面。
分为公平锁和非公平锁,默认非公平。
AQS的state状态表示线程获取该锁的可重入次数
lock() 获取锁
void lockinterruptibly()方法
void tryLock()
void unLock()

2.2 ReentReadwriteLock

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值