数组:
数组的理解:数组(Array),是多个相同类型数据一定顺序排列的集合,并使用一个名字命名, 并通过编号的方式对这些数据进行统一管理。
数组的特点:
- 1数组是序排列的
- 2数组属于引用数据类型的变量。数组的元素,既可以是基本数据类型,也可以是引用数据类型
- 3创建数组对象会在内存中开辟一整块连续的空间
- 4数组的长度一旦确定,就不能修改。
数组的分类:
- ① 照维数:一维数组、二维数组、。。。
- ② 照数组元素的类型:基本数据类型元素的数组、引用数据类型元素的数组
数据结构:
1.数据与数据之间的逻辑关系:集合、一对一、一对多、多对多
2.数据的存储结构:
线性表:顺序表(比如:数组)、链表、栈、队列
树形结构:二叉树
图形结构:
并行和并发的区别:
并发,指的是多个事情,在同一时间段内同时发生了。 并行,指的是多个事情,在同一时间点上同时发生了。并发的多个任务之间是互相抢占资源的。
并行的多个任务之间是不互相抢占资源的、
只有在多CPU或者一个CPU多核的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的。
线程和进程的关系
进程(process) :程序的一次执行过程,或是正在运行的一个程序。
说明:进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
线程(thread) :进程可进一步细化为线程,是一个程序内部的一条执行路径。
说明:线程作为调度和执行的单位,每个线程拥独立的运行栈和程序计数器(pc),线程切换的开销小。
进程可以细化为多个线程。
每个线程,拥有自己独立的:栈、程序计数器。
多个线程,共享同一个进程中的结构:方法区、堆。
一个Java应用程序java.exe,其实至少三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
创建多线程方法:
方式一:继承Thread类的方式:
-
- 创建一个继承于Thread类的子类
-
- 重写Thread类的run() --> 将此线程执行的操作声明在run()中
-
- 创建Thread类的子类的对象
-
- 通过此对象调用start():①启动当前线程 ② 调用当前线程的run()
说明两个问题:
问题一:我们启动一个线程,必须调用start(),不能调用run()的方式启动线程。
问题二:如果再启动一个线程,必须重新创建一个Thread子类的对象,调用此对象的start().
方式二:实现Runnable接口的方式:
-
- 创建一个实现了Runnable接口的类
-
- 实现类去实现Runnable中的抽象方法:run()
-
- 创建实现类的对象
-
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
-
- 通过Thread类的对象调用start()
两种方式的对比:
- 开发中:优先选择:实现Runnable接口的方式
- 原因:1. 实现的方式没类的单继承性的局限性
-
2. 实现的方式更适合来处理多个线程共享数据的情况。
- 联系:public class Thread implements Runnable
- 相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
目前两种方式,要想启动线程,都是调用的Thread类中的start()。
新增方式一:实现Callable接口。 — JDK 5.0新增
- 创建一个实现Callable接口的实现类,同时重写Callable接口的call()方法;
- 创建Callable接口的实现类对象;
- 通过FutureTask线程结束处理类的有参构造方法来封装Callable接口实现类对象;
- 使用参数为FutureTask类对象的Thread有参构造方法创建Thread线程实例;
调用线程实例的start()方法启动线程;
参考:https://blog.csdn.net/qq_43803371/article/details/118752392
如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大? -
- call()可以返回值的。
-
- call()可以抛出异常,被外面的操作捕获,获取异常的信息
-
- Callable是支持泛型的
新增方式二:使用线程池
说明:
- 好处:
- 1.提高响应速度(减少了创建新线程的时间)
- 2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 3.便于线程管理
-
corePoolSize:核心线程数
-
maximumPoolSize:最大线程数
-
keepAliveTime:线程没任务时最多保持多长时间后会终止
死锁的理解:
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
1、出现死锁后,不会出现异常,不会出现提示,只是所的线程都处于阻塞状态,无法继续
2、我们使用同步时,要避免出现死锁。
在有些情况下死锁是可以避免的。本文将展示三种用于避免死锁的技术:
加锁顺序
加锁时限
死锁检测
线程通信涉及到的三个方法:
- wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
- notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
- notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
面试题:sleep() 和 wait()的异同?
- 1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
- 2.不同点:1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
-
2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
-
3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
Java解决方案:同步机制
在Java中,我们通过同步机制,来解决线程的安全问题。
方式一:同步代码块
- synchronized(同步监视器){
-
//需要被同步的代码
- }
- 说明:1.操作共享数据的代码,即为需要被同步的代码。 -->不能包含代码多了,也不能包含代码少了。
-
2.共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
-
3.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
-
要求:多个线程必须要共用同一把锁。
- 补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。
- 方式二:同步方法
-
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
- 关于同步方法的总结:
同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
非静态的同步方法,同步监视器是:this
private synchronized void show(){ //同步监视器:this
静态的同步方法,同步监视器是:当前类本身
private static synchronized void sale()
{ //记住加上static修饰,默认同步监视器就是当前类的Class对象}
synchronized (this) { }称之为同步代码块
同步方法3 使用特殊域变量(volatile)实现线程同步
a.volatile关键字为成员变量变量的访问提供了一种免锁机制,
b.使用volatile修饰成员变量相当于告诉虚拟机该域可能会被其他线程更新,
c.因此每次使用该成员变量就要重新计算,而不是使用寄存器中的值
d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量
方式三:Lock锁 — JDK5.0新增
在Java多线程中,可以使用synchronized
关键字实现线程之间的同步互斥,在jdk1.5后新增的ReentrantLock
类同样可达到此效果,且在使用上比synchronized
更加灵活。
- 面试题:synchronized 与 Lock的异同?
- 相同:二者都可以解决线程安全问题
- 不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
- Lock需要手动的启动同步(lock(),同时结束同步也需要手动的实现(unlock())
使用的优先顺序:
-
Lock —> 同步代码块(已经进入了方法体,分配了相应资源 ) —> 同步方法(在方法体之外)
private Lock lock = new ReentrantLock();
synchronized和lock锁
1.synchronized是java中的一个关键字,可以使用为同步函数或者同步函数块表示同步,里面包含了锁和解锁的过程,只允许一个线程进入,调用的是JVM内部的方法。
lock锁是java1.5后新增加的一种类,也可以锁线程,但是需要用lock()和unlock手动上锁和解锁,调用的是通过static 内部类sync来进行锁操作。
2.synchronized中一旦对一个线程上锁,其他竞争的线程都会处于等待状态无法操作。
而用lock上锁可以使用tryLock方法,获取锁失败时返回false,这样没有竞争获得锁的线程也可以执行其它的操作, 而不至于使线程进入休眠。也可以限定处于竞争锁队列的时间,更加灵活。
3.synchronized中的notify(),wait()不能唤醒和等待特定的线程。
而lock锁提供了一个方法可以创建多个Condition对象,这个对象同样可以使用await(),signal()唤醒和等待线程,这样多线程时就可以指定对一类相同类型线程进行唤醒或者等待操作,也变得更加灵活。
ReentrantLock、synchronized
(1)synchronized是独占锁
,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。
(2)synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。
(3)synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以相应中断。
ReentrantLock好像比synchronized关键字没好太多,我们再去看看synchronized所没有的,一个最主要的就是ReentrantLock还可以实现公平锁机制。什么叫公平锁呢?也就是在锁上等待时间最长的线程将获得锁的使用权。通俗的理解就是谁排队时间最长谁先执行获取锁。
volatile和synchronized的区别
volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
使用volatile 关键字修饰共享变量时,变量就会有以下特点:
1、变量对其他线程具有可见性。
2、禁止进行指令重排,保证了有序性。
3、保证单操作原子性,对任意单个volatile变量的读写具有原子性,但对于复合操作不保证原子性,如x++。
为什么volatile能保证有序性不能保证原子性
对于内存模型的三大特性:有序性、原子性、可见性。
- 原子性
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。 - 对于可见性,Java提供了volatile关键字来保证可见性。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。 - 在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
理解:既然i是被volatile修饰的变量,那么对于i的操作应该是线程之间是可见的啊,就算A.,B两个线程都同时读到i的值是5,但是如果A线程执行完i的操作以后应该会把B线程读到的i的值置为无效并强制B重新读入i的新值也就是6然后才会进行自增操作才对。
1、线程读取i
2、temp = i + 1
3、i = temp
当 i=5 的时候A,B两个线程同时读入了 i 的值, 然后A线程执行了 temp = i + 1的操作, 要注意,此时的 i 的值还没有变化,然后B线程也执行了 temp = i + 1的操作,注意,此时A,B两个线程保存的 i 的值都是5,temp 的值都是6, 然后A线程执行了 i = temp (6)的操作,此时i的值会立即刷新到主存并通知其他线程保存的 i 值失效, 此时B线程需要重新读取 i 的值那么此时B线程保存的 i 就是6,同时B线程保存的 temp 还仍然是6, 然后B线程执行 i=temp (6),所以导致了计算结果比预期少了1。
参考:https://www.cnblogs.com/simpleDi/p/11517150.html
线程
线程的五种状态:
graph LR
新建 --> 就绪;
就绪 --> 运行;
运行 --> 就绪;
运行 --> 阻塞;
阻塞 --> 就绪;
运行 --> 死亡;
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,实现线程同步的方法有很多,临界区对象就是其中一种。
在线程同步时,线程可能由于以下情况被阻塞:
同步阻塞。就是被锁阻塞。
等待阻塞。被条件变量阻塞。
其它。调用sleep(), join()或等待IO操作时的阻塞。
String、StringBuffer、StringBuilder三者的对比
String:不可变的字符序列;底层使用char[]存储
StringBuffer:可变的字符序列;线程安全的,效率低;底层使用char[]存储
StringBuilder:可变的字符序列;jdk5.0新增的,线程不安全的,效率高;底层使用char[]存储
对比String、StringBuffer、StringBuilder三者的执行效率
从高到低排列:StringBuilder > StringBuffer > String
StringBuffer、StringBuilder中的常用方法
增:append(xxx)
删:delete(int start,int end)
改:setCharAt(int n ,char ch) / replace(int start, int end, String str)
查:charAt(int n )
插:insert(int offset, xxx)
长度:length();
*遍历:for() + charAt() / toString()