Java并发编程 —— 锁策略

乐观锁
  • 认为一般情况西不会发生冲突,只有在进行数据更新的时候,才会检测并发冲突,如果没有冲突则执行修改,如果有冲突则返回修改。
  • CAS(乐观锁的具体实现)Compare And Swap —— 比较并且交换
    CAS中的三个组成部分:
    V(内存值)、A(旧值)、B(新值)
    实现原理:
    V == A?true(没有并发冲突) —> V = B
        false(并发冲突) —> 该线程将旧值A修改为此时内存中的V值,然后再进行一次比较
  • 乐观锁的事项 Atomic*
    Atomic*(乐观锁的实现)也可以解决线程不安全的问题
package LockStrategy;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * 使用Atomic类解决线程不安全问题
 * **/
public class ThreadDemo90 {
    private  static  AtomicInteger count = new AtomicInteger(0);
    private  static  final  int MAXSEIZ = 100000;
    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < MAXSEIZ; i++) {
                    count.getAndIncrement();
                }
            }
        });
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < MAXSEIZ; i++) {
                    count.getAndDecrement();
                }
            }
        });
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最终结果为:" + count);

    }
//    public static void main(String[] args) {
//        AtomicInteger count = new AtomicInteger(0);
//        count.getAndIncrement(); // i++
//        count.incrementAndGet(); // ++i
//    }
}
  • 问:CAS的底层实现原理是什么?
    答:Java 层面 CAS 的原理是 UnSafe 类,UnSafe 调用了 C++ 的本地方法,通过调用操作系统的 Atomic::cmpxchg(原子指令)来实现CAS操作(底层方法是native修饰的)

  • 乐观锁性能比较高

  • CAS 的缺点:
    CAS 存在 ABA 问题 —> AtomicInteger 是存在 ABA 问题的
    A表示预期旧值,B表示新值
    举例(银行转账):
      甲给乙转账100块钱,点击了两次提交,如下图,这样有100块钱就消失了
    在这里插入图片描述

  • 上述问题代码实现

package LockStrategy;
import java.util.concurrent.atomic.AtomicReference;
/**
 * ABA 问题演示(转账示例)
 * **/
public class ThreadDemo91 {
    private static AtomicReference money =
            new AtomicReference(100); // 初始化余额为100

    public static void main(String[] args) throws InterruptedException {

        // 转账线程1  -100
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                // AtomicReference 自带的比较并修改方法
                boolean result = money.weakCompareAndSet(100,0);  // 这就是一个转账操作
                System.out.println("第一次转账是否成功:" + result);
            }
        });
        t1.start();
        t1.join();
        // 转入100 元
        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                boolean result = money.compareAndSet(0,100);
                System.out.println("转入100元:" + result);
            }
        });
        t3.start();
        t3.join();
        // 转账线程2  -100
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                // AtomicReference 自带的比较并修改方法
                boolean result = money.weakCompareAndSet(100,0);  // 这就是一个转账操作
                System.out.println("第二次转账是否成功:" + result);
            }
        });
        t2.start();
    }
}
  • 如何解决改ABA问题?
    引入版本号,每次在修改之后跟新版本号,使用AtomicStampedReference
package LockStrategy;

import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * 使用 AtomicStampedReference 解决ABA问题 
 *  核心: 多了一个版本号
 * **/

public class ThreadDemo92 {
    private static AtomicStampedReference money =
            new AtomicStampedReference(100,1);
//    private static AtomicReference money =
//            new AtomicReference(100); // 初始化余额为100

    public static void main(String[] args) throws InterruptedException {

        // 转账线程1  -100
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                // AtomicReference 自带的比较并修改方法
                // 四个参数 : 旧值 、 预期新值、 旧版本号 、 新版本号
                boolean result = money.weakCompareAndSet(100,0,1,2);  // 这就是一个转账操作
                System.out.println("第一次转账是否成功:" + result);
            }
        });
        t1.start();
        t1.join();
        // 转入100 元
        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                boolean result = money.compareAndSet(0,100,2,3);
                System.out.println("转入100元:" + result);
            }
        });
        t3.start();
        t3.join();
        // 转账线程2  -100
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                // AtomicReference 自带的比较并修改方法
                boolean result = money.weakCompareAndSet(100,0,1,2);  // 这就是一个转账操作
                System.out.println("第二次转账是否成功:" + result);
            }
        });
        t2.start();
    }
}
  • 如果将转账的金额全部求改为1000,运行后发现全是false
    这是由于Integer 的高速缓存范围为 -128 ~ 127 ,若不在这个范围内,就会new一个新的对象
    想要解决这个问题,修改JVM的缓存就可,在IDEA的Run/Debug Configurations中修改VM optins-Djava.lang.Integer.IntegerCache.high=1000 ,这段的意思就是设置高速缓存的最大值
悲观锁
  • 认为通常情况下会出现并发冲突,所以在一开始就会加锁
    例如 synchronized
共享锁/非共享锁(独占锁)
  • 共享锁:一把锁可以被多个线程拥有,叫做共享锁。
    读写锁中的读锁就是共享锁
    读写锁:就是将一个锁分为两个,一个用于读数据的锁(也叫做读锁),另一把锁叫做写锁,读锁是可以被多个线程同时拥有的,而写锁则只能被一个线程拥有
    ReentrantReaderWriterLock()
    读写锁的优势:锁的粒度更加的小,性能也更高

  • 非共享锁:一把锁只能被一个线程拥有,叫做非共享锁。synchronized

  • 代码实现读写锁:

