03多线程常见锁

本文探讨了公平锁与非公平锁的区别,解释了ReentrantLock的公平模式,并介绍了可重入锁、自旋锁和读写锁在并发编程中的应用,如synchronized和ReentrantReadWriteLock的使用示例。了解这些概念有助于提高多线程程序的效率和正确性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1公平锁、非公平锁 

概念:所谓公平锁,就是多个线程按照申请锁的顺序来获取锁,类似排队,先到先得。而非公平锁,则是多个线程抢夺锁,会导致优先级反转或饥饿现象
区别:公平锁在获取锁时先查看此锁维护的等待队列,为空或者当前线程是等待队列的队列,则直接占有锁,否则插⼊到等待队列,FIFO原则。
非公平锁比较粗鲁,上来直接先尝试占有锁,失败则采⽤公平锁方式。非公平锁的优点是吞吐量比公平锁更大。
synchronized 和 juc.ReentrantLock 默认都是非公平锁。ReentrantLock 在构造的时候传入 true则是公平锁。

private final ReentrantLock lock = new ReentrantLock(true);//公平锁

private final ReentrantLock lock = new ReentrantLock(false);//非公平锁

查看ReentrantLock源码的构造方法 默认是非公平锁

使用方法为:

class X {
   private final ReentrantLock lock = new ReentrantLock();
   // ...

   public void m() { 
     lock.lock();  // block until condition holds
     try {
       // ... method body
     } finally {
       lock.unlock()
     }
   }
 }

2可重入锁、递归锁

可重入锁又叫递归锁,指的同⼀个线程在外层方法获得锁时,进入内层方法会自动获取锁。也就是说,线程可以进⼊任何⼀个它已经拥有锁的代码块。比如method01方法里面有 method02 方法,两个方法都有同⼀把锁,得到了method01的锁,就自动得到了method02的锁。就像有了家门的锁,厕所、书房、厨房就为你敞开了⼀样。可重⼊锁可以避免死锁的问题。

public synchronized void method1(){
    method2();
}

public synchronized void method2() {
}

 synchronized 和 ReentrantLock()默认支持可重入锁

2.1使用synchronized

/**
 * 可重入锁
 */
class PhonePlus {
    public synchronized void sendEmail(){
        System.out.println(Thread.currentThread().getName()+"=====sendEmail.....");
        sendMessage();
    }

    public synchronized void sendMessage(){
        System.out.println(Thread.currentThread().getName()+"=====sendMessage....");
    }
}

public class ReentrantLockDemo {
    public static void main(String[] args) {
        PhonePlus phonePlus = new PhonePlus();
        new Thread(()->{
            phonePlus.sendEmail();
        }, "A").start();

        new Thread(()->{
            phonePlus.sendEmail();
        }, "B").start();
    }
}

 

2.2 使用ReentrantLock();

class PhonePlus {
    Lock lock = new ReentrantLock();
    public void method01(){
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"=====method01.....");
            method02();
        } finally {
            lock.unlock();
        }
    }

    public void method02(){
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"=====method02.....");
        } finally {
            lock.unlock();
        }
    }
}

public class ReentrantLockDemo {
    public static void main(String[] args) {
        PhonePlus phonePlus = new PhonePlus();
        new Thread(()->{
            phonePlus.method01();
        }, "A").start();
        new Thread(()->{
            phonePlus.method01();
        }, "B").start();
    }
}

 

2.3验证内层方法是否获得锁

public void method02(){//去掉同步锁
    System.out.println(Thread.currentThread().getName()+"=====method02.....");
}
for (int i = 0; i < 100; i++) {//创建100线程实现高并发
    new Thread(()->{
        phonePlus.method01();
    }, i+"").start();
}

从运行结果看 method02()不加同步锁,也不会出现并发问题

3自旋锁 

所谓自旋锁,就是尝试获取锁的线程不会立即阻塞,而是采⽤循环的方式去尝试获取。自己在那儿一直循环获取,就像“自旋”⼀样。这样的好处是减少线程切换的上下文开销,缺点是会消耗CPU。CAS底层的 getAndAddInt 就是自旋锁思想

//跟CAS类似,⼀直循环比较。

while (!atomicReference.compareAndSet(null, thread)) { }

3.1CAS 比较并交换

CAS的全称为Compare-And-Swap,比较并交换,是⼀种很重要的同步思想。它是⼀条CPU并发原语。

它的功能是判断主内存某个位置的值是否为跟期望值⼀样,相同就进行修改,否则⼀直重试,直到⼀致为止。这个过程是原子的。

public static void main(String[] args) {
    AtomicInteger atomicInteger = new AtomicInteger(5);
    System.out.println(atomicInteger.compareAndSet(5, 2000) +"\t 当前数据值:" + atomicInteger.get());
    //修改
    System.out.println(atomicInteger.compareAndSet(5, 1000) +"\t 当前数据值:" + atomicInteger.get());
}

参考以前博客代码:01volatile关键字 的2.2原子性例子

查看AtomicInteger.getAndIncrement()源码

getAndAddInt()采用的是自旋锁的方式,while()判断条件内会判断共享内存中是否跟期望值相同,相同则进入循环体

3.2自旋锁实例代码:

/**
 * 自旋锁
 */
class WC {

