网站开发兼职,国内免费域名注册,重庆cms建站模板,深圳工程建设交易中心网1 作用
Java中的锁主要用于保障多并发线程情况下数据的一致性。在多线程编程中为了保障数据的一致性#xff0c;我们通常需要在使用对象或者方法之前加锁#xff0c;这时如果有其他线程也需要使用该对象或者该方法,则首先要获得锁,如果某个线程发现锁正在被其他线程使用,就会…1 作用
Java中的锁主要用于保障多并发线程情况下数据的一致性。在多线程编程中为了保障数据的一致性我们通常需要在使用对象或者方法之前加锁这时如果有其他线程也需要使用该对象或者该方法,则首先要获得锁,如果某个线程发现锁正在被其他线程使用,就会进入阻塞队列等待锁的释放直到其他线程执行完成并释放锁、该线程才有机会再次获取锁进行操作。这样就保障了在同一时刻只有一个线程持有该对象的锁并修改对象、从而保障数据的安全。
锁从乐观和悲观的角度可分为乐观锁和悲观锁从获取资源的公平性角度可分为公平锁和非公平锁从是否共享资源的角度可分为共享锁和独占锁,从锁的状态的角度可分为偏向锁、轻量级锁和重量级锁。同时,在JVM中还巧妙设计了自旋锁以更快地使用CPU资源。 2 乐观锁、悲观锁 乐观锁 乐观锁采用乐观的思想处理数据在每次读取数据时都认为别人不会修改该数据所以不会上锁但在更新时会判断在此期间别人有没有更新该数据、通常采用在写时先读出当前版本号然后加锁的方法。具体过程为:比较当前版本号与上一次的版本号如果版本号一致.则更新如果版本号不一致则重复进行读、比较、写操作。 悲观锁 悲观锁采用悲观思想处理数据在每次读取数据时都认为别人会修改数据、所以每次在读写数据时都会上锁,这样别人想读写这个数据时就会阻塞、等待直到拿到锁。 Java 中的悲观锁大部分基于AQS(Abstract Qucued Synchronized,抽象的队列同步器架构实现。AQS定义了一套多线程访问共享资源的同步框架许多同步类的实现都依赖于它例如常用的Synchronized、ReentrantLock、Semaphore、CountDownLatch等。该框架下的锁会先尝试以 CAS 乐观锁去获取锁、如果获取不到则会转为悲观锁如RetreenLock )。 比较适合写入操作比较频繁的场景如果出现大量的读取操作每次读取的时候都会进行加锁这样会增加大量的锁的开销降低了系统的吞吐量。 3 公平锁、非公平锁 公平锁 概念 是指多个线程按照申请锁的顺序来获取锁线程直接进入队列中排队队列中的第一个线程才能获得锁。 好处公平锁的优点是等待锁的线程不会饿死。 缺点是整体吞吐效率相对非公平锁要低等待队列中除第一个线程以外的所有线程都会阻塞CPU唤醒阻塞线程的开销比非公平锁大。 非公平锁 概念非公平锁是多个线程加锁时直接尝试获取锁获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用那么这个线程可以无需阻塞直接获取到锁所以有可能出现后申请锁的线程先获取锁的场景 好处非公平锁的优点是可以减少唤起线程的开销整体的吞吐效率高因为线程有几率不阻塞直接获得锁CPU不必唤醒所有线程。 缺点处于等待队列中的线程可能会饿死或者等很久才会获得锁。 4 可重入锁、非可重入锁 可重入锁又名递归锁是指在同一个线程在外层方法获取锁的时候再进入该线程的内层方法会自动获取锁前提锁对象得是同一个对象或者class不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁可重入锁的一个优点是可一定程度避免死锁。 5 共享锁、排它锁
共享锁和排它锁多用于数据库中的事物操作主要针对读和写的操作。而在 Java 中对这组概念通过 ReentrantReadWriteLock 进行了实现它的理念和数据库中共享锁与排它锁的理念几乎一致即一条线程进行读的时候允许其他线程进入上锁的区域中进行读操作当一条线程进行写操作的时候不允许其他线程进入进行任何操作。即读 读可以存在读 写、写 写均不允许存在。 共享锁也称读锁或 S 锁。如果事务 T 对数据 A 加上共享锁后则其他事务只能对 A 再加共享锁不能加排它锁。获准共享锁的事务只能读数据不能修改数据。 排它锁也称独占锁、写锁或 X 锁。如果事务 T 对数据 A 加上排它锁后则其他事务不能再对 A 加任何类型的锁。获得排它锁的事务即能读数据又能修改数据。 6 自旋锁 自旋锁认为:如果持有锁的线程能在很短的时间内释放锁资源那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞、挂起状态只需等一等(也叫作自旋),在等待持有锁的线程释放锁后即可立即获取锁这样就避免了用户线程在内核状态的切换上导致的锁时间消耗。 线程在自旋时会占用CPU,在线程长时间自旋获取不到锁时将会产CPU 的浪费甚至有时线程永远无法获取销而导致CPU 资源被永久占用所以需要设定一个自旋等待的最大时间。在线程执行的时间超讨自旋等待的 自旋锁的优缺点如下。 优点:自旋锁可以减少CPU上下文的切换对于占用锁的时间非常短或锁竞争不激烈的代码块来说性能大幅度提升因为自旋的 CPU 耗时明显少于线程阻塞、挂起、再唤醒时两次CPU上下文切换所用的时间。 缺点:在持有锁的线程占用锁时间过长或锁的竞争过于激烈时线程在自旋过程中会长时间获取不到锁资源,将引起 CPU 的浪费。所以在系统中有复杂锁依赖的情况下不适合采用自旋锁。 2.自旋锁的时间阈值 自旋锁用于让当前线程占着CPU 的资源不释放等到下次自旋获取锁资源后立即执行相关操作。但是如何选择自旋的执行时间呢?如果自旋的执行时间太长则会有大量的线程处于自旋状态且占用CPU资源造成系统资源浪费。因此、对自旋的周期选择将直接影响到系统的性能! JDK的不同版本所采用的自旋周期不同JDK 1.5为固定的时间JDK 1.6引入了适应性自旋锁。适应性自旋锁的自旋时间不得是固定值而是由上一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的可基本认为一个线程上下文切换的时间是就一个最佳时间。
7 重量级锁和轻量级锁 重量级锁是基于操作系统的互斥量Mutex Lock而实现的锁会导致进程在用户态与内核态之间切换相对开销较大。 synchronized在内部基于监视器锁Monitor实现监视器锁基于底层的操作系统的 Mutex Lock实现,因此 synchronized属于重量级锁。重量级锁需要在用户态和核心态之间做转换,所以synchronized的运行效率不高。 JDK在1.6版本以后为了减少获取锁和释放锁所带来的性能消耗及提高性能引人了轻量级锁和偏向锁。 轻量级锁是相对于重量级锁而言的。轻量级锁的核心设计是在没有多线程竞争的前提下减少重量级锁的使用以提高系统性能。轻量级锁适用于线程交替执行同步代码块的情况即互斥操作)如果同一时刻有多个线程访问同一个锁则将会导致轻量级锁膨胀为重量级锁。 轻量级锁也叫自旋锁。
8 偏向锁 除了在多线程之间存在竞争获取锁的情况,还会经常出现同一个锁被同一个线程多次获取的情况。偏向锁用于在某个线程获取某个锁之后,消除这个线程锁重人的开销,看起来似乎是这个线程得到了该锁的偏向(偏袒)。 偏向锁的主要目的是在同一个线程多次获取某个锁的情况下尽量减少轻量级锁的执行路径、因为轻量级锁的获取及释放需要多次CAS ( Compare and Swap 原于操作而偏向锁只需要在切换 ThreadID 时执行一次 CAS 原子操作因此可以提高钡的运行效率。 在出现多线程竞争锁的情况时JVM 会自动撤销偏向锁因此偏向锁的撤销操作的耗时必须少于节省下来的 CAS原子操作的耗时。 综上所述轻量级锁用于提高线程交替执行同步块时的性能偏向锁则在某个线程交替执行同步块时进一步提高性能。 锁的状态总共有4种:无锁、偏向锁、轻量级锁和重量级锁。随着锁竞争越来越激烈锁可能从偏向锁升级到轻量级锁再升级到重量级锁但在Java 中锁只单向升级不会降级。 hashtable
9 分段锁 分段锁并非一种实际的锁而是一种思想用于将数据分段并在每个分段上都单独加锁,把锁进一步细粒度化以提高并发效率。ConcurrentHashMap在内部就是使用分段锁实现的。
9 CAS
CASCompare and Swap即比较再交换。
dk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的这是一种独占锁也是是悲观锁。
对CAS的理解CAS是一种无锁算法CAS有3个操作数要更新的变量V旧的预期值E要修改的新值N。当且仅当V和E相同的情况下将内存值V修改为N否则什么都不做。
10 ABA问题
ABA问题是指在CAS操作时其他线程将变量值A改为了B但是又被改回了A等到本线程使用期望值A与当前变量进行比较时发现变量A没有变于是CAS就将A值进行了交换操作但是实际上该值已经被其他线程改变过。 解决办法 在变量前面加上版本号每次变量更新的时候变量的版本号都1即A-B-A就变成了1A-2B-3A。只要变量被某一线程修改过变量对应的版本号就会发生递增变化从而解决了ABA问题。
11 锁升级
没有优化以前synchronized是重量级锁悲观锁使用 wait 和 notify、notifyAll 来切换线程状态非常消耗系统资源线程的挂起和唤醒间隔很短暂这样很浪费资源影响性能。所以 JVM 对 synchronized 关键字进行了优化把锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。 锁的级别从低到高依次为无锁 - 偏向锁 - 轻量级锁 - 重量级锁这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。 1、无锁 没有对资源进行锁定所有的线程都能访问并修改同一个资源但同时只有一个线程能修改成功其他修改失败的线程会不断重试直到修改成功。
2、偏向锁 偏向锁的核心思想就是锁会偏向第一个获取它的线程该线程是不会主动释放偏向锁的只有当其他线程尝试竞争偏向锁才会被释放。在接下来的执行过程中该锁没有其他的线程获取则持有偏向锁的线程永远不需要再进行同步。
当一个线程访问同步块并获取锁的时候会在对象头和栈帧中的锁记录里存储偏向的线程 ID以后该线程在进入和退出同步块时不需要进行 CAS 操作来加锁和解锁只需要检查当前 Mark Word 中存储的线程是否为当前线程如果是则表示已经获得对象锁否则需要测试 Mark Word 中偏向锁的标志是否为1如果没有则使用 CAS 操作竞争锁如果设置了则尝试使用 CAS 将对象头的偏向锁指向当前线程。
偏向锁的撤销需要在某个时间点上没有字节码正在执行时先暂停拥有偏向锁的线程然后检查持有偏向锁的线程是否活着。如果线程不处于活动状态则将对象头设置成无锁状态并撤销偏向锁如果线程处于活动状态升级为轻量级锁的状态。
3、轻量级锁 轻量级锁是指当锁是偏向锁的时候被第二个线程 B 所访问此时偏向锁就会升级为轻量级锁线程 B 会通过自旋的形式尝试获取锁线程不会阻塞从而提高性能。
当前只有一个等待线程则该线程将通过自旋进行等待。但是当自旋超过一定的次数时轻量级锁便会升级为重量级锁当一个线程已持有锁另一个线程在自旋而此时又有第三个线程来访时轻量级锁也会升级为重量级锁。
4、重量级锁 指当有一个线程获取锁之后其余所有等待获取该锁的线程都会处于阻塞状态。
重量级锁将程序运行交出控制权将线程挂起由操作系统来负责线程间的调度负责线程的阻塞和执行。这样会出现频繁地对线程运行状态的切换线程的挂起和唤醒消耗大量的系统资源导致性能低下。
重量级锁通过对象内部的监视器monitor实现而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现操作系统实现线程之间的切换需要从用户态切换到内核态切换成本非常高。 总结锁的升级过程如果只有一个线程获取到锁这个锁就是偏向锁因为对象头和栈帧中的锁记录里存储偏向的线程 ID如果没有别的线程来竞争锁那么直接执行代码如果有别的线程在竞争锁那么会释放偏向锁线程会通过自旋的方式尝试获取锁这个阶段的锁叫自旋锁轻量级锁如果经过多次自旋还是没有获取到锁那么就会变成重量级锁。
原因
假设有两个线程t1,t2
如果t1获取到锁以后1ms以后就释放锁了这时候用轻量级锁会更好一点因为自旋锁不会释放资源因为线程进入阻塞就绪CPU调度竞争锁资源都是需要时间的这中间的时间可能要几十毫秒效率会比较低。
如果t1获取到锁以后60s以后才释放锁如果这时候t2一直处于自旋状态自旋状态是不会释放锁的一直占用cpu资源。
12 锁升级代码
1 java对象组成 2 代码
新建maven项目导入依赖 dependency groupIdorg.openjdk.jol/groupId artifactIdjol-core/artifactId version0.9/version /dependency main方法中测试
import org.openjdk.jol.info.ClassLayout;public class Test {public static void main(String[] args) {A a new A();System.out.println(ClassLayout.parseInstance(a).toPrintable());}
}
class A{public int a 4;public long b 5L;public boolean c true;
} 结果 3 mark word
mark word占用了8个字节64位 markword的结构定义在markOop.hpp文件 4 查看hashcode
代码中添加 System.out.println(Integer.toHexString(a.hashCode())); 4 查看偏向锁
BiasedLockingStartupDelay表示系统启动几秒钟后启用偏向锁。默认为4秒原因在于系统刚启动时一般数据竞争是比较激烈的此时启用偏向锁会降低性能。由于这里为了测试偏向锁的性能所以把延迟偏向锁的时间设置为0
JVM参数设置 -XX:BiasedLockingStartupDelay0
代码 public class Test {public static void main(String[] args) {A a new A();
// System.out.println(Integer.toHexString(a.hashCode()));System.out.println(ClassLayout.parseInstance(a).toPrintable());synchronized (a){System.out.println(ClassLayout.parseInstance(a).toPrintable());}synchronized (a){System.out.println(ClassLayout.parseInstance(a).toPrintable());}}
}
class A{public int a 4;public long b 5L;public boolean c true;
} 5 查看轻量级锁 package com.yang.test;import org.openjdk.jol.info.ClassLayout;public class Test {public static void main(String[] args) {A a new A();
// System.out.println(Integer.toHexString(a.hashCode()));System.out.println(ClassLayout.parseInstance(a).toPrintable());synchronized (a){System.out.println(ClassLayout.parseInstance(a).toPrintable());}synchronized (a){System.out.println(ClassLayout.parseInstance(a).toPrintable());}new Thread(()-{synchronized (a){System.out.println(ClassLayout.parseInstance(a).toPrintable());}}).start();}
}
class A{public int a 4;public long b 5L;public boolean c true;
} 6 重量级锁
package com.yang.test;import org.openjdk.jol.info.ClassLayout;public class Test2 {private static Object lock new Object();public static void main(String[] args) throws InterruptedException {// 去掉偏向锁延时操作System.out.println(ClassLayout.parseInstance(lock).toPrintable()); //无锁new Thread(()-{System.out.println(子线程获取锁之前打印对象头信息);System.out.println(ClassLayout.parseInstance(lock).toPrintable()); //无锁synchronized (lock){try {System.out.println(子线程获取到锁打印对象头信息);System.out.println(ClassLayout.parseInstance(lock).toPrintable());Thread.sleep(5000);System.out.println(---子线程----);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(子线程释放锁打印对象头信息);System.out.println(ClassLayout.parseInstance(lock).toPrintable());},子线程).start();Thread.sleep(1000);sync();}public static void sync(){synchronized (lock){System.out.println(主线程获取到锁打印对象头信息);System.out.println(ClassLayout.parseInstance(lock).toPrintable());}}
}关于线程id和hashcode的问题
HotSpot VM的锁实现机制是
当一个对象已经计算过identity hash code它就无法进入偏向锁状态 当一个对象当前正处于偏向锁状态并且需要计算其identity hash code的话则它的偏向锁会被撤销并且锁会膨胀为轻量级锁或者重量锁 轻量级锁的实现中会通过线程栈帧的锁记录存储Displaced Mark Word重量锁的实现中ObjectMonitor类里有字段可以记录非加锁状态下的mark word其中可以存储identity hash code的值。