关于java中的锁(转)

时间:2021-09-09 02:28:42

对于锁一直处于比较模糊的状态,最近一天晚上偶然想看看,就翻了几本书,然后弄明白了一些概念,有一些仍然没明白,例如AQS,先把搞明白的记录一下吧。

什么是线程安全?

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。代码本省封装了所有必要的正确性保障手段(互斥同步等),令调用者无需关心多线程的问题,更无需自己实现任何措施来保证多线程的正确调用。

线程之间的交互机制?

不同的线程之间会产生竞争,同样也有交互,最典型的例如数据库连接池,一组数据库连接放在一个数组中,如果线程需要数据库操作,则从池中获取链接,用完了就放回去。JVM提供了wair/notify/notifyAll方式来满足这类需求,典型的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package lock;
public class Pool {
    public Connection get(){
        synchronized (this) {
            if(free>0){
                free--;
            }else{
                this.wait();
            }
            return cacheConnection.poll();
        }
    }
    public void close(Connection conn){
        synchronized (this) {
            free++;
            cacheConnection.offer(conn);
            this.notifyAll();
        }
    }
}

在JDK5之后,并发包中提供了更多的方式来进行线程之间的交互,例如Condition中的await和signal,CountDownLatch中的countDown;

如何实现线程安全?

A、互斥同步,最常见的并发正确性保障手段,同步至多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个线程使用。

B、非阻塞同步,互斥同步的主要问题就是进行线程的阻塞和唤醒所带来的性能问题,因此这个同步也被称为阻塞同步,阻塞同步属于一种悲观的并发策略,认为只要不去做正确的同步措施,就肯定会出问题,无论共享的数据是否会出现竞争。随着硬件指令的发展,有了另外一个选择,基于冲突检测的乐观并发策略,通俗的讲就是先进性操作,如果没有其他线程争用共享数据,那操作就成功了,如果共享数据有争用,产生了冲突,那就再进行其他的补偿措施(最常见的措施就是不断的重试,直到成功为止),这种策略不需要把线程挂起,所以这种同步也被称为非阻塞同步。

C、无同步方案,简单的理解就是没有共享变量需要不同的线程去争用,目前有两种方案,一个是“可重入代码”,这种代码可以在执行的任何时刻中断它,转而去执行其他的另外一段代码,当控制权返回时,程序继续执行,不会出现任何错误。一个是“线程本地存储”,如果变量要被多线程访问,可以使用volatile关键字来声明它为“易变的“,以此来实现多线程之间的可见性。同时也可以通过ThreadLocal来实现线程本地存储的功能,一个线程的Thread对象中都有一个ThreadLocalMap对象,来实现KV数据的存储。

主内存和工作内存?

