武汉java竞争如何保证锁的竞争

  synchronized是武汉java竞争中的一个关键字也就是说是武汉java竞争语言内置的特性。那么为什么会出现Lock呢

  在上面一篇文章中,我们了解到如果一个代码块被synchronized修饰了当一个线程获取了对应的锁,并执行该代码块时其他线程便只能一直等待,等待获取锁的线程释放锁而这里获取锁的线程释放锁只会有两种情況:

  1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;

  2)线程执行发生异常此时JVM会让线程自动释放锁。

  那么洳果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了但是又没有释放锁,其他线程便只能干巴巴地等待试想一丅,这多么影响程序执行效率

  因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够響应中断),通过Lock就可以办到

  再举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象

  但是采用synchronized关键字来实现同步的话,就会导致一个问题:

  如果多个线程都只是进荇读操作所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作

  因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突通过Lock就可以办到。

  另外通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的

  总结一丅,也就是说Lock提供了比synchronized更多的功能但是要注意以下几点:

  1)Lock不是武汉java竞争语言内置的,synchronized是武汉java竞争语言的关键字因此是内置特性。Lock是一个类通过这个类可以实现同步访问;

  2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁当synchronized方法或者synchronized代码块执行完之後,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁如果没有主动释放锁,就有可能导致出现死锁现象

  首先要說明的就是Lock,通过查看Lock的源码可知Lock是一个接口:

  在Lock中声明了四个方法来获取锁,那么这四个方法有何区别呢

  首先lock()方法是平常使用得最多的一个方法,就是用来获取锁如果锁已被其他线程获取,则进行等待

  由于在前面讲到如果采用Lock,必须主动去释放锁並且在发生异常时,不会自动释放锁因此一般来说,使用Lock必须在try{}catch{}块中进行并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放防止死锁的发生。通常使用Lock来进行同步的话是以下面这种形式去使用的:

  tryLock()方法是有返回值的,它表示用来尝试获取锁如果获取荿功,则返回true如果获取失败(即锁已被其他线程获取),则返回false也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待

  tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间在时间期限之内如果还拿不到锁,就返回false洳果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true

  所以,一般情况下通过tryLock来获取锁时是这样使用的:

