单例模式与锁策略

1.单例模式

单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种:
饿汉方式(线程安全,程序启动之后,里面创建单例对象)
懒汉方式(使用时才加载,可以避免资源不必要的浪费;当有程序调用单例对象的时候才初始化)

单例模式的实现步骤(无论饿汉还是懒汉实现的模式都是一样的):

  1. 设置私有的构造函数;(为了防止其他类直接new此对象)
  2. 声明一个私有的对象属性;
  3. 提供一个公共的获取实例的方法。

默认的懒汉模式是非线程安全的,使用要对懒汉进行优化,优化改进:
全局加锁(线程安全、性能比较低)
局部加锁(线程安全,双重效验)

1.1饿汉模式

/**
 * DataSource单例模式
 * 饿汉模式
 */
public class DataSourceSingleton {
    //1.设置私有的构造函数;(为了防止其他类直接new此对象)
    private DataSourceSingleton(){

    }

    //2. 声明一个私有的对象属性;
    private static DataSourceSingleton dataSource=new DataSourceSingleton();

    //3. 提供一个公共的获取实例的方法
    public static DataSourceSingleton getInstance(){
        return dataSource;
    }
}

1.2懒汉模式

/**
 * 单例模式
 * 懒汉模式1
 */
public class DataSourceSingleton2 {
    //1. 设置私有的构造函数;(为了防止其他类直接new此对象)
    private  DataSourceSingleton2(){

    }

    //2. 声明一个私有的对象属性;
    private static DataSourceSingleton2 dataSource;

    //3. 提供一个公共的获取实例的方法。
    public static DataSourceSingleton2 getInstance(){
        if(dataSource==null){//第一次访问
            dataSource=new DataSourceSingleton2();
        }
        return dataSource;
    }

}

/**
 * 懒汉模式3
 */
public class DataSourceSingleton3 {
    //1. 设置私有的构造函数;
    private DataSourceSingleton3() {
    }
    //2. 声明一个私有的对象属性;(非线程安全的)没有volatile
    private static DataSourceSingleton3 dataSource = null;
    //3. 提供一个公共的获取实例的方法。
    public static DataSourceSingleton3 getInstance() {//DCL双重效验锁
        if (dataSource == null) {//大致分流状态
            synchronized (DataSourceSingleton3.class) {//排队执行
                if (dataSource == null) {//精细化的判断(分流)
                    dataSource = new DataSourceSingleton3();
                }
            }
        }
            return dataSource;
        }
    }

在这里插入图片描述

/**
 * 懒汉模式3
 * 单例模式的最终版本
 */
public class DataSourceSingleton3 {
    //1. 设置私有的构造函数;
    private DataSourceSingleton3() {

    }


    //2. 声明一个私有的对象属性;(非线程安全的)没有volatile
    private static volatile DataSourceSingleton3 dataSource = null;

    //3. 提供一个公共的获取实例的方法。
    public static DataSourceSingleton3 getInstance() {//DCL双重效验锁
        if (dataSource == null) {//大致分流状态
            synchronized (DataSourceSingleton3.class) {//排队执行
                if (dataSource == null) {//精细化的判断(分流)
                    dataSource = new DataSourceSingleton3();
                }
            }
        }
            return dataSource;
        }
    }

关键点:
1.加volatile
2.双重效验锁

常见锁策略

乐观锁&悲观锁

乐观锁定义

比较乐观,乐观锁认为一般情况下不会出现冲突,所以只会在更改操作的时候才对冲突进行检测,如果检查出来了冲突,就不做任何修改,如果没有冲突才进行修改。

乐观锁实现—CAS(比较并替换)机制

CAS(Compare And Swap)比较并替换,CAS 比较并替换的流程是这样的,CAS 中包含了三个操作单位:V(内存值,)、A(预期的旧址)、B(新值),比较 V 值和 A 是否相等,如果相等的话则将 V 的值更换成 B,否则就提示用户修改失败,从而实现了 CAS 的机制。

CAS缺点ABA问题

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

public class ABADemo1 {
    private static AtomicStampedReference<Integer> money=
            new AtomicStampedReference<>(100,0);

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

        //第一次转账(-50)
        Thread t1=new Thread(()->{
            int old_money=money.getReference();//先得到余额
            int version=money.getStamp();//得到版本号
            //执行花费2s
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            money.compareAndSet(old_money,old_money - 50,version,version+1);
        });
        t1.start();

        //第2次转账(-50)[不小心点击的,第一次没执行]
        Thread t2=new Thread(()->{
            int old_money=money.getReference();//先得到余额
            int version=money.getStamp();//得到版本号
            money.compareAndSet(old_money,old_money - 50,version,version+1);
        });
        t2.start();

        //给账户+50元
        Thread t3=new Thread(()->{
            //执行花费2s
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int old_money=money.getReference();
            int version=money.getStamp();//得到版本号
            money.compareAndSet(old_money,old_money+50,version,version+1);
        });
        t3.start();

