哈希表底层数据结构实际上就是数组。它利用数组支持按照下标随机访问的时候,时间复杂度是o(1)的特性。我们通过哈希函数把元素的键值映射为下标,然后将数据存储在数组中对应下标的位置。当我们按照键值查询元素时,我们使用相同的哈希函数,将键值转化为数组下标,从对应的数组下标的位置取出数据。
//JDK1.8以后的HashMap部分源码
static final int hash(Object key){
int h;
return (key == null)?0(h=key.hashCode())^(h>>>16);
}
hash算法的优化:
对每个hash值,将他的高低十六位进行异或操作,让低十六位同时保持了高低十六位的特征。同时也可以避免一些hash值后续出现冲突。
寻址算法的优化:
寻址算法就是对长度为n的数组取模,得到在数组中的位置。根据数学规律,对n取模,就是和n-1进行与运算。与运算的效率远远高于求模运算,所以采用与运算。而数组的长度通常没有很大,所以高位与出来都是0,如果不进行hash算法优化,那么高位的信息就会丢失。
综上就是JDK8的hash算法的优化。
hash冲突问题, 链表 + 红黑树 ,o(n)和o(logn)
当发生hash冲突时,会在数组中重复的位置放置一个链表,然后将value的值加入链表中。但是由于链表的查询时间复杂度是o(n),所以当链表的变的很长的时候,我们获取值会变的很慢。为了提升性能,当链表的长度到达一定值时,我们将链表转换成红黑树,红黑树的查询时间复杂度是o(logn),提升性能。
hashMap底层默认是一个数组,当这个数组满了以后,就会自动扩容,变成一个更大的数组,可以在里面放更多的元素。
hashMap的默认大小是16位的,当16存满以后就会进行***2倍扩容***,变成长度为32的数组。这个时候就要对原先数组中存储的元素进行rehash,即将他们的哈希值和(32-1)进行与运算,原本在长度为16的处于相同位置的几个元素,可能就要变换位置,不在同样的位置了。
为什么进行两倍扩容?
两倍扩容就是二进制位的上一位变成1,比如
0000 0000 0000 1111
变成
0000 0000 0001 1111
在进行rehash操作时,判断二进制结果是否多了一个bit的1,如果没多,那么就是原来的index,如果多了,那么就是index + oldcap,通过这个方式,避免rehash的时候,进行取模运算,位运算的性能更高。
注意,我们最好在使用hashMap的时候能够指定合适的hashMap的大小,来避免扩容,这样就能避免rehash操作,影响性能。
注意:创建线程的目的是为了开启一条路径,去运行指定的代码和其它代码同时运行。线程需要任务。
/*
循环的嵌套就会产生同步死锁
注意:一般run方法里都会有循环结构
分析:
定义线程的第二种方法
首先定义一个子类实现Runnable接口 复写线程的任务就是覆盖Runnable接口中的run方法
创建任务对象
创建线程对象 并将任务对象作为参数传入线程对象
让线程对象得到自己的任务 即自己的run函数
*/
//一段死锁的代码示例
class DeadLock implements Runnable
{
private boolean flag;
DeadLock(boolean flag)
{
this.flag = flag;
}
public void run()
{
if (flag)
{
while (true)//while 循环开始一段完整的线程 放在if里面
{
synchronized (MyLock.locka)
{
System.out.println(Thread.currentThread().getName()+"if locka");
synchronized(MyLock.lockb)
{
System.out.println(Thread.currentThread().getName()+"if lockb");
}
}
}
}
else
{
while (true)
{
synchronized (MyLock.lockb)
{
System.out.println(Thread.currentThread().getName()+"else lockb");
synchronized(MyLock.locka)
{
System.out.println(Thread.currentThread().getName()+"else locka");
}
}
}
}
}
}
class MyLock
{
public static final Object locka = new Object();
public static final Object lockb = new Object();
}
class DeadLockTest
{
public static void main(String[] args)
{
DeadLock d1 = new DeadLock(true);
DeadLock d2 = new DeadLock(false);
Thread t1 = new Thread(d1);
//d.flag = false;
Thread t2 = new Thread(d2);
t1.start();//调用对象的成员函数记得加上括号
t2.start();
}
}
一个lock可以挂多个Condition对象,即多个监视器。
Condition c1 = lock.newCondition();//通过已有的锁产生监视器对象
lock接口的出现替代了同步代码块或者同步函数。将同步的隐士锁变成了显示锁。同时更加灵活,可以添加多个监视器。
class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();//创建多个监视器,每次唤醒一个,提高效率
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();//获取锁
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();//释放锁
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
使用了synchronized关键字,在底层编译后的jvm指令中,会有monitorenter和monitorexit 两个指令。
那么monitorenter指令执行的时候会干什么呢?
每个对象都有关联的monitor,如果要对这个对象加锁,就必须获取这个对象关联的monitor的lock锁。
monitor里面的原理和思路大概是这样的,monitor里面有一个计数器,从0开始。如果一个线程要获取monitor的锁,就会判断他的计数器是不是0,如果是0,那么说明没有人获取锁,他就可以获取锁,然后对计数器加一。同理释放锁的时候就会减一。
如果一个线程来获取montior锁时发现,值不是0,这个线程就会陷入阻塞状态,就会等待计数器变成0然后执行。
注意monitor的锁是支持重复加锁的,就像下面这段代码
加一次锁monitor计数器就会加一 释放一次就会减一 一直到0表示锁为可以获取的状态
synchronized(myObject){
//一大堆代码
synchronized(myObject){
}
}
上一个讲解的非常好的CAS博客地址:什么是CAS机制
下面是我对CAS的理解和总结:Conmpare And Swap(比较和交换)
首先说一说CAS能解决的问题。我们都知道当多个线程对同一个数据进行操作的时候,如果没有同步就会产生线程安全问题。为了解决线程线程安全问题,我们需要加上同步代码块,操作,如加上synchronized。但是某些情况下这并不是最优选择。
synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。这个过程是一个串行的过程,效率很低。
尽管JAVA 1.6为synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过过度,但是在最终转变为重量级锁之后,性能仍然比较低。所以面对这种情况,我们就可以使用java中的“原子操作类”。
而原子操作类的底层正是用到了“CAS机制”。
CAS是英文单词Compare and Swap的缩写,翻译过来就是比较并替换。
CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。(具体实现详细的见上面的博客中介绍)
从思想上来说,synchronized属于悲观锁,悲观的认为程序中的并发情况严重,所以严防死守,CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去重试更新。
说了这么多,CAS是否是完美的呢,答案也是否定的。下面是说一说CAS的缺点:
1) CPU开销过大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。
2) 不能保证代码块的原子性
CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。
3) ABA问题
这是CAS机制最大的问题所在。
首先聊一聊ConcurrentHashMap存在的必要性,即它能解决的问题。在编程中我们常常要对一个hashMap进行多个线程的操作,这个时候为了避免线程安全问题,我们就要给她加上同步。
但是这个时候又会有新的问题产生。
我们知道hashMap的底层实现实际上是数组
多个线程过来,线程1要put的位置是数组[5],线程二要put的位置是[21]
synchronized(map({
map.put(xxx,xxx)
}
我们可以看到向两个不同的位置添加元素,也被锁管理了,这明显是没有必要的,会造成效率低下。我们需要解决这个问题。JDK并发包里推出了ConcurrentHashMap,默认实现了线程的安全性。
下面聊一聊,它是如何实现的。
在JDK 1.7 版本,它的实现方式是分段加锁,将HashMap在底层的数组分段成几个小数组,然后给每个数组分别加锁。
JDK1.8以及之后,做了一些优化和改进,锁粒度的细化。
这里仍然是一个大的数组,数组中每个元素进行put都是有一个不同的锁,刚开始进行put的时候,如果两个线程都是在数组[5]这个位置进行put,这个时候,对数组[5]这个位置进行put的时候,采取的是CAS策略。
同一时间,只有一个线程能成功执行CAS,其他线程都会失败。
这就实现了分段加锁的第一步,如果很多个线程对数组中不同位置的元素进行操作,大家是互相不会影响的。
如果多个线程对同一个位置进行操作,CAS失败的线程,就会在这个位置基于链表+红黑树来进行处理,synchronized([5]),进行加锁。
综上所述,JDK1.8之后,只有对相同位置的元素操作,才会加锁实行串行化操作,对不同位置进行操作是并发执行的。
我们上面三节分别学习了三种多个线程访问一个共享数据实现线程安全的三种解决方案,synchronized,CAS和ConcurrentHashMap(并发安全的数据结构)。下面介绍最后一种,Lock。它的底层就使用到AQS技术。Abstract Queue Synchronizer,抽象队列同步器
在创建锁时候 可以创建公平锁和非公平锁
创建非公平锁
ReentrantLock lock = new ReentrantLock();//非公平锁
创建公平锁
ReentrantLock lock = new ReentrantLock(true);//公平锁
lock.lock();
lock.unlock();
非公平锁,就是当线程1结束运行释放锁以后,它去唤醒等待队列中的线程2,但是还没等线程2CAS成功,这时候冒出来一个线程3插队,优先实现加锁,线程2CAS失败,继续等待,这就是非公平锁。
公平锁,就是线程3,在想插队时,会进行判断,等待队列中是否还有线程,如果有它就不能插队,会进入等待队列中排队。这就是公平锁。
首先说一说为什么要有线程池。
系统是不可能频繁的创建线程有销毁线程的,这样会非常影响性能,所以我们需要线程池。
ExecutorService threadPool = Executors.newFixedThreadPool(3);//corePoolSize
threadPool.submit(new Callable<>() {
@Override
public Object call() throws Exception {
return null;
}
});
执行原理: 创建线程池时,线程池里面是没有线程的。提交任务后,会首先判断线程池中的线程的数量是否小于corePoolSize,也就是上面的3,如果小于,就会创建一个线程来执行这个任务。
在执行完这个任务以后,线程是不会死掉销毁的,它会尝试从一个队列中获取(这里是无界的LinkedBlockingQueue)新的任务,如果没有新的任务,就会进入阻塞状态,等待新的任务的到来。
你持续提交任务,上述流程反复执行,只要线程池的线程数小于corePoolSize,都会直接创建新线程来执行这个任务,执行完了就尝试从无界队列中获取任务。
当我们调用上一节的函数生成fixed线程池的时候
ExecutorService threadPool = Executors.newFixedThreadPool(3);
它的底层执行的代码如下
return new ThreadPoolExecutor(
nThreads,//corePoolSize
nThreads,//maximumPoolSize
0l,//表示等待的时间
TimeUint.MiLLISECONDS,//代表等待时间是毫秒级别的
new LinkedBlockingQueue<Runnable>());//线程池放任务的队列
上面几个的参数分别是,corePoolSize,maximumPoolSize,keepAliveTime,queue这几个东西,如果你不用fixed之类的线程池,完全可以使用这个构造函数创造自己的线程池。`
corePoolSize:3
maximumPoolSize:200
keepAliveTime:60s
new ArrayBlockQueue<Runnable>(200) //这是一个有界队列
如果我们把queue创建成有界队列,假设corePoolSize所有线程都在繁忙的工作,这个时候仍然有大量的任务进入队列,队列满了,此时怎么办?
这个时候,如果你的maximumPoolSize是比corePoolSize大的,此时线程池就会继续创建额外的线程放入线程池中,来处理这些任务。这些额外创建的线程如果处理完了一个任务也会尝试从队列中获取任务来执行。线程池总共可以创建的线程的数量就是maximumPoolSize
线程池的队列满了会发生什么?
但是还有一种情况,如果任务非常多,额外线程全部创建完了,队列还是满的,此时还是有新的任务来,又该怎么办?
此时只能reject掉,有几种不同的reject策略,可以传入RejectedExecutionHandler
(1)AbortPolicy (2)DiscardPolicy (3)DiscardOldestPolicy (4) CallerRunsPolicy (5) 自定义
如果后续慢慢没有任务了,额外创建的线程出去空闲状态,那么线程会等待最大存活时间,如果在这个时间内没有获取新的任务,它就会销毁。实际上maximumPoolSize就是起到一个缓冲的作用。
综上所述,如果定制自己的线程池,要考虑到corePoolSize的数量,队列类型,最大线程数量,拒绝策略,还有线程释放的时间。
特别补充:我们常用的fixedThreadPool是无界队列,maximumPoolSize 和 corePoolSize是一样的。队列永远不会满。或者我们采取有界对列,可以将maximumPoolSize设置的很大,来缓冲。
调用超时,队列变的越来越大,此时会导致内存飙升起来,而且还可能会导致内存溢出。
通过上一个问题分析我们发现使用无界队列可能会因为线程处理任务速度比较慢,但是有很多任务堆积导致堆内存溢出。
有界队列不存在堆内存溢出的问题,但同样会有它的问题。如果我们将maximumPoolSize设置的非常大,那么当任务很多时,就会创建很多的额外线程,一台机器上,有几千个,甚至是几万个线程,每个线程都有自己的栈内存,占用一定的内存资源,创建太多的线程会导致栈内存耗尽,可能会产生栈内存溢出。
那么如果我们将maximumPoolSize设置的很小,又会导致额外线程也满了但是任务还是多出队列的限制,此时就会拒绝策略,导致某些任务不能够顺利执行。
这里提供一种自定义策略的思路作为参考,我们可以把这个任务信息持久化写入到磁盘中去,后台专门启动一个线程,后续等待线程池的工作负载降低了,可以慢慢的从磁盘中读取之前持久化的任务,重新提交到线程池中去执行。
综上所述,我们在选用线程池时要综合考虑。
必然会导致线程池中积压的任务都会丢失。
如何解决这个问题呢?
我们可以在提交任务之前,在数据库中插入这个任务的信息,更新任务的状态:未提交、已提交、已完成。提交成功后,更新它的状态是已提交状态。
系统重启后,用一个后台线程去扫描数据库里的未提交和已提交状态的任务,可以把任务的信息读取出来,重新提交到线程池里去,继续进行执行。
read、load、use、assign、store、write
public class HelloWord(){
private int data = 0;
public void increment(){
data++;
}
}
HelloWorld helloWorld = new HelloWorld();
//线程1
new Thread(){
public void run(){
helloWorld.increment();
}
}.start();
//线程2
new Thread(){
public void run(){
helloWorld.increment();
}
}.start();
上面这段代码在内存中的过程如下图所示。
原子性,就是当有一个线程在对内存中的某个数据进行操作的时候,必须要等这个线程完全操作结束后,其他线程才能够操作,这就是原子性。反之就是没有原子性,多线程默认是没有原子性的,需要我们通过各种方式来实现原子性,如同步等等。
有序性,就是代码的顺序应该和指令的顺序相同。在执行过程中不会发生指令重排,这就是有序性,反之就是没有有序性。
如果直接问volatile关键字,想要解释清楚的话,要从Java内存模型开始讲起。
volatile关键字是用来解决可见性和有序性问题,在有些罕见的条件下,可以有限的保证原子性。
但是它主要不是用来保证原子性的。
volatile保证可见性的原理,如果多个线程操作一个被volatile修饰的变量,当其中一个线程成功对组内存中的数据完成修改以后,它会将其他线程工作内存中的该变量的数据设为失效状态,迫使其它线程重新从主内存中读取变量数据,从而实现有可见性。
在很多的开源中间件系统的源码里,大量的使用了volatile。常常使用的一个场景是对于一个变量,有的线程要更新它有的线程要读取它来进行判断操作,这个时候就需要使用volatile关键字,来保证读取到最新的数据。
volatile关键字和有序性的关系,volatile是如何保证有序性,从而避免发生指令重排的。
boolean volatile flag = false;
//线程1:
prepare(); // 准备资源
flag = true;
//线程2:
while(!flag){
Thread.sleep(10000);
}
execute(); //基于准备好的资源执行操作
Java有一个happens-before原则:
编译器、指令器可能对代码重排序,乱排,要遵守一定的规则,happens-before原则,只要符合happens-before原则,那么就不能胡乱重排,如果不符合这些规则的话,那就可以自己排序。
1.程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
2、锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。
3、volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个volatile变量的读操作。必须保证先写再读。
4、传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,那么可以得出操作A
先行发生于操作C。
5、线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
6、线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断时间的发生。
7、线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测现场是否已经终止执行
8、对象终结规则:一个对象的初始完成先行发生于他的finalize()方法的开始。
上面这8条原则的意思即是,如果程序中的代码满足上述条件,就一定会按照这个规则来保证指令的顺序。
规则制定了一些特殊情况下,不允许编译器、指令器对我们写的代码进行指令重排,必须保证代码的有序性。
除了上述功能外,volatile还要其他的能够预防指令重排的规定,例如volatile前面的代码一定不能指令重排到volatile变量操作的后面,它后面的代码不能指令重排得到volatile前面。
重点
指令重排的概念>happens-before>volatile起到避免指令重排
volatile+原子性:不能够保证原子性,只有在一些极端情况下能保证原子性。
保证原子性:synchronized,lock,加锁。
(1)lock指令:volatile保证可见性
对volatile修饰的变量,执行写操作的话,JVM会发送一条lock前缀指令给CPU,CPU在计算完后会立即将这个值写回主内存,同时因为MESI缓存一致性协议,所 以各个CPU都会对总线进行嗅探,自己本地缓存中的数据是否被修改了。
如果发现被修改了,那么CPU就会将自己的本地缓存数据过期掉,然后从主内存中重新加载最新的数据。
(2)内存屏障:禁止重排序
并发这一块的知识非常的深,synchronized、volatile,的层都对应着一套复杂的cpu级别的硬件原理,大量的内存屏障原理;lock API,concurrentHashmap,都是各种复杂的jdk级别的源码。如果有时间可以自己多花时间买书看。
程序的耦合和解耦 IOC叫控制翻转其目的就是为了降低程序的耦合性
package com.iteima.jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
/**
* 程序的耦合
* 耦合:程序间的依赖关系
* 包括:
* 类之间的依赖
* 方法间的依赖
* 解耦:
* 降低程序间的依赖关系
* 实际开发中:
* 应该做到:编译期不依赖,运行时才依赖。
* 解耦的思路:
* 第一步:使用反射来创建对象,而避免使用new关键字。
* 第二步:通过读取配置文件来获取要创建的对象全限定类名
*
*/
public class JdbcDemo1 {
public static void main(String[] args) throws Exception{
//1.注册驱动
// DriverManager.registerDriver(new com.mysql.jdbc.Driver());
Class.forName("com.mysql.jdbc.Driver");
//2.获取连接
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/eesy","root","1234");
//3.获取操作数据库的预处理对象
PreparedStatement pstm = conn.prepareStatement("select * from account");
//4.执行SQL,得到结果集
ResultSet rs = pstm.executeQuery();
//5.遍历结果集
while(rs.next()){
System.out.println(rs.getString("name"));
}
//6.释放资源
rs.close();
pstm.close();
conn.close();
}
}
为什么我们在进行Jdbc注册驱动的时候要使用class.forname()目的就是将编译时期的错误转换成运行时的异常。解耦的解决方案就是将new操作用反射来实现,但是反射就要用到全限定类名来生成对象,我们通过读取配置文件的方式来获取全限定类名,这样就可以避免在全限定类名产生变化时要修改源码。
package com.itheima.factory;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* 一个创建Bean对象的工厂
*
* Bean:在计算机英语中,有可重用组件的含义。
* JavaBean:用java语言编写的可重用组件。
* javabean > 实体类
*
* 它就是创建我们的service和dao对象的。
*
* 第一个:需要一个配置文件来配置我们的service和dao
* 配置的内容:唯一标识=全限定类名(key=value)
* 第二个:通过读取配置文件中配置的内容,反射创建对象
*
* 我的配置文件可以是xml也可以是properties
*/
public class BeanFactory {
//定义一个Properties对象
private static Properties props;
//定义一个Map,用于存放我们要创建的对象。我们把它称之为容器
private static Map<String,Object> beans;
//使用静态代码块为Properties对象赋值
static {
try {
//实例化对象
props = new Properties();
//获取properties文件的流对象
InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
props.load(in);
实例化容器是因为如果不将创建的对象放入到容器中,那么由于垃圾回收机制就会被回收了
//实例化容器
beans = new HashMap<String,Object>();
//取出配置文件中所有的Key
Enumeration keys = props.keys();
//遍历枚举
while (keys.hasMoreElements()){
//取出每个Key
String key = keys.nextElement().toString();
//根据key获取value
String beanPath = props.getProperty(key);
//反射创建对象 反射创建对象调用的是默认构造函数
Object value = Class.forName(beanPath).newInstance();
//把key和value存入容器中
beans.put(key,value);
}
}catch(Exception e){
throw new ExceptionInInitializerError("初始化properties失败!");
}
}
/**
* 根据bean的名称获取对象
* @param beanName
* @return
*/
public static Object getBean(String beanName){
return beans.get(beanName);
}
/**
* 根据Bean的名称获取bean对象
* @param beanName
* @return
此种方法创建对象是多例的 所以要读取配置文件的时候
public static Object getBean(String beanName){
Object bean = null;
try {
String beanPath = props.getProperty(beanName);
// System.out.println(beanPath);
bean = Class.forName(beanPath).newInstance();//每次都会调用默认构造函数创建对象
}catch (Exception e){
e.printStackTrace();
}
return bean;
}*/
}
需要一个配置文件来配置我们的bean对象,配置文件里面的信息是唯一标识和全限定类名的一一对应的关系,获得全限定类名以后通过反射的方式创建bean对象,这就是工厂模式。
单例可能造成成员变量的线程安全问题,但是dao,service中一般都没有成员变量,所以不用考虑线程安全问题可以使用单例设计模式。
还有set方法注入,实际开发中通常使用set方法注入。
Spring 核心框架里面,最关键的两个机制,就是ioc和aop。根据xml配置或者注解,去实例化我们所有的bean,管理bean之间的依赖注入,让类与类之间的耦合性降低,维护代码的时候更加方便轻松。
AOP是面向切面编程,简单的说就是把我们重复的代码抽取出来,在需要执行的时候,使用动态代理技术,在不修改源码的基础上,对我们已有的方法进行增强。
AOP的作用和优势
作用:在程序运行期间,不修改源码对已有方法进行增强。
优势:减少重复代码,提高开发效率,维护方便
AOP的实现方式:使用动态代理技术
在不改变源码的基础上,对一个类中的方法进行增强。其特点是:字节码随用随创建,随用随加载。它与静态代理的区别也在于此。因为静态代理是字节码一上来就创建好,并完成加载。装饰者模式就是静态代理的一种体现。
package com.itheima.cglib;
/**
* 一个生产者
*/
public class Producer implements IProducer{
/**
* 销售
* @param money
*/
@override
public void saleProduct(float money){
System.out.println("销售产品,并拿到钱:"+money);
}
/**
* 售后
* @param money
*/
@override
public void afterService(float money){
System.out.println("提供售后服务,并拿到钱:"+money);
}
}
package com.itheima.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 模拟一个消费者
*/
public class Client {
public static void main(String[] args) {
final Producer producer = new Producer();
/**
* 动态代理:
* 特点:字节码随用随创建,随用随加载
* 作用:不修改源码的基础上对方法增强
* 分类:
* 基于接口的动态代理
* 基于子类的动态代理
* 基于接口的动态代理:
* 涉及的类:Proxy
* 提供者:JDK官方
* 如何创建代理对象:
* 使用Proxy类中的newProxyInstance方法
* 创建代理对象的要求:
* 被代理类最少实现一个接口,如果没有则不能使用
* newProxyInstance方法的参数:
* ClassLoader:类加载器
* 它是用于加载代理对象字节码的。和被代理对象使用相同的类加载器。固定写法。
* Class[]:字节码数组
* 它是用于让代理对象和被代理对象有相同方法。固定写法。
* InvocationHandler:用于提供增强的代码
* 它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
* 此接口的实现类都是谁用谁写。
*/
IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
producer.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 作用:执行被代理对象的任何接口方法都会经过该方法
* 方法参数的含义
* @param proxy 代理对象的引用
* @param method 当前执行的方法
* @param args 当前执行方法所需的参数
* @return 和被代理对象方法有相同的返回值
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//提供增强的代码
Object returnValue = null;
//1.获取方法执行的参数
Float money = (Float)args[0];
//2.判断当前方法是不是销售
if("saleProduct".equals(method.getName())) {
returnValue = method.invoke(producer, money*0.8f);
}
return returnValue;
}
});
proxyProducer.saleProduct(10000f);
}
}
package com.itheima.cglib;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* 模拟一个消费者
*/
public class Client {
public static void main(String[] args) {
final Producer producer = new Producer();
/**
* 动态代理:
* 特点:字节码随用随创建,随用随加载
* 作用:不修改源码的基础上对方法增强
* 分类:
* 基于接口的动态代理
* 基于子类的动态代理
* 基于子类的动态代理:
* 涉及的类:Enhancer
* 提供者:第三方cglib库
* 如何创建代理对象:
* 使用Enhancer类中的create方法
* 创建代理对象的要求:
* 被代理类不能是最终类
* create方法的参数:
* Class:字节码
* 它是用于指定被代理对象的字节码。
*
* Callback:用于提供增强的代码
* 它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
* 此接口的实现类都是谁用谁写。
* 我们一般写的都是该接口的子接口实现类:MethodInterceptor
*/
Producer cglibProducer = (Producer)Enhancer.create(producer.getClass(), new MethodInterceptor() {
/**
* 执行北地阿里对象的任何方法都会经过该方法
* @param proxy
* @param method
* @param args
* 以上三个参数和基于接口的动态代理中invoke方法的参数是一样的
* @param methodProxy :当前执行方法的代理对象
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//提供增强的代码
Object returnValue = null;
//1.获取方法执行的参数
Float money = (Float)args[0];
//2.判断当前方法是不是销售
if("saleProduct".equals(method.getName())) {
returnValue = method.invoke(producer, money*0.8f);
}
return returnValue;
}
});
cglibProducer.saleProduct(12000f);
}
}
jdk动态代理是面向接口的动态代理。如果你的类是实现了某个接口,spring aop会使用jdk动态代理,生成一个和你实现同样接口的一个代理类,构造一个实例对象出来。
cglib动态代理是通过子类实现的动态代理。如果我们的类没有实现接口,spring aop会改用cglib 来实现动态代理,他是生成你的类的一个子类,可以动态生成字节码,覆盖你的方法,在方法里面加入增强的方法。
bean的作用域如下图所示:
是线程不安全的。spring bean默认来说是单例的,是线程不安全的。但是java web系统中,一般来说很少在spring bean中放一些实例变量,通常都是多个组件互相调用,最终去访问数据库的,所以一般结果就是多个线程并发的访问数据库。
spring ioc 和 aop ,动态代理技术,bean的线程安全问题,事务机制
事务的实现原理:如果说你加了一个@Transactional注解,此时spring就会使用AOP的思想,对你的这个方法在执行之前去开启事务,执行完毕之后,根据你方法是否报错,来决定回滚还是提交事务。
事务传播机制的理解:事务传播机制的理解
事务传播机制
比如说,我们现在有一段业务逻辑,方法A调用方法B,我希望的是如果说方法A出错了,此时仅仅回滚方法A,不能回滚方法B,必须得用REQUIRES-NEW,传播机制,让他们两的事务是不同的。
方法A调用方法B,如果出错,方法B只能回滚他自己,方法A可以带着方法B一起回滚,NESTED嵌套事务。
工厂模式,单例模式,代理模式
spring ioc 核心的设计模式的思想体现。
注意上图是前后端没有分离的架构,如果是前后端分离的架构,那么在第十步应该是返回一个json串,然后前端获取json串以后对页面进行渲染,然后返回html页面给浏览器。而不是放在后端中由后端来完成。
tomcat部署,tomcat自己就是基于java来开发的,我们启动的不是自己的系统,是一个tomcat是一个jvm进程,我们写得系统只不过是一些代码,放在tomcat的目录里,tomcat会加载我们的代码到jvm中去。
JVM最常用的内存区域有三块 栈内存,堆内存和永久代。栈内存是线程每个线程独享的,堆内存是共享的,永久代中存储的是类的信息。
java8 之后将永久代中的常量池放到了堆内存中,永久代变成了metaspace(元区域)。
我们的jvm的内存其实是有限制的,不可能是无限的,昂贵的资源,2核4G的机器,堆内存也就2GB左右,4核8G的机器堆内存也就4G左右,栈内存也需要空间,metaspace区域放类信息也需要空间。
jvm中有一个内存分代模型,年轻代和老年代,加在一起是堆内存,其中年轻代又分为三部分。年轻代和老年代的比例是我们可以设置的。
比如说年轻代一共是2GB内存,给老年代是2GB内存,默认情况下eden和2个s的比例是:8:1:1,eden是1.6GB, s是0.2GB
如果eden区域满了,此时必然触发垃圾回收,young gc ,ygc。谁是可以回收的垃圾对象?没有被引用的对象就是可以被回收的对象。
垃圾回收有一个概念,叫做stop the world,停止你的jvm里的工作线程的运行,然后扫描所有的对象,判断哪些可以回收,哪些不可以回收。
年轻代,大部分情况下,对象生存周期是很短的,可能0.01ms之内,线程执行了3个方法,创建了几个对象,0.01ms之后方法就都执行结束了,此时那几个对象就会在0,01ms之内变成垃圾,可以回收了。
复制算法,一次young gc,年轻代的垃圾回收。将eden中存活的对象复制到s1中,清空eden:
将s1和eden中存活的复制到s2中,清空eden和s1;将s2和eden存活的复制到s1中,清空eden和s2.
三种场景,第一种场景,有的对象在年轻代里面熬过了很多次的垃圾回收,例如15次垃圾回收,此时会认为这个对象是要长期存活的对象。例如Spring容器中的bean对象。
第二种情况就是s区放不下存活的对象。
第三种情况就是特别大的对象。反复移动大对象消耗性能。
老年代对象越来越多,老年代内存空间也会满,是不是可以使用类似的年轻代的复制算法?
答案是否定的,因为在老年代的对象中,很多都是长期引用的,spring容器管理的各种bean。
长期存活的对象是比较多的,可能甚至都有几百mb
对老年代而言,他里面的垃圾对象可能是没有那么多的。一开始采用标记-清理的方式,找出那些垃圾对象,让后直接把垃圾对象在老年代里清理掉,这样就会产生内存 碎片的问题。
后来采用标记-整理的方法,把老年代里的存活对象标记出来,移动到一起,存活对象压缩到一片内存空间去
剩余的空间都是垃圾整个给清理掉,剩余的都是连续的可用的内存空间,解决了内存碎片的问题。
垃圾回收器
parnew (新生代)+ cms(老年代)的组合, g1 直接分代回收 。g1可以实现新生代和老年代的垃圾一起回收。新版本,慢慢的就是主推g1垃圾回收器了,以后会淘汰掉parnews+cms组合,但是现在使用jdk8~jdk9居多,所以还是parnew+cms组合居多。
cms分成好几个阶段,初始标记,并发标记,并发清理等等,老年代垃圾回收是比较慢的,一般起码比年轻代垃圾回收慢个10倍以上。所以将它拆分成几个阶段,尽可能得让其和运行的其它线程并发执行。
不是说java语言可以跨平台 ,而是各个不同的平台都可以有让java语言运行的环境而已。
程序从源码到运行可以分为以下几个阶段,编码-编译-运行-调试。java在编译阶段体现了跨平台的特性。编译的过程大概是这样的:首先将java源码转化成class字节码文件,这是第一次编译,class字节码文件就是可以到处运行的文件。然后java字节码会被转化为目标机器的机器代码,这是由JVM来执行的,就是java的第二次编译。
“到处运行”的关键和前提就是JVM。因为在第二次编译的过程中JVM起着至关重要的作用。在可以运行java虚拟机的地方都内含着一个JVM操作系统。从而实现到到处运行的效果。需要强调的是,java不是编译机制,而是解释机制。java字节码的设计充分考虑了JIT这一及时编译方式,可以将字节码直接转化成高性能的本地机器码,这同样是虚拟机的一个构成部分。
网线,海底电缆等都属于网络的物理层,在物质层面将两台电脑连接起来,然后传递0/1的电路信号。
一套协议,能够实现将物理层的01信号进行分组等处理,规定电信号的含义是什么。
很多年前,每个公司都又自己定义的电路信号分组方式,但是后来出来了以太网协议,一组电信号是一个数据包,叫一个帧,每个帧分成两个部分,标头和数据,标头包含一些说明性的东西,比如说发送者、接受者和数据类型之类的。
以太网规定了,每个网卡必须得包含一个mac地址,mac地址就是这个网卡的唯一标识。
以太网协议规定了,接入网络里的所有设备,都要有网卡,以太网协议离得那个数据包,在数据链路层传输的数据包,必须从一个电脑的网卡传输到另外一个电脑的网卡,而这个网卡的地址就叫作所谓的mac地址。每块网卡出厂的时候,就有一个唯一的mac地址,48位的二进制,但是一般用12个16进制数字表示,前6个是厂商编号,后6个是网卡流水号。
但是以太网的数据包怎么从一个mac地址发送到另一个mac地址呢?这个不是精准推送的,在以太网中,如果一个电脑发送了一个数据包出去,会广播给局域网内的所有电脑设备的网卡,然后每天电脑都从数据包里获取接收者的mac地址,跟自己的mac地址对比一下,如果一样就说明是自己的数据包。
但是上面这种广播的方式,仅仅是针对一个子网内的电脑,会广播,否则一个电脑不能广播数据包给全世界所有的电脑吧,是仅仅广播给一个子网里面的电脑。
上面说到,子网内的电脑,通过以太网发数据包,对局域网内的电脑,是广播出去的。MAME如何知道哪些电脑在一个子网内呢?这就要靠网络层了,这里就有一套Ip地址,IP地址就可以让我们区分哪些电脑是一个子网的。
网络层里面有个IP协议,ip协议定义的地址就叫作ip地址.有ipv4和v6两个版本,目前使用你较多的是IPV4,是32个二级制数字组成,但是一般用4个十进制数字表示,范围是0.0.0.0到255.255.255.255之间。
每台计算机都会分配一个ip地址,ip地址前24位(就是前面三个十进制数字),代表了网络,后8位代表了主机。如果几台电脑是一个子网的。那么前三个十进制数字一定是一样的。
但是实际上上面就是举个例子,其实单从ip地址是看不出来那些机器是一个子网的,因为从10进制是判断不出来的。需要通过ip地址的二进制来判断,结合一个概念叫作子网掩码。比如说ip地址是192.168.56.1,子网掩码是255.255.255.0。知道了子网掩码之后,如果要判断两个ip地址是不是一个子网的,就分别把两个ip地址和自己的子网掩码进行二进制与运算,比较一下代表网络的那部分。
子网掩码的二进制是:111111111.111111111.111111111.00000000,然后跟ip地址的二进制做与运算,通过二进制来比较网络部分的地址是不是一模一样的。
有了网络层的ip地址之后,两台在子网内的电脑终于可以通过广播+mac地址判断来传输数据包进行通信了。但是如果发现要接收数据包的计算机不在子网内,那么就不能用过广播来发送数据包,需要通过路由来发送数据包。就是我们下面要说的路由。
举个例子,两个局域网之间如果通过一个路由器进行通信的话,是怎么进行的。
大概过程就是,路由器配置了两块网卡,每个网卡可以连接到一个局域网内。
局域网1内的电脑要发送数据到局域网2里面的电脑,在数据包里写上自己的ip地址和对方的ip地址。但是他们不再一个局域网内,于是局域网1内的电脑,通过交换机将数据包发送给路由器(网关),这个过程需要将路由器的一块网卡的ip地址对应的mac地址写到数据包的头部,然后才能通过交换机广播出去,路由器收到之后比较自己一块网卡的mac地址,就知道是来找自己的。
接着路由器接收到数据包之后,就会在局域网2内,将目标机器的ip地址对应的mac地址写入头部,接着再次通过交换机发送广播通知,发送给局域网2内的电脑。
如何知道ip地址和mac的对应关系?一个局域网内的每台机器都有自己的ARP cache,用来在一个局域网内让各个设备都知道每个设备的ip地址和mac地址的对应关系。一遍就是某个机器发送广播通知自己的ip地址和mac地址的对应关系,然后每个机器给他一个回应,以此类推。
下面介绍几个概念:
网关
网关其实就是路由器的一种,运作在网络层。可以把路由器上的ip地址认为就是网关,路由器上每个网卡都有mac地址和对应的ip地址。路由器虽然有mac地址,但是不能通过mac地址寻址的,必须通过ip地址寻址,所以路由器其实是工作在网络层的设备。(电脑上有默认网关)
网络交换机
也是一种设备,工作在数据链路层,路由器是工作在网络层。网络交换机是通过mac地址来寻址和传输数据包的;但是路由器是通过ip地址寻址和传输数据包的。网络交换机主要用于局域网的通信,一般你架设一个局域网,里面的电脑通信是通过数据链路层发送数据包,通过mac地址来广播的,广播的时候就是通过网络交换机这个设备来实现的;路由器一般用来连入因特网。
LAN WAN WLAN
局域网,广域网,无限局域网
假设你访问百度网站,先通过mac地址和交换机广播到默认网关,然后进行一层一层网关在寻址,一直到找到百度所在的那个服务器的ip地址和对应的mac地址(这里也用到交换机在子网的广播),然后传输数据。
这里还有一个问题,就是一台机器上,有很多程序使用一个网卡进行通信,比如浏览器、QQ等。
所以还需要一个端口的概念。端口号是0~65536的范围内, 0到1023被系统占用了,定义其它的就可以。
网络层基于ip协议,进行主机之间的寻址和通信,传输层是建立在某个主机的某个端口,到另外一个主机的某个端口的连接和通信的。这个通信就是通过socket来实现的,通过socket就可以基于tcp/ip协议完成上面所说的一系列比如基于ip地址和mac地址转换和寻址,通过路由通信,而且会建立一个端口到另外一个端口的连接。
udp和 tcp,都是传输层的协议,但是一个是可靠的一个是不可靠的。
传输层的tcp协议,仅仅只是规定了一套基于端口的点对点通信的协议,包括如何建立连接,如何发送消息和读取消息,但是实际上如果你要基于tcp协议来开发,应该使用socket,它里面就是实现了tcp协议。(实际上底层还是上面介绍的各种东西)
比较常见的,应用层的协议就是http协议,进行网络通信。
4层:数据链路层(以太网协议) ,网络层(ip协议),传输层(tcp协议 ),应用层(http协议)
7层:物理层,会话层,表示层和会话层合并成应用层。
最后我们看一下自己的网络设置,一般包含ip地址,子网掩码,网关地址,DNS地址。前面三个我们已经介绍过了,ip地址和子网掩码使用来划分子网的,判断哪些ip地址在一个子网内。同时ip地址和mac地址关联起来,唯一确定网卡。网关地址可以认为就是路由器上的那个网卡的ip地址吧。
那么DNS是什么呢?我们一般是通过ip地址+mac地址+端口号来定位一个通信目标的,但是如果浏览器上输入一个www.baidu.com?这个时候是先把www.baidu.com发给DNS服务器,然后DNS服务器告诉你对应的ip地址。
首先我们假设个电脑设置了几个东西:
ip地址:192.168.31.37
子网掩码:255.255.255.0
网关地址:192.168.31.1
DNS地址:8.8.8.8
百度的ip地址:172.194.16.08
三次握手
建立三次握手的时候,TCP报头用到了下面几个东西,ACK,SYN,FIN
第一次握手,客户端发送连接请求报文,此时SYN=1,ACK=0,这就是说这是一个连接请求,接着客户端就处于SYN_SEND状态,等待服务器响应。
第二次握手,服务器端收到SYN=1的请求报文,返回一个确认报文,ack = x+1,SYN=1,ACK=1,seq=y,发送给客户端,自己处于SYN_RECV状态
第三次握手,客户端发送报文确认信息。
四次挥手
第一次挥手,客户端发送报文,FIN=1,seq=u,此时进去FIN-WAIT-1状态
第二次挥手,服务端收到报文,这时候进入CLOSE-WAIT状态,并返回一个报文,
ACK=1,ack=u+1,seq=v.客户端收到这个报文之后进入FIN-WAIT-2状态,此时客户端到服务器端的连接就释放了。
第三次挥手,服务端发送释放报文,服务器端进入LAST-ACK状态
第四次挥手,客户端收到连接释放的报文之后,发送应答报文,进入TIME_WAIT状态,等待一会客户端进入CLOSED状态,服务端收到报文之后就进入了CLOSED状态。
其实就是说一说http请求和响应的规范。
http本身是没有所谓的长连接和短连接之说,其实是http下层的tcp连接是长连接还是短连接。
短连接就是对于每一次请求和响应,都经过tcp三次挥手建立连接,然后四次挥手断开连接。
但是如果一个网页有CSS样式,大量的图片,还有JS等等需要多次请求,如果采取短连接很显然不合适,性能非常差。
这个时候就有长连接的概念,长连接就是TCP再经过三次挥手连接后可以处理多个请求和响应。
myisam,不支持事务,不支持外键约束,索引文件和数据文件分开,这样在内存中可以缓存更多的索引,对查询的性能会更好,适用于少量的从插入大量查询的场景。
innodb是现在最常用的存储引擎,是mysql5.5之后的默认存储引擎。主要特点就是支持事务,走聚簇索引,强制要求有主键,支持外键约束,高并发、大数据量、高可用等相关成熟的数据库架构,分库分表、读写分离、主备切换,全部都可以基于innodb存储引擎来实现。到这里就可以展开来说一说是怎么支撑大数据的,怎么进行读写分离的。
主从同步的实现:首先主服务器将数据的变化记录到BinaryLog中,接下来从服务器会开启一个线程将主服务器中的数据操作同步到自己的RelayLog中,然后在开启一个线程执行该操作,更新从服务器中的数据。
b+树, b+树只有叶子节点上有数据
myisam 最大的特点就是数据文件和索引文件是分开的,会先在索引文件里搜索,得到一个物理地址,然后再到数据文件中定位一个行的。
innodb存储索引的实现,跟myisam最大的区别在于,innodb数据文件本身就是一个索引文件,key就是主键,叶子节点的data就是那个数据行。
innodb存储引擎,要求必须要有主键,可以默认内置的就会根据主键建立一个索引,叫做聚簇索引。
如果对某个非主键的字段创建一个索引,那么最后那个叶子节点的data就是主键的值,因为可以用主键的值到聚簇索引里根据主键再次查找到数据。
所以这里就明白了一个道理,为什么innodb下不要用UUID生成的1超长字符串作为主键?因为这样会导致所有的索引的data都是那个主键值,最终导致索引会变的很大,浪费很多的磁盘空间。
还有一个注意事项,一般innodb表里面,建议使用统一的auto_increment自增值作为主键值,因为这样可以保持聚簇索引直接加记录就行,如果用那种不是单调递增的主键值,可能会导致b+树分裂后重新组织,浪费时间。
索引的使用规则
最左前缀匹配规则,这个东西是和联合索引相关联的,很多时候不是一个一个字段分别搞一个一个的索引,而是针对几个索引建立一个联合索引。了解记住最左前缀匹配规则。
索引的缺点以及注意事项
索引是有缺点的,比如常见的就是会增加磁盘消耗,因为要占用磁盘空间。同时高并发的时候频繁插入和修改索引,就会导致性能的损耗。建议尽量少创建索引,比如一个表一两个索引。
三个基本面,存储引擎,索引,事务(了解事务的隔离级别,基于spring的事务支持)
事务就是一个并发控制单位,包含多个步骤的操作被事务所管理,要么同时成功,要么同时失败。
1,脏读
脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。
当一个事务正在多次修改某个数据,而在这个事务中这多次的修改都还未提交,这时一个并发的事务来访问该数据,就会造成两个事务得到的数据不一致。例如:用户A向用户B转账100元,对应SQL命令如下
update account set money=money+100 where name=’B’; (此时A通知B) update account set money=money - 100 where name=’A’;
当只执行第一条SQL时,A通知B查看账户,B发现确实钱已到账(此时即发生了脏读),而之后无论第二条SQL是否执行,只要该事务不提交,则所有操作都将回滚,那么当B以后再次查看账户时就会发现钱其实并没有转。
2,不可重复读
不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。
例如事务T1在读取某一数据,而事务T2立马修改了这个数据并且提交事务给数据库,事务T1再次读取该数据就得到了不同的结果,发送了不可重复读。
不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。
在某些情况下,不可重复读并不是问题,比如我们多次查询某个数据当然以最后查询得到的结果为主。但在另一些情况下就有可能发生问题,例如对于同一个数据A和B依次查询就可能不同,A和B就可能打起来了……
3,虚读(幻读)
幻读是事务非独立执行时发生的一种现象。例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。
幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。
设置不同的隔离级别就能解决上述问题。
隔离级别:(必须得知道)
MVCC 多版本并发控制的机制,mysql实现不可重复读。需要了解(不记得了就看视频)(需要熟知)
myql 的锁的类型 ,一般是表锁,行锁和页锁。
一般myisam会加表锁,就是myisam引擎下,执行查询的时候,会默认加个表共享锁,也就是表读锁,这个时候别人只能来查不能读写数据,myisam写的时候,也会加个表独占锁,也就是表写锁,别人不能读也不能写。
页级锁,一般也很少用。
重点聊一聊行锁,innodb引擎一般用行锁,但是也有表锁。
innodb的行锁有共享锁和排他锁两种,共享锁就是,多个事务都可以加共享锁,读同一行数据,但是别的事务不能写这行数据;排他锁,就是一个事务可以写这行数据,别的事务只能读,不能写。
insert,update,delete, innodb会自动给那一行加行级排他锁。
select,innodb啥锁都不加,因为innodb默认实现了可重复读,也就是mvcc(多版本并发控制机制),所以多个事务随便读一个数据,一般不会有冲突,大家读自己那个快照就可以了,不涉及到锁的问题。
注意innodb从来不会主动给自己加个共享锁,除非你手动加一个共享锁。
悲观锁和乐观锁
mysql里的悲观锁是走 select * from table where id =1 for update,意思就是我很悲观,我担心自己拿不到这把锁,我必须先锁死,然后就我一个人可以干这个事儿,别人都干不了,不能加共享锁,也不能加排他锁。(悲观锁要少用,容易产生很严重的死锁问题)
乐观锁,就是不需要提前搞一把锁,但是在查询数据的时候加入版本号,接着再执行各种业务逻辑之后再修改,在修改的时候会比较当前版本号和我之前查出来的版本号是不是一样的,如果是一样的就修改然后将版本号加1,否则就不会更新任何一行数据,此时就重新查询后再次更新。
死锁:常见的死锁就是两个事务分别都持有一个锁,结果还是去请求别人持有的那把锁,结果就是谁也出不来,死锁了。
死锁如何排查:找dba看一下死锁日志,就ok了,然后根据对应的sql,找一下对应的代码,具体判断一下为什么死锁了。
看执行计划,判断有没有走索引。
/**
* 饿汉式单例设计模式
* 类一加载就有对象
*/
public class Single {
//私有化构造函数
private Single() {
}
//创建私有并静态的本类对象
private static Single s = new Single();
//定义公有并静态的方法
public static Single getInstance() {
return s;
}
}
//懒汉模式 需要的时候才会加载
public class Single2 {
private Single2() {
}
private static Single2 s = null;
public static Single2 getInstance() {
if (s == null) {
s = new Single2();
}
return s;
}
}