1.基础知识
1.1 Java语言特点
- 编译型:编译器针对特定的平台将某种高级语言源代码一次性“编译”成可被该平台硬件执行的机器码;运行效率高,无法跨平台工作。C\C++
- 解释型:解释器对源程序逐行解释成特定平台的机器码并立即执行的语言,效率低,跨平台.比如python
- Java既是编译型又是解释型。源文件--通过javac编译--二进制的.class文件(字节码[byte code],与平台无关)--JVM(java虚拟机)--特定平台机器码[machine code]。
- 编译后的字节码与平台无关,所以Java所谓的跨平台就是在不同平台上安装了不同的jvm,而在不同平台上生成的.class文件都是一样的,而.class文件再由对应平台的jvm解释成对应平台的机器码执行。它首先将源代码编译成字节码,然后依赖各种不同平台上的虚拟机来解释执行字节码,从而实现了“一次编写,到处运行”的跨平台特性。
1.2 字符串
- String 长度大小不可变,String 是被 final 修饰的,他的长度是不可变的,就算调用 String 的concat 方法,那也是把字符串拼接起来并重新创建一个对象,把拼接后的 String 的值赋给新创建的对象
String\StringBuffer\StringBuilder 特点,区别,应用场景
- 都是 final 类, 都不允许被继承;
- String是不可变对象, 因此在每次对 String 类型进行改变的时候其实都等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象,所以经常改变内容的字符串最好不要用 String ,因为每次生成对象都会对系统性能产生影响。
- StringBuffer和StringBuilder,前者线程安全,后者线程不安全,前者速度慢一些
StringBuffer每次结果都会对 StringBuffer 对象本身进行操作,而不是生成新的对象,再改变对象引用。所以在一般情况下推荐使用 StringBuffer ,特别是字符串对象经常改变的情况下
1.3 序列化
目的
序列化:把Java对象转换为字节序列。
反序列化:把字节序列恢复为原先的Java对象。
目的:方便存储和网络传输
实现
实现Serializable接口
- 该接口只是一个可序列化的标志,并没有包含实际的属性和方法。
- 为了保证安全性,可以使用transient关键字进行修饰不必序列化的属性。因为在反序列化时,private修饰的属性也能看到。
进一步的升华
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
如果一个对象既不是字符串、数组、枚举,而且也没有实现Serializable接口的话,在序列化时就会抛出NotSerializableException异常!
原来Serializable接口也仅仅只是做一个标记用!!!
它告诉代码只要是实现了Serializable接口的类都是可以被序列化的!然而真正的序列化动作不需要靠它完成。
serialVersionUID号的作用
1、serialVersionUID是序列化前后的唯一标识符
2、默认如果没有人为显式定义过serialVersionUID,那编译器会为它自动声明一个!
第1个问题: serialVersionUID序列化ID,可以看成是序列化和反序列化过程中的“暗号”,在反序列化时,JVM会把字节流中的序列号ID和被序列化类中的序列号ID做比对,只有两者一致,才能重新反序列化,否则就会报异常来终止反序列化的过程。
第2个问题: 如果在定义一个可序列化的类时,没有人为显式地给它定义一个serialVersionUID的话,则Java运行时环境会根据该类的各方面信息自动地为它生成一个默认的serialVersionUID,一旦像上面一样更改了类的结构或者信息,则类的serialVersionUID也会跟着变化!
所以,为了serialVersionUID的确定性,写代码时还是建议,凡是implements Serializable的类,都最好人为显式地为它声明一个serialVersionUID明确值!
这里面有个点,关注下,如果是自己默认生成了一个uid,但是这个类的结构发生了变化,由于默认生成的uid还是之前的,此时反序列化其实是可以成功的!
特殊情况
1、凡是被static修饰的字段是不会被序列化的
对于第一点,因为序列化保存的是对象的状态而非类的状态,所以会忽略static静态域也是理所应当的。
2、凡是被transient修饰符修饰的字段也是不会被序列化的
如果在序列化某个类的对象时,就是不希望某个字段被序列化(比如这个字段存放的是隐私值,如:密码等),那这时就可以用transient修饰符来修饰该字段。
序列化的受控
序列化和反序列化的过程其实是有漏洞的,因为从序列化到反序列化是有中间过程的,如果被别人拿到了中间字节流,然后加以伪造或者篡改,那反序列化出来的对象就会有一定风险了。
毕竟反序列化也相当于一种 “隐式的”对象构造 ,因此我们希望在反序列化时,进行受控的对象反序列化动作。
那怎么个受控法呢?
答案就是: 自行编写readObject()函数,用于对象的反序列化构造,从而提供约束性。
private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
// 调用默认的反序列化函数
objectInputStream.defaultReadObject();
// 手工检查反序列化后学生成绩的有效性,若发现有问题,即终止操作!
if (0 > score || 100 < score) {
throw new IllegalArgumentException("学生分数只能在0到100之间!");
}
}
原因:反射的作用
1.4 面对对象
Java的四个基本特性(抽象、封装、继承和多态)
- 抽象:抽象是将一类对象的共同特征总结出来构造类的特征,包括数据抽象和行为抽象,抽象只关注行为和属性,不关注这些行为的细节是什么。
- 继承:继承是从已有类得到继承信息创建新类的过程,是一种特殊化和一般化的关系,is-a的关系,同时子类可以添加新的方法和覆盖父类已有的方法。
- 封装:就是把属于同一类事物的共性归到一个类中,以方便使用,用户无需知道对象内部方法实现的细节,但可以根据对象提供的外部接口访问该对象。
- 多态:多态就是相同的事物,调用相同的方法,但是表现的行为却不同,实现的三个必要条件:继承,重写和父类引用指向子类对象。
面对对象编程的五个基本原则
- 单一职责原则:(Single-Resposibility Principle)一个类,最好只做一件事,只有一个引起它的变化。
- 开放封闭原则(Open-Closed principle):对于扩展是开放的,对于修改是关闭的
- Liskov(里式)替换原则(Liskov-Substituion Principle):任何时候都可以用子类型替换掉父类型。子类一定是增加父类的能力而不是减少父类的能力
- 依赖倒置原则(Dependecy-Inversion Principle):具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。
- 接口隔离原则(Interface-Segregation Principle):使用多个小的专门的接口,而不要使用一个大的总接口
- 一个单词:立方体(solid),很好记!!!
static 关键字
- 被静态修饰的成员函数只能访问静态成员,不可以访问非静态成员。静态是随着类的加载而加载的,因此可以直接用类进行访问。
- 静态的成员属于类,随着类的加载而加载到静态方法区内存,当类加载时,此时不一定有实例创建,没有实例,就不可以访问非静态的成员。
-
生命周期(Lifecycle):
静态方法(Static Method)与静态成员变量一样,属于类本身,在类装载的时候被装载到内存(Memory),不自动进行销毁,会一直存在于内存中,直到JVM关闭。
非静态方法(Non-Static Method)又叫实例化方法,属于实例对象,实例化后才会分配内存,必须通过类的实例来引用。不会常驻内存,当实例对象被JVM 回收之后,也跟着消失。 - 静态类常用来做工具类,比如Collections,Arrays等等
类可见性修饰符
- public 所有可见
- private 仅对本类可见
- protected 本包和子类可见
- default 本包可见(没有修饰符)
抽象类与接口
- 说明
- 接口是一种规范,就像现实中生产主板和内存条或者网卡的不是同一家产商,但是为何内存或者网卡插入到主板上就能用呢,因为他们都遵守了某种规范。
- 抽象类是对类更高的抽象,它所体现的是一种模板设计,作为多个子类的父类,可以理解为系统实现过程中的中间产品,这个中间产品已经实现了系统的部分功能,但是不能当做最终产品,还需要进一步完善。
- 相似点:二者都不能实例化,用于被其他类实现和继承;二者都可以包含抽象方法,实现接口或者继承抽象类的普通子类都必须实现这些抽象方法。
- 区别
- 属性:接口没有普通属性,只有 public static final修饰的属性,可以省略;而抽象类可以有普通属性,也可以有静态属性。
- 方法:接口中方法都没有方法体,默认为public abstract 修饰,对于抽象类则都可以
- 构造函数:接口没有构造函数,抽象类有构造函数,但是不能new对象,而是用于子类调用初始化抽象类的操作
- 一个类只能有一个直接父类,包括抽象类,而类可以有多个接口,相当于弥补Java不能多继承的不足
重载和重写
- 重写:重写是子类对父类的允许访问的方法的实现过程进行重新编写, 方法名和形参都不能改变。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。
-
两同两小一大
- 方法名相同,参数类型相同
- 子类返回类型小于等于父类方法返回类型,
- 子类抛出异常小于等于父类方法抛出异常,
- 子类访问权限大于等于父类方法访问权限。
- 父类申明变量指向子类实例,该父类变量不能调用父类不存在的变量和方法,否则会抛异常
- 重载(overloading) 是在一个类里面,方法名字相同,而参数不同【参数类型或者参数个数或者参数次序不同】。返回类型可以相同也可以不同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。最常用的地方就是构造器的重载。
final关键字
- 使用final的类不能被继承
- 使用final的方法不能被子类所重写,但是可以被重载。
- 使用final的变量即为常量,常量不能被修改,常量名要求全部为大写
- 当final修饰基本类型变量的时候,不能对基本类型变量重新赋值,因此基本类型变量不能被改变。
- 但对于引用类型变量而言,它保存的仅仅是一个引用,final只保证这个引用类型变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以发生改变。
- 即修饰一个变量时,是指引用变量不能变 ,引用变量所指向的对象中的内容 还是可以改变的。
- final 定义的变量,不是必须要在定义的同时完成初始化,也可以在构造方法中完成初始化。
多态
- 三大特性:继承、封装、多态
- 多态:同一操作,用于不同的对象,可以有不同的解释,有不同的执行结果,这就是多态。简单来说就是:父类的引用指向子类对象
- 多态的实现:
- 方法重载实现的是编译时的多态性(也成为前绑定)
- 方法重写实现的运行时的多态性(也称为后绑定)
-
存在的三个必要条件
继承
重写
父类引用指向子类对象,比如:
Parent p=new Child();
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。
多态的好处:减少代码量;可以使程序有良好的扩展(多态保证);并可以对所有类的对象进行通用处理即代码的维护性(继承保证)
- 多态执行问题时:“成员变量,静态方法看左边;非静态方法:编译看左边,运行看右边。”意思是:当父类变量引用子类对象时(Fu f = new Zi();,在这个引用变量f指向的对象中,他的成员变量和静态方法与父类是一致的,他的非静态方法,在编译时是与父类一致的,运行时却与子类一致(发生了复写),当然如果子类没有复写还是使用父类的。
- 向上转型:父类 父类对象=子类实例 【程序自动完成】 子类对象--->父类对象
- 向上转型会遗失和父类不一样的方法
- 向下转型:子类 子类对象=(子类)父类实例 【明确指出要转型的子类类型】父类对象--->子类对象
- 向下转型【自己的理解是向下转型既可以调用子类覆盖父类的方法,也可以调用父类独有的方法,也可以调用子类自己的方法】
public class JavaLearning {
public static void main(String[] args) {
A a=new B();//向上转型 子类-->父类
B b=(B) a;//向下转型 子类-->父类
b.fun1();//调用方法被复写的方法
b.fun2();//调用父类的方法
b.fun3();//调用子类自己定义的方法
}
}
class A{
public void fun1() {
System.out.println("A-->public void fun1(){}");
}
public void fun2() {
this.fun1();
}
}
class B extends A{
public void fun1() {
System.out.println("B-->public void fun1(){}");
}
public void fun3() {
System.out.println("B-->public void fun3(){}");
}
}
- 在对象进行向下转型前,必须发生对象向上转型,否则将会出现异常。
代码块执行顺序
-
代码块的分类
- 普通代码块:直接在方法或语句中定义的代码块
- 构造器:类的构造函数
- 静态代码块:使用static关键字声明的代码块
-
执行顺序如下
- 类的初始化阶段:最先执行主方法类中的静态代码块,再执行最顶层父类的静态代码块,然后依次向下,执行当前类的静态初始化代码块。(而且只执行一次)
- 对象的初始化阶段:先执行最顶层父类的普通代码块、构造器(先无参再有参),然后依次向下,执行当前类的普通代码块、构造器(先无参、再有参),new几次调用几次。
- 主方法中的普通代码块和构造器不会被调用。
(1) 父类静态对象和静态代码块[注意:如果该方法调用的时候,main函数不是在子类中,而是另外一个类,则,应该先调用主方法所在类的父类的静态方法,然后是主方法类的静态方法,之后才是调用类的父类的静态方法]
(2) 子类静态对象和静态代码块
(3) 父类非静态对象和非静态代码块
(4) 父类构造函数(先无参,再有参)
(5) 子类 非静态对象和非静态代码块
(6) 子类构造函数
其中:类中静态块按照声明顺序执行,并且(1)和(2)不需要调用new类实例的时候就执行了(意思就是在类加载到方法区的时候执行的)
2. 容器
3.多线程
3.1 基础概念
线程安全:多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。 比如无状态对象一定是线程安全的。
如何保证线程安全:
对变量使用:volatile
volatile变量每次被线程访问时,都强迫线程从主内存中重读该变量的最新值,而当该变量发生修改变化时,也会强迫线程将最新的值刷新回主内存中。这样一来,不同的线程都能及时的看到该变量的最新值。对程序段进行加锁:(synchronized,lock)
线程间共享变量使用:ThreadLocal
ThreadLocal是JDK引入的一种机制,它用于解决线程间共享变量,使用ThreadLocal声明的变量,即使在线程中属于全局变量,针对每个线程来讲,这个变量也是独立的,即线程隔离的。只要线程存活并且ThreadLocal
实例可以访问,每个线程都保存对其线程局部变量副本的隐式引用; 线程消失后,线程本地实例的所有副本都将被垃圾收集(除非存在对这些副本的其他引用)。
多线程如何通信:Object中的方法,wait(),notify(),notifyAll()
有状态和无状态对象
- 有状态就是有数据存储功能。有状态对象(Stateful Bean),就是有实例变量的对象 ,可以保存数据,是非线程安全的。在不同方法调用间不保留任何状态。
- 无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有实例变量的对象 .不能保存数据,是不变类,是线程安全的。
3.2 核心API
3.2.1 多线程的实现
继承Thread/实现Runnable接口/
实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以理解为任务是通过线程驱动从而执行的。
1. 三者区别
-
查看源码的区别:
- a.继承Thread : 由于子类重写了Thread类的run(), 当调用start()时, 直接找子类的run()方法
- b.实现Runnable : 构造函数中传入了Runnable的引用, 成员变量记住了它, start()调用run()方法时内部判断成员变量Runnable的引用是否为空, 不为空编译时看的是Runnable的run(),运行时执行的是子类的run()方法
-
继承Thread
- 好处是:可以直接使用Thread类中的方法,代码简单
- 弊端是:如果已经有了父类,就不能用这种方法
-
实现Runnable接口
- 好处是:即使自己定义的线程类有了父类也没关系,因为有了父类也可以实现接口,而且接口是可以多实现的
- 弊端是:不能直接使用Thread中的方法需要先获取到线程对象后,才能得到Thread的方法,代码复杂
-
实现Callable接口
- 实现Callable接口要实现call方法,并且线程执行完毕后会有返回值。其他的两种都是重写run方法,没有返回值。
2.start方法与run方法
- 用 start方法来启动线程,是真正实现了多线程, 通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法。但要注意的是,此时无需等待run()方法执行完毕,即可继续执行下面的代码。所以run()方法并没有实现多线程。
- run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码。
3.2.2 API
-
Thread.sleep(millis);
休眠,控制当前线程休眠若干毫秒,到设置时间自动醒过来 -
thread.setDaemon(true);
设置一个线程为守护线程, 该线程不会单独执行, 当其他非守护线程都执行结束后, 自动退出(可以设置多个守护线程,遵守一个非守护线程) -
thread.join([long millis])
: 等待这个线程死亡或者最多等待millis毫秒
在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。
对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先于 b 线程的输出。
public class JoinExample {
private class A extends Thread {
@Override
public void run() {
System.out.println("A");
}
}
private class B extends Thread {
private A a;
B(A a) {
this.a = a;
}
@Override
public void run() {
try {
a.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B");
}
}
public void test() {
A a = new A();
B b = new B(a);
b.start();
a.start();
}
}
-
thread.setPriority(int i);
{1,10} 设置优先级
sleep() 与 yield()的区别
- sleep() 方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行;
- 线程执行 sleep() 方法后转入阻塞(blocked)状态,而执行 yield() 方法后转入就绪(ready)状态;
- sleep() 方法声明抛出InterruptedException,而 yield() 方法没有声明任何异常;
- sleep() 方法比 yield() 方法(跟操作系统相关)具有更好的可移植性。
3.2.3 wait()与notify()
- 什么时候需要通信
- 多个线程并发执行时, 在默认情况下CPU是随机切换线程的
- 如果我们希望他们有规律的执行, 就可以使用通信, 例如每个线程执行一次打印
- 怎么通信
- 如果希望线程等待, 就调用wait()
- 如果希望唤醒等待的线程, 就调用notify();
- 这两个方法必须在同步代码中执行, 并且使用同步锁对象来调用
- 多个线程通信的问题
- notify()方法是随机唤醒一个线程
- notifyAll()方法是唤醒所有线程
- 如果多个线程之间通信, 需要使用notifyAll()通知所有线程, 用while来反复判断条件
- 使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。
public class Printer {
static Object obj=new Object();
private int flag=1;
public synchronized void print1() throws InterruptedException {
while(flag!=1) {
this.wait();
}
System.out.print("黑");
System.out.print("马");
System.out.print("程");
System.out.print("序");
System.out.print("员");
System.out.print("\r\n");
flag=2;
this.notify();
}
public void print2() throws InterruptedException {
synchronized (this) {
while(flag!=2) {
this.wait();
}
System.out.print("上");
System.out.print("海");
System.out.print("交");
System.out.print("大");
System.out.print("\r\n");
flag=1;
this.notify();
}
}
小结
- 在同步代码块中,用哪个对象锁,就用哪个对象调用wait方法
- 为什么wait方法和notify方法定义在Object这类中?
- 因为锁对象可以是任意对象,Object是所有的类的基类,所以wait方法和notify方法需要定义在Object这个类中
- sleep方法和wait方法的区别?
- sleep是Thread类的方法,wait是Object类的方法
- sleep()方法是线程类(Thread)的静态方法,必须传入参数,参数就是时间,时间到了自动醒来,因为调用 sleep 不会释放对象锁。期间进入阻塞状态
- wait() 是 Object 类的方法,对此对象调用 wait()方法导致本线程放弃对象锁(线程暂停执行),进入等待此对象的等待锁定池,只有针对此对象发出 notify 方法(或 notifyAll)后本线程才进入对象锁定池准备获得对象锁进入就绪状态。
- Thread.sleep() 和 Object.wait(),都可以抛出 InterruptedException。这个异常是不能忽略的,因为它是一个检查异常(checked exception)
3.2.4 线程的六种状态
- 新建(NEW):创建线程对象,但是并未启动
- 就绪:有执行的资格,但是没有执行的权利
- 运行:有执行的资格,有执行的权利
- 阻塞(blocked):没有执行资格,没有执行权
请求获取 monitor lock 从而进入 synchronized 函数或者代码块,但是其它线程已经占用了该 monitor lock,所以出于阻塞状态。要结束该状态进入从而 RUNABLE 需要其他线程释放 monitor lock。
- 等待(waiting)
无限期等待:等待其它线程显式地唤醒
阻塞和等待的区别在于,阻塞是被动的,它是在等待获取 monitor lock。而等待是主动的,通过调用 Object.wait() 等方法进入。
限期等待:无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。
|进入方法| 退出方法|
|Thread.sleep() 方法| 时间结束
|设置了 Timeout 参数的 Object.wait() 方法| 时间结束 / Object.notify() / Object.notifyAll()|
|设置了 Timeout 参数的 Thread.join() 方法| 时间结束 / 被调用的线程执行完毕|
- 死亡:TERMINATED
3.2.5 中断
一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。
interrupt
通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。
Executor 的中断操作
调用 Executor 的 shutdown() 方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。
如果只想中断 Executor 中的一个线程,可以通过使用 submit() 方法来提交一个线程,它会返回一个 Future<?> 对象,通过调用该对象的 cancel(true) 方法就可以中断线程。
3.3 各种锁
Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。
1.同步代码块synchronized(obj)
- 适用情况
- 当多线程并发, 有多段代码同时执行时, 我们希望某一段代码执行的过程中CPU不要切换到其他线程工作. 这时就需要同步.
- 如果两段代码是同步的, 那么同一时间只能执行一段, 在一段代码没执行结束之前, 不会执行另外一段代码
- 使用synchronized关键字加上一个锁对象来定义一段代码, 这就叫同步代码块
- 多个同步代码块如果使用相同的锁对象, 那么他们就是同步的
- 注意,锁对象可以任意对象,但是被锁的对象需要保证是同一把锁,不能用匿名对象。
- 当两个并发线程(thread1和thread2)访问同一个对象(syncThread)中的synchronized代码块时,在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。Thread1和thread2是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。
- 使用synchronized关键字修饰一个方法, 该方法中所有的代码都是同步的
- 非静态同步函数的锁是:this;静态的同步函数的锁是:该类的字节码对象
2.volatile
Volatile自身特性:
- Volatile 是轻量级的synchronized,它在多处理器开发过程中保证了共享变量的“可见性”,可见性是指当一个线程的某个共享变量发生改变时,另一个线程能够读取到这个修改的值。Voaltile变量修饰的变量在进行写操作时在多核处理器下首先将当前处理器缓存行的数据写回到系统内存中。为了保证一致性,其他处理器嗅探到总线上传播的数据,发现数据被修改了自己缓存地址的数据无效
volatile 关键字的作用
- 保证内存的可见性
- 防止指令重排
- 注意:volatile并不保证原子性
- 前提:即变量真正独立于其他变量和自己以前的值
- 您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
- (1)对变量的写操作不依赖于当前值。
- (2)该变量没有包含在具有其他变量的不变式中。
- 实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
- 第一个条件的限制使 volatile 变量不能用作线程安全计数器。虽然增量操作(x++)看上去类似一个单独操作,实际上它是一个由读取-修改-写入操作序列组成的组合操作,必须以原子方式执行,而 volatile 不能提供必须的原子特性。实现正确的操作需要使 x 的值在操作期间保持不变,而 volatile 变量无法实现这点。
内存可见性
- volatile保证可见性的原理是在每次访问变量时都会进行一次刷新,因此每次访问都是主内存中最新的版本。
所以volatile关键字的作用之一就是保证变量修改的实时可见性。
volatile与synchronized区别
1.volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取,它在多处理器开发过程中保证了共享变量的“可见性;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住;
2.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的;
3.volatile仅能实现变量的修改可见性,不能保证原子性,没有sychronized安全;而synchronized则可以保证变量的修改可见性和原子性;
4.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞;
3.ReentranLock(可重入锁)
- 1.同步
- 使用ReentrantLock类的lock()和unlock()方法进行同步
- 2.通信
- 使用ReentrantLock类的newCondition()方法可以获取Condition对象
- 需要等待的时候使用Condition的await()方法, 唤醒的时候用signal()方法
- 不同的线程使用不同的Condition, 这样就能区分唤醒的时候找哪个线程了
class Printer {
private int flag = 1;
private ReentrantLock r=new ReentrantLock();
private Condition c1=r.newCondition();
private Condition c2=r.newCondition();
private Condition c3=r.newCondition();
public void print1() throws InterruptedException {
r.lock();
while (flag!=1) {
c1.await();
}
System.out.print("黑");
System.out.print("马");
System.out.print("程");
System.out.print("序");
System.out.print("员");
System.out.print("\r\n");
flag=2;
c2.signal();
r.unlock();
}
public void print2() throws InterruptedException {
r.lock();
while(flag!=2) {
c2.await();
}
System.out.print("上");
System.out.print("海");
System.out.print("交");
System.out.print("大");
System.out.print("\r\n");
flag=3;
c3.signal();
r.unlock();
}
public void print3() throws InterruptedException {
r.lock();
while(flag!=3) {
c3.await();
}
System.out.print("塑");
System.out.print("性");
System.out.print("院");
System.out.print("\r\n");
flag=1;
c1.signal();
r.unlock();
}
}
synchronized与ReenTrantLock的区别与使用
- (锁的实现)synchronized是依赖于JVM实现的,是依托JVM控制实现,而lock是JDK实现的,就是用户自己敲代码实现的。
- (性能) 新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
- (等待可中断)当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。ReentrantLock 可中断,而 synchronized 不行。
- (公平锁) 公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。 - (锁的获取)synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其 他线程只能依靠阻塞来等待线程释放锁。Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就 是CAS操作(Compare and Swap)。
- (锁的释放)synchronized:以获取锁的线程执行完同步代码,释放锁,若锁执行发生异常,jvm会让线程释放锁;而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
ReenTrantLock独有的能力
- ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
- ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
- ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。
ReenTrantLock实现的原理
简单来说,ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。
4.ThreadLocal
5.死锁
- 死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程
- 死锁满足的四个条件(必须同时满足)
- 互斥等待 (要有锁):一个资源每次只能被一个进程使用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放 (抢到了锁却在等待另外一个锁)
- 循环等待 【按id获取,避免获得了A,在等B;另一个获得了B再等A】
- 无法剥夺的等待 (循环无超时,一直等待)
- 死锁防止
- 破除互斥等待 --》 一般无法破除
- 破除hold and wait --》 一次获得所有资源【获取了from锁,如果to已锁,那么释放from锁,一定时间后再来尝试获取from和to】
- 破除循环等待 --》 按顺序获取资源 【按照account的id大小获取,先获取小的id,再获取大的id】
- 破除无法剥夺的等待 --》 加入超时
3.4 线程池
1.概述
- 程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。
- 假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。
2.线程池参数涉及
2.1意义
- 线程池的线程数量设置过多会导致线程竞争激烈;
- 如果线程数量设置过少的话,还会导致系统无法充分利用计算机资源;
2.2 原理
- Java 线程的创建与销毁将会消耗一定的计算机资源,从而增加系统的性能开销。
在 HotSpot VM 的线程模型中,Java 线程被一对一映射为内核线程。Java 在使用线程执行程序时,需要创建一个内核线程;当该 Java 线程被终止时,这个内核线程也会被回收。
- 大量创建线程同样会给系统带来性能问题,当线程数量太大,被创建的执行线程同时在争取 CPU 资源,又会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。
总之线程数量多了不行,少了也不行。
为了解决上述两类问题,Java 提供了线程池概念,对于频繁创建线程的业务场景,线程池可以创建固定的线程数量。线程池可以提高线程复用,又可以固定最大线程使用量,防止无限制地创建线程。
2.3 线程池框架Executor
包括 ScheduledThreadPoolExecutor 和 ThreadPoolExecutor 两个核心线程池。前者是用来定时执行任务,后者是用来执行被提交的任务。
阿里规范中建议不要使用Executors 创建线程池,建议使用ThreadPoolExecutor 来创建线程池:因为选择使用 Executors 提供的工厂类,将会忽略很多线程池的参数设置,工厂类一旦选择设置默认参数,就很容易导致无法调优参数设置,从而产生性能问题或者资源浪费。
ThreadPoolExecutor中的参数:
corePoolSize:线程池的核心线程数量
maximumPoolSize:线程池的最大线程数
keepAliveTime:当线程数大于核心线程数时,多余的空闲线程存活的最长时间
unit:时间单位
workQueue:任务队列,用来储存等待执行任务的队列
threadFactory:线程工厂,用来创建线程,一般默认即可
handler:拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
线程池有两个线程数的设置,一个为核心线程数,一个为最大线程数。在创建完线程池之后,默认情况下,线程池中并没有任何线程,等到有任务来才创建线程去执行任务。
但有一种情况排除在外,就是调用 prestartAllCoreThreads()
或者 prestartCoreThread()
方法的话,可以提前创建等于核心线程数的线程数量,这种方式被称为预热,在抢购系统中就经常被用到。
当创建的线程数等于 corePoolSize 时,提交的任务会被加入到设置的阻塞队列中。当队列满了,会创建线程执行任务,直到线程池中的数量等于 maximumPoolSize。
当线程数量已经等于 maximumPoolSize 时, 新提交的任务无法加入到等待队列,也无法创建非核心线程直接执行,我们又没有为线程池设置拒绝策略,这时线程池就会抛出 RejectedExecutionException 异常,即线程池拒绝接受这个任务。
当线程池中创建的线程数量超过设置的 corePoolSize,在某些线程处理完任务后,如果等待 keepAliveTime 时间后仍然没有新的任务分配给它,那么这个线程将会被回收。线程池回收线程时,会对所谓的“核心线程”和“非核心线程”一视同仁,直到线程池中线程的数量等于设置的 corePoolSize 参数,回收过程才会停止。
即使是 corePoolSize 线程,在一些非核心业务的线程池中,如果长时间地占用线程数量,也可能会影响到核心业务的线程池,这个时候就需要把没有分配任务的线程回收掉。
我们可以通过 allowCoreThreadTimeOut
设置项要求线程池:将包括“核心线程”在内的,没有任务分配的所有线程,在等待 keepAliveTime 时间后全部回收掉。
2.4 计算线程数量
前提:环境具有多变性,设置一个绝对精准的线程数其实是不大可能的,但我们可以通过一些实际操作因素来计算出一个合理的线程数,避免由于线程池设置不合理而导致的性能问题。
一般多线程执行的任务类型可以分为 CPU 密集型和I/O 密集型,根据不同的任务类型,我们计算线程数的方法也不一样。
CPU密集型
这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。
一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
I/O 密集型
这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
常规计算公式
WT:线程等待时间ST:线程时间运行时间
总结
要根据具体情况,计算出一个大概的数值,再通过实际的性能测试,计算出一个合理的线程数量。
2.5 拒绝策略发生的时机
当前提交任务数大于(maxPoolSize + queueCapacity)时就会触发线程池的拒绝策略了。
3.5 AQS
1.CountDownLatch
- 用来控制一个或者多个线程等待多个线程。
-
维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒。
2.CyclicBarrier
用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。
和 CountdownLatch 相似,都是通过维护计数器来实现的。线程执行 await() 方法之后计数器会减 1,并进行等待,直到计数器为 0,所有调用 await() 方法而在等待的线程才能继续执行。
CyclicBarrier 和 CountdownLatch 的一个区别是,CyclicBarrier 的计数器通过调用 reset() 方法可以循环使用,所以它才叫做循环屏障
3.Semaphore
Semaphore 类似于操作系统中的信号量,可以控制对互斥资源的访问线程数。
public class SemaphoreExample {
public static void main(String[] args) {
final int clientCount = 3;
final int totalRequestCount = 10;
Semaphore semaphore = new Semaphore(clientCount);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalRequestCount; i++) {
executorService.execute(()->{
try {
semaphore.acquire();
System.out.print(semaphore.availablePermits() + " ");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
});
}
executorService.shutdown();
}
}