赞
踩
根据线程是否要锁住同步资源,分为悲观锁(锁)和乐观锁(不锁)
悲观锁 :认为自己再使用数据的时候一定有别的线程来修改数据,在获取数据的时候会先加锁,确保数据不会被别的线程修改。
锁实现方式:关键字 Synchornized,接口 Lock 的实现类
适用场景:写操作较多
乐观锁:认为自己再使用数据的时候不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候判断之前有没有别的线程更新了这个数据。
锁实现方式:CSA 算法,例如 AtomicInteger 类的原子自增是通过 CAS 自旋(如果内存中的版本与该线程中复制到的版本号不同时,会自旋,自旋是重新去读内存中的信息到线程中,再进行修改操作,操作完再尝试更新)实现的
适用场景:读操作较多,不加锁的特点能够使其读操作的性能大幅度提升。
读锁(共享锁)(用数据库来举例)
针对同一份数据,多个读操作可以同时进行而不互相影响。
当一个进程对表加了读锁后:该进程和其他进程都可对该表进行读操作; 该进程不能对表进行修改会产生 error;
该进程在释放该表的读锁前也不能读取其他的表;其他进程想对该表进行修改时,会进入阻塞状态,当锁释放后完成修改。
写锁(排它锁):
当写操作没有完成前,会阻断其他写锁和读锁。进程能够读自己上写锁的表;
进程能够写自己上写锁的表;该进程在释放该表的写锁之前不能读取其他表;
其他进程要读这个上了写锁的表,会进入阻塞状态,等锁释放后,完成读操作。
读写锁:ReentrantReadWriteLock lock= new ReentrantReadWriteLock();
读写锁下面分读锁和写锁,进行写操作可以上写锁:
lock.writeLock() 进行读操作可以上读锁:
lock.readLock()(读不加锁的话可能会产生脏读这些问题)
自旋锁:当一个线程在获取锁的过程中,发现锁已经被其他线程获取,那么该线程循环等待,然后不断等待该锁是否能够被成功获取,自旋知道获取到锁才会退出。
自旋锁的意义及使用场景:
因为阻塞与唤醒需要操作系统切换 cpu 状态(涉及到上下文切换),需要消耗一定时间。有时自旋的时间比阻塞唤醒所需要的时间还短
自旋锁:固定次数自旋。自旋次数完成后还没有拿到锁,就认为更新失败
自适应自旋锁:假设不同线程持有同一个锁对象的时间基本相当,竞争程度趋于稳定,因此,可以根据上一次自旋的时间与结果调整下一次自旋的时间。
JDK1.6 中 可 以 通 过 -XX : -UseSpining 参 数 关 闭 自 旋 锁 优 化 , - XX:PreBlockSpin 参数修改默认的自旋次数
JDK1.7 之后自旋锁的参数被取消,虚拟机不再支持用户配置自旋锁,自旋锁总是会被执行,并且自旋次数也由虚拟机自动调整。
隐式锁,synchronized 是基于 jvm 的内置锁,加锁与解锁的过程不需要我们在代码中人为控制,jvm 会自动去加锁和解锁
显式锁,整个加锁跟解锁过程需要手动编写代码去控制,例如 ReentrantLock
可重入锁:一个线程已经获得某个锁,可以再次获取锁而不会出现死锁。就是可以重复获取相同的锁。
只判断这个锁有没有被锁上,只要被锁上申请锁的线程都会被要求等待。实现简单。在锁设计时,不仅判断锁有没有被锁上,还会判断锁是谁锁上的,当就是自己锁上的时候,那么他依旧可以再次访问临界资源,并把加锁次数加一。设计了加锁次数,以在解锁的时候,可以确保所有加锁的过程都解锁了,其他线程才能访问。
不可重入锁:当 A 方法获取 lock 锁去锁住一段需要做原子性操作的 B 方法时,如果这段 B 方法又需要
锁去做原子性操作,那么 A 方法就必定要与 B 方法出现死锁。这种会出现问题的重入一把锁的情况,叫不可重入锁。在锁设计时, 只判断这个锁有没有被锁上,只要被锁上申请锁的线程都会被要求等待。实现简单。
在Java里面,最基本的互斥同步手段就是synchronized关键字,这是一种块结构(Block Structured)的同步语法。synchronized关键字经过Javac编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。这两个字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象。如果Java源码中的synchronized明确指定了对象参数,那就以这个对象的引用作为reference。如果没有明确指定,那将根据synchronized修饰的方法类型(如实例方法或类方法),来决定是取代码所在的对象实例还是取类型对应的Class对象来作为线程要持有的锁。
自JDK 5起,Java类库中新提供了java.util.concurrent包(J.U.C包),其中的java.util.concurrent.locks.Lock接口便成了Java的另一种全新的互斥同步手段。基于Lock接口,用户能够以非块结构(Non-Block Structured)来实现互斥同步,从而摆脱了语言特性的束缚,改为在类库层面去实现同步,这也为日后扩展出不同调度算法、不同特征、不同性能、不同语义的各种锁提供了广阔的空间。
Lock: 是Java中的接口,可重入锁、悲观锁、独占锁、互斥锁、同步锁。
Lock需要手动获取锁和释放锁。就好比自动挡和手动挡的区别
Lock 是一个接口,而 synchronized 是 Java 中的关键字, synchronized 是内置的语言实现。
synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。
Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。
通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
Lock 可以通过实现读写锁提高多个线程进行读操作的效率。
ReentrantLock是Java中的类 : 继承了Lock类,可重入锁、悲观锁、独占锁、互斥锁、同步锁。
划重点
相同点:
主要解决共享变量如何安全访问的问题
都是可重入锁,也叫做递归锁,同一线程可以多次获得同一个锁,
保证了线程安全的两大特性:可见性、原子性。
不同点:
ReentrantLock 就像手动汽车,需要显示的调用lock和unlock方法, synchronized 隐式获得释放锁。
ReentrantLock如果获取时间过长会自动释放,synchronized获取不到锁会一直等待
ReentrantLock 是 API 级别的, synchronized 是 JVM 级别的
ReentrantLock 可以实现公平锁、非公平锁,默认非公平锁,synchronized 是非公平锁,且不可更改。
ReentrantLock 通过 Condition 可以绑定多个条件
synchronized适合于并发低的情况,因为synchronized存在锁升级,如果升级为重量级锁将会持续向cpu申请锁资源;ReentrantLock提供了阻塞队列,在高并发的情况下挂起,减少竞争,提高并发能力
引入了锁升级机制、自旋锁和自适应自旋、锁消除、锁粗化
自旋锁与自适应自旋
在许多应用中,锁定状态只会持续很短的时间,为了这么一点时间去挂起恢复线程,不值得。我们可以让等待线程执行一定次数的循环,在循环中去获取锁。这项技术称为自旋锁,它可以节省系统切换线程的消耗,但仍然要占用处理器。在 JDK1.4.2 中,自选的次数可以通过参数来控制。 JDK 1.6又引入了自适应的自旋锁,不再通过次数来限制,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
锁消除
虚拟机在运行时,如果发现一段被锁住的代码中不可能存在共享数据,就会将这个锁清除。
锁粗化
当虚拟机检测到有一串零碎的操作都对同一个对象加锁时,会把锁扩展到整个操作序列外部。如StringBuffer 的 append 操作。
Lock的实现是基于AQS实现的,
AQS 使用一个被volatile修饰的 int 类型state变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。
状态信息通过 protected
类型的getState()
,setState()
,compareAndSetState()
进行操作
//返回同步状态的当前值
protected final int getState() {
return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
AQS 定义两种资源共享方式
ReentrantLock
又可分为公平锁和非公平锁:
CountDownLatch
、Semaphore
、 CyclicBarrier
、ReadWriteLock
我们都会在后面讲到。所有通过AQS实现功能的类都是通过修改state的状态来操作线程的同步状态。比如在
ReentrantLock
中,一个锁中只有一个state
状态,当state
为0时,代表所有线程没有获取锁,当state
为1时,代表有线程获取到了锁。通过是否能把state
从0设置成1,当然,设置的方式是使用CAS设置,代表一个线程是否获取锁成功。
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障,内存屏障会提供3个功能:
是利用CPU原语来实现的,java的方法无法直接访问底层的系统,需要通过native方法来访问,Unsafe类里面的所有CAS方法都是native修饰的,也就是说unsafe类中的方法都直接调用操作系统底层资源执行相应的任务,JVM会帮助我们实现出CAS的汇编指令,这是完全依赖于硬件的功能,在实行的过程中不允许被中断,所以CAS是原子操作
每个Thread线程内部都有一个Map。Map里面存储线程本地对象(key)和线程的变量副本(value),但是,Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收。
当线程没有结束,但是ThreadLocal已经被回收,则可能导致线程中存在ThreadLocalMap<null, Object>的键值对,造成内存泄露。
为了防止此类情况的出现,我们有两种手段。
1、使用完线程共享变量后,显示调用ThreadLocalMap.remove方法清除线程共享变量;
既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。
2、JDK建议ThreadLocal定义为private static,这样ThreadLocal的弱引用问题则不存在了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。