Java——锁浅析

1. 简介

在多线程中如果涉及到对共享资源的并发读写,这时就会产生资源的争夺。而在资源争夺中,第一想到的就是使用锁 ,对共享资源进行数据保护。

java中提供了2种基本也是最常用的锁:synchronized、Lock!

2. 对象锁和类锁

对象锁:

  • 在java中,每个对象都会有一个monitor对象,这个对象其实就是java对象锁,通常也被称作为"内置锁"或"对象锁",类的对象有多个,所有对象锁也有多个,相互之间互不干涉。

类锁:

  • 在java中,针对每一个类都有一个锁,可以称作为"类锁",类锁实际也是通过对象锁实现的,即类的Class对象锁,每一个类有且仅有一个Class对象(JVM加载类的时候产生),所以每一个类只有一个类锁。

3. synchronized 详解

synchronized 是java中的关键字,利用锁的机制来实现同步,来达到对共享资源保护。
在这里插入图片描述
实例:synchronized实现的双重校验实现的单例模式

package passtra;

public class Singlethon{
    
    private volatile static Singlethon uniqueInstance;
    
    private Singlethon(){
        
    }
    
    public static Singlethon getUniqueInstance(){
        //先判断对象是否实例化过,没有实例化进入加锁代码
        if(uniqueInstance==null){
            //类对象加锁
            synchronized(Singlethon.class){
                if(uniqueInstance ==null){
                    uniqueInstance=new Singlethon();
                }
            }
        }
        return uniqueInstance;
    }
}

锁的机制有2种特质:

  • 互斥性:在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程中的协调机制。互斥性我们往往称作原子性
  • 可见性:确保在锁被释放之前,对共享变量所做的修改,对其余的线程是可见的(也就是说在获得锁的时候应获得最新的共享变量值),否则对于另外一个线程在操作共享变量是自己在本地缓存的副本上,这样就会引发数据的不一致。

synchronized:

  • 是一种互斥锁,一次只能允许一个线程进入被锁住的代码块
  • 是一种内置锁/监视器锁,Java中每个对象都有一个内置锁(监视器,也可以理解成锁标记),而synchronized就是使用对象的内置锁(监视器)来将代码块(方法)锁定的
  • 保证了线程的原子性。(被保护的代码块是一次被执行的,没有任何线程会同时访问)
  • 保证了可见性。(当执行完synchronized之后,修改后的变量对其他的线程是可见的)

synchronized实现原理:

  • synchronized是悲观锁,在字节码层被映射成两个指令:monitorenter和monitorexit,当一个线程遇到monitorenter指令时,会尝试去获取锁,如果获取成功,锁的数量+1,(因为synchronized是一个可重入锁,需要使用锁计数来判断锁的情况),如果没有获取到锁,就会阻塞;当线程遇到monitorexit指令时,锁计数-1,当计数器为0时,线程释放锁;如果线程遇到异常,虚拟机会释放锁

4. Lock

Lock完全用Java写成,在java这个层面是无关JVM实现的。在java.util.concurrent.locks包中有很多Lock的实现类,常用的有ReentrantLock、ReadWriteLock(实现类ReentrantReadWriteLock),其实现都依赖java.util.concurrent.AbstractQueuedSynchronizer类,实现思路都大同小异,因此我们以ReentrantLock作为讲解切入点

lock是一个java接口,一般使用ReentrantLock这个实现类。

经过观察ReentrantLock把所有Lock接口的操作都委派到一个Sync类上,该类继承了AbstractQueuedSynchronizer

  private final Sync sync;

    abstract static class Sync extends AbstractQueuedSynchronizer{
 public void lock() {
        sync.lock();
    }

 public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
}

