多线程中的锁

简介: 多线程中的锁有很多,但往往不是独立存在的,而是穿插共存的,接下来带你看看多线程中最常见的锁。come on!

多线程中的锁

WangScaler: 一个用心创作的作者。

声明:才疏学浅,如有错误,恳请指正。

  • 公平锁
  • 非公平锁
  • 可重入锁
  • 自旋锁
  • 读写锁(独占锁/共享锁)
  • 互斥锁

非公平锁

非公平锁就像去饭店吃饭,虽然你先到,但是你后面的和你前面的如果点的一样,可能老板同时将他们的一块做出来提高效率,这样你在你后边的之后执行。非公平锁有可能造成优先级反转(上述情况)或者饥饿现象(上述情况的极端情况,一直没给你做)。当线程进来会先尝试占有锁,如果成功则占有锁,如果失败则采用公平锁的方式。像传统的Synchronized就是非公平锁,ReentrantLock默认的也是非公平锁。

package com.wangscaler.lock;
​
/**
 * @author WangScaler
 * @date 2021/8/7 10:07
 */
​
public class Programmer {
    public synchronized void sayHello() throws Exception {
        System.out.println(Thread.currentThread().getName() + ": Hello");
        sayWorld();
​
    }
​
    public synchronized void sayWorld() throws Exception {
        System.out.println(Thread.currentThread().getName() + " World");
    }
​
    public static void main(String[] args) {
        int num = 3;
        Programmer programmer = new Programmer();
        for (int i = 0; i < num; i++) {
            new Thread(() -> {
                try {
                    programmer.sayHello();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }
    }
}

上述示例的执行结果

0: Hello
0 World
2: Hello
2 World
1: Hello
1 World

从执行结果来看,印证了Synchronized是非公平锁,我们可以看到线程是1是在线程2之后执行的。同样的我们将synchronized换成Lock lock = new ReentrantLock();的方式经过测试也是非公平的。为了更直观的看到效果,你可以多增加几个线程。

接下来我们将上述的Synchronized换成公平锁的形式看看结果。

公平锁

公平锁:就像排队买饭一样,先来后到。当线程进来,先查看锁的维护队列,如果不为空则加入等待,为空则占有锁。ReentrantLock(true)为公平锁。

将sayHello方法修改为以下的代码:

Lock lock = new ReentrantLock(true);
​
public void sayHello() {
    lock.lock();
    try {
        System.out.println(Thread.currentThread().getName() + ": Hello");
        sayWorld();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        lock.unlock();
    }
}

再次来看打印结果

0: Hello
0 World
1: Hello
1 World
2: Hello
2 World

变成了按线程的启动顺序执行的公平方式,不会出现线程抢占的情况。

可重入锁

可重入锁:即线程在外层函数获得锁后,递归时也可继续自动获取锁。Synchronized、ReentrantLock就是可重入锁。

在公平锁的示例的基础上将sayHello修改成下述代码

   Lock lock = new ReentrantLock();
​
    public void sayHello(int i) {
        if (i < 2) {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + ": Hello");
                sayWorld();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
            sayHello(++i);
        } else {
            return;
        }
    }

同时将main方法中线程的调用programmer.sayHello();修改为programmer.sayHello(0);

再次执行

0: Hello
0 World
0: Hello
0 World
1: Hello
1 World
1: Hello
1 World
2: Hello
2 World
2: Hello
2 World

我们发现ReentrantLock可重入锁得到了验证,线程0递归时继续获得锁,继续执行,直到线程0释放锁,其他线程才可以得到锁去执行。那么此时开启公平锁会怎么样呢?我们将Lock lock = new ReentrantLock();修改为Lock lock = new ReentrantLock(true);发现公平锁优先,上述的代码变成了0-1-2,0-1-2交替执行,也就是线程0执行完第一遍递归时,将进入排队的队列,等待下一次获得锁,而不是直接获得锁。

自旋锁

自旋锁:尝试获取锁的线程循环尝试获取锁,不会阻塞而是,从而减少了上下文切换的消耗,但是会消耗CPU。CAS就是自旋锁的经典案例。

package com.wangscaler.lock;
​
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
​
/**
 * @author WangScaler
 * @date 2021/8/9 9:20
 */
​
public class SpinLock {
    AtomicReference<Thread> atomicReference = new AtomicReference<>();
​
    public void getLock() {
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "开始获取锁");
        while (!atomicReference.compareAndSet(null, thread)) {
​
        }
    }
​
    public void releaseLock() {
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread, null);
        System.out.println(Thread.currentThread().getName() + "释放锁");
    }
​
    public static void main(String[] args) throws InterruptedException {
        SpinLock spinLock = new SpinLock();
        new Thread(() -> {
            spinLock.getLock();
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                spinLock.releaseLock();
            }
        }, "MySpinLock").start();
        TimeUnit.SECONDS.sleep(2);
        new Thread(() -> {
            spinLock.getLock();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                spinLock.releaseLock();
            }
        }, "MySpinLock2").start();
    }
}