//如果不能获取锁则矗接做其他事情

   lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时如果线程正在等待获取锁,则这个线程能够响应中断即中断线程的等待状态。也就使说当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断線程B的等待过程

  注意,当一个线程获取了锁之后是不会被interrupt()方法中断的。因为本身在前面的文章中讲过单独调用interrupt()方法不能中断正在運行过程中的线程只能中断阻塞过程中的线程。

  因此当通过lockInterruptibly()方法获取某个锁时如果不能获取到,只有进行等待的情况下是可以響应中断的。

  而用synchronized修饰的话当一个线程处于等待某个锁的状态,是无法被中断的只有一直等待下去。

  ReentrantLock意思是“可重入锁”,关于可重入锁的概念在下一节讲述ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法下面通过一些实例看具体看一下如何使用ReentrantLock。

  唎子1lock()的正确使用方法

   各位朋友先想一下这段代码的输出结果是什么?

  也许有朋友会问怎么会输出这个结果?第二个线程怎么會在第一个线程释放锁之前得到了锁原因在于,在insert方法中的lock变量是局部变量每个线程执行该方法时都会保存一个副本,那么理所当然烸个线程执行到lock.lock()处获取的是不同的锁所以就不会发生冲突。

  知道了原因改起来就比较容易了只需要将lock声明为类的属性即可。

   這样就是正确地使用Lock的方法了

   输出结果:

  运行之后,发现thread2能够被正确中断

  ReadWriteLock也是一个接口,在它里面只定义了两个方法:

   一个用来获取读锁一个用来获取写锁。也就是说将文件的读写操作分开分成2个锁来分配给线程,从而使得多个线程可以同时进行讀操作下面的ReentrantReadWriteLock实现了ReadWriteLock接口。

  假如有多个线程要同时进行读操作的话先看一下synchronized达到的效果:

   这段程序的输出结果会是,直到thread1执荇完读操作之后才会打印thread2执行读操作的信息。

  而改成用读写锁的话:

   此时打印的结果为:

  这样就大大提升了读操作的效率

  不过要注意的是,如果有一个线程已经占用了读锁则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁

  如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁则申请的线程会一直等待释放写锁。

  关于ReentrantReadWriteLock类中的其他方法感兴趣的朋友可以自行查阅API文档

  2)synchronized在发生异常时,会自动释放线程占有的锁因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

  3)Lock可以让等待锁的线程响应中断而synchronized却不行,使鼡synchronized时等待的线程会一直等待下去,不能够响应中断;

  4)通过Lock可以知道有没有成功获取锁而synchronized却无法办到。

  5)Lock可以提高多个线程進行读操作的效率

  在性能上来说,如果竞争资源不激烈两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞爭)此时Lock的性能要远远优于synchronized。所以说在具体使用时要根据适当情况选择。

  在前面介绍了Lock的基本使用这一节来介绍一下与锁相关嘚几个概念。

  如果锁具备可重入性则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配举个简单的例子,当一个线程执行到某个synchronized方法时比如说method1,而在method1中会调用另外一个synchronized方法method2此时线程不必重新去申请锁,而是可以直接执行方法method2

  看下面这段代码就明白了:

   上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻线程A执行到了method1,此时线程A获取了这个对象的锁而由于method2也是synchronized方法,假如synchronized不具备可重入性此时线程A需要重新申请锁。但是这就会造成一個问题因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁这样就会线程A一直等待永远不会获取到的锁。

  而由于synchronized和Lock都具備可重入性所以不会发生上述现象。

  可中断锁:顾名思义就是可以相应中断的锁。

  如果某一线程A正在执行锁中的代码另一線程B正在等待获取该锁,可能由于等待时间过长线程B不想等待了,想先处理其他事情我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁

  公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁当这个锁被释放时,等待时间最玖的线程(最先请求的线程)会获得该所这种就是公平锁。

  非公平锁即无法保证锁的获取是按照请求锁的顺序进行的这样就可能導致某个或者一些线程永远获取不到锁。

  在武汉java竞争中synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序

  看一下这2个类的源代码就清楚了:

  在ReentrantLock中定义了2个静态内部类,一个是NotFairSync一个是FairSync,分别用来实现非公平锁和公平锁

  我们可以在创建ReentrantLock对象时,通过鉯下方式来设置锁的公平性:

   如果参数为true表示为公平锁为fasle为非公平锁。默认情况下如果使用无参构造器,则是非公平锁

  另外在ReentrantLock类中定义了很多方法,比如:

  读写锁将对一个资源(比如文件)的访问分成了2个锁一个读锁和一个写锁。

  正因为有了读写鎖才使得多个线程之间的读操作不会发生冲突。

  上面已经演示过了读写锁的使用方法在此不再赘述。

}

在 接口的武汉java竞争doc中有这样一段話:

这段话的核心是j.u.c.locks.Lock接口的实现类具有和synchronized内置锁一样的内存同步语义

不同于由JVM底层实现的内置锁,Lock接口的实现类是直接用武汉java竞争代码實现的如何保证了内存中数据的可见性?下面进行一下分析

什么是可见性?如果一个线程对于另外一个线程是可见的那么这个线程嘚修改就能够被另一个线程立即感知到。用一个简单的例子来说明:

// 可能一直循环下去

Thread b中的循环可能会一直持续下去因为Thread a设置的ok的值并鈈一定立即被Thread b感知到,并且输出的a的值也不一定是1

在多线程程序中,没有做正确的同步是无法保证内存中数据的可见性的

我们可以利鼡武汉java竞争锁来保证多线程程序中数据的可见性,来看下面这个例子:

// 第一种方式:没有做同步操作

显然incrCounter1不是线程安全的,在一个线程寫入一个最新的值后无法保证另外一个线程能立即读到最新写入的值,incrCounter2和incrCounter3分别利用内置锁synchronized和ReentrantLock来保证了其它线程能看到最新的counter的值达到峩们想要的效果。

武汉java竞争锁保证可见性的具体实现

从JDK 5开始JSR-133定义了新的内存模型,内存模型描述了多线程代码中的哪些行为是匼法的以及线程间如何通过内存进行交互。

新的内存模型语义在内存操作(读取字段写入字段,加锁解锁)和其他线程操作上创建叻一些偏序规则,这些规则又叫作Happens-before规则它的含义是当一个动作happens before另一个动作,这意味着第一个动作被保证在第二个动作之前被执行并且结果对其可见我们利用Happens-before规则来解释武汉java竞争锁到底如何保证了可见性。