使用lock显示加锁,可以判断锁的状态,有多种获取锁的方式,获取失败的线程不一定需要阻塞

 //需要参与同步的方法
    private void method(Thread thread){
        lock.lock();
        try {
            System.out.println("线程名"+thread.getName() + "获得了锁");
        }catch(Exception e){
            e.printStackTrace();
        } finally {
            System.out.println("线程名"+thread.getName() + "释放了锁");
            lock.unlock();
        }
    }

lock实现原理:

  • lock是使用CAS乐观锁(CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。)

5. AQS,抽象队列同步器

QS即AbstractQueuedSynchronizer的缩写,是并发编程中实现同步器的一个框架,它在内部定义了一个int state变量,用来表示同步状态

AQS基于一个FIFO双向队列实现,被设计给那些依赖一个代表状态的原子int值的同步器使用,在AQS中即为一个叫state的int值,该值通过CAS进行原子修改。

在AQS中存在一个FIFO队列,队列中的节点表示被阻塞的线程,队列节点元素有4种类型, 每种类型表示线程被阻塞的原因,这四种类型分别是:

  • CANCELLED : 表示该线程是因为超时或者中断原因而被放到队列中
  • CONDITION : 表示该线程是因为某个条件不满足而被放到队列中,需要等待一个条件,直到条件成立后才会出队
  • SIGNAL : 表示该线程需要被唤醒
  • PROPAGATE : 表示在共享模式下,当前节点执行释放release操作后,当前结点需要传播通知给后面所有节点

由于一个共享资源同一时间只能由一条线程持有,也可以被多个线程持有,因此AQS中存在两种模式,如下

  • 独占模式: 独占模式表示共享状态值state每次只能由一条线程持有,其他线程如果需要获取,则需要阻塞,如JUC中的ReentrantLock
  • 共享模式:共享模式表示共享状态值state每次可以由多个线程持有,如JUC中的CountDownLatch

6. 锁的升级

Java SE1.6为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”,所以在Java SE1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。

锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

如果你向一个变量写值,而这个变量接下来可能会被另一个线程所读取,或者你从一个变量读值,而它的值可能是前面由另一个线程写入的,此时你就必须使用同步。

7. synchronized、lock区别

7.1 sychronized

sychronized用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码,它是在软件层面依赖JVM实现同步。

synchronized 方法或语句的使用提供了对与每个对象相关的隐式监视器锁的访问,但却强制所有锁获取和释放均要出现在一个块结构中:当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的词法范围内释放所有锁.

synchronized 方法:

  • 在修饰代码块的时候需要一个reference对象作为锁的对象.
  • 在修饰方法的时候默认是当前对象作为锁的对象.
  • 在修饰类时候默认是当前类的Class对象作为锁的对象.

synchronized 方法控制对类成员变量的访问:每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法的类实例的锁方能 执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。

这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态(因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方法均被声明为 synchronized)

缺陷:
若将复杂方法声明为synchronized 将会大大影响效率,比如说将线程类的方法 run() 声明为 synchronized ,由于在线程的整个生命期内它一直在运行,因此将导致它对本类任何 synchronized 方法的调用都永远不会成功。

如何解决?
通过 synchronized关键字来声明synchronized 块。

synchronized(lock) {
// 访问或修改被锁保护的共享状态
}

其中的代码必须获得对象 syncObject (类实例或类)的锁方能执行。由于可以针对任意代码块,且可任意指定上锁的对象,故灵活性较高。

当两个并发线程访问同一个对象中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

当一个线程访问对象的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)同步代码块。其他线程对对象中所有其它synchronized(this)同步代码块的访问将被阻塞。

如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁。

7.2 lock

Lock 接口实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition 对象。在硬件层面依赖特殊的CPU指令实现同步更加灵活。

什么是Condition ?
Condition 接口将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。
其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。

虽然 synchronized 方法和语句的范围机制使得使用监视器锁编程方便了很多,而且还帮助避免了很多涉及到锁的常见编程错误,但有时也需要以更为灵活的方式使用锁。

