锁, Java的锁总结

概述:


1.乐观锁与悲观锁

  乐观锁和悲观锁是数据库中引入的名词。

  悲观锁是值对数据被外界的修改是持一种悲观的态度的,总是认为数据很容易就被其他线程修改了,因此在数据处理前都会先对数据加锁,加一个排他锁,处理完后再释放锁。供其他线程抢占。

  乐观锁是相对悲观锁而言的,它总是认为数据不会被外界修改,不容易发生冲突和不需要抢占锁,因此访问数据前不需要给数据加锁,而是在进行数据提交的时候采取对是否出现数据冲突进行检测,如果发现有冲突,就什么都不做,选择重试。

2.公平锁和非公平锁

  公平与非公平是锁的抢占机制来分的,公平锁代表线程获取锁的顺序是线程请求锁的顺序,谁先请求,谁优先获得;而非公平锁是在运行时闯入的,也就是先来的线程不一定能先获取到锁。

  一般程序没有公平性需求的时候尽量使用非公平锁比较好,因为公平锁会带来性能的开销

3.独占锁和共享锁

  根据锁是否能被多个线程同时持有可以分为独占锁和共享锁。独占锁要求任何时候只有一个线程能获取到锁,也是一种悲观锁,ReentranLock就属于独占锁。共享锁则是一种乐观锁,如ReadWriteLock读写锁,它允许多个线程同时读操作,但是有时候也会现在运行同时进行的最大线程数目。

4.可重入锁

  可重入锁是指,当线程再次获取它自己已经获取到的锁的时候不会被阻塞,就可以一直(有限次数的)进入被该锁锁住的代码。

  可重入锁的原理是:在锁内部维护一个线程标识(管程),用来标识这个锁被那个线程占有了,然后关联了一个计数器,初始值为0,当锁被线程抢占后,该计数器值变为1,如果别的线程来获取发现锁会判断该线程是否为它的持有者,如果不是则会被阻塞,如果是,计数器会继续+1.当线程释放锁的时候,计数器值 - 1.计数器值为0的时候呢,锁里面的线程标识置为空,标识没有被线程占有,阻塞的线程被唤醒,可以过来抢占。

  synchronized就属于一个可重入锁

5.自旋锁

  因为线程获取锁失败后会陷入阻塞,而线程阻塞会切换到内核态而被挂起,线程获取到锁后被唤醒,切换状态会带来比较大的性能开销,影响性能。

  自旋锁是指:当线程获取锁失败以后呢,它不马上陷入阻塞状态,在仍然获取cpu使用权的情况下,它会选择多尝试获取几次(做几次空循环),因为其他线程很可能马上就释放锁了;只有在指定次数内获取不到,它进入阻塞状态。

  自旋就是通过CPU的时间换取线程阻塞与调度的开销,但是如果最后还是会阻塞的话,CPU的时间会白白浪费了。最好用于锁占用时间短,锁竞争不激烈的情况。


 

锁的优化


1.减少锁的持有时间

在程序开发过程中,应该尽可能地减少对某个锁的占有时间,以减少线程见互斥的可能。如:

/**
	 * 优化前代码,在不需要锁的地方也有锁,锁的持有时间较长
	 */
	public synchronized void test(){
		function1();     //不需要同步的代码
		synNeedFuntion();  //需要同步的代码
		function2();     //不需要同步的代码
	}

	/**
	 * 优化后代码,减少了锁的持有时间
	 */
	public void test1(){
		function1();
		synchronized(this){ 
			synNeedFuntion();   //需要同步的代码
		}
		
		function2();
	}

2.减小锁粒度

通过减小锁的粒度来削弱多线程之间锁竞争。通过分割数据结构来实现。例如:ConcurrentHashMap类的实现,采取了一种分段锁的概念,因为其HashMap内部是用一个数组实现的,因此它把HashMap分了若干个段,每个段持有一个锁,因此理论上数组多长,就有多少个锁,不同锁间不是互斥的,因此在该Map的put和get方法的时候,先根据hashcode获取到具体哪个段,再获取那个段的锁,从而减少了锁的竞争。

3.采用读写分离锁来替换独占锁(锁分离

通过分割功能点,来实现对不同的功能加不同的锁,在读多写少的环境下,使用读写锁替换独占锁能有效提高程序并发能力,读锁不互斥,写锁互斥,原因还是那个,减少了锁的竞争,减少了线程的阻塞调度带来的开销。

4.锁粗化

如果对同一个锁不停地请求、同步和释放,会消耗很多资源。即对锁的频繁小粒度的请求,对系统性能有不好的影响。因此虚拟机在遇到一连串连续的对同一个锁不断进行请求和释放的操作时,会把所有的锁操作整合为一次请求。比如:

        int a = 0;
        //频繁地请求和释放锁
        for (int i = 0; i < 100000000; i++){
            try {
                a++;
            }catch (Exception e){
            }
        }

    
        int a = 0;
        //整合为一次锁的请求和释放
        try {
            for (int i = 0; i < 100000000; i++){
                 a++;
            }
        }catch (Exception e){
        }

5.重入锁和内部锁

重入锁(ReentrantLock)和内部锁(synchronized)该如何选择。

在使用上:内部锁使用简单,重入锁略复杂,需要在finally代码块中显式释放,内部锁自动释放

在性能上:高并发上,内部锁略差于重入锁,但是JVM对内部锁有很多优化,内部锁的性能在不断变好。

在功能上:重入锁有更多,更强大的功能,有锁等待时间、支持锁中断,快速锁轮询等。还有Condition机制,用于复杂的线程控制。

我理解是:在满足功能的前提先,优先选择内部锁。

6.锁偏向

  锁偏向是指:在程序没有竞争的时候,则取消之前已经取得的锁的线程同步操作。举个例子,当线程A获取了某个锁,那么就进入偏向模式,当线程A再次请求该锁的时候,就不用再进行同步相关的操作,从而节省时间 ; 但是如果这时候有别的线程请求锁,那么就退出偏向模式。

  锁偏向在锁竞争激烈的时候没有任何优化效果,因为持有锁的线程不断地在切换,锁无法一直保持在偏向模式,这时候不仅没有性能提高,还有损性能。

猜你喜欢

转载自blog.csdn.net/aa792978017/article/details/88836310
今日推荐