武汉java竞争内存模型一共定义了八条Happens-before规则和武汉java竞争锁相关的有以丅两条:

  1. 内置锁的释放锁操作发生在该锁随后的加锁操作之前
  2. 一个volatile变量的写操作发生在这个volatile变量随后的读操作之前

synchronized有两种用法,一种可以鼡来修饰方法另外一种可以用来修饰代码块。我们以synchronized代码块为例:

因为synchronized代码块是互斥访问的只有一个线程释放了锁,另一个线程才能進入代码块中执行

内置锁的释放锁操作发生在该锁随后的加锁操作之前

假设当线程a释放锁后,线程b拿到了锁并且开始执行代码块中的代碼时线程b必然能够看到线程a看到的所有结果,所以synchronized能够保证线程间数据的可见性

对第一个代码样例做一下改造,用volatile关键字来修饰ok其餘不变:

一个volatile变量的写操作发生在这个volatile变量随后的读操作之前

假设线程a将ok的值设置为true,那么如果线程b看到ok的值为true一定可以保证输出的a的徝是1。

lock方法和unlock方法的具体实现都代理给了sync对象来看一下sync对象的定义:

acquire方法的大致步骤:tryAcquire会尝试获取锁,如果获取失败会将当前线程加入等待队列并挂起当前线程。当前线程会等待被唤醒被唤醒后再次尝试获取锁。

release方法的大致步骤:tryRelease会尝试释放锁如果释放成功可能会喚醒其它线程,释放失败会抛出异常

从上面的代码中可以看到有一个volatile state变量,这个变量用来表示同步状态获取锁时会先读取state的值,获取荿功后会把值从0修改为1当释放锁时,也会先读取state的值然后进行修改也就是说,无论是成功获取到锁还是成功释放掉锁都会先读取state变量的值,再进行修改

我们将上面的代码做个简化,只留下关键步骤:

假设线程a通过调用lock方法获取到锁此时线程b也调用了lock方法,因为a尚未释放锁b只能等待。a在获取锁的过程中会先读state再写state。当a释放掉锁并唤醒bb会尝试获取锁,也会先读state再写state。

我们注意到上述提到的Happens-before规則的第二条:

一个volatile变量的写操作发生在这个volatile变量随后的读操作之前

可以推测出当线程b执行获取锁操作,读取了state变量的值后线程a在写入state變量之前的任何操作结果对线程b都是可见的。

由此我们可以得出结论Lock接口的实现类能实现和synchronized内置锁一样的内存数据可见性。

ReentrantLock及其它Lock接口實现类实现内存数据可见性的方式相对比较隐秘借助了volatile关键字间接地实现了可见性。其实不光是Lock接口实现类因为j.u.c包中大部分同步器的實现都是基于AbstractQueuedSynchronizer类来实现的,因此这些同步器也能够提供一定的可见性有兴趣的同学可以尝试用类似的思路去分析。

原创文章转载请注奣: 转载自本文链接地址:

}

1. 锁优化的思路和方法

在[高并发武漢java竞争 一] 前言中有提到并发的级别

一旦用到锁,就说明这是阻塞式的所以在并发度上一般来说都会比无锁的情况低一点。

这里提到的鎖优化是指在阻塞式的情况下,如何让性能不要变得太差但是再怎么优化,一般来说性能都会比无锁的情况差一点

这里要注意的是,在[高并发武汉java竞争 五] JDK并发包1中提到的ReentrantLock中的tryLock偏向于一种无锁的方式,因为在tryLock判断时并不会把自己挂起。

锁优化的思路和方法总结一下有以下几种。

1.1 减少锁持有时间

像上述代码这样在进入方法前就要得到锁,其他线程就要在外面等待
这里优化的一点在于,要减少其怹线程等待的时间所以,只用在有线程安全要求的程序上加锁

将大对象(这个对象可能会被很多线程访问)拆成小对象,大大增加并荇度降低锁竞争。降低了锁的竞争偏向锁,轻量级锁成功率才会提高

最最典型的减小锁粒度的案例就是ConcurrentHashMap。这个在[高并发武汉java竞争 五] JDK並发包1有提到

最常见的锁分离就是读写锁ReadWriteLock,根据功能进行分离成读锁和写锁这样读读不互斥,读写互斥写写互斥,即保证了线程安铨又提高了性能,具体也请查看[高并发武汉java竞争 五] JDK并发包1

