锁策略总结

乐观锁

乐观锁:它认为一般情况下不会发生并发冲突,所以只有在进行数据更新的时候,才会检测并发冲突,如果没有冲突,则直接修改,如果有冲突,则返回失败。

CAS(Compare And Swap 比较并且交换):
V(内存值)
A(旧值)
B(新值)
具体流程:V==A?true(没有并发冲突)->V=B; : false(并发冲突)

线程的解决方案:加锁、ThreadLocal、Atomic*

乐观锁 Atomic*

我们先来看一个示例:

public class ThreadDemoGJ1 {
    private  static int count = 0;
    private static final int maxSize = 100000; //最大循环次数

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < maxSize; i++) {
                    count++;
                }
            }
        });
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < maxSize; i++) {
                    count--;
                }
            }
        });
        t2.start();
        t1.join();
        t2.join();
        System.out.println("结果: " + count);
    }
}

在这里插入图片描述
发现结果不为0,因为有并发冲突
接下来我们采用乐观搜:

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadDemoGJ2 {
    private  static AtomicInteger count = new AtomicInteger(0);
    private static final int maxSize = 100000; //最大循环次数

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < maxSize; i++) {
                    //count++;
                    count.getAndIncrement();
                }
            }
        });
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < maxSize; i++) {
                    //count--;
                    count.getAndDecrement();
                }
            }
        });
        t2.start();
        t1.join();
        t2.join();
        System.out.println("结果: " + count);


//        AtomicInteger count = new AtomicInteger(0);
//        count.getAndIncrement();  //i++
//        count.incrementAndGet();  //++i
    }
}

在这里插入图片描述

CAS底层实现原理:
答:Java层面CAS的实现是UnSafe类
Unsafe类调用了C++的本地方法,通过调用操作系统的Atomic::cmpxchg来实现CAS

优点:性能比高

缺点:ABA问题 ->上面代码中的AtomicInteger 是存在ABA问题的 A:预期旧值 B:新值

演示一下ABA问题:
在这里插入图片描述

import java.util.concurrent.atomic.AtomicReference;

/**
 * ABA 问题演示
 */
public class ThreadDemoGJ4 {
    private static AtomicReference money = new AtomicReference(100);
    public static void main(String[] args) throws InterruptedException {
        //转账线程1(-100)
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //转账
                boolean result =  money.compareAndSet(100,0);
                System.out.println("转账线程1(-100)"+result);
            }
        });
        t1.start();
        t1.join();
        //转入100元
        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                //+100
               boolean result =  money.compareAndSet(0,100);
                System.out.println("转入100(+100)"+result);
            }
        });
        t3.start();
        t3.join();
        //转账线程2(-100)
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                //转账
                boolean result =  money.compareAndSet(100,0);
                System.out.println("转账线程2(-100)"+result);
            }
        });
        t2.start();
    }
}

在这里插入图片描述
发现t3转入的一百元影响了t2的执行。本来预想结果第二次转账应该是是失败的,因为余额没有100元。

那么如何解决这个问题呢?

ABA统一解决方案:增加版本号,每次修改之后,更新版本号。(AtomicStampedReference)添加了版本号
演示:

import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * 解决ABA问题
 */
public class ThreadDemoGJ5 {
    private static AtomicStampedReference money = new AtomicStampedReference(100,1);
    public static void main(String[] args) throws InterruptedException {
        //转账线程1(-100)
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //转账
                boolean result =  money.compareAndSet(100,0,1,2);
                System.out.println("转账线程1(-100)"+result);
            }
        });
        t1.start();
        t1.join();
        //转入100元
        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                //+100
               boolean result =  money.compareAndSet(0,100,2,3);
                System.out.println("转入100(+100)"+result);
            }
        });
        t3.start();
        t3.join();
        //转账线程2(-100)
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                //转账
                boolean result =  money.compareAndSet(100,0,1,2);
                System.out.println("转账线程2(-100)"+result);
            }
        });
        t2.start();
    }
}

在这里插入图片描述
这次发现第二次转账失败了,因为两次转账的版本号不同。

那如果把金额改为1000呢?我们来看看

import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * 解决ABA问题
 */
public class ThreadDemoGJ6 {
    private static AtomicStampedReference money = new AtomicStampedReference(1000,1);
    public static void main(String[] args) throws InterruptedException {
        //转账线程1(-100)
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //转账
                boolean result =  money.compareAndSet(1000,0,1,2);
                System.out.println("转账线程1(-1000)"+result);
            }
        });
        t1.start();
        t1.join();
        //转入100元
        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                //+100
               boolean result =  money.compareAndSet(0,1000,2,3);
                System.out.println("转入1000(+1000)"+result);
            }
        });
        t3.start();
        t3.join();
        //转账线程2(-100)
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                //转账
                boolean result =  money.compareAndSet(1000,0,1,2);
                System.out.println("转账线程2(-1000)"+result);
            }
        });
        t2.start();
    }
}

在这里插入图片描述
发现三次操作都失败了,那是因为Integer高速缓存(-127 -> +128)
设置应用程序参数(-D) ,设置Integer高速缓存的最大值:
在这里插入图片描述

悲观锁

悲观锁:他认为通常情况下会出现并发冲突,所以在进入方法之后就会进行检测。
!!!synchronized是悲观锁。

怎么理解乐观锁和悲观锁的,具体怎么实现呢?
答:乐观锁->CAS->Atomic*,CAS组成是由三个东西组成:V (内存值)A(预期旧值) B(新值)组成。执行的时候使用V==A,如果为true,则表明没有并发冲突,则可以直接修改,否则不能修改。CAS通过C++提供的UnSafe中的本地方法实现的(CompareAndSwapXXX)。C++是通过调用系统的Atomic::cmpxchg(原子指令)来实现的
悲观锁:->synchronized 在Java中是将锁的id存放到到对象头中。synchronized在jvm层面是通过监视器锁来实现,在操作系统层面是通过互斥锁mutex实现。

synchronized优化(锁消除):
JDK1.6锁升级过程:
无锁
偏向锁(第一个线程第一次访问)将线程id放在对象头中的偏向锁标识。
轻量级锁(自旋)
重量级锁

共享锁与非共享锁

共享锁:一把可以被多个线程拥有,这就叫共享锁
读写锁中的读锁就是共享锁。
非共享锁(独占锁):一把锁只能被一个线程拥有,这就叫非共享锁。
synchronized是非共享锁

读写锁

读写锁:就是将一把锁分成两个,一个用于读数据的锁,另一把锁叫做写锁。读锁可以被多个线程同时拥有,而写锁只能被一个线程拥有。

读写锁的具体实现:

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

public class ThreadDemoGJ7 {
    public static void main(String[] args) {
        //创建一个读写锁
        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        //读锁
        ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
        //写锁
        ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

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

        //任务1:读锁
        executor.execute(new Runnable() {
            @Override
            public void run() {
                //加锁
                readLock.lock();
                try{
                    //业务逻辑处理
                    System.out.println(Thread.currentThread().getName() + "执行了读锁操作" + new Date());

                        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());

                    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;
}

可重入锁

可重入锁:当一个线程获取一个锁之后,可以重复进入。

public class ThreadDemoGJ8 {
    //创建锁
    private static Object lock = new Object();
    public static void main(String[] args) {
        synchronized (lock){
            System.out.println("第一次进入锁");
            synchronized (lock){
                System.out.println("第二次进入锁");
            }
        }
    }
}

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宋丹尼尔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值