多线程性能调优(一)--锁

       现在的业务场景用到多线程的情况越来越多,那么多线程调优就是一个无法避开的话题,而线程调优主要是避免锁竞争,减少上下文切换。所以先简单说说【锁】。

       在并发编程中,多个线程访问同一个共享资源时,我们必须考虑如何维护数据的原子性,为了保证数据的原子性,就必须用到锁。

【锁的分类】

       Synchronized锁 和 Lock锁

       

【Synchronized同步锁的实现原理】

   Synchronized是JVM实现的一种内置锁,锁的获取和释放由JVM隐式实现。每次获取和释放锁操作都会带来用户态和内核态的切换,从而增加了系统性能开销。   

   在JDK1.6之前,Synchronized非常耗资源,JDK1.6开始,对Synchronized锁进行了优化,性能有很大提高。

    Synchronized同步锁实现由两种方式,一种是修饰方法,一种是修饰代码块。

// 关键字在实例方法上,锁为当前实例
 public synchronized void method1() {
     // code
 }
 
 // 关键字在代码块上,锁为括号里面的对象
 public void method2() {
     Object o = new Object();
     synchronized (o) {
         // code
     }
 }

 

 JVM中同步是基于进入和退出管程(Monitor)对象实现的,每个对象实例都会有一个Monitor,Monitor可以和对象一起创建、销毁。多个线程同时访问一段同步代码时,多个线程会先被存放在EntryList集合中,处于block状态的线程,都会被加入到该列表。接下来当线程获取到Monitor对象时,Monitor是依靠底层操作系统的MutexLock来实现互斥,线程申请Mutex成功,则持有改Mutex,其他线程将无法获取到该mutex;如果线程调用wait方法,就会释放当前持有的mutex,并且该线程会进入到WaitSet集合中,等待下次被唤醒。如果当前线程顺利执行完方法,就会释放Mutex。

 

      总的来说,Synchronized同步锁的实现,因Monitor是依赖底层操作系统实现的,存在用户态和内核态之间的切换,增加了系统性能开销。为了提高性能,JDK1.6引入了偏向锁、轻量级锁、重量级锁的概念,来减少锁竞争带来的上下文切换,实现这个优化,是在java对象中新增了对象头。在JDK1.6开始,对象实例在堆内存中被分为三部分:对象头、实例数据和对象充填。其中Java对象头包括Mark Word、指向类的指针和数组长度组成。而Mark Word记录了对象和锁的有关信息。

       Synchronized同步锁就是从偏向锁开始的,随着竞争越来越激烈,偏向锁升级到轻量级锁,最终升级到重量级锁。

      JVM 在 JDK1.6 中引入了分级锁机制来优化 Synchronized,当一个线程获取锁时,首先对象锁将成为一个偏向锁,这样做是为了优化同一线程重复获取导致的用户态与内核态的切换问题;其次如果有多个线程竞争锁资源,锁将会升级为轻量级锁,它适用于在短时间内持有锁,且分锁有交替切换的场景;轻量级锁还使用了自旋锁来避免线程用户态与内核态的频繁切换,大大地提高了系统性能;但如果锁竞争太激烈了,那么同步锁将会升级为重量级锁。

     减少锁竞争,是优化 Synchronized 同步锁的关键。我们应该尽量使 Synchronized 同步锁处于轻量级锁或偏向锁,这样才能提高 Synchronized 同步锁的性能;通过减小锁粒度来降低锁竞争也是一种最常用的优化方法;另外我们还可以通过减少锁的持有时间来提高 Synchronized 同步锁在自旋时获取锁资源的成功率,避免 Synchronized 同步锁升级为重量级锁。

 

在高并发、高负载的场景下,我们可以通过设置JVM参数关闭偏向锁和自旋锁。

-XX:-UseBiasedLocking // 关闭偏向锁(默认打开)

-XX:-UseSpinning // 参数关闭自旋锁优化 (默认打开) 
-XX:PreBlockSpin // 参数修改默认的自旋次数。JDK1.7 后,去掉此参数,由 jvm 控制

【Lock同步锁】

        Lock锁是通过java代码来实现显示获取和释放的一种锁。

        从性能方面上来说,在并发量不高、竞争不激烈的情况下,Synchronized 同步锁由于具有分级锁的优势,性能上与 Lock 锁差不多;但在高负载、高并发的情况下,Synchronized 同步锁由于竞争激烈会升级到重量级锁,性能则没有 Lock 锁稳定。

 

 

【Lock同步锁实现原理】

         Lock是一个接口类,常用的实现类有ReenTrantLock(独占锁)、ReentrantReadWriteLock(RRW读写锁)

        ReenTrantLock(独占锁)

         ReenTrantLock(独占锁)同一时间只允许一个线程访问。示例代码如下:

    public class MyService {
 
    private Lock lock = new ReentrantLock();
 
    public void testMethod() {
        lock.lock();
        for (int i = 0; i < 5; i++) {
            System.out.println("ThreadName=" + Thread.currentThread().getName()
                    + (" " + (i + 1)));
        }
        lock.unlock();
    }
 
}

 

      我们知道,对于同一份数据进行读写,如果一个线程在读数据,而另一个线程在写数据,那么读到的数据和最终的数据就会不一致;如果一个线程在写数据,而另一个线程也在写数据,那么线程前后看到的数据也会不一致。这个时候我们可以在读写方法中加入互斥锁,来保证任何时候只能有一个线程进行读或写操作。

         在大部分业务场景中,读业务操作要远远大于写业务操作。而在多线程编程中,读操作并不会修改共享资源的数据,如果多个线程仅仅是读取共享资源,那么这种情况下其实没有必要对资源进行加锁。如果使用互斥锁,反倒会影响业务的并发性能。那么在这种场景下,有没有什么办法可以优化下锁的实现方式呢?

        由于ReenTrantLock(独占锁)同一时间只允许一个线程访问,在一些业务场景中,比如读多写少的场景,这种场景是一种非常场景的场景,比如商品活动秒杀,总共有200件商品,但是可能好几十万人抢。几十万人读数据,但是最多有200人是写数据。

  ReentrantReadWriteLock(RRW读写锁)

     针对这种读多写少的场景,Java 提供了另外一个实现 Lock 接口的读写锁 RRW。我们已知 ReentrantLock 是一个独占锁,同一时间只允许一个线程访问,而 RRW 允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。读写锁内部维护了两个锁,一个是用于读操作的 ReadLock,一个是用于写操作的 WriteLock。

public class TestRTTLock {

 private double x, y;

 private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
 // 读锁
 private Lock readLock = lock.readLock();
 // 写锁
 private Lock writeLock = lock.writeLock();

 public double read() {
  // 获取读锁
  readLock.lock();
  try {
   return Math.sqrt(x * x + y * y);
  } finally {
   // 释放读锁
   readLock.unlock();
  }
 }

 public void move(double deltaX, double deltaY) {
  // 获取写锁
  writeLock.lock();
  try {
   x += deltaX;
   y += deltaY;
  } finally {
   // 释放写锁
   writeLock.unlock();
  }
 }

}

  

   

      不管使用 Synchronized 同步锁还是 Lock 同步锁,只要存在锁竞争就会产生线程阻塞,从而导致线程之间的频繁切换,最终增加性能消耗。因此,如何降低锁竞争,就成为了优化锁的关键。

     在 Synchronized 同步锁中,可以通过减小锁粒度、减少锁占用时间来降低锁的竞争。 Lock 锁的灵活性,通过锁分离的方式来降低锁竞争。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

一路奔跑1314

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

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

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

打赏作者

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

抵扣说明:

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

余额充值