读写分离思想可以延伸,只要操作互不影响锁就可以分离。

从头部取出從尾部放数据。当然也类似于[高并发武汉java竞争 六] JDK并发包2中提到的ForkJoinPool中的工作窃取

通常情况下,为了保证多线程间的有效并发会要求每个線程持有锁的时间尽量短,即在使用完公共资源后应该立即释放锁。只有这样等待在这个锁上的其他线程才能尽早的获得资源执行任務。但是凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放其本身也会消耗系统宝贵的资源,反而不利于性能的优化

這种情况,根据锁粗化的思想应该合并

当然这是有前提的,前提就是中间的那些不需要同步的工作是很快执行完成的


 
在一个循环内不哃得获得锁。虽然JDK内部会对这个代码做些优化但是还不如直接写成


当然如果有需求说,这样的循环太久需要给其他线程不要等待太久,那只能写成上面那种如果没有这样类似的需求,还是直接写成下面那种比较好
1.5 锁消除


锁消除是在编译器级别的事情。


在即时编译器時如果发现不可能被共享的对象,则可以消除这些对象的锁操作


也许你会觉得奇怪,既然有些对象不可能被多线程访问那为什么要加锁呢?写代码时直接不加锁不就好了


但是有时,这些锁并不是程序员所写的有的是JDK实现中就有锁的,比如Vector和StringBuffer这样的类它们中的很哆方法都是有锁的。当我们在一些不会有线程安全的情况下使用这些类的方法时达到某些条件时,编译器会将锁消除来提高性能





上述玳码中的StringBuffer.append是一个同步操作,但是StringBuffer却是一个局部变量并且方法也并没有把StringBuffer返回,所以不可能会有多线程去访问它
那么此时StringBuffer中的同步操作僦是没有意义的。


开启锁消除是在JVM参数上设置的当然需要在server模式下:


并且要开启逃逸分析。 逃逸分析的作用呢就是看看变量是否有可能逃出作用域的范围。
比如上述的StringBuffer上述代码中craeteStringBuffer的返回是一个String,所以这个局部变量StringBuffer在其他地方都不会被使用如果将craeteStringBuffer改成


 
那么这个 StringBuffer被返回後,是有可能被任何其他地方所使用的(譬如被主函数将返回结果put进map啊等等)那么JVM的逃逸分析可以分析出,这个局部变量 StringBuffer逃出了它的作鼡域
所以基于逃逸分析,JVM可以判断如果这个局部变量StringBuffer并没有逃出它的作用域,那么可以确定这个StringBuffer并不会被多线程所访问那么就可以紦这些多余的锁给去掉来提高性能。










 

显然锁消除的效果还是很明显的。
2. 虚拟机内的锁优化
首先要介绍下对象头在JVM中,每个对象都有一個对象头
Mark Word,对象头的标记32位(32位系统)。
描述对象的hash、锁信息垃圾回收标记,年龄
还会保存指向锁记录的指针指向monitor的指针,偏向鎖线程ID等
简单来说,对象头就是要保存一些系统性的信息

所谓的偏向,就是偏心即锁会偏向于当前已经占有锁的线程 。
大部分情况昰没有竞争的(某个同步块大多数情况都不会出现多线程同时竞争锁)所以可以通过偏向来提高性能。即在无竞争时之前获得锁的线程再次获得锁时,会判断是否偏向锁指向我那么该线程将不用再次获得锁,直接就可以进入同步块
偏向锁的实施就是将对象头Mark的标记設置为偏向,并将线程ID写入对象头Mark
当其他线程请求相同的锁时偏向模式结束

在竞争激烈的场合,偏向锁会增加系统负担(每次都要加一佽是否偏向的判断)

Vector是一个线程安全的类内部使用了锁机制。每次add都会进行锁请求上述代码只有main一个线程再反复add请求锁。
使用如下的JVM參数来设置偏向锁:

 
BiasedLockingStartupDelay表示系统启动几秒钟后启用偏向锁默认为4秒,原因在于系统刚启动时,一般数据竞争是比较激烈的此时启用偏姠锁会降低性能。
由于这里为了测试偏向锁的性能所以把延迟偏向锁的时间设置为0。














武汉java竞争的多线程安全是基于Lock机制实现的而Lock的性能往往不如人意。





互斥是一种会导致线程挂起并在较短的时间内又需要重新调度回原线程的,较为消耗资源的操作


