一、单例模式
什么是单例模式?
Gof23 设计模式之一,单例模式是保证一个类只有一个实例
好处:
- 节约内存资源
- 便于维护
- 满足某些特定的业务需求,如 太阳类只有一个实例
怎么实现单例模式?
1)将构造方法私有化
2)定义静态的实例
3)定义静态方法返回实例
单例模式分为几种?
饿汉式:定义静态实例时就创建对象
懒汉式:定义静态实例不创建对象,调用静态方法判断对象为空再创建对象
懒汉式案例:
/**
* 懒汉式,线程安全带
*/
public class LazyStyle {
private static LazyStyle lazyStyle = new LazyStyle();
public static LazyStyle getInstance() {
return lazyStyle;
}
}
饿汉式案例: (手写)
/**
* 饿汉式,多線程的情況先線程不安全
*/
public class HungryHanStyle {
private static HungryHanStyle hungryHanStyle = null;
private HungryHanStyle() {
System.out.println("构造方法");
}
public static HungryHanStyle getInstance() {
// A线程进入,判断对象为空,准备创建对象,B线程抢占CPU进入,A线程判断为空,创建对象,A线程获得CPU后继续创建对象
if (hungryHanStyle == null) {
hungryHanStyle = new HungryHanStyle();
}
return hungryHanStyle;
}
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+":"+getInstance().hashCode());
}).start();
}
}
}
出现问题:
多线程环境下创建多个对象
分析问题:
第 14 行代码判断对象为 null,A线程进入,判断对象为空,准备创建对象,B线程抢占CPU进入,A线程判断为空,创建对象,A线程获得CPU后继续创建对象。
多线程的调度是抢占式的,无法保证程序代码能完整执行。
二、并发编程的三大特性
并发编程的三大特性:
- 原子性:
线程执行的程序指令能完整的执行,全部执行,或全部不执行
- 可见性:
对于某个数据,某个线程进行的修改,其他所有线程都可见
- 有序性:
线程中的程序指令是按最初编写的顺序执行的
原子性
线程安全问题:多线程的调度是抢占式的,无法保证程序代码能完整执行,则会出现数据不一致的问题。
解决线程安全:通过上锁机制。
synchronized 关键字 sɪŋkrənaɪzd
翻译过来是:同步
可以给方法或代码块上锁,让线程有序的完整执行
方法上锁(同步方法),当第一个线程执行方法时,持有锁,其他线程无法进入方法,线程执行完后,自动释放锁,其他线程才能进入。
public synchronized 返回值 方法名(...){
}
代码锁(同步代码块)
public 返回值 方法名(...){
synchronized(锁对象){
代码块...
}
}
任何成员变量对象都可以作为锁对象
- 可以创建新对象作为锁对象
- 成员方法还可以使用 this 作为锁
- 静态方法可以使用 类名.class 作为锁
同步方法如果是非静态的,默认将 this 作为锁,如果是静态,默认将类名.class 作为锁
synchronized 能保证线程安全,但会带来性能上的损失
双检索单利模式代码
/**
* DCL double check lock 双检索单利模式
*/
public class MySingleton {
// volatile 有序
private static volatile MySingleton instance = null;
// 私有构造方法
private MySingleton() {
System.out.println("执行构造方法");
}
private static MySingleton getInstance() {
// 判断当前对象不为null,不执行同步块,从而提高性能
if (instance == null) {
// 保证判断和创建对象原子执行
synchronized (MySingleton.class) {
if (instance == null) {
instance = new MySingleton();
}
}
}
return instance;
}
public static void main(String[] args) {
// 模拟多线程调用单利模式
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ":" + MySingleton.getInstance().hashCode());
}).start();
}
}
}
为什么会 synchornized 降低性能
synchronized 属于互斥锁,一个线程持有锁时,会阻塞其他线程
线程的两个状态:
- 用户态
- JVM 能够管理的状态
- 内核态
- JVM 不能管理,由操作系统管理
上下文切换
线程在抢占资源时,发现资源上锁,线程从用户态转为内核态进行等待,线程获得锁,重新执行前会从内核态转为用户态,转换过程会降低性能。
synchronized 的优化
jdk1.6 之后对 synchronized 关键字进行了优化
- 锁消除
- 如果 JVM 发现同步方法或同步块中没有线程竞争的资源,会消除锁
public class Demo1(){
public synchronized void test(){
System.out.println("hello world");
}
}
- 锁膨胀
- 如果 jvm 发现在大量循环中使用锁,会优化将锁放到循环外部
public class Demo1(){
public void test2(){
for(int i=0;i<100;i++){
synchronized(this){
...
}
}
}
// jvm优化 -->锁膨胀
synchronized(this){
for(int i=0;i<100;i++){
...
}
}
}
- 锁升级
- 锁的状态:
- 无锁:没有任何线程竞争情况下,不会去加锁
- 偏向锁:如果只有一个线程使用锁,锁会在对象头中间记录线程的 id,如果是这个线程就直接放行
- 轻量级锁:出现少量竞争情况,或通过 CAS 乐观锁机制进行线程的调度,不会出现上下文切换,会出现自旋等待(消耗 CPU)
- 重量级锁:出现大量竞争的情况下,会转换为重量级锁(互斥锁),线程会出现上下文切换
- 锁的状态:
synchronized 在 1.6 后,上锁的过程叫锁升级:无锁--->偏向锁--->轻量级锁--->重量级锁
只能升级不能降级
Synchronized 的原理
利用反编译 class 类文件:javap -c 类名
自动上锁和释放锁实现的原理,一旦给方法或代码块加 synchronized。JVM 会启动 Monitro 监视器控制上锁的代码,线程进入后,监视器中计数器加 1,其他线程进入时,监视器的计数器不为 0,就不允许其他线程进入,线程执行完代码后,计数器减 1,监视器在让其他线程进入。
ReentrantLock 类 rɪˈentrənt lɑːk
是 java.util.concurrent.lock 包提供工具类
ReentrantLock 重入锁(递归锁)
重入锁:发生方法递归情况下,持有锁的线程,可以重新持有该锁
非重入锁:方法递归的情况下,持有锁的线程,不能重新持有该锁
创建方法一:
ReentrantLock lock=new ReentrantLock();
创建方法二:
ReentrantLock lock=new ReentrantLock(true/false);
布尔值由于指定该锁是公平或非公平锁,true 公平,false 非公平(默认)
公平锁:会维护等待线程的队列,锁释放后会优先让等待时间长的线程拿到锁,降低线程的饥饿,降低线程的效率。
非公平锁:所有线程都去抢锁,谁抢到谁执行,有的线程会一种饥饿,提高效率。
使用方法:需要手动上锁和释放锁。
lock.lock(); //上锁
try{
// 上锁的业务
}finally{
lock.unlock(); //释放锁
}
ReentantLock 使用案例
public class LockDemo {
//创建重入锁
public ReentrantLock lock = new ReentrantLock();
public void testLock() {
//上锁
lock.lock();
try {
for (int i = 0; i < 2; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
} finally {
//释放锁
lock.unlock();
}
}
public static void main(String[] args) {
LockDemo lockDemo = new LockDemo();
for (int i = 0; i < 5; i++) {
new Thread(lockDemo::testLock).start();
}
}
}
主要方法:
方法名 | 使用 |
lock() | 上锁 |
unlock() | 释放锁 |
getQueuedLength() | 获得公平锁线程排队长度 |
boolen tryLock() | 上锁并获得上锁是否成功 |
boolen tryLock(long,TimeUnit) | 在一定时间内上锁并获得上锁是否成功 |
Condition newCondition() | 获得条件对象 |
boolen isFair() | 是否公平锁 |
面试题:
synchronized 和 ReentrantLock 的区别?
- 上锁机制不同:synchronized 是 jvm 自动上锁,ReentrantLock 需要代码手动上锁和释放锁。
- 锁的类型不同:synchronized 是非公平锁,ReentrantLo 可以设置公平锁和非公平锁
- 性能不同:ReentrantLock 高于 synchroized
- 功能不同:ReentrantLock 提供了非常丰富的方法,功能大大强大与 synchronized
- 编程难度不同:synchronized 更加简单,ReentrantLock 更复杂
可见性
对于共享资源,一个线程修改后,其他线程都可以看到修改后的状态
原因:CPU 有多个内核,每个内核都有独立的存储单元(寄存器、L1L12L3 缓存),每个内核都执行线程,线程中的数据会从主内存中缓存到不同的内核中,线程修改一个内核中的数据,另一个内核不能及时修改。
volatile 关键字的作用:用于修饰变量,保证变量的可见性。
被修饰的变量只保存在主内存中,所有线程都直接读写主内存,避免了可见性问题
MESL 内存一致性协议
线程的可见性案例:
/**
* 线程可见性
*/
public class VolatileDemo implements Runnable {
//控制线程的停止
private boolean stop = false;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (stop) {
System.out.println("停止线程!!");
break;
}
System.out.println(Thread.currentThread().getName() + "-->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) {
VolatileDemo demo = new VolatileDemo();
Thread thread = new Thread(demo);
thread.start();
//停止子线程
try {
thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
demo.stop = true;
}
}
/**
* 确保线程之间的可见性
*/
public class VolatileDemo2 {
public volatile boolean stop = false;
public void testVolatile() {
System.out.println("程序执行了");
while (stop) {
}
System.out.println("程序结束了");
}
public static void main(String[] args) throws InterruptedException {
VolatileDemo2 demo2 = new VolatileDemo2();
new Thread(() -> {
demo2.testVolatile();
}).start();
Thread.sleep(3000L);
demo2.stop = true;
System.out.println("主线程序结束");
}
}
面试题
volatile 和 synshornized 的区别
- synshronized 能实现原子性、可见性、有序性;volatile 能实现可见性和有序性
- synchronized 更加重量级,消耗更多资源;volatile 更加轻量级
- synchroinized 用在方法或代码块上面;volatile 只能用于变量
有序性
程序指令是按编写的顺序执行的。
JVM 会对程序指令进行优化,可能导致程序指令从排序
Object object=new Object();
创建对象的过程:
- 分配内存创建对象
- 对属性初始化
- 将内存地址赋值给引用
指令重排可能出现:1、3、2 可能将还没有完成初始化的对象交给用户,导致问题
// 静态实例 volatile 防止指令重排
private static volatile MySingleton instance = null;
原子类
变量的++和 --
分为三个步骤:
1)读取原始值
2)计算新值
3)保存为新值
多线程环境下,不能完整执行,可能导致线程安全问题
原子类案例:
public class AtomicDemo {
//static int count = 0;
//原子整数
private static AtomicInteger atomicInteger = new AtomicInteger();
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
//count++;
//相当于count++
atomicInteger.incrementAndGet();
}).start();
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count:" + atomicInteger);
}
}
解决方案:
1)上锁(效率太低)
2)使用原子类
常用的原子类:
- AtomicInteger 原子整数
- AtomicLong 原子长整数
- AtomicBoolean 原子布尔
AtomicInteger 用法
AtomicInteger count = new AtomicInteger(初始值);
自增: getAndIncrement() 或 IncrementAndGet()
自减: getAndDecrement() 或 DecrementAndGet()
原子类实现的原理:
使用乐观锁实现的
悲观锁:getAndincrement()或 IncrementAndGet()
乐观锁:getAndDecrement()或 DecrementAndGet()
java 中的乐观锁机制是 CAS:比较和交换 Compare And Swap
对变量进行修改时,先读取变量的原始值,要修改时,在读取变量当前内存中的值,如果当前值和原始值相同,就用新值覆盖原始值;如果当前值和原始值不同,就表示出现其他线程修改了改值,放弃修改。
CAS 机制可能会出现 ABA 问题:假设原始值是 A,线程 1 将其改为 B,线程 2 将其改为 A,前面的线程发现值相同,以为没有线程并发问题出现。
如何解决 ABA 问题:引入版本号机制,给变量加版本号,每次修改版本号加 1,比较时判断原始值和当前值是否相同还要判断版本号是否改变。
总结:
要掌握的内容: