线程与进程
- 线程就是一个指令流,将一条条指令以一定的顺序交给CPU运行,是操作系统进行运算调度的最小单位
- 进程是正在运行程序的实例,进程包含了线程
- 不同进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间。
并发与并行
- 并发(多线程操作同一个资源,交替执行)
CPU一核, 模拟出来多条线程,天下武功,唯快不破,快速交替 - 并行(多个人一起行走, 同时进行)
CPU多核,多个线程同时进行 ; 使用线程池操作
同步和异步
- 同步:发出一个调用以后,一直等待,直到得到结果返回
- 异步:调用在发出以后,不用等待返回结果,直接返回
线程创建的方式
-
继承Thread类
- 重写run方法,调用start方法开启线程
-
实现runnable接口
- 重写run方法,调用start方法开启线程
-
实现Callabel接口
- 重写call方法(有返回值,用Future/FutureTask的get方法获取),调用start方法启动线程
-
线程池创建创建线程
runnable接口和callable接口的区别
- runnable接口的run方法没有返回值,callable接口的call方法有返回值,可以通过Future/FutureTask的get方法获取
- runnable的run方法不能抛出异常,只能捕获,callable的call方法可以抛出异常
run和start方法的区别
- run方法封装了线程要执行的方法,可以被调用多次(普通方法调用)
- start方法用来启动线程,通过该方法执行run方法,只能被调用一次
线程的状态
public enum State {// 创建NEW,// 可运行RUNNABLE,// 阻塞BLOCKED,// 等待,死死地等WAITING,// 超时等待TIMED_WAITING,// 终止TERMINATED;
}
- 新建
- 当一个线程对象被创建,但还未调用 start 方法时处于新建状态
- 此时未与操作系统底层线程关联
- 可运行
- 调用了 start 方法,就会由新建进入可运行
- 此时与底层线程关联,由操作系统调度执行
- 终结
- 线程内代码已经执行完毕,由可运行进入终结
- 此时会取消与底层线程关联
- 阻塞
- 当获取锁失败后,由可运行进入 Monitor 的阻塞队列阻塞,此时不占用 cpu 时间
- 当持锁线程释放锁时,会按照一定规则唤醒阻塞队列中的阻塞线程,唤醒后的线程进入可运行状态
- 等待
- 当获取锁成功后,但由于条件不满足,调用了 wait() 方法,此时从可运行状态释放锁进入 Monitor 等待集合等待,同样不占用 cpu 时间
- 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的等待线程,恢复为可运行状态
- 有时限等待
- 当获取锁成功后,但由于条件不满足,调用了 wait(long) 方法,此时从可运行状态释放锁进入 Monitor 等待集合进行有时限等待,同样不占用 cpu 时间
- 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的有时限等待线程,恢复为可运行状态,并重新去竞争锁
- 如果等待超时,也会从有时限等待状态恢复为可运行状态,并重新去竞争锁
- 还有一种情况是调用 sleep(long) 方法也会从可运行状态进入有时限等待状态,但与 Monitor 无关,不需要主动唤醒,超时时间到自然恢复为可运行状态
死锁
多个线程被阻塞,他们中的一个或多个线程都在等待某个资源被释放,从而造成无限期的阻塞
预防死锁
- 破坏请求并保持条件:一次性申请所有的资源
- 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。
避免死锁
银行家算法
wait 和 sleep 方法的不同
- wait是Object的方法,sleep是Thread的方法
- wait会释放锁,sleep不会释放锁
- wait必须在同步代码块中使用,sleep可以在任何地方使用
- wait不需要捕获异常,sleep必须要捕获异常
sleep与yield方法的不同
- sleep方法给其他线程运行机会时不会考虑线程优先级,yield方法只会给相同或更高优先级的线程运行机会
- 线程执行sleep方法后进入超时等待状态,执行yield方法后转入就绪状态
- sleep方法声明会抛出异常,yield方法声明不会抛出异常
- sleep方法中需要指定时间参数,yield方法不需要,受jvm控制
synchronized关键字
作用:
- 原子性:一个或多个操作要么全部执行成功,要么全部执行失败。
synchronized
关键字可以保证只有一个线程拿到锁,访问共享资源。 - 可见性:当一个线程对共享变量进行修改后,其他线程可以立刻看到。执行
synchronized
时,会对应执行lock
、unlock
原子操作,保证可见性。 - 有序性:程序的执行顺序会按照代码的先后顺序执行
用法:
- 修饰普通方法
- 修饰静态方法
- 指定对象,修饰代码块
特点
- 阻塞未获的锁、竞争同一个对象锁的线程
- 获取锁无法设置超时
- 非公平锁、悲观锁、可重入锁、独占锁
- 控制等待和唤醒需要结合加锁对象的wait(), notify(),notifyAll()方法
- 锁的功能是JVM层面实现的
- 加锁代码块执行完毕或执行过程中出现异常会自动释放锁
原理
- 同步方法通过加ACC_SYNCHRONIZED标识实现线程执行权的控制
- 同步代码块是通过monitorenter和monitorexit指令获取线程的执行权
BlockingQueue
父类:Collection
实现类:ArrayBlockingQueue、LinkedBlockingQueue
Semphore
- acquire
- release
线程池
三大方法
- newSingleThreadExecutor
- newFixedThreadPool
- newCachedThreadPool
七大参数
-
核心线程数
-
最大线程数=核心线程数+工作队列大小
-
生存时间
-
时间单位
-
工作队列
-
线程工厂
-
拒绝策略
四大拒绝策略
1.AbortPolicy:直接抛出异常,默认策略;
2.CallerRunsPolicy:用调用者所在的线程来执行任务;
3.DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
4.DiscardPolicy:直接丢弃任务;
确定线程池的核心线程数
- IO密集型任务
一般来说:文件读写、DB读写、网络请求等
推荐:核心线程数大小设置为2N+1 (N为计算机的CPU核数)
- CPU密集型任务
一般来说:计算型代码、Bitmap转换、Gson转换等
推荐:核心线程数大小设置为N+1 (N为计算机的CPU核数)
//获取CPU核数
Runtime().getRuntime().availableProcessors();
四大函数式接口
函数式接口:接口中只有一个抽象方法
使用Lamda表达式进行简化
-
Function(函数型接口)有传入值,有返回值
Function<String,String> function=i->{return i}; function.apply("hello");
-
Predicate(判断型接口) 有传入值,返回一个bool类型
Predicate<String> predicate=i->{return i.empty();};
predicate.test("hi");
- Consumer(消费型接口)有传入值,没有返回值
Consumer<String> consumer=i->{System.out.println(i);};
consumer.accept("a");
-
Supplier(供给型接口)无传入值,有返回值
Supplier<String> supplier=()->{return "hi"} supplier.get();
JMM(Java内存模型)
是jvm规范中所定义的一种内存模型。围绕着原子性、可见性、有序性三个特征建立起来的。
关于JMM的一些同步的约定:
1、线程解锁前,必须把共享变量立刻刷回主存。
2、线程加锁前,必须读取主存中的最新值到工作内存中!
3、加锁和解锁是同一把锁
八种内存交互操作:
- lock(锁定),作用于主内存
- read(读取),作用于主内存
- load(加载),作用于工作内存
- use(使用),作用于工作内存
- assign(赋值),作用于工作内存
- store(存储),作用于工作内存
- write(写入),作用于主内存
- unlock(解锁),作用于主内存
JMM对八种内存交互操作的规定:
- 不允许read、load、store、write操作之一单独出现,也就是read操作后必须load,store操作后必须write。
- 不允许线程丢弃他最近的assign操作,即工作内存中的变量数据改变了之后,必须告知主存。
- 不允许线程将没有assign的数据从工作内存同步到主内存。
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过load和assign操作。
- 一个变量同一时间只能有一个线程对其进行lock操作。多次lock之后,必须执行相同次数unlock才可以解锁。
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值。在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。
- 一个线程对一个变量进行unlock操作之前,必须先把此变量同步回主内存。
volatile
- 保证可见性
- 不保证原子性
- 禁止CPU进行指令重排序
CAS
- compareandSet,是一种乐观锁的思想,在不加锁的情况下可以保证线程操作共享资源的原子性,利用了自旋锁
- 底层依赖于Unsafe类,直接去操作内存。Unsafe类中都是native修饰的方法。
- CAS操作包含三个操作数,内存值(V),期望值(A),新值(B)。若内存中的值与期望值一样,则将内存中的值更新为新值B。
- 缺点:导致ABA问题(狸猫换太子)
- ABA:如果一个线程t1正修改共享变量的值A,但还没修改,此时另一个线程t2获取到CPU时间片,将共享变量的值A修改为B,然后又修改为A,此时线程t1检查发现共享变量的值没有发生变化,但是实际上却变化了。
- 解决办法:AutomicStampedReference,CompareAndSet方法会判断当前的引用是否等于预期引用,再判断当前版本号是否跟原来的一致,全部相等,才更新值。
自旋锁
循环加锁 -> 等待的机制
优点:减少上下文切换的消耗
缺点:循环占有,浪费CPU资源
乐观锁与悲观锁
- 乐观锁:不怕别的线程来修改共享变量,如:CAS,适合读操作多的场景
- 悲观锁:防止其他线程来修改共享变量,有上锁、解锁的操作,如:synchronized,适合写操作多的场景
独享锁与共享锁
- 独享锁一次只能被一个线程持有,如Synchronized,ReentrantLock
- 共享锁一次可以被多个线程持有,如ReadWriteLock中返回的ReadLock
AQS
- AQS是一个Java线程同步的框架,是JDK中多锁工具的核心实现框架
- AQS中维护了一个信号量state和一个由线程组成的双向链表队列。
- 在可重入的这个场景下,state用来表示加锁的次数,0表示无锁,每加一次锁state就加1,释放锁就减1;
可重入锁ReentrantLock
可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出死锁
- 相较于synchronized 特点
- 可以设置超时时间
- 可中断
- 可设置公平锁
- 实现原理:CAS+AQS
synchronized与ReentrantLock的区别
- synchronized是非公平锁,ReentrantLock可以设置公平锁
- ReentrantLock可以设置超时时间
- synchronized竞争锁时会一直等待,ReentrantLock可以尝试获取锁,并得到获取结果
- 锁等待和唤醒的方式不同,synchronized使用wait,notify,notifyAll方法,ReentrantLock使用Condition的await,signal,signalAll方法
- synchronized是JVM层面实现的,ReentrantLock是基于JDK代码层面实现的
- synchronized的加锁、释放锁是自动的,而ReentrantLock需要手动设置
单例模式
- 饿汉式
/*** 饿汉式单例*/
public class Hungry {/*** 可能会浪费空间private byte[] data1=new byte[1024*1024];private byte[] data2=new byte[1024*1024];private byte[] data3=new byte[1024*1024];private byte[] data4=new byte[1024*1024];private Hungry(){}private final static Hungry hungry = new Hungry();public static Hungry getInstance(){return hungry;}}
- 懒汉式DCL
//懒汉式单例模式
public class LazyMan {private static boolean key = false;private LazyMan(){synchronized (LazyMan.class){if (key==false){key=true;}else{throw new RuntimeException("不要试图使用反射破坏异常");}}System.out.println(Thread.currentThread().getName()+" ok");}private volatile static LazyMan lazyMan;//双重检测锁模式 简称DCL懒汉式public static LazyMan getInstance(){//需要加锁if(lazyMan==null){synchronized (LazyMan.class){if(lazyMan==null){lazyMan=new LazyMan();/*** 1、分配内存空间* 2、执行构造方法,初始化对象* 3、把这个对象指向这个空间** 就有可能出现指令重排问题* 比如执行的顺序是1 3 2 等* 我们就可以添加volatile保证指令重排问题*/}}}return lazyMan;}//单线程下 是ok的//但是如果是并发的public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {//Java中有反射
// LazyMan instance = LazyMan.getInstance();Field key = LazyMan.class.getDeclaredField("key");key.setAccessible(true);Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);declaredConstructor.setAccessible(true); //无视了私有的构造器LazyMan lazyMan1 = declaredConstructor.newInstance();key.set(lazyMan1,false);LazyMan instance = declaredConstructor.newInstance();System.out.println(instance);System.out.println(lazyMan1);System.out.println(instance == lazyMan1);}
}
- 静态内部类
//静态内部类
public class Holder {private Holder(){}public static Holder getInstance(){return InnerClass.holder;}public static class InnerClass{private static final Holder holder = new Holder();}
}
反射导致单例不安全
- 枚举
//enum 是什么? enum本身就是一个Class 类
public enum EnumSingle {INSTANCE;public EnumSingle getInstance(){return INSTANCE;}
}class Test{public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {EnumSingle instance1 = EnumSingle.INSTANCE;Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);declaredConstructor.setAccessible(true);//java.lang.NoSuchMethodException: com.ogj.single.EnumSingle.<init>()EnumSingle instance2 = declaredConstructor.newInstance();System.out.println(instance1);System.out.println(instance2);}
}
使用枚举,我们就可以防止反射破坏了。
反编译源码
//enum 是什么? enum本身就是一个Class 类
public enum EnumSingle {INSTANCE;public EnumSingle getInstance(){return INSTANCE;}
}class Test{public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {EnumSingle instance1 = EnumSingle.INSTANCE;Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);declaredConstructor.setAccessible(true);//java.lang.NoSuchMethodException: com.ogj.single.EnumSingle.<init>()EnumSingle instance2 = declaredConstructor.newInstance();System.out.println(instance1);System.out.println(instance2);}
}
ConcurrentHashMap
- 1.7中底层采用分段的数组+链表
- 1.8中底层采用数组+链表/红黑树,CAS+Synchronized来保证并发安全
ThreadLocal
ThreadLocal为每个线程都分配一个独立的线程副本从而解决了变量并发访问冲突的问题。ThreadLocal 同时实现了线程内的资源共享。
三个主要方法:
- set(value) 设置值
- get() 获取值
- remove() 清除值
在使用ThreadLocal的时候,强烈建议:务必手动remove
Mysql
索引底层
索引是帮助Mysql高效获取数据的数据结构,MySQL的默认存储引擎InnoDB采用B+树来存储索引。
-
B+树阶数更多,路径更短
-
磁盘读写代价地:非叶子节点存储指针,只有叶子结点才存储数据。
-
B+树方便扫库和区间查询
聚簇索引和非聚簇索引(二级索引)
- 根据该索引可以查出一行数据(所有列)
- 非聚簇索引只能查出主键,然后再根据主键查出一行数据(回表查询)
回表查询
通过二级索引找出对应的主键,再通过主键找到聚集索引中对应的整行数据,这个过程就是回表。
覆盖索引
覆盖索引:指列所有返回值都可以通过该索引查到
索引失效的几种情况
- 违反联合索引的最左前缀原则:
- 模糊查询时%出现在左侧:like %
- or连接的条件两侧中有一侧没有建立索引
- 对添加了索引的字段进行运算或者类型转换
最左前缀法则:查询从索引的最左列开始,并且不跳过索引中的列。若跳跃了某一列,后面的字段索引失效
事务的特性
- A 原子性
- C 一致性
- I 隔离性
- D 持久性
并发事务带来的问题
- 读脏数据 :一个事务对数据进行了修改,修改的数据还没写入到数据库中,这时另一个事务也访问了这条数据,这条数据为脏数据,读脏数据
- 不可重复读 :一个事务A多次读取同一数据,另一事务B也访问该数据,在A读数据的间隙中,B的修改导致A两次读到的数据可能不太一样。
- 幻读 :事务A读取了几行数据,事务B插入了一些数据。在随后的查询中,事务A发现了一些原本不存在的数据,就像发生了幻觉。
如何解决并发事务带来的问题(事务隔离级别)
- 未提交读(read uncommitted),什么都解决不了
- 提交读(read committed),解决脏读
- 重复读(repeatable read)默认隔离级别,解决脏读、不可重复读
- 串行化(serializable):解决脏读、不可重复读、幻读,但性能比较低
redo log 和 undo log
- redo log日志记录的是数据页的物理变化,服务宕机可用来同步数据
- undo log 主要记录的是逻辑日志,当事务回滚时,通过逆操作恢复原来的数据,比如我们删除一条数据的时候,就会在undo log日志文件中新增一条delete语句,如果发生回滚就执行逆操作;
redo log保证了事务的持久性,undo log保证了事务的原子性和一致性
MVCC
事务隔离通过加锁和MVCC实现的
-
mvcc的意思是多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,它的底层实现主要是分为了三个部分,隐藏字段,undo log日志,readView读视图.
-
隐藏字段是指:在mysql中给每个表都设置了隐藏字段,有一个是trx_id(事务id),记录每一次操作的事务id,是自增的;另一个字段是roll_pointer(回滚指针),指向上一个版本的事务版本记录地址
-
undo log主要的作用是记录回滚日志,存储老版本数据,在内部会形成一个版本链,在多个事务并行操作某一行记录,记录不同事务修改数据的版本,通过roll_pointer指针形成一个链表
-
readView解决的是一个事务查询选择版本的问题,在内部定义了一些匹配规则和当前的一些事务id判断该访问那个版本的数据,不同的隔离级别快照读是不一样的,最终的访问的结果不一样。如果是rc(read commited)隔离级别,每一次执行快照读时生成ReadView,如果是rr(repead read)隔离级别仅在事务中第一次执行快照读时生成ReadView,后续复用
主从同步原理
MySQL主从复制的核心就是二进制日志(BINLOG),它记录了所有的DDL(数据定义语言)和DML(数据操纵语言),不包括SELECT,SHOW.
- 主库在提交事务,会把数据变更记录到binlog中
- 从库读取主库的binlog文件,写入到从库的中继日志relaylog中
- 从库重做relaylog中的事件,将改变自己的数据
分库分表
作用:分担访问压力,解决存储压力
-
垂直分库:以表为依据,根据业务将不同表分到不同库中
- 提高IO
-
垂直分表:以字段为依据,根据字段属性将不同字段分到不同表中
- 冷热数据分离
-
水平分库:将一个库的数据分到不同库中 abc=a+b+c
- 提高系统的稳定性和可用性
-
水平分表:将一个表的数据拆分到多个表中(可以在同一个库中)
- 优化单一表数据量过大产生的性能问题