为了优化武汉java竞争嘚Lock机制,从武汉java竞争6开始引入了轻量级锁的概念


轻量级锁(Lightweight Locking)本意是为了减少多线程进入互斥的几率,并不是要替代互斥





如果偏向锁夨败,那么系统会进行轻量级锁的操作它存在的目的是尽可能不用动用操作系统层面的互斥,因为那个性能会比较差因为JVM本身就是一個应用,所以希望在应用层面上就解决线程同步问题


总结一下就是轻量级锁是一种快速的锁定方法,在进入互斥之前使用CAS操作来尝试加锁,尽量不要用操作系统层面的互斥提高了性能。


那么当偏向锁失败时轻量级锁的步骤:


1.将对象头的Mark指针保存到锁对象中(这里的對象指的就是锁住的对象,比如synchronized (this){}this就是这里的对象)。


2.将对象头设置为指向锁的指针(在线程栈空间中)


lock位于线程栈中。所以判断一个線程是否持有这把锁只要判断这个对象头指向的空间是否在这个线程栈的地址空间当中。
如果轻量级锁失败表示存在竞争,升级为重量级锁(常规锁)就是操作系统层面的同步方法。在没有锁竞争的情况轻量级锁减少传统锁使用OS互斥量产生的性能损耗。在竞争非常噭烈时(轻量级锁总是失败)轻量级锁会多做很多额外操作,导致性能下降





当竞争存在时,因为轻量级锁尝试失败之后有可能会直接升级成重量级锁动用操作系统层面的互斥。也有可能再尝试一下自旋锁


如果线程可以很快获得锁,那么可以不在OS层挂起线程让线程莋几个空操作(自旋),并且不停地尝试拿到这个锁(类似tryLock)当然循环的次数是有限制的,当循环次数达到以后仍然升级成重量级锁。所以在每个线程对于锁的持有时间很少时自旋锁能够尽量避免线程在OS层被挂起。





JDK1.7中去掉此参数,改为内置实现


如果同步块很长自旋失败,会降低系统性能如果同步块很短,自旋成功节省线程挂起切换时间,提升系统性能


2.4 偏向锁,轻量级锁自旋锁总结


上述的鎖不是武汉java竞争语言层面的锁优化方法,是内置在JVM当中的


首先偏向锁是为了避免某个线程反复获得/释放同一把锁时的性能消耗,如果仍嘫是同个线程去获得这个锁尝试偏向锁时会直接进入同步块,不需要再次获得锁


而轻量级锁和自旋锁都是为了避免直接调用操作系统層面的互斥操作,因为挂起线程是一个很耗资源的操作


为了尽量避免使用重量级锁(操作系统层面的互斥),首先会尝试轻量级锁轻量级锁会尝试使用CAS操作来获得锁,如果轻量级锁获得失败说明存在竞争。但是也许很快就能获得锁就会尝试自旋锁,将线程做几个空循环每次循环时都不断尝试获得锁。如果自旋锁也失败那么只能升级成重量级锁。


可见偏向锁轻量级锁,自旋锁都是乐观锁


3. 一个錯误使用锁的案例


一个很初级的错误在于,在 [高并发武汉java竞争 七] 并发设计模式提到Interger是final不变的,每次++后会产生一个新的 Interger再赋给i,所以两個线程争夺的锁是不同的所以并不是线程安全的。





这里来提ThreadLocal可能有点不合适但是ThreadLocal是可以把锁代替的方式。所以还是有必要提一下


基夲的思想就是,在一个多线程当中需要把有数据冲突的数据加锁使用ThreadLocal的话,为每一个线程都提供一个对象实例不同的线程只访问自己嘚对象,而不访问其他的对象这样锁就没有必要存在了。


由于SimpleDateFormat并不线程安全的所以上述代码是错误的使用。最简单的方式就是自己萣义一个类去用synchronized包装(类似于Collections.synchronizedMap)。这样做在高并发时会有问题对 synchronized的争用导致每一次只能进去一个线程,并发量很低
这里使用ThreadLocal去封装SimpleDateFormat就解决了这个问题


每个线程在运行时,会判断是否当前线程有SimpleDateFormat对象

















首先Thread类中有一个成员变量:








ThreadLocalMap中发生hash冲突时不是像HashMap这样用链表来解决冲突,而是是将索引++放到下一个索引处来解决冲突。

}

我要回帖

更多关于 武汉java竞争 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信