Java并发编程(Java中的锁)
文章目录
Java锁
- Java锁的作用?
Java中的锁主要用于保障多并发线程情况下数据的一致性。 - 怎样保障数据的一致性?
在多线程编程中为了保障数据的一致性,我们通常需要在使用对象或者方法之前加锁,这时如果有其他线程线程也需要使用该对象或者该方法,则首先要获得锁,如果某个线程发现锁正在被其他线程使用,就会进入阻塞队列等待锁的释放,直到其他线程执行完成并释放锁,该线程才有机会再次获得锁进行操作。这样就保障了在同一个时刻只有一个线程持有该对象的锁并修改对象,从而保障数据的安全。 - 所得分类:
- 乐观和悲观角度:乐观锁和悲观锁
- 获取资源的的公平性角度:公平锁和非公平锁
- 是否共享资源的角度:共享锁和独享锁
- 锁的状态:偏向锁、轻量级锁和重量级锁
- JVM设计了自旋锁==》更快的使用CPU资源
乐观锁
- 乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
- java 中的乐观锁基本都是通过 CAS(Compare And Swap,比较和交换) 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
悲观锁
- 悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。
- Java中的悲观锁大部分基于AQS(Abstract Queued Synchronized,抽象的队列同步器)架构实现。AQS定义了一套多线程访问共享资源的同步框架,许多同步类的实现都依赖于它,例如常用的Synchronized、ReentrantLock、Semaphore、CountDownLatch等AQS框架下的锁则是先尝试CAS乐观锁去获取锁,获取不到,才会转换为悲观锁,如 RetreenLock。
自旋锁
- 自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
- 线程自旋是需要消耗 cpu的,说白了就是让 cpu 在做无用功,如果一直获取不到锁,那线程也不能一直占用 cpu 自旋做无用功,所以需要设定一个自旋等待的最大时间。
- 如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
自旋锁的优缺点
- 优点:自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的CPU消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换,所以自旋锁减少了CPU上下文的切换。
- 缺点:但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用 cpu 做无用功,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要 cpu 的线程又不能获取到 cpu,造成 cpu 的浪费。所以这种情况下我们要关闭自旋锁。
自旋锁的时间阈值
- 自旋锁用于让当前线程占着CPU的资源不释放,等到下次自旋获取锁资源后立即执行相关操作。但是如何选择自旋的执行时间呢?**如果自旋的执行时间太长,则会有大量的线程处于自旋状态且占用CPU资源,造成系统资源浪费。**因此,对自旋的周期选择将直接影响到系统的性能!
- JDK的不同版本所采用的自旋周期不同,JDK 1.5为固定DE时间,JDK 1.6引入了适应性自旋锁。适应性自旋锁的自旋时间不再是固定值,而是由上一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的,可基本认为一个线程上下文切换的时间是就一个最佳时间。
synchronized
- synchronized关键字用于为Java对象、方法、代码块提供线程安全的操作。
- synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重入锁。
- 在使用synchronized修饰对象时,同一时刻只能有一个线程对该对象进行访问;在synchronized修饰方法、代码块时,同一时刻只能有一个线程执行该方法体或代码块,其他线程只有等待当前线程执行完毕并释放锁资源后才能访问该对象或执行同步代码块。
- Java中的每个对象都有个monitor对象,加锁就是在竞争monitor对象。对代码块加锁是通过在前后分别加上monitorenter和monitorexit指令实现的,对方法是否加锁是通过一个标记位来判断的。
synchronized的作用范围
- 作用于成员变量和非静态方法时,锁住的是对象的实例(this);
- 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
- synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
synchronized的用法简介
1. 修饰方法
public synchronized void method(){
//todo
}
2. 修饰代码块
public void method(){
synchronized (this){
//todo
}
}
- 修饰方法和修饰一个代码块类似,只是作用范围不一样,修饰代码块是大括号括起来的范围,而修饰方法范围是整个函数,两种方法是等价的,都是锁定了整个方法的内容。
- 具体示例:
上锁:
public class SynchronizedThread implements Runnable{
private static int count;
//构造器初始化count
public SynchronizedThread() {
count = 0;
}
@Override
public void run() {
synchronized (this){
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName()+":"+(count++));
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
}
调用:
- 情况一:两个线程t1和t2拿的是不同的Runnable对象
public class Demo02 extends TestThread{
public static void main(String[] args) {
SynchronizedThread s1 = new SynchronizedThread();
SynchronizedThread s2 = new SynchronizedThread();
Thread t1 = new Thread(s1);
Thread t2 = new Thread(s2);
t1.start();
t2.start();
}
}
运行结果:可以看出两个线程同时在执行,为什么?我们可以看到synchronized的作用范围,作用对象是方法的时候,锁住的是对象的实例(this),而我们的两个线程分别拿的并不是同一个对象,每个对象只有一个锁与之相关联。
Thread-0:0
Thread-1:1
Thread-0:2
Thread-1:3
Thread-0:4
Thread-1:4
Thread-0:5
Thread-1:6
Thread-1:7
Thread-0:7
- 情况二:针对情况一,我们对两个线程传入同一个Runnable对象
public class Demo02 extends TestThread{
public static void main(String[] args) {
SynchronizedThread s = new SynchronizedThread();
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
t1.start();
t2.start();
}
}
运行结果:我们可以看到t1线程完成后,t2线程才可以开始。这是因为两个并发线程访问同一个对象的synchronized 代码块时,在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。
Thread-0:0
Thread-0:1
Thread-0:2
Thread-0:3
Thread-0:4
Thread-1:5
Thread-1:6
Thread-1:7
Thread-1:8
Thread-1:9
- 银行存钱取钱的例子:
- 账户:
public class Account {
private String name;
private double amount;
public Account(String name, double amount) {
this.name = name;
this.amount = amount;
}
/**
* 存钱
* @param money 存的钱
*/
public void saveMoney(double money){
amount = amount + money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 取钱
* @param money 取的钱
*/
public void drawMoney(double money){
amount = amount - money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 查看余额
* @return
*/
public double getAmount() {
return amount;
}
}
- 账户操作类:
public class AccountOperator implements Runnable{
private Account account;
public AccountOperator(Account account) {
this.account = account;
}
@Override
public void run() {
//一个用户账号存钱 取钱的时候, 同一个账号 在其他地方不能进行存钱取钱
synchronized (this){
//取钱
account.drawMoney(1000);
//存钱
account.saveMoney(1500);
System.out.println(Thread.currentThread().getName()+":"+account.getAmount());
}
}
}
- 测试
public class Demo {
public static void main(String[] args) {
//账户
Account account = new Account("小明",2500);
//银行
AccountOperator operator = new AccountOperator(account);
//多个银行对同一个账户进行存取钱操作
Thread t1 = new Thread(operator,"银行A");
Thread t2 = new Thread(operator,"银行B");
Thread t3 = new Thread(operator,"银行C");
t1.start();
t2.start();
t3.start();
}
}
运行结果:我们每次都是取1000,存1500,所以每次都会增加500
银行A:3000.0
银行C:3500.0
银行B:4000.0
- 两个并发线程访问同一个对象,线程A执行synchronized的方法时候,线程B只执行非synchronized的方法并不会受阻塞。
3. 修饰静态方法:锁定这个类的所有对象
public synchronized static void method(){
//todo
}
具体用法:
public class SynchronizedThread implements Runnable{
private static int count;
//构造器初始化count
public SynchronizedThread() {
count = 0;
}
public synchronized static void method(){
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName()+":"+(count++));
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
@Override
public synchronized void run() {
method();
}
}
调用:
public class Demo02 extends TestThread{
public static void main(String[] args) {
SynchronizedThread s1 = new SynchronizedThread();
SynchronizedThread s2 = new SynchronizedThread();
Thread t1 = new Thread(s1);
Thread t2 = new Thread(s2);
t1.start();
t2.start();
}
}
运行结果:我们可以看到两个线程是访问类SynchronizedThread的两个不同对象,但是线程2还是受阻塞了,原因就是线程1和线程2都是调用的synchronized修饰的静态方法,锁住的是这个SynchronizedThread类。
Thread-0:0
Thread-0:1
Thread-0:2
Thread-0:3
Thread-0:4
Thread-1:5
Thread-1:6
Thread-1:7
Thread-1:8
Thread-1:9
4. 修饰一个类
public static void method(){
synchronized (SynchronizedThread.class){
//todo
}
}
具体使用:(同步线程)
public class SynchronizedThread implements Runnable{
private static int count;
//构造器初始化count
public SynchronizedThread() {
count = 0;
}
public static void method(){
synchronized (SynchronizedThread.class){
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName()+":"+(count++));
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
@Override
public synchronized void run() {
method();
}
}
给class加锁和给静态方法加锁是一样的,所有对象公用一把锁。
5.小结
- synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
- 在定义接口方法时不能使用synchronized关键字。
- 构造方法不能使用synchronized关键字,但是可以使用synchronized代码块来进行同步。
- synchronized关键字不能被继承。虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。例如:在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法(super.method()),这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。
synchronized的实现原理
在synchronized内部包括ContentionList、EntryList、WaitSet、OnDeck、Owner、! Owner这6个区域,每个区域的数据都代表锁的不同状态。
- Wait Set:等待集合,哪些调用 wait 方法被阻塞的线程被放置在这里。
- Contention List:锁竞争队列,所有请求锁的线程首先被放在这个竞争队列中。
- Entry List:竞争候选列表,Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中。
- OnDeck:竞争候选者,在同一时刻,最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck;
- Owner:当前已经获取到锁资源的线程被称为 Owner;
- !Owner:在Owner线程释放锁后,会从Owner的状态变成!Owner。
synchronized实现:
-
synchronized在收到新的锁请求时首先自旋,如果通过自旋也没有获取锁资源,则将被放入锁竞争队列ContentionList中。
-
JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList 会被大量的并发线程进行 CAS (Compare And Swap,比较和交换)访问,为了降低对尾部元素的竞争,JVM 会将一部分线程移动到 EntryList 中作为候选竞争线程。
-
Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定EntryList 中的某个线程为 OnDeck 线程(一般是最先进去的那个线程)。
-
Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck,OnDeck 需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM 中,也把这种选择行为称之为“竞争切换”。
-
OnDeck 线程获取到锁资源后会变为 Owner 线程,而没有得到锁资源的仍然停留在 EntryList中。如果 Owner 线程被 wait 方法阻塞,则转移到 WaitSet 队列中,直到某个时刻通过 notify或者 notifyAll 唤醒,会重新进去 EntryList 中。
-
处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux 内核下采用 pthread_mutex_lock 内核函数实现的)。
-
Synchronized 是非公平锁。 Synchronized 在线程进入 ContentionList 时,等待的线程会先尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁资源。
-
每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的。
-
synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁消耗的时间比有用操作消耗的时间更多。
-
Java1.6,synchronized 进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后推出的 Java1.7 与 1.8 中,均对该关键字的实现机理做了优化。引入了偏向锁和轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。
-
锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀。
-
JDK 1.6 中默认是开启偏向锁和轻量级锁,可以通过-XX:-UseBiasedLocking 来禁用偏向锁。
ReentrantLock
- ReentantLock 继承接口 Lock 并实现了接口中定义的方法,他是一种可重入的独占锁,除了能完成synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。
- 独占锁指该锁在同一时刻只能被一个线程获取,而获取锁的其他线程只能在同步队列中等待;可重入锁指该锁能够支持一个线程对同一个资源执行多次加锁操作。
- 可重入锁也叫作递归锁,指在同一线程中,在外层函数获取到该锁之后,内层的递归函数仍然可以继续获取该锁。在Java环境下,ReentrantLock和synchronized都是可重入锁。
- ReentrantLock支持公平锁和非公平锁的实现。公平指线程竞争锁的机制是公平的,而非公平指不同的线程获取锁的机制是不公平的。
ReentrantLock的使用
ReentrantLock有显式的操作过程,何时加锁、何时释放锁都在程序员的控制之下。具体的使用流程是定义一个ReentrantLock,在需要加锁的地方通过lock方法加锁,等资源使用完成后再通过unlock方法释放锁。
ReentrantLock使用:
1)lock()必须紧跟try代码块,且unlock()要放到finally第一行。
2)ReentrantLock锁可以反复进入,允许连续两次获得同一把锁,两次释放同一把锁。
3)获取锁和释放锁的次数要相同,如果释放锁的次数多于获取锁的次数,Java就会抛出java.lang.IllegalMonitorStateException异常;如果释放锁的次数少于获取锁的次数,该线程就会一直持有该锁,其他线程将无法获取锁资源。
//step2:定义一一个ReentrantLock
public static ReentrantLock lock = new ReentrantLock();
.........
//step2:上锁
lock.lock();
//lock.lock(); 可重入锁
try {
i++;
}finally {
lock.unlock();//step3:释放锁
//lock.unlock();可重入锁
}
具体代码示例:
public class ReentrantLockDemo implements Runnable{
//Step1:
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0;
@Override
public void run() {
for (int j = 0; j < 10; j++) {
lock.lock();
//lock.lock(); 可重入锁
try {
i++;
}finally {
lock.unlock();
//lock.unlock();可重入锁
}
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockDemo reentrantLock = new ReentrantLockDemo();
Thread thread = new Thread(reentrantLock);
thread.start();
thread.join();
System.out.println(i);
}
}
ReentrantLock如何避免死锁
1.响应中断
在synchronized中如果有一个线程尝试获取一把锁,则其结果是要么获取锁继续执行,要么保持等待。ReentrantLock还提供了可响应中断的可能,即在等待锁的过程中,线程可以根据需要取消对锁的请求。
代码测试:
- 定义两把锁lock1和lock2,两个线程thread1和thread2,thread1先占用lock1,再占用lock2; thread2则先占用lock2,后占用lock1,这便形成了thread1和thread2之间的相互等待,在两个线程都启动时便处于死锁状态。
- 我们就可以采用响应中断来解决这个问题,设置一个等待时间,如果超过这个时间,设置某个线程主动中断,释放对lock的申请,同时释放以获得的lock,让其他线程获得lock继续执行下去。
- 代码示例中设置了,如果等待时间过长,等待时间设置为3s,thread2就会主动interrupt,释放对lock1的申请和以获得的lock2,让thread1获得lock2,继续执行。
public class InterruptResponse {
//Step1: 定义两把锁 lock1和lock2
public ReentrantLock lock1 = new ReentrantLock();
public ReentrantLock lock2 = new ReentrantLock();
//Step2: 定义两个线程 thread1和thread2
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
//lockInterruptibly() :如果当前线程未被中断,则获得锁
lock1.lockInterruptibly();
try {
//逻辑操作
Thread.sleep(500);
}catch (InterruptedException e){
e.printStackTrace();
}
lock2.lockInterruptibly();
System.out.println(Thread.currentThread().getName()+"执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//isHeldByCurrentThread():业务逻辑执行完后,检查当前线程释放持有该锁,有的话释放
if (lock1.isHeldByCurrentThread()){
lock1.unlock();
}
if (lock2.isHeldByCurrentThread()){
lock2.unlock();
}
System.out.println(Thread.currentThread().getName()+"退出");
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
//lockInterruptibly() :如果当前线程未被中断,则获得锁
lock2.lockInterruptibly();
try {
//逻辑操作
Thread.sleep(500);
}catch (InterruptedException e){
e.printStackTrace();
}
lock1.lockInterruptibly();
System.out.println(Thread.currentThread().getName()+"执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//isHeldByCurrentThread():业务逻辑执行完后,检查当前线程释放持有该锁,有的话释放
if (lock1.isHeldByCurrentThread()){
lock1.unlock();
}
if (lock2.isHeldByCurrentThread()){
lock2.unlock();
}
System.out.println(Thread.currentThread().getName()+"退出");
}
}
});
public static void main(String[] args) {
long time = System.currentTimeMillis();
InterruptResponse response = new InterruptResponse();
response.thread1.start();
response.thread2.start();
//自旋一段时间,如果等待时间过长,则会发生死锁等问题,主动中断并释放锁
while (true){
if (System.currentTimeMillis() - time >= 3000){
response.thread2.interrupt();
}
}
}
}
运行结果:
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at test.gaolang.reentrant.InterruptResponse$2.run(InterruptResponse.java:63)
at java.lang.Thread.run(Thread.java:748)
Thread-0执行完毕
Thread-1退出
Thread-0退出
2.可轮询锁
通过boolean tryLock()获取锁。如果有可用锁,则获取该锁并返回true,如果无可用锁,则立即返回false。
boolean b = lock2.tryLock();
3.定时锁
通过boolean tryLock(long time, TimeUnit unit) throws InterruptedException获取定时锁。如果在给定的时间内获取到了可用锁,且当前线程未被中断,则获取该锁并返回true。如果在给定的时间内获取不到可用锁,将禁用当前线程,并且在发生以下三种情况之前,该线程一直处于休眠状态。
- 当前线程获取到了可用锁并返回true。
- 当前线程在进入此方法时设置了该线程的中断状态,或者当前线程在获取锁时被中断,则将抛出InterruptedException,并清除当前线程的已中断状态。
- 当前线程获取锁的时间超过了指定的等待时间,则将返回false。如果设定的时间小于等于0,则该方法将完全不等待。
Lock 接口的主要方法
- void lock(): 执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁. 相反, 如果锁已经被其他线程持有, 将禁用当前线程, 直到当前线程获取到锁。
- boolean tryLock():如果锁可用, 则获取锁, 并立即返回 true, 否则返回 false. 该方法和lock()的区别在于, tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前线程被禁用, 当前线程仍然继续往下执行代码. 而 lock()方法则是一定要获取到锁, 如果锁不可用, 就一直等待, 在未获得锁之前,当前线程并不继续向下执行。
- void unlock():执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程并不持有锁, 却执行该方法, 可能导致异常的发生。
- Condition newCondition():条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的 await()方法,而调用后,当前线程将缩放锁。
- getHoldCount() :查询当前线程保持此锁的次数,也就是执行此线程执行 lock 方法的次数。
- getQueueLength():返回正等待获取此锁的线程估计数,比如启动 10 个线程,1 个线程获得锁,此时返回的是 9。
- getWaitQueueLength:(Condition condition)返回等待与此锁相关的给定条件的线程估计数。比如 10 个线程,用同一个 condition 对象,并且此时这 10 个线程都执行了condition 对象的 await 方法,那么此时执行此方法返回 10。
- hasWaiters(Condition condition):查询是否有线程等待与此锁有关的给定条件(condition),对于指定 contidion 对象,有多少线程执行了 condition.await 方法。
- hasQueuedThread(Thread thread):查询给定线程是否等待获取此锁。
- hasQueuedThreads():是否有线程等待此锁。
- isFair():该锁是否公平锁。
- isHeldByCurrentThread(): 当前线程是否保持锁锁定,线程的执行 lock 方法的前后分别是 false 和 true。
- isLock():此锁是否有任意线程占用。
- lockInterruptibly():如果当前线程未被中断,获取锁。
- tryLock():尝试获得锁,仅在调用时锁未被线程占用,获得锁。
- tryLock(long timeout TimeUnit unit):如果锁在给定等待时间内没有被另一个线程保持,则获取该锁。
公平锁和非公平锁
-
公平锁:
加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得 -
非公平锁:
加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待
- 非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护一个队列
- Java 中的 synchronized 是非公平锁,ReentrantLock 默认的 lock()方法采用的是非公平锁。
- 因为公平锁需要在多核的情况下维护一个锁线程等待队列,基于该队列进行锁的分配,因此效率比非公平锁低很多。Java中的synchronized是非公平锁,ReentrantLock默认的lock方法采用的是非公平锁。
共享锁和独占锁
- 共享锁
独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock 就是以独占方式实现的互斥锁。
独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线
程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。
- 独占锁
共享锁则允许多个线程同时获取锁,并发访问 共享资源,如:ReadWriteLock。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
1)AQS 的内部类 Node 定义了两个常量 SHARED 和 EXCLUSIVE,他们分别标识 AQS 队列中等待线程的锁获取模式。
2) java 的并发包中提供了 ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问,或者被一个 写操作访问,但两者不能同时进行。
ReadWriteLock 读写锁
为了提高性能,Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的,你只要上好相应的锁即可。
- 读锁
如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁 - 写锁
如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁! - Java 中读写锁有个接口 java.util.concurrent.locks.ReadWriteLock ,也有具体的实现ReentrantReadWriteLock。
- 代码实现:
public class ReaderWriter {
private final Map<String,Object> cache = new HashMap<String,Object>();
private final ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock();
//定义读写锁
private final Lock readLock = rwlock.readLock();
private final Lock writeLock = rwlock.writeLock();
//在读数据时,加读锁
public Object get(String key){
readLock.lock();
try {
return cache.get(key);
}finally {
readLock.unlock();
}
}
//写数据时加写锁
public Object put(String key,Object value){
writeLock.lock();
try {
return cache.put(key,value);
}finally {
writeLock.unlock();
}
}
}
- ReentrantReadWriteLock的加锁和解锁操作最终都调用内部类Sync提供的方法。Sync对象通过继承AQS(Abstract Queued Synchronizer)进行实现。AQS的内部类Node定义了两个常量SHARED和EXCLUSIVE,分别标识AQS队列中等待线程的锁获取模式。
ReentrantLock 与 synchronized
- ReentrantLock 通过方法 lock()与 unlock()来进行加锁与解锁操作,与 synchronized 会 被 JVM 自动解锁机制不同,ReentrantLock 加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用 ReentrantLock 必须在 finally 控制块中进行解锁操作。
- ReentrantLock 相比 synchronized 的优势是可中断、公平锁、多个锁。这种情况下需要使用 ReentrantLock。
Condition 类和 Object 类锁方法区别区别
- Condition 类的 awiat 方法和 Object 类的 wait 方法等效
- Condition 类的 signal 方法和 Object 类的 notify 方法等效
- Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效
- ReentrantLock 类可以唤醒指定条件的线程,而 object 的唤醒是随机的
tryLock 和 lock 和 lockInterruptibly 的区别
- tryLock若有可用锁,则获取该锁并返回true,否则返回false,不会有延迟或等待;tryLock(long timeout, TimeUnit unit)可以增加时间限制,如果超过了指定的时间还没获得锁,则返回false。
- lock若有可用锁,则获取该锁并返回true,否则会一直等待直到获取可用锁。
- 在锁中断时lockInterruptibly会抛出异常,lock不会。
重量级锁和轻量级锁
- 重量级锁是基于操作系统的互斥量(Mutex Lock)而实现的锁,会导致进程在用户态与内核态之间切换,相对开销较大。
- synchronized在内部基于监视器锁(Monitor)实现,监视器锁基于底层的操作系统的Mutex Lock实现,因此synchronized属于重量级锁。重量级锁需要在用户态和核心态之间做转换,所以synchronized的运行效率不高。
- JDK在1.6版本以后,为了减少获取锁和释放锁所带来的性能消耗及提高性能,引入了轻量级锁和偏向锁。
- 轻量级锁是相对于重量级锁而言的。轻量级锁的核心设计是在没有多线程竞争的前提下,减少重量级锁的使用以提高系统性能。轻量级锁适用于线程交替执行同步代码块的情况(即互斥操作),如果同一时刻有多个线程访问同一个锁,则将会导致轻量级锁膨胀为重量级锁。
偏向锁
-
除了在多线程之间存在竞争获取锁的情况,还会经常出现同一个锁被同一个线程多次获取的情况。偏向锁用于在某个线程获取某个锁之后,消除这个线程锁重入的开销,看起来似乎是这个线程得到了该锁的偏向(偏袒)。
-
偏向锁的主要目的是在同一个线程多次获取某个锁的情况下尽量减少轻量级锁的执行路径,因为轻量级锁的获取及释放需要多次CAS(Compare and Swap)原子操作,而偏向锁只需要在切换ThreadID时执行一次CAS原子操作,因此可以提高锁的运行效率。
-
在出现多线程竞争锁的情况时,JVM会自动撤销偏向锁,因此偏向锁的撤销操作的耗时必须少于节省下来的CAS原子操作的耗时。
-
轻量级锁用于提高线程交替执行同步块时的性能,偏向锁则在某个线程交替执行同步块时进一步提高性能。
-
锁的状态总共有4种:无锁、偏向锁、轻量级锁和重量级锁。随着锁竞争越来越激烈,锁可能从偏向锁升级到轻量级锁,再升级到重量级锁,但在Java中锁只单向升级,不会降级。
分段锁
分段锁并非一种实际的锁,而是一种思想,用于将数据分段并在每个分段上都单独加锁,把锁进一步细粒度化,以提高并发效率。ConcurrentHashMap在内部就是使用分段锁实现的。
锁优化
-
减少锁持有时间
只用在有线程安全要求的程序上加锁来尽量减少同步代码块对锁的持有时间。 -
减小锁粒度
将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。最最典型的减小锁粒度的案例就是ConcurrentHashMap。 -
锁分离
锁分离指根据不同的应用场景将锁的功能进行分离,以应对不同的变化,最常见的锁分离思想就是读写锁(ReadWriteLock),它根据锁的功能将锁分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,既保证了线程的安全性,又提高了性能。
操作分离思想可以进一步延伸为只要操作互不影响,就可以进一步拆分,比如LinkedBlockingQueue从头部取出数据,并从尾部加入数据。
- 锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化 。 - 锁消除
锁消除是在编译器级别的事情。在即时编译器时,如果发现不可能被共享的对象,则可以消除这
些对象的锁操作,多数是因为程序员编码不规范引起。
Semaphore信号量
-
Semaphore是一种基于计数的信号量,在定义信号量对象时可以设定一个阈值,基于该阈值,多个线程竞争获取许可信号,线程竞争到许可信号后开始执行具体的业务逻辑,业务逻辑在执行完成后释放该许可信号。在许可信号的竞争队列超过阈值后,新加入的申请许可信号的线程将被阻塞,直到有其他许可信号被释放。Semaphore 可以用来构建一些对象池,资源池之类的,比如数据库连接池。
-
Semaphore的基本用法:
//1:创建一个计数阈值为 5 的信号量对象:只能有5个线程同时访问
Semaphore semaphore = new Semaphore(5);
try {
//2:申请许可
semaphore.acquire();
try {
//3:业务逻辑代码
}catch (Exception e){
}finally {
//4:释放许可
semaphore.release();
}
}catch (InterruptedException e){
}
- Semaphore对锁的申请和释放和ReentrantLock类似,通过acquire方法和release方法来获取和释放许可信号资源。Semaphone.acquire方法默认和ReentrantLock. lockInterruptibly方法的效果一样,为可响应中断锁,也就是说在等待许可信号资源的过程中可以被Thread.interrupt方法中断而取消对许可信号的申请。
- Semaphore 也实现了可轮询的锁请求与定时锁的功能,除了方法名 tryAcquire 与 tryLock不同,其使用方法与 ReentrantLock 几乎一致。Semaphore 也提供了公平与非公平锁的机制,也可在构造函数中进行设定。
- Semaphore的锁释放操作也需要手动执行,因此,为了避免线程因执行异常而无法正常释放锁,释放锁的操作必须在finally代码块中完成。
AtomicInteger
- AtomicInteger ,一个提供原子操作的 Integer 的类,常见的还有AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference 等,他们的实现原理相同,区别在与运算对象类型的不同。令人兴奋地,还可以通过 AtomicReference<V>将一个对象的所有操作转化成原子操作。
- 我们知道,在多线程程序中,诸如++i 或 i++等运算不具有原子性,是不安全的线程操作之一。我们可以通过synchronized或ReentrantLock将该操作变成一个原子操作,但是synchronized和ReentrantLock均属于重量级锁。但 JVM 为此类操作特意提供了一些同步类,使得使用更方便,且使程序运行效率变得更高。通过相关资料显示,通常AtomicInteger的性能是 ReentantLock 的好几倍。
- 使用方法:
public class AtomicIntegerDemo implements Runnable {
//step 1:定义一个原子操作数
static AtomicInteger safeCounter = new AtomicInteger(0);
@Override
public void run() {
for (int m = 0; m < 1000000; m++) {
safeCounter.getAndIncrement(); //step 2:对原子操作数执行自增操作
}
}
}
public class AtomicIntegerDemoTest {
public static void main(String[] args) throws InterruptedException {
AtomicIntegerDemo mt = new AtomicIntegerDemo();
Thread t1 = new Thread(mt);
Thread t2 = new Thread(mt);
t1.start();
t2.start();
Thread.sleep(500);
System.out.println(AtomicIntegerDemo.safeCounter.get());
}
}
运行结果:2000000
- 原子操作即是进行过程中不能被中断的操作,针对某个值的原子操作在被进行的过程中,CPU绝不会再去进行其他的针对该值的操作。为了实现这样的严谨性,原子操作仅会由一个独立的CPU指令代表和完成。原子操作是无锁的,常常直接通过CPU指令直接实现。
本文地址:https://blog.csdn.net/qq_43466788/article/details/109203544