JAVAEE多线程(synchronized和reentrantlock)

发布于:2025-04-11 ⋅ 阅读:(35) ⋅ 点赞:(0)

目录

1.CAS。

1.1CAS是什么。

12 CAS有哪些应用。

1.3解决CAS里面的ABA问题。

2.synchronized.

2.1.基本特性。

2.1.1互斥性。

2.1.2可重入性。

 2.1.3可见性

2.1.4异常时自动释放锁

2.2.使用方法

2.2.1.同步实例方法

 2.2.2同步静态方法

 2.2.3同步代码块(最常用)

2.3 synchronized的优化操作。

2.3.1.锁升级

2.3.2.锁消除

2.3.3锁粗化

3.reentrantlock.

3.1reentrantlock锁的定义。 

 3.2reentrantlock锁的用法。

4.synchronized和reentrantlock的区别。


 

1.CAS。

1.1CAS是什么。

 CAS全程叫做“Compare and swap”,意思就是“比较并交换”。CAS是一种无锁的院子操作技术,在多线程环境下能高效实现线程安全,避免了加锁和解锁的性能开销操作。

CAS的操作分为以下三步:

1.比较V(内存数据)与A(寄存器的数值)是否相等。

2.如果相等就将B(寄存器里面的数值)与V中的数值进行交换;要是不相等继续进入循环,重复这个操作。

3.返回操作是否成功(true ,false)。

12 CAS有哪些应用。

 1.实现原子类。

在JAVA里面的 java.util.concurrent.atomic 包下面,提供了一系列的由CAS实现的原子类,如:AtomicInteger ,AtomicLong,AtomicReference 等。这些原子类可以在多线程环境下实现对变量的原子操作,避免了使用传统的锁机制所带来的性能开销。

例如:

    private static AtomicInteger atomicInteger = new AtomicInteger(0);
    private static AtomicLong atomicLong = new AtomicLong(0);
    private static AtomicBoolean atomicBoolean = new AtomicBoolean(true);

代码展示:
 

private static AtomicInteger atomicInteger = new AtomicInteger(0);

    public static void main (String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                atomicInteger.getAndIncrement();
            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                atomicInteger.getAndIncrement();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("输出结果为:" + atomicInteger.get());
    }

运行结果:
 

上述代码呢,我们之前在前面举过例子,之前使用的是锁从而达到线程安全的程度,但是在这里我们没有使用锁,使用的是JAVA提供的线程安全的原子类。 

2.实现自旋锁。

自旋锁的定义: 

自旋锁是一种忙等锁,当一个线程尝试获取自旋锁的时候,如果发现当前锁已经被其他的线程持有的话,该线程不会进入阻塞等待,而是会不断地进行循环检查锁是否已经被释放了,重复这个操作,直到获取到这个锁。 

 当我们刚开始的时候,给线程初始化为空的时候,然后进行CAS操作,判断是否相同,要是为false的情况的话,在这个CAS操作的外面就有一个循环,直到使满足条件的话,才能跳出去。

3.实现无锁队列。

4.实现并发容器。

5.实现线程安全的计数器。

1.3解决CAS里面的ABA问题。

 我们前面在介绍CAS的时候,说的是比较内存中的数值和寄存器里面的数值是否一样,要是一样的话就将改变的寄存器数字赋值给内存中位置。其中我们说到了看寄存器里面的数值和内存里面的数值是否一样,这里面我们就会出现一个问题,我们怎么知道他们的数值是相等的呢,怎么判断它不是经过了两次改变后又变回刚开始的数值的。这就是存在的问题,也就是CAS里面的ABA问题。

 下面我举个例子简单的说一下:

例如我去银行取钱的时候,我需要进行取钱的时候,我取500元,现在假设有两个线程,两个线程内存读取到为1000,我目标数值就是变为500,当线程一在执行的时候,执行正确,之后呢这个时候我的朋友在这一时刻同时给我转账了500元,这使得我的本金又变为1000,这在线程2执行的时候就会发现现在的金额和刚开始的一样就重复进行了扣款,这就导致了错误的出现。

 出现问题了我们就要进行解决,解决办法呢就是在修改的时候引入版本号,通过增加一个变量计算出执行操作的次数。

2.synchronized.

我们在之前就已经说过了 synchronized锁了。

这里呢我们就在简单的总结下:

2.1.基本特性。

2.1.1互斥性。

同一时刻,只允许一个线程持有 synchronized锁,当其他线程也想访问该锁的时候就只能等待持有的线程将这个锁释放掉之后才会进行访问。

2.1.2可重入性。

 可重入性说的是同一个线程在多次获取同一把 synchronized锁的时候,不会出现死锁的情况。

 2.1.3可见性

当一个线程执行完程序之后,将 synchronized锁释放了之后,JVM就会将该线程在代码块中共享变量所做的修改操作刷新到内存里面,使得例外一个线程获取的时候,JVM会将主内存里面的最新更新值刷新到自己的工作内存里面,这样就保证了不同线程对同一变量修改的正确性。

2.1.4异常时自动释放锁

当在执行到 synchronized锁里面的代码块的时候要是出现异常的话,JVM 会自动释放该线程持有的锁,这样就避免了死锁的出现。

2.2.使用方法

 在之前我们介绍的时候已经介绍过了,这里就不过多的介绍了,大家可以查看我之前的博客。

2.2.1.同步实例方法

注意:当 synchronized 用于修饰实例方法时,该方法在同一时刻只能被一个线程访问。这是因为它锁定的是当前对象实例(即this)。

