一、线程安全问题
1.线程安全是啥 :就是一段代码在多线程中,并发执行后,和我们的预期结果不一样(例如两个线程同时给一个变量++,但是++操作没有加锁,这时候就可能导致最后++的结果和我们想要的结果不一样)就出现了bug
2.原因
- 因为操作系统对于线程的调度是随机的,是抢占式执行的,所以不知道cpu这时候还是下时候执行的是哪个指令(可以加锁)
- 多个线程同时修改同一个变量,就可能导致++操作两次,但是值只改变1次的结果。(可以加锁)
- 修改的操作不是原子的(指令不是只有一个,是多个,因为操作系统对于线程的调度是抢占式执行的,所以这个修改操作的多个指令中间是否穿插了其他的指令,就不确定)
- 内存的可见性(是因为编译器优化,比如我们在锁里面使用的一个变量,需要我们在锁外面输入结果来确定是否来解锁,因为计算机等待我们输入的时间是不确定的,所以这时候编译器就会把这个对象的变量的值直接存到它的工作区,直接从工作区来拿取值来判断是否解锁,这时候我们输入结果会存储在内存中,但是它只会在工作区拿值,所以内存中的值的改变不会影响工作区的值,这就是内存可见性问题)可以使用volatile关键字
- 指令重排序问题
二、wait 和 notify 简单了解
wait / notify 可以协调线程之间执行逻辑的顺序。可以让后执行的逻辑等待先执行的逻辑先跑,虽然无法直接干预操作系统对于线程的调度顺序,但是可以让后执行的逻辑(线程)等待,等到先执行的逻辑跑完了,再执行后面的逻辑。
- join也是等另一个线程彻底执行完之后,才执行。
- wait 是等到另一个线程执行 notify之后,继续走(不需要另一个线程执行完)。
当多个线程竞争同一把锁 的时候,获取锁的线程释放后接下来是哪个线程拿到锁,这个事情是不确定的。 因为操作系统对线程的调度是随机的,此时一个A线程拿到锁,其他线程都是再线程阻塞等待的状态,这时候A线程释放锁,接下来是哪个线程拿到锁是不确定的,有可能还是A线程再次拿到这个锁。
三、线程饥饿/线程饿死
我们把每个线程都当成一个鸟宝宝,cpu 就是鸟妈妈,鸟妈妈需要每隔一段时间给鸟宝宝喂食物,来确保鸟宝宝的正常成长。这样的场景就是 wait 和 notify的经典场景。
当拿到锁的线程,发现要执行的任务还不成熟,就会使用 wait 阻塞等待。wait 和 notify 都是 Object 的方法,因为java 中所有的对象都是 Object 的子类,所以 java 中的任意对象都提供了 wait 和 notify方法。
四、wait 方法
当这个线程拿到锁之后使用 wait 方法之后,就会把锁给释放,让接下来的线程来拿到锁执行它的逻辑,一直等到其他线程对这个锁使用 notify 方法,才有可能随即唤醒该锁下的 wait 线程。
其实为什么要释放锁呢?假设wait 不会释放锁。
我们使用 wait 方法的前提是有一个锁对象 ,我们对这个锁使用 wait 方法来触发等待 ,我们等待的过程中必须得把这个锁给释放了,如果不释放,其他线程执行逻辑准备拿这个锁的时候就会触发锁阻塞等待 ,等待该线程释放锁 ,但是该线程因为使用 wait 等待其他线程使用 notify ,这时候就会因为这个锁的释放问题触发了死循环。所以 wait 等待的时候会把锁对象给释放掉。
wait 的锁对象必须是和 synchronized 的锁对象是同一个
下面的代码就是一段 wait 和 notify的简单示例。
java
public class J6_03_blogs {
public static void main(String[] args) throws InterruptedException {
Object locker1 = new Object();
Thread t1 = new Thread(() -> {
synchronized(locker1){
try {
System.out.println("t1线程上锁等待");
locker1.wait();
System.out.println("t1线程 wait 等待结束");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Thread t2 = new Thread(() -> {
synchronized (locker1){
locker1.notify();
System.out.println("t2 线程唤醒 t1线程因为 wait 方法阻塞等待");
}
});
t1.start();
Thread.sleep(3000);
t2.start();
}
}
五、单例设计模式
设计模式是属于我们程序员的棋谱,它把一些典型的问题场景整理了出来,并且针对这些场景,写出了代码应该怎么写,给出了具体方案和指导建议。
单例设计模式,就是强制要求一个类不可以创建多个对象 ,就是这个类只能由一个对象,具体核心做法就是给构造函数私有化,这时候谁都拿不到它的构造方法,只能这个类内部的函数可以拿到构造方法,这时候在这个类里面的方法中只使用一次构造方法就可以实现只有一个类对象的单例模式。
六、饿汉模式
饿汉模式 是尽量早的创建该类的实例化,在构造的时候就构造好了这个类的对象。
在这个类里面,私有化构造方法,私有一个类的对象, 最后我们要获取这个类对象都是通过getInstance 这个方法来获取类对象,这就保证了这个类只实例化出了一个对象。
饿汉模式在开始的时候就创建好了 ,后续只是读取,不涉及创建,所以在多线程下是线程安全的。
java
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public Singleton getInstance() {
return instance;
}
}
七、懒汉模式
懒汉模式是尽量晚的创建对象,等到使用的时候才会创建。
这里获取类对象的时候涉及到了对象的创建 ,**创建操作不是原子的,**所以我们需要对这个创建操作进行分析。
java
public class SingletonLazy {
private static SingletonLazy instance = null;
private SingletonLazy() {
}
public SingletonLazy getInstance() {
if(instance == null) {
instance = new SingletonLazy();
}
return instance;
}
}
7.1 存在多个线程同时来获取对象的时候会有多次构造。
单独给创建那一行代码加锁,这样加锁保证了创建对象的安全,但是降低了效率 ,当多个线程来访问的时候,因为创建操作不知道多长时间,所以这么多线程都得阻塞等待 在 **synchronized(this)**这个操作上,后续也会创建对象,还是无法解决问题。
java
public class SingletonLazy {
private static SingletonLazy instance = null;
private SingletonLazy() {
}
public SingletonLazy getInstance() {
if(instance == null) {
//////////////////
synchronized(this) {
instance = new SingletonLazy();
}
//////////////////
}
return instance;
}
}
7.2 存在多个线程同时来获取对象的时候会有多次构造。
给判断函数加上锁 ,这时候第一个拿到锁的线程可以执行下面判断和创建的操作,并且其他线程都会阻塞在第一个锁对象获取的这一步。等带第一个线程执行完它的逻辑之后才会解除阻塞等待,这种方式虽然解决了多线程的创建多个对象的过程,但是会因为第一个线程加锁,其他对象的阻塞,导致程序效率低下。
java
public class SingletonLazy {
private static SingletonLazy instance = null;
private SingletonLazy() {
}
public SingletonLazy getInstance() {
synchronized (this) {
if (instance == null) {
instance = new SingletonLazy();
}
}
return instance;
}
}
7.3 处理多个线程访问因为锁对象的阻塞等待问题
**我们对这个加上判断是否为空的操作,来提升效率,**但是这种如果编译器优化,就不知道构造函数这个指令,在计算机中的调度是什么的。
这三个指令一般是
- 申请内存空间
- 在内存中构造函数(初始化)
- 把内存空间的首地址给引用变量
但是编译器可能优化成
- 申请内存空间
- 把内存空间的首地址给引用变量
- 在内存中构造函数(初始化)
这时候因为内存可见性问题,就是计算机对指令的随机调度问题,
java
public class SingletonLazy {
private static SingletonLazy instance = null;
private SingletonLazy() {
}
public SingletonLazy getInstance() {
if(instance == null) {
synchronized (this) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
}
7.4 纠正内存可见性问题
我们给instance 这个变量加上 volatile 这个变量,来保证指令的执行的准确性。确保每次读取操作都是读取内存的值,关于该变量的读取和修改操作不会触发重排序问题。这样就写出来了一个可以使用的懒汉模式的代码。
java
public class SingletonLazy {
private static volatile SingletonLazy instance = null;
private SingletonLazy() {
}
public SingletonLazy getInstance() {
if(instance == null) {
synchronized (this) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
}