上述的结果是

MySpinLock开始获取锁
MySpinLock2开始获取锁
MySpinLock释放锁
MySpinLock2释放锁

即线程MySpinLock启动之后,将获得锁,进入10s的等待,而2s之后(为了线程MySpinLock2启动时,线程MySpinLock已经获得锁,当然没必要等待那么长时间)线程MySpinLock2启动,此时线程MySpinLock2将无法获得锁进入循环,直到线程MySpinLock等待10s后释放锁,线程MySpinLock2才能获取到锁,等待1s后释放锁。原理如下:

  • 首先线程MySpinLock通过getLock获取锁,while循环首次比较时,期望值A是null真实值V也是null,期望值与真实值相同,则将新值B的值(新线程MySpinLock)与之前的真实值进行交换。
  • 此时真实值则变为新线程的值。
  • 当第二个线程MySpinLock2进入的时候,因为真实值是上个线程MySpinLock的值,则比较值为false,加上!取反之后为true,所以会进入循环等待。
  • 直到上一个线程MySpinLock通过releaseLock方法释放锁,将真实值改为null。
  • 第二个线程MySpinLock2,此时比较期望值和真实值都是null,将成功获得锁。

如果是耗时较长的情况,建议不要使用自旋锁,因为当某个线程获得锁,在执行的过程中,其他线程陷入循环,会大量的消耗CPU。

读写锁

读写锁(独占锁/共享锁):独占锁只能被一个线程所持有,一般是写操作,Synchronized、ReentrantLock就是独占锁,其次还有ReentrantReadWriteLock的写锁。共享锁可以被多个线程同时持有,一般是读操作,ReentrantReadWriteLock的读锁

读共享锁,写独占锁。即多个读操作可以共同进行,但是读写不可共存,写写更不可共存即为互斥锁。

package com.wangscaler.lock;
​
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
​
/**
 * @author WangScaler
 * @date 2021/8/9 10:38
 */
​
public class ReadWrite {
    Map<String, String> map = new HashMap<>();
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
​
    public void setName() {
        lock.writeLock().lock();
        try {
            String keyAndValue = Thread.currentThread().getName();
            String name = "线程" + keyAndValue;
            System.out.println(name + "准备写入");
            map.put(keyAndValue, keyAndValue);
            System.out.println(name + "写入完成,值为:"+map.get(keyAndValue));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.writeLock().unlock();
        }
​
    }
​
    public void getName(String key) {
        lock.readLock().lock();
        try {
            String name = "线程" + Thread.currentThread().getName();
            System.out.println(name + "正在读取key:" + key);
            String value = map.get(key);
            System.out.println(name + "的读取结果是" + value);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.readLock().unlock();
        }
​
    }
​
    public static void main(String[] args) {
        int num = 3;
        ReadWrite readWrite = new ReadWrite();
        for (int i = 0; i < num; i++) {
            new Thread(() -> {
                readWrite.setName();
            }, String.valueOf(i)).start();
        }
        for (int i = num; i < num + 3; i++) {
            int finalI = i - 3;
            String key = String.valueOf(finalI);
            new Thread(() -> {
                readWrite.getName(key);
            }, String.valueOf(i)).start();
        }
    }
}

结果如下:

线程1准备写入
线程1写入完成,值为:1
线程0准备写入
线程0写入完成,值为:0
线程2准备写入
线程2写入完成,值为:2
线程3正在读取key:0
线程3的读取结果是0
线程5正在读取key:2
线程4正在读取key:1
线程5的读取结果是2
线程4的读取结果是1

