《深入理解JAVA虚拟机》笔记——线程同步及同步锁

目录

同步

1. 互斥同步(阻塞同步)

1.1 Synchronized关键字

1.2 ReentrantLock

读写锁

重入锁

2. 非阻塞同步

3.互斥同步中的锁优化

3.1 自旋锁

3.2 锁消除

3.3 锁粗化

3.4 轻量级锁

3.5 偏向锁

 

同步

多个线程并发访问共享数据时,保证数据在同一个时刻只被一个(或者是一些,使用信号量的时候)线程使用。

1. 互斥同步(阻塞同步)

实现同步的手段之一,存在线程阻塞和唤醒带来的性能问题,是一种悲观的并发策略:无论共享数据是否出现竞争都要加锁、维护锁计数器、用户态到核心态的转换(Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间,即重量级,有优化空间,见第6点)。临界区、信号量(semaphore)、互斥量(mutex)是主要的互斥实现方式(系统层面)。Volatile是最轻量的同步机制

java实现互斥同步的方法

1.1 Synchronized关键字

锁的是对象的引用,对象实例或者静态类。经编译后会在同步块前后形成monitorenter和monitorexit两个字节码,前者尝试获取对象锁,成功则锁计数器加1,失败则线程进入同步队列,状态变为BLOCKED。后者会将锁计数器减1,计数器为0,释放锁。

  • 对象锁:同步代码块 synchronized(this){}

              同步方法 public synchronized void method()

              自定义一个对象  Object object = new Object(); public void method(){ synchronized(object){}}

  • 类锁: 每个类都会有唯一一个class对象,类锁锁的就是这个class对象,类锁本质还是对象锁

       同步代码块 synchronized(M.class){}

       同步方法 public static synchronized void Method(),静态确保了对于该类的任何实例对象,该方法都只有一份

      自定义一个静态对象:类似于类锁,因为obj在虚拟机只有一份 :

private static Object obj = new Object();

       private void method(){

           Synchronized(obj){}

         }

1.2 ReentrantLock

lock()和unlock()配合try/finally语句块来完成。它有几个特点:锁绑定多个条件:指一个ReentrantLock对象可以同时绑定多个Condition对象,如果要和多于一个的条件关联的时候,只需要多次调用newCondition()方法即可 ;等待可中断:指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助;不公平锁:不保证按申请锁的时间顺序来依次获得锁,reentranlock支持带布尔值的构造函数来实现公平锁。synchronized也是不公平锁。公平锁避免了饥饿现象,但需要维持一个有序队列,需要空间去操作,而且每次把挂起的线程公平执行需要上下文切换耗费时间,因此公平锁在时间空间上都比非公平锁效率低。

ReentrantLock lock = new ReentrantLock(); //参数默认false

lock.lock(); //如果被其它资源锁定,会在此等待锁释放,达到暂停的效果

try {

//操作

} finally {

lock.unlock(); //释放锁,如果不在finally里,若操作抛了异常,则锁无法被释放

}
  • 其他方法:boolean tryLock(): 如果锁可用,则获取锁,并立即返回true,否则返回,tryLock()只是"试图"获取锁,如果锁不可用,不会导致当前线程被禁用,当前线程仍然继续往下执行代码
  • 条件condition:由static Condition =lock.newCondition()创建,其方法有:condition.await(),signal(),signalAll(),调用这些方式实现显式锁下线程之间的通信,使用 “条件”必须首先获取锁。一个Lock里可以定义多个Condition。这里用signal通知就可以做到针对性通知,而线程原生的notify是随机挑一个通知

读写锁

ReentrantReadWriteLock implements ReadWriteLock。从名可知这是可重入锁。Synchronized和reentrantLock都是排它锁,即同时只能有一个线程持有锁;而读写锁允许多线程同时读但不能写,写的时候排它,要考虑数据一致性

“读写分离”的思想:内部分别实现了ReadLock和WriteLock,大大提高了性能,因为读总比写多,如果都是排他锁则耗时很长,时间累加

private ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock getLock = lock.readLock();//获取读锁
private final Lock setLock = lock.writeLock();//获取写锁

//读取商品的方法getnum():
   getLock.lock();//上读锁
   try{
     读取 return this.goodsInfo
   }finally{
   getLock.unlock();//释放锁
   }

//写商品的方法里setnum(number):
    setLock.lock();
    try{
    //修改
    goodsInfo.changenum(number);
    }
    finally{ 
    setLock.unlock();
    }



重入锁

一个线程可以反复多次拿同一把锁,不至于自己被自己锁死。具体概念就是:自己可以再次获取自己的内部锁。Synchronized和ReentrantLock均是可重入锁。以下是示例

  • 示例1:当一个线程得到一个对象后,再次请求该对象锁时是可以再次得到该对象的锁的,只不过锁会加1,当完成一次请求方法后锁会减1。调用method1()方法时,已经获得了锁,此时内部调用method2()方法时,判断本身已经具有该锁,所以可以再次获取

  • 示例2:比如下面的incr2()方法,进行递归调用时发现自己已经拿到锁了,就不会阻塞,而是继续运行。这就是可重入

补充:Synchronized内置锁和lock显式锁的区别

lock 可中断锁(lockInterruptibly() 的用法体现了Lock的可中断性)、可绑定多个contidtion、可创建公平锁(构造方法参数设置为true)

2. 非阻塞同步

乐观并发策略,先尝试操作,数据有争用,再采取补偿措施,一般为不断重试直到成功为止,不需要把线程挂起。

典型,CAS操作:本质是处理器的CAS指令。包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。Java中 java.util.concurrent.atomic 包相关类就是 CAS的实现。CAS的缺陷:

(1)ABA问题:如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。JUC提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性

(2) 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。

(3) 只能保证一个共享变量的原子操作。解决:把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

3.互斥同步中的锁优化

3.1 自旋锁

请求不到锁的时候,让线程执行一个忙循环,稍微等待下看是否锁被释放。这适合于锁被占用时间很短的情况。自旋次数的默认值是10次,用户可以使用参数-XX:PreBlockSpin来更改。 在JDK 1.6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100个循环。

3.2 锁消除

虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。

3.3 锁粗化

原则上加锁的范围越小越好,将同步块的作用范围限制到尽可能小。但如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部

3.4 轻量级锁

在没有多线程竞争的前提下,减少传统重量级锁使用操作系统互斥量产生的性能消耗。轻量级锁状态:虚拟机用CAS操作,将对象头mark word部分更新为指向线程栈帧中的lock record的指针,mark word的锁标志位置为00,表示该同步对象处于轻量级锁定状态。膨胀状态:如果更新操作失败,虚拟机会检查mark word是否指向当前线程的栈帧,如果是则直接进入同步代码块,否则说明对象已被其他线程抢占,此时两线程抢占同一个对象,轻量级锁不再有效,膨胀为重量级锁,即mark word被更新为指向重量级锁(互斥量)的指针。

3.5 偏向锁

相对于轻量级锁来说,线程一旦持有偏向锁,并且该锁没有被其他线程获取,线程以后将不再进行任何同步操作,连CAS操作都不需要。当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”,即偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(例如Locking、Unlocking及对Mark Word的Update等)。当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。(启用参数-XX:+UseBiasedLocking,这是JDK 1.6的默认值

补充:

对象头:HotSpot虚拟机的对象头分两部分,mark word部分(32bit或64bit)存储哈希码、GC分代年龄、锁标志位,另一部分存储指向方法区对象类型数据的指针。对于一个同步对象和一个要访问它的线程来说,初始状态是对象没有被锁定,此时虚拟机为当前线程创建lock record的空间,存储着mark word的拷贝

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值