代码展示:

 public synchronized void increment(){
        //具体执行方法
    }

    public static void main(String[] args) {
        Demo5 threadd = new Demo5();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                 threadd.increment();
            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                 threadd.increment();
                 //执行该方法的时候,不同时使两个线程同时进行执行该方法
            }
        });
    }

 2.2.2同步静态方法

注意:当 synchronized用于修饰静态方法的时候,它锁定的是当前类的对象,意味着同一时刻只有一个线程可以访问该类所有的同步静态方法。

代码展示:

 public static synchronized void increment(){
        //具体执行方法
    }

    public static void main(String[] args) {
        Demo5 threadd = new Demo5();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                 threadd.increment();
            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                 threadd.increment();
                 //执行该方法的时候,不同时使两个线程同时进行执行该方法
            }
        });
    }

 2.2.3同步代码块(最常用)

 这种方式的话就会更灵活的控制锁的范围。

代码展示:
 

private static Object lock  = new Object();
    public static int count = 0;
    public static void main (String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (lock){
                    count++;
                }
            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (lock){
                    count++;
                }
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(count);

2.3 synchronized的优化操作。

我们前面介绍了synchronized锁,大家应该也是对其有一点的了解了.synchronized锁是重量级锁,在竞争的时候,会导致频繁地阻塞和唤醒,导致性能开销较大,还有就是粒度太大了,下面我们通过一下办法对该锁进行优化。


2.3.1.锁升级

JVM 将synchronized锁分为无锁,偏向锁,轻量级锁,重量级锁 状态。

我们先给代价解释下上面几个锁分别指什么:

偏向锁:偏向锁不是真正的加锁,而是给这个对象一个标记记住这个锁属于哪个阶段,通过先不加锁的操作获取这个对象的内容,要是其他的线程来竞争这个锁的时候才会在这个线程的前一步加上锁,其他的锁就无法获取。偏向锁就相当于“延迟加锁”,就是非必要不加锁。要是可以不加锁的情况下获取到内容就会降低加锁的开销。

轻量级锁:当线程的数量增多的时候,偏向锁就不适应了,为了能够保证安全性就升级为轻量级锁。轻量级锁首先它会先检查对象头中的锁标志位,如果对象处于无锁状态,JVM 会在当前线程的栈帧中创建一个锁记录,并使用CAS操作,同时将对象头指向锁记录的地址。只要CAS执行成功就会获得轻量级锁。y要是没有执行成功就会类似自旋锁,继续等待。

重量级锁:当线程数目再次增加的时候就会升级为重量级锁。重量级锁依赖于操作系统的互斥性来实现的,当锁被其他的线程获取到的时候,就会让他们进行阻塞,直到释放。(这就是轻量级锁和重量级锁的区别,轻量级锁和自旋锁差不多,通过CAS实现的)。
 

2.3.2.锁消除

锁消除顾名思义就是将没用的锁进行去除。这个通过编译器和JVM进行判断,只要这个锁可有可无就直接进行去除。

2.3.3锁粗化

一段逻辑要是出现多次加锁,解锁的操作的时候,JVM会自动进行锁的粗化,

下面我通过一个图给大家解释一下:

我们要是发现有上述的这样情况,JVM会自动给我们优化为下面的情况。上图中上面是(粗),下面是(细)。 

我们在通过代码进行解释:
 

Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (lock){
                    count++;
                }
            }
        });

        Thread t2 = new Thread(()->{
            synchronized (lock){
                for (int i = 0; i < 50000; i++) {
                    count++;
                }
            }
        });

上述代码中,t1线程对应的粗,t2对应的细。 

3.reentrantlock.

前面呢我们介绍了 synchronized锁我们知道了这个的用途,下面呢我们再介绍一种其他的锁——reentrantlock锁。

3.1reentrantlock锁的定义。 

reentrantlock锁也和 synchronized锁是一样的,也满足互斥性和可重入性。


 3.2reentrantlock锁的用法。

 reentrantlock和synchronized用法是不一样的,synchronized锁是自动实现加锁和解锁的操作的,只要在括号的里面就是实体,不需要我们担心忘记出锁。但是reentrantlock是需要我们自己进行加锁和解锁的操作的,分别使用lock(),unlock()。

除过上述的加锁操作外,还提供了另外一种用于处理死等的情况,trylock(时间限制)加锁操作,要是在一定的时间不能获取到锁的时候就会进行放弃,不会持续死等下去。

4.synchronized和reentrantlock的区别。

1.synchronized是一个关键字,是JVM内部实现的,reentrantlock是标准库中的一个类,在JVM外部实现的。

2.使用synchronized不需要手动释放锁,当走出大括号的时候就会自动解锁;reentrantlock需要我们手动进行加锁和解锁的操作,在项目中使用起来比较灵活,但是我们容易在解锁的时候出错。

3.synchronized在申请失败的时候就会进行死等操作;reentrantlock可以通过trylock等待一段时间后要是还未获取成功,就会自己进行放弃。

4.synchronized是非公平锁,就是遵守每个线程概率均等;reentrantlock默认的时候是非公平锁,可以通过构造方法传入一个true开启公平锁模式。

5.当我们锁竞争不激烈的时候,使用synchronized,效率更高;当锁竞争激烈的时候,使用reentrantlock,搭配着trylock()可以更灵活的控制加锁的行为,而不是进行死等。

 


网站公告

今日签到

点亮在社区的每一天
去签到