首先面向对象类似于找什么人做什么事,比如我们需要一个随机数,就可以调用Random类,使用它的方法。与面向过程的编程思想不同,面向过程的编程思想跟注重解决问题所需要的步骤,该去如何设计,然后一步步的实现,面向对象的思维更多的是考虑如何去选择合适的工具,然后组织到一起干一件事。
追问)面向对象的三大特征:封装,继承,多态
封装:我们通过封装,只为用户提供接口,而隐藏了内部的具体实现。比如我们使用jdbc每次都需要进行注册驱动,建立连接,创建SQL的语句,运行语句,处理运行结果,释放资源以上六步,就可以对以上步骤进行封装,就引入了mybatis,只需要调用其方法而不需要在意mybatis内部是怎么执行的。再比如,javabean的属性私有,提供getset对外访问,因为属性的赋值或者获取逻辑只能由javabean本身决定。而不能由外部胡乱修改。
private String name;
public void setName(String name){
this.name = "xxx"+name;
}
该name有自己的命名规则,明显不能由外部直接赋值
继承:继承就是父类提取子类们共有的方法,而子类只需要着重于自己独有的方法,优点也在于减少代码冗余。
多态:首先多态的需要的三个条件继承
,方法重写
,父类引用指向子类对象
,父类的引用指向的子类的不同就会有不同的实现。多态的弊端在于无法调到子类特有的方法。
父类类型 变量名 = new 子类对象 ;
变量名.方法名();
JDK:
Java Develpment Kit java 开发工具
JRE:
Java Runtime Environment java运行时环境 JVM:
java Virtual Machine java 虚拟机
因为每个操作系统都有自己的jvm所以就能做到一次编译处处运行
==对比的是栈中的值,基本数据类型是变量值,引用类型是堆中内存对象的地址
equals:object中默认也是采用==比较,通常会重写,例如String
//不做处理的equals
public boolean equals(Object obj) {
return (this == obj);
}
//可以看出,String类中被复写的equals()方法其实是比较两个字符串的内容。
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String) anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
常见题目
public class StringDemo {
public static void main(String args[]) {
String str1 = "Hello";
String str2 = new String("Hello");
String str3 = str2; // 引用传递
System.out.println(str1 == str2); // false
System.out.println(str1 == str3); // false
System.out.println(str2 == str3); // true
System.out.println(str1.equals(str2)); // true
System.out.println(str1.equals(str3)); // true
System.out.println(str2.equals(str3)); // true
}
}
以下不构成重载
public double add(int a,int b)
public int add(int a,int b)
(1) 修饰成员变量
(2) 修饰局部变量
系统不会为局部变量进行初始化,局部变量必须由程序员显示初始化。因此使用final修饰局部变量时,即可以在定义时指定默认值(后面的代码不能对变量再赋值),也可以不指定默认值,而在后面的代码中对final变量赋初值(仅一次)
public class FinalVar {
final static int a = 0;//再声明的时候就需要赋值或者静态代码块赋值
/**
* static{
* a = 0; }
*/
final int b = 0;//再声明的时候就需要赋值或者代码块中赋值或者构造器赋值
/*{
b = 0;
}*/
public static void main(String[] args) {
final int localA; //局部变量只声明没有初始化,不会报错,与final无关。
localA = 0;//在使用之前一定要赋值
// localA = 1; 但是不允许第二次赋值
}
}
(3) 修饰基本类型数据和引用类型数据
public class FinalReferenceTest {
public static void main() {
final int[] iArr = {1, 2, 3, 4};
iArr[2] = -3;//合法
iArr = null;//非法,对iArr不能重新赋值
final Person p = new Person(25);
p.setAge(24);//合法
p = null;//非法
}
}
为什么局部内部类和匿名内部类只能访问局部final变量?
编译之后会生成两个class文件,Test.class Test1.class
public class Test {
public static void main(String[] args) {
}
//局部final变量a,b
public void test(final int b) {//jdk8在这里做了优化, 不用写,语法糖,但实际上也是有的,也不能修改
}
}
final int a = 10; //匿名内部类
new Thread() {
public void run(){
System.out.println(a);
System.out.println(b);
}
;
}.start();
class OutClass {
private int age = 12;
public void outPrint(final int x) {
class InClass {
public void InPrint() {
System.out.println(x);
System.out.println(age);
}
}
new InClass().InPrint();
}
}
首先需要知道的一点是: 内部类和外部类是处于同一个级别的,内部类不会因为定义在方法中就会随着方法的执行完毕就被销毁。
这里就会产生问题:当外部类的方法结束时,局部变量就会被销毁了,但是内部类对象可能还存在(只有没有人再引用它时,才会死亡)。这里就出现了一个矛盾:内部类对象访问了一个不存在的变量。为了解决这个问题,就将局部变量复制了一份作为内部类的成员变量,这样当局部变量死亡后,内部类仍可以访问它,实际访问的是局部变量的"copy"。这样就好像延长了局部变量的生命周期
将局部变量复制为内部类的成员变量时,必须保证这两个变量是一样的,也就是如果我们在内部类中修
改了成员变量,方法中的局部变量也得跟着改变,怎么解决问题呢?
就将局部变量设置为final,对它初始化后,我就不让你再去修改这个变量,就保证了内部类的成员变量和方法的局部变量的一致性。这实际上也是一种妥协。使得局部变量与内部类内建立的拷贝保持一致。
性能:StringBuilder > StringBuffer > String
场景:经常需要改变字符串内容时使用后面两个,优先使用StringBuilder,多线程
使用共享变量
时使用StringBuffer。
接口的设计目的,是对类的行为进行约束,制定规则,也就是提供一种机制,可以强制要求不同的类具有相同的行为。它只约束了行为的有无,但不对如何实现行为进行限制。就好比接口IUserDao接口会定义对user的CRUD操作,所以他的实现类UserDaoimpl就必须对其进行实现。
而抽象类的设计目的,是代码复用。当不同的类具有某些相同的行为,且其中一部分行为的实现方式一致时,就可以使用抽象类提取共有方法。
当你关注一个事物的本质的时候,用抽象类;当你关注一个操作的时候,用接口。
//向上转型:
Person person = new Student(); //安全的
//向下转型:
Teacher teacher = (Teacher)person; //不安全的
向上转型,是多态。
向下转型,为了防止编译错误,需要用到instanceof
向下转型需要记住,不是什么类都能强转的。必须是父与子的关系。所以我们用来对所有想要强转的类进行约束。
Integer i1 = new Integer(12); //自动拆箱
Integer i2 = new Integer(12); //自动拆箱
System.out.println(i1 == i2);//false
Integer i3 = 126; //自动装箱
Integer i4 = 126; //自动装箱
int i5 = 126;
System.out.println(i3 == i4);//true
System.out.println(i3 == i5);//true
Integer i6 = 128;
Integer i7 = 128;
int i8 = 128;
System.out.println(i6 == i7);//false
System.out.println(i6 == i8);//true
new:
一旦new,就是开辟一块新内存,结果肯定是false
不new:
看范围
Integer做了缓存,-128至127,当你取值在这个范围的时候,会采用缓存的对象,所以会相等
当不在这个范围,内部创建新的对象,此时不相等
实际比较的是数值,Integer会做拆箱的动作,来跟基本数据类型做比较
此时跟是否在缓存范围内或是否new都没关系
public static int[] bubbleSort(int[] array){
if(array.length <= 1){
return array;
}
//重复n次冒泡
for(int i=0;i<array.length;i++){
//是否可以提交退出冒泡的标记
boolean flag = false;
//相邻之间两两比较,并且每次减少一位参与比较
for(int j=0;j<array.length-i-1;j++){
if(array[j] > array[j+1]){
//需要交换
int temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
flag = true;//有数据交换,不能提前退出
}
}
if(!flag){
//没有数据交换,提前退出冒泡比较
break;
}
}
return array;
}
hashCode()是Object类的方法,返回一个int的整数的哈希码。这个哈希码的作用是确定该对象在哈希表中的索引位置。散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)效率极高。
HashSet:对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,看该位置是否有值,如果没有,HashSet会假设对象没有重复出现。但是如果发现有值,这时会调用equals()方法来检查两个对象是否真的相同。如果两者相同,HashSet就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样就大大减少了equals的次数,相应就大大提高了执行速度。
(1)HashMap方法没有synchronized修饰,线程非安全,HashTable线程安全; (2)HashMap允许key和value为null,而HashTable不允许
底层实现:数组+链表实现
jdk8开始链表高度到8,数组长度超过64,链表转变为红黑树,元素以内部类Node节点存在
数组扩容同于ArrayList
执行put(k,v)的时候,先根据k进行一次hash,得到所在的segment段,然后二次hash得到它的段内的位置。每个段的锁是独立的,所以不同段之间不会存在线程阻塞,从而实现安全且效率。
继承于HashMap,所以先谈谈HashMap的底层。
1,初始化大小是16,如果事先知道数据量的大小,建议修改默认初始化大小。 减少扩容次数,提高性能 ,这是我一直会强调的点
2,最大的装载因子默认是0.75,当HashMap中元素个数达到容量的0.75时,就会扩容。 容量是原先的两倍
3,HashMap底层采用链表法来解决冲突。 但是存在一个问题,就是链表也可能会过长,影响性能 于是JDK1.8,对HashMap做了进一步的优化,引入了红黑树。 当链表长度超过8,且数组容量大于64时,链表就会转换为红黑树当红黑树的节点数量小于6时,会将红黑树转换为链表。 因为在数据量较小的情况下,红黑树要维护自身平衡,比链表性能没有优势。
其次,LinkedHashMap就是链表+散列表的结构,其底层采用了Linked双向链表来保存节点的访问顺序,所以保证了有序性。
Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做字节码(即扩展名为 .class的文件),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。这也就是解释了Java的编译与解释并存的特点。
Java源代码---->编译器---->jvm可执行的Java字节码(即虚拟指令)---->jvm---->jvm中解释器----->机器可执行的二进制机器码---->程序运行
采用字节码的好处:
Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。
序列化是指把Java对象转换为字节序列的过程,而反序列化是指把字节序列恢复为Java对象的过程。
当执行序列化时,我们写对象到磁盘中,会根据当前这个类的结构生成一个版本号ID,当反序列化时,程序会比较磁盘中的序列化版本号ID跟当前的类结构生成的版本号ID是否一致,如果一致则反序列化成功,否则,反序列化失败。加上版本号,有助于当我们的类结构发生了变化,依然可以之前已经序列化的对象反序列化成功。也就是新版本兼容老版本。
Java中的所有异常都来自顶级父类Throwable
。
Error是虚拟机内部错误
栈内存溢出错误:StackOverflowError(递归,递归层次太多或递归没有结束)
堆内存溢出错误:OutOfMemoryError(堆创建了很多对象)
Exception是我们编写的程序错误,分为RuntimeException和非运行时异常
RuntimeException:也称为LogicException
为什么编译器不会要求你去try catch处理?
本质是逻辑错误,比如空指针异常,这种问题是编程逻辑不严谨造成的
应该通过完善我们的代码编程逻辑,来解决问题
例如:
算数异常,
空指针,
类型转换异常,
数组越界,
NumberFormateException(数字格式异常,转换失败,比如“a12”就会转换失败)
非RuntimeException:
编译器会要求我们try catch或者throws处理本质是客观因素造成的问题,比如FileNotFoundException写了一个程序,自动阅卷,需要读取答案的路径(用户录入),用户可能录入是一个错误的路径,所以我们要提前预案,写好发生异常之后的处理方式,这也是java程序健壮性的一种体现
例如:
IOException,
SQLException,
FileNotFoundException,
NoSuchFileException,
NoSuchMethodException
JDK自带有三个类加载器:
BootStrapClassLoader
是ExtClassLoader的父类加载器,默认负责加载%JAVA_HOME%lib下的jar包和class文件。 ExtClassLoader
是AppClassLoader的父类加载器,负责加载%JAVA_HOME%/lib/ext文件夹下的jar包和class类。 AppClassLoader
是自定义类加载器的父类,负责加载classpath下的类文件(自己写的代码以及引入的jar包)。是系统类加载器也是线程上下文加载器。双亲委派模型的好处:
引用计数法,可能会出现A 引用了 B,B 又引用了A,这时候就算他们都不再使用>了,但因为相互引用 计数器=1 永远无法被回收。
GC Roots的对象有:
可达性算法中的不可达对象并不是立即死亡的,对象拥有一次自我拯救的机会。对象被系统宣告死亡至少要经历两次标记过程:第一次是经过可达性分析发现没有与GC Roots相连接的引用链,第二次是在由虚拟机自动建立的Finalizer队列中判断是否需要执行finalize()方法。
当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达(finalize方法中没有引入其他对象),则进行回收,否则,对象“复活”。
每个对象只能触发一次finalize()方法。
继承Thread
实现Runable接口
实现Callable接口(可以获取线程执行之后的返回值)
但实际后两种,更准确的理解是创建了一个可执行的任务,要采用多线程的方式执行,还需要通过创建Thread对象来执行,比如 new Thread(new Runnable(){}).start();这样的方式来执行。在实际开发中,我们通常采用线程池的方式来完成Thread的创建,更好管理线程资源。
class MyThread extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":running.....");
}
}
public static void main(String[] args){
MyThread thread = new MyThread();
//正确启动线程的方式
//thread.run();//调用方法并非开启新线程
thread.start();
}
class MyTask implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":running....");
}
}
public static void main(String[] args){
MyTask task = new MyTask();
//task.start(); //并不能直接以线程的方式来启动
//它表达的是一个任务,需要启动一个线程来执行
new Thread(task).start();
}
class MyTask2 implements Callable<Boolean>{
@Override
public Boolean call() throws Exception {
return null;
}
}
明确一点:
本质上来说创建线程的方式就是继承Thread,就是线程池,内部也是创建好线程对象来执行任务。
sleep就是把cpu的执行资格和执行权释放出去,不再运行此线程,当定时时间结束再取回cpu资源,参与cpu的调度,获取到cpu资源后就可以继续运行了。而如果sleep时该线程有锁,那么sleep不会释放这个锁,而是把锁带着进入了冻结状态,也就是说其他需要这个锁的线程根本不可能获取到这个锁。也就是说无法执行程 序。如果在睡眠期间其他线程调用了这个线程的interrupt方法,那么这个线程也会抛出 interruptexception异常返回,这点和wait是一样的。
yield()执行后线程直接进入就绪状态,马上释放了cpu的执行权,但是依然保留了cpu的执行资格,所以有可能cpu下次进行线程调度还会让这个线程获取到执行权继续执行。
join()执行后线程进入阻塞状态,例如在线程B中调用线程A的join(),那线程B会进入到阻塞队列,直到线程A结束或中断线程
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("22222222");
}
});
t1.start();
t1.join();
// 这行代码必须要等t1全部执行完毕,才会执行
System.out.println("1111");
}
输出结果
22222222
1111
不是线程安全,应该是内存安全,堆是共享内存,可以被所有线程访问
堆是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了要还给操作系统,要不然就是内存泄漏。
在Java中,堆是Java虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚拟机启动时创建。堆所存在的内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
栈是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈。栈空间不需要在高级语言里面显式的分配和释放。
目前主流操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个进程只能访问分配给自己的内存空间,而不能访问别的进程的,这是由操作系统保障的。在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以访问到该区域,这就是造成问题的潜在原因。
守护线程:为所有非守护线程(用户线程)提供服务的线程。其他线程结束时,守护线程就会中断。
注意: 由于守护线程的终止是自身无法控制的,因此千万不要把IO、File等重要操作逻辑分配给它;因为它不靠谱;
守护线程的作用是什么?
举例,GC垃圾回收线程:就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
应用场景:
(1) 来为其它线程提供服务支持的情况;
(2) 或者在任何情况下,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用;反之,如果一个正在执行某个操作的线程必须要正确地关闭掉否则就会出现不好的后果的话,那么这个线程就不能是守护线程,而是用户线程。通常都是些关键的事务,比方说,数据库录入或者更新,这些操作都是不能中断的。
thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
在Daemon线程中产生的新线程也是Daemon的。守护线程不能用于去访问固有资源,比如读写操作或者计算逻辑。因为它会在任何时候甚至在一个操作
的中间发生中断。
每一个 Thread 对象均含有一个 ThreadLocalMap
类型的成员变量 threadLocals
,它存储本线程中所有ThreadLocal
对象及其对应的值
ThreadLocalMap
由一个个Entry
对象构成
Entry
继承自 WeakReference<ThreadLocal<?>>
,一个 Entry
由 ThreadLocal
对象和 Object 构成。由此可见, Entry
的key是ThreadLocal
对象,并且是一个弱引用。当没指向key的强引用后,该 key就会被垃圾收集器回收
当执行set方法时,ThreadLocal
首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap
对象。再以当前ThreadLocal
对象为key,将值存储进ThreadLocalMap
对象中。
get方法执行过程类似。ThreadLocal
首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap
对象。再以当前ThreadLocal
对象为key,获取对应的value。
由于每一条线程均含有各自私有的ThreadLocalMap
容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。
使用场景:
Spring框架在事务开始时会给当前线程绑定一个Jdbc Connection,在整个事务过程都是使用该线程绑定的 connection来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的ThreadLocal来实现这种隔离
内存泄露为程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光,不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。
强引用:使用最普遍的引用(new),一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。
弱引用:JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用 java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。
ThreadLocal的实现原理,每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal 实例,value为线程变量的副本
threadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时, Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有thread线程退出以后,value的强引用链条才会断掉,但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链(红色链条)
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
ThreadLocal正确的使用方法
private long count = 0;
public void calc() {
count++;
}
那程序中原子性指的是最小的操作单元,比如自增操作,它本身其实并不是原子性操作,分了3步的, 包括读取变量的原始值、进行加1操作、写入工作内存。所以在多线程中,有可能一个线程还没自增完,可能才执行到第二部,另一个线程就已经读取了值,导致结果错误。那如果我们能保证自增操作是一个原子性的操作,那么就能保证其他线程读取到的一定是自增后的数据。
关键字:synchronized
//线程1
boolean stop = false; while(!stop){
doSomething();
}
//线程2
stop = true;
如果线程2改变了stop的值,线程1一定会停止吗?不一定。当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
关键字:volatile、synchronized、final
int a = 0;
bool flag = false;
public void write() {
a = 2; //1
flag = true; //2
}
public void multiply() {
if (flag) { //3
int ret = a * a;//4
}
}
write方法里的1和2做了重排序,线程1先对flag赋值为true,随后执行到线程2,ret直接计算出结果, 再到线程1,这时候a才赋值为2,很明显迟了一步。
关键字:volatile、synchronized
volatile本身就包含了禁止指令重排序的语义,而synchronized关键字是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则明确的。
synchronized关键字同时满足以上三种特性,但是volatile关键字不满足原子性。 在某些情况下,volatile的同步机制的性能确实要优于锁(使用synchronized关键字或
java.util.concurrent包里面的锁),因为volatile的总开销要比锁低。 我们判断使用volatile还是加锁的唯一依据就是volatile的语义能否满足使用的场景(原子性)
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;
如果线程2改变了stop的值,线程1一定会停止吗?不一定。当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
int a = 0;
bool flag = false;
public void write() {
a = 2; //1
flag = true; //2
}
public void multiply() {
if (flag) { //3
int ret = a * a;//4
}
}
write方法里的1和2做了重排序,线程1先对flag赋值为true,随后执行到线程2,ret直接计算出结果,再到线程1,这时候a才赋值为2,很明显迟了一步。
但是用volatile修饰之后
(1) 使用volatile关键字会强制将修改的值立即写入主存
(2) 使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效)
(3) 由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
i++:其实是两个步骤,先加加,然后再赋值。不是原子性操作,所以volatile不能保证线程安全
一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务。阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。
阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活、不至于一直占用cpu资源
在创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率。就好比一个企业里面有10个(core)正式工的名额,最多招10个正式工,要是任务超过正式工人数(task > core)的情况下,工厂领导(线程池)不是首先扩招工人,还是这10人,但是任务可以稍微积压一下,即先放到队列去(代价低)。10个正式工慢慢干,迟早会干完的,要是任务还在继续增加,超过正式工的加班忍耐极限了(队列满了),就的招外包帮忙了(注意是临时工)要是正式工加上外包还是不能完成任务,那新来的任务就会被领导拒绝了(线程池的拒绝策略)。
线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过Thread创建线程时的一个线程必须对应一个任务的限制。
在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对Thread进行了封装,并不是每次执行任务都会调用Thread.start()来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中run方法,将run方法当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的run方法串联起来。
原文:https://www.cnblogs.com/huangwentian/p/14607195.html