    AtomicReference<Thread> atomicReference = new AtomicReference<>();
    //锁门
    public void lock() {
        Thread currentThread = Thread.currentThread();
        //判断当前wc中 是否有人使用
        while(!atomicReference.compareAndSet(null, currentThread)){
            System.out.println(Thread.currentThread().getName()+"等待进入中===");
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) {e.printStackTrace(); }
        }
        System.out.println(Thread.currentThread().getName()+"====在卫生间中...");
    }

    //开门
    public void unlock() {
        Thread currentThread = Thread.currentThread();
        atomicReference.compareAndSet(currentThread, null);
        System.out.println(currentThread.getName()+"=====unlock...");
    }
}

public class SpinLockDemo {
    public static void main(String[] args) {
        WC wc = new WC();
        new Thread(()->{
            wc.lock();//锁门
            try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) {e.printStackTrace(); }
            wc.unlock();//开门
        }, "AA").start();
        new Thread(()->{
            wc.lock();//锁门
            try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) {e.printStackTrace(); }
            wc.unlock();//开门
        }, "BB").start();
    }
}

 

 自旋锁好处:循环比较获取直到成功为⽌,没有类似wait的阻塞。

通过CAS操作完成⾃旋锁,A线程先进来调⽤lock方法自己持有锁5秒钟,B随后进来后发现当前有线程持有锁,不是null,所以只能通过⾃旋等待,直到A释放锁后B随后抢到。

4读写锁

读锁是共享的,写锁是独占的。 juc.ReentrantLock 和 synchronized 都是独占锁,独占锁就是⼀个

锁只能被⼀个线程所持有。有的时候,需要读写分离,那么就要引⼊读写锁,即 juc.ReentrantReadWriteLock 。

独占锁:指该锁⼀次只能被⼀个线程所持有。对ReentrantLock和Synchronized而言都是独占锁

共享锁:指该锁可被多个线程所持有

对ReenntrantReadWriteLock其读锁是共享锁,其写锁是独占锁。

读锁的共享锁可保证并发读是⾮常⾼效的,读写、写读、写写的过程是互斥的。

比如缓存,就需要读写锁来控制。缓存就是⼀个键值对,以下Demo模拟了缓存的读写操作,读的 get⽅法使⽤了 ReentrantReadWriteLock.ReadLock() ,写的 put ⽅法使⽤了ReentrantReadWriteLock.WriteLock() 。这样避免了写被打断,实现了多个线程同时读。

实例代码:

/**
 * 多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。
 * 但是,如果有一个线程想去写共享资料,就不应该再有其他线程可以对该资源进行读或写
 * 小总结:
 * 		读-读 能共存
 * 		读-写 不能共存
 * 		写-写 不能共存
 */
class Cache {
    private Map<String, Object> map = new ConcurrentHashMap<>();

    private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    /**
     * 记录哪个线程来 签字
     * @param key 线程名
     * @param value 签字内容
     */
    public void put(String key, Object value){
        readWriteLock.writeLock().lock();
        try {
            System.out.println(key+"====正在签字: "+ value);
            //耗时操作
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) {e.printStackTrace(); }
            map.put(key, value);
            System.out.println("===签字完成===");
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

    /**
     * 获取指定线程 对应的value
     * @param key 线程名
     * @return 签字内容
     */
    public Object get(String key){
        Object result = null;
        readWriteLock.readLock().lock();
        try {
            System.out.println(key + "====正在看大家的签字:");
            //耗时操作
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) {e.printStackTrace(); }
            result = map.get(key);
            System.out.println(key+"===看完了===");
        } finally {
            readWriteLock.readLock().unlock();
        }
        return result;
    }
}

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        Cache cache = new Cache();
        //写
        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(()->{
                cache.put("线程名"+temp, "数据"+temp);
            }, String.valueOf(i)).start();
        }
        //读
        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(()->{
                cache.get("线程名"+temp);
            }, String.valueOf(i)).start();
        }
    }
}

运行结果:写入数据环节是同步,阻塞访问

 读取数据环节是支持并发访问的

可以运行看下效果,因不能上传运行结果视频 ,需要自己手动运行理解一下:读-读 能共存,读-写 不能共存,写-写 不能共存的概念

Java 中,多线程编程时可能会遇到共享资源竞争的问题,这可能导致数据不一致性和并发错误。为了解决这些问题,Java 提供了多种同步机制,其中最常见的几种包括: 1. **synchronized 关键字**:这是最基本的同步原语,当一个方法或代码块被 synchronized 修饰时,只有一个线程可以访问该代码。它会自动获取并释放当前对象的监视(monitor),即对象内部的 `this` 对象。 2. **ReentrantLock**:这是一个更高级别的互斥,提供了比 synchronized 更强的控制力。它允许显式地获取、释放,以及检查是否已被持有,这对于复杂的同步场景非常有用。 3. **Semaphore**:信号量是一种计数器,用于限制同时访问特定资源的线程数量。它允许多个线程通过,但只有当信号量剩余许可数大于0时。 4. **CountDownLatch** 和 **CyclicBarrier**:这两个类主要用于线程间的协作,CountDownLatch 有一个倒计时器,所有等待它的线程需要等到倒计时结束后才能继续;而 CyclicBarrier 允许一组线程到达某个屏障点后一起继续执行。 5. **Atomic 类型**:如 AtomicInteger 和 AtomicLong 等原子变量提供了一种无需就能安全更新值的方式,适用于简单的线程安全操作。 6. **ThreadLocal**:这是一种作用于当前线程的局部变量,每个线程都有自己的 ThreadLocal 变量副本,彼此之间不会相互干扰。 了解和合理使用这些对于编写健壮的并发程序至关重要。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值