Java内存模型中规定了所有变量都存储在主内存中,每个线程还有自己的工作内存,线程的工作内存中保存了改线程使用到的变量的主内存副本拷贝,线程对于变量的所有操作(读取和赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量,不同线程之间无法直接访问对方工作内存中的变量,线程间值的传递均需要通过主内存来完成。这里的主内存和工作内存,和java中堆的模型不是一个层次,主内存主要对应java堆中对象的实例数据部分。

什么是自旋锁?

自旋锁在JDK1.6之后就默认开启了。基于之前的观察,共享数据的锁定状态只会持续很短的时间,为了这一小段时间而去挂起和恢复线程有点浪费,所以这里就做了一个处理,让后面请求锁的那个线程在稍等一会,但是不放弃处理器的执行时间,看看持有锁的线程能否快速释放。为了让线程等待,所以需要让线程执行一个忙循环也就是自旋操作。

在jdk6之后,引入了自适应的自旋锁,也就是等待的时间不再固定了,而是由上一次在同一个锁上的自旋时间及锁的拥有者状态来决定。

什么是锁消除?

虚拟机即时编译器在运行时,对于代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。如果判断一段代码,在椎上的所有数据都不会逃逸出去被其他线程访问到,那么认为他是线程私有的,同步加锁也就没有必要做了。

什么是锁粗化?

原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制的尽量小,仅仅在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快的拿到锁。大部分情况下,这儿原则是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至锁出现在循环体内,即使没有线程竞争,频繁的进行互斥操作也会导致不必要的性能损耗。

什么是偏向锁?

在JDK1.之后引入的一项锁优化,目的是消除数据在无竞争情况下的同步原语。进一步提升程序的运行性能。 偏向锁就是偏心的偏,意思是这个锁会偏向第一个获得他的线程,如果接下来的执行过程中,改锁没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步。偏向锁可以提高带有同步但无竞争的程序性能,也就是说他并不一定总是对程序运行有利,如果程序中大多数的锁都是被多个不同的线程访问,那偏向模式就是多余的,在具体问题具体分析的前提下,可以考虑是否使用偏向锁。

关于轻量级锁?

为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”,所以在Java SE1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率,下文会详细分析

什么场景下适合volatile?

volatile能够实现可见性,但是无法保证原子性。可见性指一个线程修改了这个变量的指,新值对于其他线程来说是可以立即得知的。而普通变量是不能做到这一点的,变量值在线程间传递均需要通过主内存完成。volatile的变量在各个线程的工作内存中不存在一致性问题(各个线程的工作内存中volatile变量也可以存在不一致的情况,但是由于每次使用之前都要先刷新,执行引擎看不到不一致的情况,因此可以认为不存在一致性问题)但是java里面的运算并非原子操作的,导致volatile变量运算在并发下一样是不安全的。

什么是CAS?

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。Java中通过Unsafe来实现了CAS。

如何实现互斥同步?

java中最基本的互斥就是synchronized关键字,synchronized在经过编译后,会在同步块的前后分别形成monitorenter和moitorexit这两个字节码指令。在执行monitorenter指令时,首先要去尝试获取对象的锁,如果这个对象没有被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的,在执行monitorexit指令时会把锁计数器减1,当计数器为0时,锁就被释放了。如果获取对象的锁失败,当当前线程就要阻塞等待,知道对象的锁被另一个线程释放为止。synchronized对于同一个线程来说是可重入的,不会出现自己把自己锁死的问题。除了synchronized指望,JUC中的Lock也能实现互斥同步,ReentrantLock,写法上更加可见,lock和unlock配合try/finally来配合完成,ReentrantLock比synchronized有几个高级的特性。

ReentrantLock的高级特性有那几个?

1、等待可中断,当持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,改为处理其他事情;

2、可以实现公平锁,公平锁指多个线程在等待同一个锁时,必须按照申请锁的顺序依次获得锁,synchronized是非公平锁,ReentrantLock默认也是非公平的,只不过可以通过构造函数来制定实现公平锁;

3、锁绑定多个条件,ReentrantLock对象可以同时绑定多个Condition对象,在synchronized中,锁对象的wait/notify/notifyall方法可以实现一个隐含的条件,如果要多一个条件关联的时候,就需要额外的增加一个锁;

关于锁的几个使用建议?

1、使用并发包中的类,并发包中的类大多数采用了lock-free等算法,减少了多线程情况下的资源的锁竞争,因此对于线程间的共享操作的资源而言,应尽量使用并发包中的类来实现;

2、尽可能少用锁,没必要用锁的地方就不要用了;

3、拆分锁,即把独占锁拆分为多把锁(这个不一定完全适用);

4、去除读写操作的互斥锁,在修改时加锁,并复制对象进行修改,修改完毕之后切换对象的引用,而读取是则不加锁,这种方式成为CopyOnWrite,CopyOnWriteArrayList就是COW的典型实现,可以明显提升读的性能;

关于sunchronized的几个注意点?

1、当一个线程访问object的一个synchronized(this)同步代码块时, 另一个线程仍然可以访问该object中的非synchronized(this)同步代码块;

2、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时, 一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块;

3、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时, 其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞;

4、Java中的每一个对象都可以作为锁,对于同步方法,锁是当前实例对象,对于静态同步方法,锁是当前对象的Class对象,对于同步方法块,锁是Synchonized括号里配置的对象;

代码地址:https://github.com/iamzhongyong/about_java_lock

参考书籍

《分布式java应用基础和实践》

《深入java虚拟机》

《java并发编程实践》

http://www.infoq.com/cn/articles/java-se-16-synchronized