        t1.join();
        t2.join();
        t3.join();
        System.out.println("最终账号余额:"+money.getReference());
    }
}

解决方案:
引入版本号,每次操作之后让版本号+1,执行的时候判断版本号和值,就可以解决ABA问题。

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

/**
 * ABA问题和解决方案
 */
public class ABADemo1 {
    private static AtomicStampedReference<Integer> money=
            new AtomicStampedReference<>(100,0);

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

        //第一次转账(-50)
        Thread t1=new Thread(()->{
            int old_money=money.getReference();//先得到余额
            int version=money.getStamp();//得到版本号
            //执行花费2s
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            money.compareAndSet(old_money,old_money - 50,version,version+1);
        });
        t1.start();

        //第2次转账(-50)[不小心点击的,第一次没执行]
        Thread t2=new Thread(()->{
            int old_money=money.getReference();//先得到余额
            int version=money.getStamp();//得到版本号
            money.compareAndSet(old_money,old_money - 50,version,version+1);
        });
        t2.start();

        //给账户+50元
        Thread t3=new Thread(()->{
            //执行花费2s
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int old_money=money.getReference();
            int version=money.getStamp();//得到版本号
            money.compareAndSet(old_money,old_money+50,version,version+1);
        });
        t3.start();

        t1.join();
        t2.join();
        t3.join();
        System.out.println("最终账号余额:"+money.getReference());
    }
}

悲观锁定义和应用

定义:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
应用:synchronized、Lock 都是悲观锁。

公平锁&非公平锁

公平锁:所有任务来了之后先排队,线程空闲之后去任务队列按顺序执行最早任务
非公平锁:ReentrantLock lock = new ReentrantLock(false)。如果构造函数不传递参数,则默认是非公平锁。
公平锁:ReentrantLock lock= new ReentrantLock(true)。

读写锁

读写锁(Readers-Writer Lock)顾名思义是一把锁分为两部分:读锁和写锁,其中读锁允许多个线程同时获得,因为读操作本身是线程安全的,而写锁则是互斥锁,不允许多个线程同时获得(写锁),并且写操作和读操作也是互斥的,总结来说,读写锁的特点是:读读不互斥、读写互斥、写写互斥。

读写锁的使用场景

多读少写的业务操作

import java.time.LocalDateTime;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 演示读写锁的使用
 */
public class ReadWriteLockDemo1 {
    public static void main(String[] args) {
        //创建读写锁
        final ReentrantReadWriteLock readWriteLock=new ReentrantReadWriteLock();
        //创建读锁
        final ReentrantReadWriteLock.ReadLock readLock=readWriteLock.readLock();
        //创建写锁
        final ReentrantReadWriteLock.WriteLock writeLock=readWriteLock.writeLock();
        //线程池
        ThreadPoolExecutor executor=new ThreadPoolExecutor(5,5,
                0, TimeUnit.SECONDS,new LinkedBlockingQueue<>(100));
        //启动线程执行任务【读操作】
        executor.submit(()->{
            //加锁操作
            readLock.lock();
            try{
                //执行业务逻辑
                System.out.println("执行读锁1:"+ LocalDateTime.now());
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally{
                readLock.unlock();
            }
        });

        //启动线程执行任务【读操作2】
        executor.submit(()->{
            //加锁操作
            readLock.lock();
            try{
                //执行业务逻辑
                System.out.println("执行读锁2:"+ LocalDateTime.now());
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally{
                readLock.unlock();
            }
        });

        //创建新线程执行写任务
        executor.submit(()->{
            try {
                System.out.println("执行写锁1:"+LocalDateTime.now());
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                //释放锁
                writeLock.unlock();
            }
        });

        //创建新线程执行写任务2
        executor.submit(()->{
            try {
                System.out.println("执行写锁2:"+LocalDateTime.now());
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                //释放锁
                writeLock.unlock();
            }
        });
    }
}

独占锁(互斥锁)

独占锁是指任何时候都只有一个线程能执行资源操作。
synchronized、Lock。

共享锁

共享锁指定是可以同时被多个线程读取,但只能被一个线程修改。比如 Java 中的ReentrantReadWriteLock 就是共享锁的实现方式,它允许一个线程进行写操作,允许多个线程读操作。

可重入锁&自旋锁

可重入锁

可重入锁指的是该线程获取了该锁之后,可以无限次的进入该锁锁住的代码。

/**
 *可重入锁
 */
public class ThreadDemo18 {
    public static void main(String[] args) {
        synchronized (ThreadDemo18.class){
            System.out.println("线程执行进入了方法");
            synchronized (ThreadDemo18.class){
                System.out.println("线程执行又进入了方法");
                synchronized (ThreadDemo18.class){
                    System.out.println("线程执行又又进入了方法");
                }
            }
        }
    }
}

自旋锁

自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗 CPU。
synchronized自适应自旋锁(自选次数是不固定的)

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值