package LockStrategy;

import java.util.Date;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ThreadDemo96 {

    public static void main(String[] args) {
        // 创建一个读写锁
        ReentrantReadWriteLock readWriteLock =
                new ReentrantReadWriteLock();// true 表示为公平锁,默认为false

        // 读锁
        ReentrantReadWriteLock.ReadLock readLock =
                readWriteLock.readLock();

        // 写锁
        ReentrantReadWriteLock.WriteLock writeLock =
                readWriteLock.writeLock();

        // 声明线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                10,10,0, TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(1000)
        );

        // 任务1:执行读锁
        executor.execute(new Runnable() {
            @Override
            public void run() {
                // 加锁
                readLock.lock();
                try {
                    // 业务逻辑处理
                    System.out.println("线程名:"+ Thread.currentThread().getName() +
                            "执行了读锁操作:" + new Date());
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }finally {
                    // 释放锁
                    readLock.unlock();
                }
            }
        });
        // 任务2:执行读锁
        executor.execute(new Runnable() {
            @Override
            public void run() {
                // 加锁
                readLock.lock();
                try {
                    // 业务逻辑处理
                    System.out.println("线程名:"+ Thread.currentThread().getName() +
                            "执行了读锁操作:" + new Date());
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }finally {
                    // 释放锁
                    readLock.unlock();
                }
            }
        });
        // 任务3:执行写锁
        executor.execute(new Runnable() {
            @Override
            public void run() {
                // 加锁
                writeLock.lock();
                try {
                    // 业务逻辑
                    System.out.println("线程名:"+ Thread.currentThread().getName() +
                            "执行了写锁操作:" + new Date());
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    writeLock.unlock();
                }
            }
        });

        // 任务4:执行写锁
        executor.execute(new Runnable() {
            @Override
            public void run() {
                // 加锁
                writeLock.lock();
                try {
                    // 业务逻辑
                    System.out.println("线程名:"+ Thread.currentThread().getName() +
                            "执行了写锁操作:" + new Date());
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    writeLock.unlock();
                }
            }
        });

    }
}
  • 注意事项:读写锁中的读锁和写锁是互斥的

公平锁/非公平锁
  • 公平锁:锁的获取顺序必须和线程方法的先后顺序保持一致,就角左公平锁。
    公平锁的优点:执行是有序的,结果是可以预期的
    new ReentrantLock(true)
  • 非公平锁:锁得获取顺序和线程获取锁的前后顺序无关,就叫做公平锁(默认所策略)
    非公平锁的优点:性能比较高的
    new ReentrantLock() / new ReentrantLock(false) / synchronized

自旋锁
  • 通过死循环一直尝试获取锁
while(true){
	if(尝试获取锁){
		return;
	}
}
可重入锁
  • 当一个线程获取了一个锁之后,可以重复的进入
package LockStrategy;
/**
 * 演示可重入锁
 * **/
public class ThreadDemo97 {
    // 创建锁
    private static Object lock = new Object();

    public static void main(String[] args) {
        // 第一次进入锁
        synchronized (lock){
            System.out.println("第一次进入锁");
            synchronized (lock){
                System.out.println("第二次进入锁");
            }
        }
    }

}

总结:
  • 问:你是怎么理解乐观锁和悲观锁的,具体是怎么实现的?
    答:乐观锁认为通常情况下不会出现并发冲突,所以只有在提交数据的时候才会检测是否有冲突;悲观锁认为事情总是发生在最坏的情况,所以每次在第一次进入之后都叫进行加所操作
    具体实现:
    1、乐观锁的实现为: 有一种实现机制为 CAS(比较并替换),CAS在JAVA中的具体实现是 Atomic家族。CAS 是由 V(内存中值)、 A(预期旧值、B(预期新值)三个组成,在执行的时候,使用V和A进行对比,如果结果为 true 这表明没有并发冲突,则可以直接修改,否则不能修改。 CAS 使用过调用C++ 提供的 UnSafe 中的本地方法(CompareAndSwapXXX)来实现。C++是通过调用操作系统的Atomic::cmpxchg(原子指令)来实现的
    2、悲观锁的实现为:synchronized 在Java中是将锁的的 ID 存放到对象头来实现,synchronized 在 JVM 层面是通过监视锁来实现,synchronized 在操作系统层面是通过互斥锁 mutex 实现

  • 问:你是否了解读写锁?
    答:读写锁是将锁的粒度分的更细了,分为读操作和写操作,并且读操作和写操作之间是互斥的,互斥的原因是为了防止出现脏数据(脏读)。读锁是可以多个线程共同拥有的,所以可以是共享锁;写锁是独占锁,在一个线程操作的时候其他线程不能操作。读写锁在java中使用ReentrantReadWriteLock创建,通过readWriteLock.readLock()readWriteLock.writeLock()分别得到读锁和写锁

  • 问:什么叫做自旋锁,自旋锁的缺点是什么?
    答:自旋锁就是通过死循环一直尝试去获取锁。自旋锁的缺点是,如果发生死锁,则会一直自旋(循环),会带来额外的开销。java中通过对对自旋增加明确的次数,当自旋次数大于规定次数还没有获取到锁,那就将它放到锁的等待队列中,等待有锁的时候再去唤醒它

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值