线程安全基础知识点

一、单例模式

什么是单例模式?

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 的区别?

  1. 上锁机制不同:synchronized 是 jvm 自动上锁,ReentrantLock 需要代码手动上锁和释放锁。
  2. 锁的类型不同:synchronized 是非公平锁,ReentrantLo 可以设置公平锁和非公平锁
  3. 性能不同:ReentrantLock 高于 synchroized
  4. 功能不同:ReentrantLock 提供了非常丰富的方法,功能大大强大与 synchronized
  5. 编程难度不同: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 的区别

  1. synshronized 能实现原子性、可见性、有序性;volatile 能实现可见性和有序性
  2. synchronized 更加重量级,消耗更多资源;volatile 更加轻量级
  3. synchroinized 用在方法或代码块上面;volatile 只能用于变量

有序性

程序指令是按编写的顺序执行的。

JVM 会对程序指令进行优化,可能导致程序指令从排序

Object object=new Object();

创建对象的过程:

  1. 分配内存创建对象
  2. 对属性初始化
  3. 将内存地址赋值给引用

指令重排可能出现: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,比较时判断原始值和当前值是否相同还要判断版本号是否改变。

总结:

要掌握的内容:

1)手写双检索的单利模式

2)并发编程三大特征,解释下

3)为什么会出现线程安全问题,解决的方法有哪些

4)synchronized 做了哪些优化

5)synchronized 的原理

6)synchronized 和 ReentrantLock 有什么区别

7)什么是重入锁、公平锁非公平锁

8)为什么会出现可见性问题,如何解决

9)volatile 关键字有什么作用,和 synchronized 的区别

10)原子类启到什么作用,实现的原理是什么

  • 28
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值