例如,某些遍历并发访问的数据结果的算法要求使用 “hand-over-hand” 或 “chain locking”:获取节点 A 的锁,然后再获取节点 B 的锁,然后释放 A 并获取 C,然后释放 B 并获取 D,依此类推。Lock 接口的实现允许锁在不同的作用范围内获取和释放,并允许以任何顺序获取和释放多个锁,从而支持使用这种技术。

随着灵活性的增加,也带来了更多的责任。不使用块结构锁就失去了使用 synchronized 方法和语句时会出现的锁自动释放功能。在大多数情况下,应该使用以下语句:

Lock l = ...; //lock接口的实现类对象
l.lock();
     try {
         // access the resource protected by this lock
     } finally {
         l.unlock();
     }

在java.util.concurrent.locks包中有很多Lock的实现类,常用的有ReentrantLock、ReadWriteLock(实现类ReentrantReadWriteLock).它们是具体实现类,不是java语言关键字。

ReentrantLock:
一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。

class X {
   private final ReentrantLock lock = new ReentrantLock();
   // ...

   public void m() { 
     lock.lock();  // block until condition holds
     try {
       // ... method body
     } finally {
       lock.unlock()
     }
   }
 }

重入性:指的是同一个线程多次试图获取它所占有的锁,请求会成功。当释放锁的时候,直到重入次数清零,锁才释放完毕。

ReentrantLock 的lock机制有2种:忽略中断锁和响应中断锁,这给我们带来了很大的灵活性。

比如:如果A、B 2个线程去竞争锁,A线程得到了锁,B线程等待,但是A线程这个时候实在有太多事情要处理,就是 一直不返回,B线程可能就会等不及了,想中断自己,不再等待这个锁了,转而处理其他事情。这个时候ReentrantLock就提供了2种机制,第一,B线程中断自己(或者别的线程中断它),但是ReentrantLock 不去响应,继续让B线程等待,你再怎么中断,我全当耳边风(synchronized原语就是如此);第二,B线程中断自己(或者别的线程中断它),ReentrantLock 处理了这个中断,并且不再等待这个锁的到来,完全放弃。

ReentrantLock相对于synchronized多了三个高级功能:

  • 等待可中断在持有锁的线程长时间不释放锁的时候,等待的线程可以选择放弃等待.
  • 公平锁按照申请锁的顺序来一次获得锁称为公平锁.synchronized的是非公平锁,ReentrantLock可以通过构造函数实现公平锁new RenentrantLock(boolean fair)
    公平锁和非公平锁。这2种机制的意思从字面上也能了解个大概:即对于多线程来说,公平锁会依赖线程进来的顺序,后进来的线程后获得锁。而非公平锁的意思就是后进来的锁也可以和前边等待锁的线程同时竞争锁资源。对于效率来讲,当然是非公平锁效率更高,因为公平锁还要判断是不是线程队列的第一个才会让线程获得锁。
  • 绑定多个Condition 通过多次newCondition可以获得多个Condition对象,可以简单的实现比较复杂的线程同步的功能.通过await(),signal();

7.3 用法区别

  • synchronized:锁的范围是整个方法或synchronized块部分
  • Lock:是方法调用,可以跨方法,灵活性更大

synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。

独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。

Lock用的是乐观锁方式。每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

ReentrantLock必须在finally中释放锁,否则后果很严重,编码角度来说使用synchronized更加简单,不容易遗漏或者出错。

ReentrantLock提供了可轮询的锁请求,他可以尝试的去取得锁,如果取得成功则继续处理,取得不成功,可以等下次运行的时候处理,所以不容易产生死锁,而synchronized则一旦进入锁请求要么成功,要么一直阻塞,所以更容易产生死锁。

一般情况下都是用synchronized原语实现同步,除非下列情况使用ReentrantLock

  • 某个线程在等待一个锁的控制权的这段时间需要中断
  • 需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程
  • 具有公平锁功能,每个到来的线程都将排队等候
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yawn__

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值