单例模式
1.单例模式
单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种:
饿汉方式(线程安全,程序启动之后,里面创建单例对象)
懒汉方式(使用时才加载,可以避免资源不必要的浪费;当有程序调用单例对象的时候才初始化)
单例模式的实现步骤(无论饿汉还是懒汉实现的模式都是一样的):
- 设置私有的构造函数;(为了防止其他类直接new此对象)
- 声明一个私有的对象属性;
- 提供一个公共的获取实例的方法。
默认的懒汉模式是非线程安全的,使用要对懒汉进行优化,优化改进:
全局加锁(线程安全、性能比较低)
局部加锁(线程安全,双重效验)
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自适应自旋锁(自选次数是不固定的)