从结果可以看出,

  • 读写锁在写的时候是独占锁,只能被一个线程所占有,所以当线程1先拿到锁,线程0、2都得等待线程1执行完释放锁之后才能执行。
  • 在读的时候,出现了线程5、4同时读取的情况,从而可以看出读允许多个线程同时操作。
  • 虽然多个线程都启动了,但是没有出现写操作和读操作同时存在的情况,可看出存在互斥锁。
目录
相关文章
|
1月前
|
安全 编译器 C#
C#学习相关系列之多线程---lock线程锁的用法
C#学习相关系列之多线程---lock线程锁的用法
|
3月前
多线程并发锁的方案—原子操作
多线程并发锁的方案—原子操作
|
2月前
|
安全 Java C++
解释Python中的全局解释器锁(GIL)和线程安全的概念。
解释Python中的全局解释器锁(GIL)和线程安全的概念。
26 0
|
3月前
|
数据处理
多线程与并发编程【线程对象锁、死锁及解决方案、线程并发协作、生产者与消费者模式】(四)-全面详解(学习总结---从入门到深化)
多线程与并发编程【线程对象锁、死锁及解决方案、线程并发协作、生产者与消费者模式】(四)-全面详解(学习总结---从入门到深化)
43 1
|
1月前
|
存储 安全 Java
并发编程知识点(volatile、JMM、锁、CAS、阻塞队列、线程池、死锁)
并发编程知识点(volatile、JMM、锁、CAS、阻塞队列、线程池、死锁)
71 3
|
8天前
|
安全 Java 调度
Java并发编程:深入理解线程与锁
【4月更文挑战第18天】本文探讨了Java中的线程和锁机制,包括线程的创建(通过Thread类、Runnable接口或Callable/Future)及其生命周期。Java提供多种锁机制,如`synchronized`关键字、ReentrantLock和ReadWriteLock,以确保并发访问共享资源的安全。此外,文章还介绍了高级并发工具,如Semaphore(控制并发线程数)、CountDownLatch(线程间等待)和CyclicBarrier(同步多个线程)。掌握这些知识对于编写高效、正确的并发程序至关重要。
|
12天前
|
存储 缓存 Java
线程同步的艺术:探索 JAVA 主流锁的奥秘
本文介绍了 Java 中的锁机制,包括悲观锁与乐观锁的并发策略。悲观锁假设多线程环境下数据冲突频繁,访问前先加锁,如 `synchronized` 和 `ReentrantLock`。乐观锁则在访问资源前不加锁,通过版本号或 CAS 机制保证数据一致性,适用于冲突少的场景。锁的获取失败时,线程可以选择阻塞(如自旋锁、适应性自旋锁)或不阻塞(如无锁、偏向锁、轻量级锁、重量级锁)。此外,还讨论了公平锁与非公平锁,以及可重入锁与非可重入锁的特性。最后,提到了共享锁(读锁)和排他锁(写锁)的概念,适用于不同类型的并发访问需求。
43 2
|
12天前
|
Java 程序员 编译器
Java中的线程同步与锁优化策略
【4月更文挑战第14天】在多线程编程中,线程同步是确保数据一致性和程序正确性的关键。Java提供了多种机制来实现线程同步,其中最常用的是synchronized关键字和Lock接口。本文将深入探讨Java中的线程同步问题,并分析如何通过锁优化策略提高程序性能。我们将首先介绍线程同步的基本概念,然后详细讨论synchronized和Lock的使用及优缺点,最后探讨一些锁优化技巧,如锁粗化、锁消除和读写锁等。
|
20天前
|
安全 Java 调度
深入理解Java中的线程安全与锁机制
【4月更文挑战第6天】 在并发编程领域,Java语言提供了强大的线程支持和同步机制来确保多线程环境下的数据一致性和线程安全性。本文将深入探讨Java中线程安全的概念、常见的线程安全问题以及如何使用不同的锁机制来解决这些问题。我们将从基本的synchronized关键字开始,到显式锁(如ReentrantLock),再到读写锁(ReadWriteLock)的讨论,并结合实例代码来展示它们在实际开发中的应用。通过本文,读者不仅能够理解线程安全的重要性,还能掌握如何有效地在Java中应用各种锁机制以保障程序的稳定运行。
|
1月前
|
Linux API C++
【Linux C/C++ 线程同步 】Linux API 读写锁的编程使用
【Linux C/C++ 线程同步 】Linux API 读写锁的编程使用
21 1