java自旋锁

一 序

       锁作为并发共享数据,保证一致性的工具,这些JDK已经写好提供的锁(如 synchronized 和 ReentrantLock等等 )为我们开发提供了便利,但是锁的背景原理也很重要。整理下常见的锁。

        由于在多处理器系统环境中有些资源因为其有限性,有时需要互斥访问(mutual exclusion),这时会引入锁的机制,只有获取了锁的线程才能获取资源访问。即是每次只能有且只有一个线程能获取锁,才能进入自己的临界区,同一时间不能两个或两个以上进程进入临界区,当退出临界区时释放锁。(锁就是并发变成串行,也就是等待)通常没有获取到锁的线程有两种处理方式:1 自旋。2 阻塞。

二 自旋锁原理

   这里先借鉴下linux内核的自旋锁,再说下Java的。

  自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是 否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。其作用是为了解决某项资源的互斥使用。因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远 高于互斥锁。虽然它的效率比互斥锁高,但是它也有些不足之处
    1、自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低。
    2、在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁,调用有些其他函数也可能造成死锁,如 copy_to_user()、copy_from_user()、kmalloc()等。

因此我们要慎重使用自旋锁,自旋锁只有在内核可抢占式或SMP(多处理器)的情况下才真正需要。

     好了,概念明白了,如果两个线程资源竞争不是特别激烈,而处理器阻塞一个线程引起的线程上下文的切换的代价高于等待资源的代价的时候(锁的已保持者保持锁时间比较短),那么线程B可以不放弃CPU时间片,而是在“原地”忙等,直到锁的持有者释放了该锁,这就是自旋锁的适用场景:自旋锁使用者保持锁时间非常短。

三 jdk 实现

   CAS是一种系统原语(所谓原语属于操作系统用语范畴。原语由若干条指令组成的,用于完成一定功能的一个过程。primitive or atomic action 是由若干个机器指令构成的完成某种特定功能的一段程序,具有不可分割性·即原语的执行必须是连续的,在执行过程中不允许被中断)。CAS是Compare And Set的缩写。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
     在x86 平台上,CPU提供了在指令执行期间对总线加锁的手段。CPU芯片上有一条引线#HLOCK pin,如果汇编语言的程序中在一条指令前面加上前缀"LOCK",经过汇编以后的机器代码就使CPU在执行这条指令的时候把#HLOCK pin的电位拉低,持续到这条指令结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的原子性。

      sun.misc.Unsafe是JDK里面的一个内部类,这个类为JDK严格保护,因为他提供了大量的低级的内存操作和系统功能。下面我们看到的JDK的JUC包里面大量使用了。

     Jdk1.5以后,提供了java.util.concurrent.atomic包,这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。实际上是借助硬件的相关指令来实现的,不会阻塞线程(或者说只是在硬件级别上阻塞了)。atomic包《Java并发编程的艺术》第7章介绍了,后面单独整理。

举例:AtomicInteger

    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

我们再看看别的实现方式,

public interface Lock {

	 public void lock();  
     
	 public void unlock();  
}
import java.util.concurrent.atomic.AtomicReference;

public class Spinlock implements Lock{  
	
	private AtomicReference<Thread> sign =new AtomicReference<>();

	  public void lock(){
	    Thread current = Thread.currentThread();
	    while(!sign.compareAndSet(null, current)){
	    }
	  }

	  public void unlock(){
	    Thread current = Thread.currentThread();
	    sign.compareAndSet(current, null);
	  }
	
}
public class TestSpinLock {
	 static  int  sum=0;  
	 static  long alltime =0L;
	 static int   count =0;
	static Spinlock lock = new Spinlock();  
	// static TASLock lock = new TASLock();
	//  private static ReentrantLock lock = new ReentrantLock();
	 public static int incrCounter3() {
		 long start = System.nanoTime(); 
	    try {
	        lock.lock();
	        long duration = System.nanoTime() - start;  
	        alltime += duration;
	       // System.out.println(lock.toString() + " time cost is " + duration + " ns,all:"+alltime);  
	        return sum++;
	    } finally {
	        lock.unlock();
	    }
	  
	 }
	 public synchronized static int incrCounter2() {
	        return count++;
	    }
	 
	public static void main(String[] args) throws InterruptedException {
		// TODO Auto-generated method stub
       for(int j=0;j<100;j++){
        for (int i = 0; i < 100; i++) {  
        	Thread t = new Thread(new Runnable(){        		
        		  @Override  
                  public void run() { 
        		//  System.out.println(Thread.currentThread().getName()+" run");
        		  TestSpinLock.incrCounter3();
        		  TestSpinLock.incrCounter2();
        		  }
        	});
        	t.start();  
        }       
			
        Thread.sleep(1000);
	     System.out.println(sum +":"+count+":"+alltime);  	
       }  		
	}    	
}

外面循环100次,为了取平均,因为每次运行时间不同。里面是开100个线程去并发计算。跟系统自带的方式去对比结果是否正确。spinlock使用了CAS原子操作,lock函数将owner设置为当前线程,并且预测原来的值为空。unlock函数将owner设置为null,并且预测值为当前线程。当有第二个线程调用lock操作时由于owner值不为空,导致循环一直被执行,直至第一个线程调用unlock函数将owner设置为null,第二个线程才能进入临界区。可以测试,线程越多,竞争越激烈,自旋锁效率越低。

   其它改进算法demo,分为两步,第一步通过读操作来获取锁状态,当锁可获取时,第二步再通过CAS操作来尝试获取锁,减少了CAS的操作次数。并且第一步的读操作是处理器直接读取自身高速缓存,不会产生缓存一致性流量,不占用总线资源.缺点是在锁高争用的情况下,线程很难一次就获取锁,CAS的操作会大大增加。

public class TTASLock implements Lock{  
  
private AtomicBoolean mutex = new AtomicBoolean(false);  
      
    @Override  
    public void lock() {  
        while(true){  
            // 第一步使用读操作,尝试获取锁,当mutex为false时退出循环,表示可以获取锁  
            while(mutex.get()){}  
            // 第二部使用getAndSet方法来尝试获取锁  
            if(!mutex.getAndSet(true)){  
                return;  
            }                   
        }  
    }  
  
    @Override  
    public void unlock() {  
        mutex.set(false);  
    }  
  
    public String toString(){  
        return "TTASLock";  
    }  
}  

当然网上还列举了很多改进算法,TicketLock (访问顺序的问题),采用链表的形式进行排序:CLHlock 和MCSlock。

  突然想到之前的一个知识点:wait,notify的假唤醒问题,不能用if判断,需要用while循环,没理解是不是自旋的一种方式。

猜你喜欢

转载自blog.csdn.net/bohu83/article/details/80813938