一、JVM定义Java Virtual Machine（Java虚拟机），它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。我们一开始接触Java时第一课所下的JDK中，就包含了JVM和屏蔽操作系统差异的组件。其中JVM在各个操作系统中是一致的，而屏蔽操作系统差异的组件在不同系统上各不相同（类比与不同系统需要下载不同JDK）二、类的生命周期类的生命周期包括：类的加载→连接→初始化→使用→卸载1、类的加载：查找并加载类的二进制数据（.class文件），即把硬盘上的.class文件加载到JVM内存中2、连接：确定类与类之间的关系（如 student.setAddress(address)）（1）验证：校验.class文件正确性，防止损坏或被篡改（2）准备：给static静态变量分配内存，并赋予初始化默认值（如 static int num = 10在准备阶段num会赋予Integer的默认值0，在初始化阶段再将0修改成10）。在准备阶段，JVM只有类没有对象。Public class Student { static int age;// 准备阶段初始化为0 String name; }（3）解析：把类中的符号引用转为直接引用前期阶段，还不知道类的具体内存地址，只能用 cn.tangsong.Student来代替 Student类，这就是符号引用；而在解析阶段，JVM就可以将 cn.tangsong.Student映射成实际内存地址，这种使用内存地址来代替类方法就称为直接引用3、初始化：给static变量赋予正确的值4、使用：对象的初始化、对象的垃圾回收、对象的销毁5、卸载：即正常结束JVM结束生命周期时机：正常结束、异常结束/错误、System.exist()、操作系统异常三、JVM内存模型(Java Memory Model)JMM用于定义所有线程的共享变量（即方法外的全局变量，不能是局部变量）的访问规则。JMM将内存划分为主内存区和工作内存区：主内存区：真实存放变量工作内存区：主内存中变量的副本，供各个线程使用细节点：1、工作内存区是私有的，主内存区是公有的。 各个线程只能访问自己的私有工作内存区，不能访问其他线程的工作内存，也不能访问主内存2、不同线程之间，可以通过主内存间接的访问其他线程的工作内存Lock：将主内存中的变量，标识为一条线程的独占状态Read：将主内存中的变量，读取到工作内存中Load：将2中读取的变量拷贝到工作内存对应的变量副本中（i放到i，而不是i1、i2）Use：把工作内存中的变量副本，传递给线程去使用Assign：把线程正在使用的变量，传递给工作内存中的变量副本中Store：将工作内存中变量副本的值，传递到主内存中Write：将变量副本作为一个主内存中的变量进行存储Unlock：解决线程的独占状态3、JMM并不是真正存在，是一种规则规定了程序中各个变量的访问方式。 CPU中包括了CPU缓存、CPU寄存器、CPU主内存（RAM也叫主存）。线程映射到到操作系统上就是内核线程，工作内存和主内存映射到硬件上可以是CPU缓存、CPU寄存器、CPU主内存，是交叉关系没有固定内容。所以工作内存和主内存的数据流转在CPU层面来看是没有什么方向性的。从读取速度来说CPU寄存器>CPU缓存>主内存，所以，当一个CPU需要访问主内存时，它会将主内存的一部分读入其CPU缓存。它甚至可能将缓存的一部分读入其内部的CPU寄存器，然后对其进行操作。当CPU需要将结果写回主存时，它将把数值从其内部CPU寄存器冲到CPU缓存中，并在某个时间将数值冲回主存。4、Java线程与操作系统的关系：在Window和Linux中，Java线程是基于一对一模型实现的，使用线程的时候在Java虚拟机内部是调用当前操作系统的内核线程来完成当前任务的。每个线程映射一个内核线程，通过线程能够调用内核线程，再由操作系统内核将任务映射到各个处理器上。内核线程是由操作系统内核支持的线程，由操作系统内核完成线程的切换，内核操作调度器对线程执行调度，并将线程映射到各个处理器上，每个内核线程都可以一视为内核的一个分身，是操作系统可以处理多个任务的原因。并发操作的原因是线程映射到操作系统的内核线程，然后内核操作调度器对内核线程进行调度并映射到多处理器上。通常我们所说的并发指的是时间片轮转法的假象的并发。线程的处理瓶颈主要在于操作系统内核线程的线程数。每个操作系统都有一个限制，即最大可创建的线程数。如果Java线程的数量低于操作系统内核线程的线程数，那么可能会导致产能过剩。因为Java线程无法充分利用操作系统提供的多线程处理能力，部分处理器资源可能没有得到有效利用。多个内核线程可以使用多个处理器来并行执行任务，这将允许更多的线程同时运行，提高整体的处理能力。当系统具有多个处理器时，操作系统会将任务分配给不同的处理器来并行处理。然而，如果系统只有一个处理器，那么多个内核线程将共用这一个处理器的计算资源。这种情况下，多个线程之间会进行时间片轮转，通过操作系统的调度算法来分配处理器时间。这就是所谓的并发执行，不同线程在时间上交替执行，但实际上每个时刻只有一个线程在执行。线程安全：就是存在共享资源，多个线程对共享资源进行操作。线程将变量从主内存中拷贝到工作内存中，对变量的副本进行修改，同时另一个线程拷贝了变量的副本进行了修改，这个时候，操作完成后向主内存中刷新数据这时候就出现问题，引起线程的一致性问题、可见性问题。Volatile概念： JVM提供的一个轻量级的同步机制作用：防止JVM对long/double等64位的非原子性协议进行的误操作（读/写半个数据）可以使变量对所有的线程立即可见（某一个线程如果修改了工作内存中的变量副本，那么加上volatile 之后，该变量就会立刻同步到其他线程的工作内存中）volatile会迅速同步到主内存再到各个工作内存中禁止指令的重排序优化计算机内存的基本单位是字节(Byte)，每个字节都有一个唯一的地址，并且可以单独进行读写操作。线程在读写操作时，通常是以字（Word）为单位进行的。一个字通常是4个字节或8个字节，具体取决于计算机架构和操作系统，是通过处理器提供的原子性读写指令来实现的。处理器能够保证对内存中一个字的读写操作是原子的，这样就避免了读/写半个数据的情况。然而，volatile关键字与原子性并不直接相关。volatile关键字主要用于确保变量的可见性和禁止指令重排，并不能直接提供原子性保证。所以，说 volatile关键字可以防止JVM对64位非原子操作的误操作并不准确。对于长整型（long）和双精度浮点型（double）这样的64位数据类型，在某些平台上，对它们的读写操作可能会被拆分成多个32位的操作，这样就可能出现读/写半个数据的情况。使用 volatile关键字可以确保对 long和 double类型的变量进行读写操作时的可见性，但并不能解决拆分操作导致的非原子性问题。原子性：num = 10;非原子性：int num = 10; -> int num; num =10;重排序： 排序的对象就是原子性操作，目的是JVM为了提高执行效率，自我优化int a = 10; //1 int a ; a = 10 ; int b; //2 b = 20; //3 int c = a * b; //4重排序不会影响单线程的执行结果，因此以上程序在经过重排序后，可能的执行结果：1,2,3,4、2,3,1,4//2 3 1 4 int b; b = 20; int a = 10; int c = a * b;但是在多线程下，重排序就会出现问题：//双重检查式的懒汉式单例模式 public class Singleton { private static Singleton instance = null ;//单例 private Singleton(){} public static Singleton getInstance(){ if(instance == null){ synchronized (Singleton.class){ if(instance == null){ instance = new Singleton() ;//不是一个原子性操作 } } } return instance ; } }以上代码可能会出现问题，因为 instance = new Singleton() 不是一个原子性操作，会在执行时拆分成以下动作：JVM会分配内存地址、内存空间使用构造方法实例化对象instance = 第1步分配好的内存地址根据重排序的知识，可知，以上3个动作在真正执行时 可能1、2、3，也可能是1、3、2。如果在多线程环境下，使用1、3、2可能出现问题：解决方案： private volatile static Singleton instance = null ;//单例volatile是通过内存屏障防止重排序问题：在volatile写操作前，插入StoreStore屏障在volatile写操作后，插入StoreLoad屏障在volatile读操作前，插入LoadLoad屏障在volatile读操作后，插入LoadStore屏障volatile是否能保证原子性、保证线程安全？都不能！volatile只能保证可见性，要想保证原子性/线程安全，可以使用原子包 java.util.cocurrent.atomic中的类，该类能够保证原子性的核心，是因为提供了 compareAndSet()方法，该方法提供了 CAS算法（无锁算法）。如果要保证原子性的话， 同一时刻只能有一个线程或者CPU能够执行成功 ，底层是需要对硬件进行加锁的，只有某个CPU或者线程锁定了，享有独占的权限，它的操作才能是不被其它CPU或者线程打断的。import java.util.concurrent.atomic.AtomicInteger; public class TestVolatile { // static volatile int num = 0; static AtomicInteger num = new AtomicInteger(0); public static void main(Math[] args) throws InterruptedException { for (int i = 0; i < 100; i++) { // 每个线程将num累计3万次；100个线程在线程安全时，结果应该300万 new Thread(() -> { for (int j = 0; j < 30000; j++) { // num++;// 不是一个原子性操作 num.incrementAndGet(); /* num = num + 1 : ① num+1 ② num = ①的结果 2个线程同时执行 num +1 （假设此时num的值是10） 线程A： 10 +1 -> 11 线程B： 10 + 1 -> 11 */ } }).start();// lambda } Thread.sleep(1000); System.out.println(num); } }原子性操作：基本类型的读取和变量赋值、引用reference的赋值操作、java.util.cocurrent.atomic包下的操作非原子性操作：除上之外的，如自增 i++、对象实例化 instance = new Instance()、对象赋值 y = xCAS算法：CAS（Compare-and-Swap）算法是一种无锁算法，用于解决并发环境下的数据竞争问题。它通过比较内存中的值与期望值是否一致，如果一致则修改内存中的值，否则放弃修改。CAS算法的底层实现依赖于硬件提供的原子操作指令。具体来说，它使用了处理器提供的原子性指令，例如 cmpxchg（在x86架构中）或 ldrex/strex（在ARM架构中），这些指令可以在一个原子操作中完成比较和交换两个值。1.首先，获取内存位置的当前值。2.然后，与期望值进行比较。如果相等，则表示内存位置的值没有被其他线程修改过。3.如果相等，将新值写入内存位置，完成更新并返回成功。4.如果不相等，则表示其他线程已经修改了内存位置的值，此时无法完成更新，需要重新尝试。通过循环不断重试，CAS算法保证了原子性操作，避免了传统的锁机制所带来的开销和可能导致的死锁问题。CAS算法的核心思想是利用硬件提供的原子操作指令，在硬件层面上保证了操作的原子性，从而避免了使用锁所带来的性能损失。它是一种乐观并发控制方法，通过检查共享数据是否发生变化来解决竞争问题，而不是使用锁来限制并发访问。在高并发环境下，CAS算法具有较好的性能表现，但也存在一些问题，例如ABA问题。为了解决这些问题，通常需要结合版本号/时间戳、引入额外的标记位、使用带有回调函数的CAS来确保数据的一致性和正确性。JVM运行时的内存区域与上述工作内存、主内存等是维度不同的划分，本质是一个东西，分为五大部分：程序计数器程序计数器，也叫行号指示器，指向当前线程所执行的字节码指令的地址。可以理解为class文件中的行号。一般情况下，程序计数器是行号。但如果正在执行的方法是native本地方法（如Object类下调用操作系统），则程序计数器的值为 undefined。程序计数器是唯一一个 不会产生内存溢出的区域。(代码量不可能出现内存溢出1024^3*255 byte)goto的本质就是改变的程序计数器的值（java中没有goto，goto在java中是唯一的保留字）虚拟机栈描述方法执行时的内存模型：方法在执行的同时，会在虚拟机栈中创建一个栈帧（先进后出）栈帧中包含：方法的局部变量表、操作数据栈、动态链接、方法出口信息等当方法太多时，就可能发生栈溢出异常 StackOverflowError，或者内存溢出异常 OutOfMemoryError：public static void main(String[] args) { main(new String[]{"abc","abc"}); }本地方法栈原理和结构与虚拟机栈一致，不同点在于虚拟机栈中存放的 jdk或我们自己编写的方法，而本地方法栈调用的 操作系统底层的方法。堆堆是Java虚拟机（JVM）管理的内存区域之一，用于存储对象实例（对象、数组，除了基础类型都是对象）。它在JVM启动时被创建，是JVM中最大的一块内存区域。GC（垃圾收集器）主要管理堆内存中的对象回收和内存释放。堆本身是线程共享的，但可以在堆内部划分出多个线程私有的缓冲区，例如线程私有的分配指针（TLABs）。堆内存的物理空间可以是不连续的，只要逻辑上是连续的即可，这是由于堆内存的动态分配特性。堆内存可以被划分为新生代和老年代，其中新生代和老年代的大小比例通常是1:2。新生代又可以进一步划分为Eden区、Survivor 0区（S0）和Survivor 1区（S1），大小比例通常是8:1:1。新生代主要用于存放生命周期较短、小型的对象，而较大（集合、数组、字符串）或生命周期较长的对象会被存放在老年代。使用 -XX:PretenureSizeThreshold设置大对象直接进入老年代的阈值，默认为30MB。 使用 -XX:MaxTenuringThreshold=20设置对象在新生代和老年代中的晋升年龄的阈值，即生命周期，默认为15次Minor GC。在新生代中的对象经过一次Minor GC后，如果仍然存活，会被转移到Survivor区（S区）。之后，如果Survivor区中的对象仍然存活，它们的年龄会增加。当年龄达到一定阈值时，对象会被晋升到老年代。新生代的回收采用复制算法，目的是为了避免碎片产生。只有一个Survivor区可用，在垃圾回收期间，对象存活的部分会被移动到空闲的Survivor区，而不是被清理掉。对老年代进行垃圾回收的操作通常被称为Major GC或Full GC。总结：大部分对象都存在于新生代，新生代的空间小、回收频率高、效率高；而老生代的空间大、增长速度慢、回收频率低。意义：可以根据项目中对象大小的数量，设置新生代或老生代的空间容量，从而提高GC性能。异常：对象太多会导致内存异常。java.lang.OutOfMemoryError: Java heap spacepublic class TestHeap { public static void main(String[] args) { List list = new ArrayList(); while(true) { list.add(new int[1024*1024]); } } }虚拟机常用参数：-Xms128m ：JVM启动时的大小，一般设置为与总大小一致 -Xmn32m：新生代大小 -Xmx128：总大小 jvm总大小= 新生代 + 老生代 （+ 永久代，也叫持久代在jdk8后废弃了，转移到了方法区，主要存放clss信息这类元数据的静态数据，默认64M）# java 启动参数, 请谨慎变更 CMD java -server -Xmx8g -Xms8g -Xmn3g -XX:+UseG1GC -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=512m -XX:+PrintGC -XX:+PrintGCDetails -XX:MaxGCPauseMillis=100 \ -XX:InitiatingHeapOccupancyPercent=35 -XX:MinMetaspaceFreeRatio=50 -XX:MaxMetaspaceFreeRatio=80 \ -XX:+PrintTenuringDistribution -XX:+PrintGCDateStamps -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses \ -Xloggc:/data/logs/${POD_NAME}-gc-%p-%t.log -XX:+PrintHeapAtGC \ -XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCApplicationStoppedTime -XX:PrintFLSStatistics=1 -XX:-OmitStackTraceInFastThrow \ -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/logs/${POD_NAME}-oom-%p-%t.hprof \ -javaagent:/data/html/transmittable-thread-local-2.12.6.jar \ -jar /data/html/${APP_NAME}.jar -Djava.security.egd=file:/dev/urandom \ --server.port=8080 --spring.profiles.active=prod --cli --server.address=0.0.0.0方法区方法区存放：类的元数据（描述类的信息）、常量池（存放编译期间产生的字面量(如"abc"字符串常量池)、符号引用）、方法信息（方法数据、方法代码）GC在方法区一般回收：类的元数据（描述类的信息）、常量池常量：常量顾名思义就在常量池中，具体在调用这个常量的方法所在的常量池中。常量在编译时就会被解析并直接替换为其对应的值，而不需要在运行时进行初始化。这种优化称为"编译时常量折叠"（Compile-time Constant Folding），它可以提高执行效率和代码的可读性。方法区中数据如果太多，也会抛异常OutOfMemory异常PS：导致内存溢出的异常OutOfMemoryError，除了虚拟机中的4个区域以外，还可能是直接内存。在NIO技术中会使用到直接内存。类的使用方式类的初始化：JVM只会在首次主动使用一个类/接口时，才会初始化它们 。主动使用1、new构造类的使用package init; public class Test1 { static { System.out.println("Test1..."); } public static void main(String[] args) { new Test1();// 首次主动使用才会打印 new Test1(); } }2、访问类/接口的静态成员（属性、方法）package init; class A { static int i = 10; static { System.out.println("A..."); } static void method() { System.out.println("A method..."); } } public class Test2 { public static void main(String[] args) { // A.i = 1 ;// 赋值静态属性，首次主动使用才会打印 // A.i = 1 ; // System.out.println(A.i);// 调用静态属性 A.method();// 调用静态方法 } }PS:1.main()本身也是一个静态方法，也此main()的所在类也会在执行被初始化2.如果成员变量既是static，又是final ，即常量，则不会被初始化；而如果常量的值是一个随机值，则会被初始化 (为了安全，保障在第一次使用时才生成随机数，否则在程序加载出来时就预设了值，这时在字节码把class文件拿到就可以把随机数解析出来了)class A { final static int i = 10; } public class Test { // 在Test中，保存了A中产生的常量 i=10 public static void main(String[] args) { System.out.println(A.i); } } // .class文件 public class Test { public Test() { } public static void main(String[] args) { System.out.println(10); } }class Father { public static int 1 = 10; static { System.out.plintln("father...");// i源自Father，打印 } } class Son extends Father { // public static int 1 = 100;// 若子类也有i，则子类初始化，打印；子类初始化父类必然初始化，先打印 static { System.out.plintln("Son...");// 不打印 } } public class Test { public static void main(String[] args) { System.out.plintln(Son.i); } }3、使用Class.forName("init.B")执行反射时使用的类（B类）package init; class B { static int i = 10; static { System.out.println("B..."); } } public class Test3 { public static void main(String[] args) { // 反射的三种方式 Class b = B.class;// 被动使用，不会打印 B b = new B();// 反射前已经初始化，无法验证 b.getClass();// 被动使用，不会打印 Class.forName("init.B");// 主动使用，会打印 } }4、初始化一个子类时，该子类的父类也会被初始化public class Father { static { System.out.println("Father..."); } } public class Son extends Father { public static void main(String[] args) { new Son(); } }5、动态语言在执行所涉及的类 也会被初始化（动态代理）被动使用除主动使用之外全部为被动使用，如构造类数组package init; class BD { static { System.out.println("BD..."); } } public class BeiDong { public static void main(String[] args) { BD[] bds = new BD[3];// new构造数组 } }
IO这块其实相当于是工具类，一般来说在企业开发中基本已经集成了各种IOUtils了。可能更多的就是在文件上传/报表中用缓冲流进行读写操作。（4399校招中就有一道IO拷贝的手写代码题，但感觉更多的是考研对于异常和流关闭的处理）[...]
前言一、数据预热，如果是增删改的情况下，不推荐用多线程去预热，因为可能触发间隙锁甚至是表锁。二、多线程处理查的情况下，严禁将 Connection、Statement、ResultSet等数据库连接对象作为共享变量！尤其是后两者。[...]
前言：在学习IO的时候发现，InputStreamReader这个转换字节流和字符流的桥梁。这正是IO设计的核心思想：装饰模式。主要参考：《设计模式之禅》[...]
在Prometheus推送监控指标到Pushgateway上的时候，需要先在本地做一个缓存减少开销，这时候的缓存通常用的类型就是跳表，即 ConcurrentSkipListMap。[...]
Java成神之路 虽然目前只有基础篇，但是对于打地基来说可以算是典中典了，另外也出了实体书，有能力的话还是可以支持下的。一、面向对象1、面向对象和面向过程面向过程是自顶向下的编程模式。主要是把问题拆分成一个个步骤，每个步骤用函数实现。总之不要问为什么，上来先反手一个函数定义就完事了。例如实现各种排序等等。面向对象是将事务高度抽象化的编程模式。同样是要把问题拆分成一个个步骤，但是把每个步骤都抽象成对象，基于不同对象以及对象的能力进行业务逻辑的组合实现。例如造车，把车的各个部件定义成属性，然后在把车抽象成一个Car类。举一个栗子：要设计一个象棋。面向过程：重点在于怎么实现每一步（的函数）“开始—黑走—棋盘—白走—棋盘—判断—循环”面向对象：重点在于各司其职。黑白双方对象负责演算，棋盘对象负责画布，规则对象负责判断。这样优势在于不用重复造轮子，一次创建，多次使用。所以，面向过程占用资源相对低,速度相对快。而面向对象刚好相反。2、面向对象的三大特性和五大基本原则三大特性封装(Encapsulation)、继承(Inheritance)、多态(Polymorphism)就不在多赘述了。稍微提一句：我认为，多态应该是一种运行期特性，Java 中的重写是多态的体现。不过也有人提出重载是一种静态多态的想法，这个问题在 StackOverflow 等网站上有很多人讨论，但是并没有什么定论。我更加倾向于重载不是多态。（可以看下文的第3点）重点介绍以下五大基本原则：单一职责原则(Single-Responsibility)：一个类最好只做一件事，只有一个引起它的变话。 职责多变化就多，职责依赖也多，就会产生影响，从而损伤内聚性和耦合度。开放封闭原则(Open-Closed)：对扩展开放，对修改封闭。 其核心思想就是对抽象编程，而不对具体编程。因为抽象相对稳定，让类依赖于固定的抽象，所以修改就是封闭的；而通过面向对象的继承和多态机制，又可实现对抽象类的继承，通过覆写其方法来改变固有行为，实现新的拓展方法，所以就是开放的。Liskov 替换原则(Liskov-Substitution)：子类必须能够替换其基类，从而在运行期内能够识别子类。是保证继承复用的基础。 Liskov 替换原则能够保证系统具有良好的拓展性，同时实现基于多态的抽象机制，能够减少代码冗余，避免运行期的类型判别。依赖倒置原则(Dependecy-Inversion)：高层模块不依赖于底层模块，二者都同依赖于抽象；抽象不依赖于具体，具体依赖于抽象。依赖于抽象，就是对接口编程，不要对实现编程。 即MVC的分层思想：在依赖之间定义一个抽象的接口(Service)使得高层模块(Controller)调用接口，而底层模块(ServiceImpl)实现接口的定义，以此来有效控制耦合关系，达到依赖于抽象的设计目标。接口隔离原则(Interface-Segregation)：使用多个小而专门的接口，而不要使用一个大的总的接口。 即，接口应该内聚，避免“胖”接口。业务中可以用多重继承分离方法，通过接口多继承来实现客户的需求。3、重载(Overloading)和重写(Overriding)两者的概念和区别在之前的《Java就业面试题大全》的17中都有体现，其中还有一题华为的面试题：为什么函数不能根据返回类型来区分重载？这里从不同周期的角度来区分它们：重载是一个编译器概念，重写是一个运行期间的概念。重载遵循所谓的“编译期绑定”，即在编译时根据参数变量的类型判断应该调用哪个方法。重写遵循所谓的“运行期绑定”，即在运行的时候，根据引用变量所指向的实际对象的类型来调用方法。因为编译期已经确定调用哪个方法，所以重载并不是多态。而重写是多态。重载是一种语言特性，是一种语法规则，与多态无关，与面向对象也无关。4、Java如何实现的平台无关性的无关性其实就是独立性，在今年的4399笔试题里面也遇到过这题：如何理解java平台的独立性？，当时主要是从体系结构无关和可移植两方面来说明。今天就重新再认识认识。无关性，或者说独立性。就是一种语言在计算机上的运行不受平台约束，一次编译，到处执行（Wrete One, Run Anywhere）。往细的来说，就是用Java创建的可执行二进制程序，能够不加改变的运行于多个平台。因此可以运行在各种嵌入式设备上（打印机、扫描仪等），好处也就不言而喻。虽然 Java 语言是平台无关的，但JVM虚拟机 却是平台有关的，不同的操作系统上面要安装对应的 JVM。 Java 虚拟机充当了桥梁，将 Class 文件转成对应平台的二进制文件等，从而屏蔽了底层操作系统和硬件的差异。而所有Java 文件都要编译成统一的 Java Class 文件，它可以在任意平台创建并被平台的Java 虚拟机装载并执行，从而实现了平台的无关性。再者Java语言规范通过规定 Java 语言中基本数据类型的取值范围和行为在所有平台的一致性，为平台无关性提供强了有力的支持。当然，如今JVM虚拟机已经可以支持很多Java以外的语言了，如Kotlin、Groovy、JRuby、Jython、Scala等5、为什么说Java中只有值传递值传递还是引用传递，最主要区别就是是直接传递的，还是传递的是一个副本。Java中其实是共享对象传递，它是值传递的一种特例，主要是把对象的引用当做值传递给方法。如果修改引用，是不会对原来的对象有任何影响的，但是如果直接修改共享对象的属性的值，是会对原来的对象有影响的。就好比家(钥匙)方法中，你复制了一把家里的钥匙给你朋友，你朋友通过复制的钥匙进去你家，把你家砸了，那你回去的时候会发现家真的被砸了。。。但是如果你朋友把这把复制的钥匙改造成他家的钥匙，就算他把他家给点了也是不会影响到你的钥匙和家的。二、Java语言基础1、Java中的关键字transienttransient变量修饰符。被 transient 修饰的成员变量，在序列化的时候其值会被忽略，在被反序列化后，transient 变量的值被设为初始值， 如 int 型的是 0，对象型的是 null。instanceof测试它左边的对象是否是它右边的类的实例，返回 boolean 的数据类型。如：str instanceof String ? true : falsevolatile在很早的一篇《Java线程详解》中，我就记录过关于volatile相关的说明，这里再更规范的学习一遍。（估计这边看完今天又得歇菜了）volatile一般被比喻成“轻量级的synchronized”，但它只能用来修饰变量，无法修饰方法以及代码块。只需要在一个可能被多线程访问的变量上使用 volatitle修饰即可。可见性 如果一个变量被 volatile 所修饰的话，在每次数据变化之后，其值都会被强制刷入主存。而其他处理器的缓存由于遵守了缓存一致性协议，也会把这个变量的值从主存加载到自己的缓存中。这就保证了一个 volatile 在并发编程中，其值在多个缓存中是可见的。（也可以理解成当多个线程访问同一个变量时，一个线程修改了这个变量的值，其他线程能够立即看得到修改的值。）有序性 volatile可以禁止指令重排优化，确保代码按顺序执行。（不具备）原子性 原子性就是要么操作内容都执行，要么都不执行。如时间片的轮换就可能出现原子性问题。保证原子性需要通过字节码指令 monitorenter(加锁)和 monitorexit(放锁)。但是volatile和这两个没有半毛钱关心。synchronizedsynchronized就满足了可见性、有序性（但是无法禁止指令重排和处理器优化）和原子性三种特性。它既可以修饰方法也可以修饰代码块。它是借助Monitor实现的，并且由于对它不断的进行优化，出现了很多种类型的锁：重量级锁、轻量级锁、偏向锁、锁消除、适应性自旋锁、锁粗化等等。在之前的《java线程详解》中有提及一部分，也可以参考这里的：Java虚拟机的锁优化技术final面试笔试中常拿来与finally, finalize做区别final 用于声明属性，方法和类，分别表示属性不可变，方法不可覆盖，类不可继承。内部类要访问局部变量，局部变量必须定义成final类型.finally 是异常处理语句结构的一部分，表示总是执行。finalize 是Object类的一个方法，在垃圾收集器执行的时候会调用被回收对象的此方法，可以覆盖此方法提供垃圾收集时的其他资源回收，例如关闭文件等。该方法是一个回调方法，不需要我们主动调用。staticJava 中共有三种变量，分别是类变量、成员变量和局部变量。他们分别存放在 JVM的方法区、堆内存和栈内存中。而static可以修饰成员变量和方法，也可以形成静态代码块，或者静态嵌套类，但是没啥用。对于变量和方法，会提升到JVM的方法区；而静态块常用于初始化类的静态变量。大多时候还用于在类装载时候创建静态资源（只在类装载入内存时执行一次）；至于静态嵌套类，则只是为了便于项目打包（不能用于嵌套的顶层）。constjava预留关键字，卑微的存在感，几乎不用。2、String一旦一个 string 对象在内存(堆)中被创建出来，他就无法被修改。特别要注意的是，String 类的所有方法都没有改变字符串本身的值，都是返回了一个新的对象。（这点面试官常问）如果你需要一个可修改的字符串，应该使用 StringBuffer 或者 StringBuilder。否则会有大量时间浪费在垃圾回收上，因为每次试图修改都有新的 string 对象被创建出来。（实际开发中POJO中实体类的优化就需要这么做）String对 +的重载以及其它拼接方式String s = "a" + "b"，因为两个变量都是编译器常量，所以会被编译器进行常量折叠成 String s = "ab"（注意，Java是不支持运算符重载的，这里的 +只是Java提供的一个语法糖）而 String s = "a" + str要用StringBulider/StringBuffer的append()方法替代，最后在用toString()方法。这样才是最优解（底层就是一个new String()）。StringBuffer线程安全 和 StringBuilder线程不安全，但是快这里还引申出华为的一道面试题：什么情况下用“+”运算符进行字符串连接比调用 StringBuffer/StringBuilder对象的 append 方法连接字符串性能更好？解析：《Java面试宝典Beta5.0》题4.1当然还可以用String类中的 concat方法，但是没必要。String newStr = str1.concat("-").concat(str2)顺带一提，JDK8中的String还提供了join方法更好的来处理字符串数组或者列表的拼接，这里就不再演示。耗时比较：StringBuilder<StringBuffer<concat<+<StringUtils.join总体来说，如果不是循环体中的字符串拼接，直接用 +就行了不用太多花里胡哨。如果是并发中就用 StirngBuffersubstring(int beginIndex, int endIndex)截取字符串并返回按index来算的话，它的区间是 [beginIndex, endIndex)String x = "abcdef"; x = x.substring(1,3); System.out.println(x); bc版本问题JDK6中String类中包含三个成员变量：字符数组 char value[]、数组第一位索引 int offset、字符个数 int count。而当用了substring时新生成的String对象指向的是堆中同一个数组，只是count和offset的值改变了。这使得如要对一段很长的字符串切割很小的一部分，但却一直引用了整个很长的字符串，导致它无法被回收，可能存在内存泄露。public String substring(int beginIndex, int endIndex) { //check boundary return new String(offset + beginIndex, endIndex - beginIndex, value); }JDK7 中，substring 方法会在堆内存中创建一个新的数组。使用 new String 创建了一个新字符串，避免对老字符串的引用。从而解决了内存泄露问题。public String substring(int beginIndex, int endIndex) { //check boundary int subLen = endIndex - beginIndex; return new String(value, beginIndex, subLen); }replaceFirst 、 replaceAll 、 replace 区别replace全家桶都(没有replaceLast)是字符替换方法，主要用来替换文字、符合正则的数据、HTML标签等。//replace() System.out.println("abac".replace("a", "\a")); //\ab\ac //replaceAll() Pattern pattern = Pattern.compile("正则表达式"); Matcher matcher = pattern.matcher("正则表达式 Hello World,正则表达式 Hello World"); System.out.println(matcher.replaceAll("Java")); //Java Hello World,Java Hello World //replaceFirst() Pattern pattern = Pattern.compile("正则表达式"); Matcher matcher = pattern.matcher("正则表达式 Hello World,正则表达式 Hello World "); System.out.println(matcher.replaceFirst("Java")); //Java Hello World,正则表达式 Hello Worldswitch的String支持对String的支持是Java7开始的新特性。底层是通过比较HashCode()和equals()方法实现的。样例可以参考这里：《java就业面试大全》第5题String有没有长度限制？别说，还真有。在编译期，要求字符串常量池中的常量不能超过 65535，并且在 javac 执行过程中控制了最大值为 65534。在运行期，长度不能超过 Int 的范围，否则会抛异常。3、常量池Java中三大常量池：字符串常量池、Class常量池、运行时常量池字符串(常量)池通过 字面量形式创造出来的字符串 String str = "abc"， JVM 中，为了减少相同的字符串的重复创建，为了达到节省内存的目的。会单独开辟一块内存，用于保存字符串常量，这个内存区域被叫做字符串常量池。当代码中出现双引号形式（字面量）创建字符串对象时，JVM 会先对这个字符串进行检查，如果字符串常量池中存在相同内容的字符串对象的引用，则将这个引用返回；否则，创建新的字符串对象，然后将这个引用放入字符串常量池，并返回该引用。（String的intern同理）这叫做字符串驻留或池化。JDK7之前，字符串常量池存放在永久代中；JDk7中移到了堆内存中；JDK8开始元空间替代了永久代，所以移到了元空间中。（运行时常量池也是这样的）Class常量池Class常量池中主要存放两大类常量：字面量（只能是右值）和符号引用（类和接口的全限定名/字段的名称和描述符/方法的名称和描述符）Class常量池主要就是用来保存常量的一个媒介场所，并且是一个中间场所。在 JVM 真的运行时，需要把常量池中的常量加载到内存中。从而进入运行时常量池。运行时常量池被分配在JVM的方法区之中，是类或接口的常量池的运行时表示形式。4、自动拆/装箱的实现这里先要了解一点：基本数据类型的变量不需要new创建，因此它们不会在堆上创建，而是直接在栈内存中存储，所以会更加高效。而因为集合类中不能放基本类型，容器要求的元素必须是Object类型。所以也就要有八大包装类，好让基本类型也有对象的性质，并且添加了属性与方法去丰富基本类型的操作。我们所说的装箱就是把基本数据类型转成包装类型，而拆箱则相反。当然Java5开始就有了自动装箱、自动拆箱。如：把基本数据类型放入集合类中的时候，会进行自动装箱；而包装类之间的运算(包括三目运算)则会被自动拆箱成基本类型，所以要小心NPE！Integer i = 10;//自动装箱，反编译后为Integer integer=Integer.valueOf(1); int b = i;//自动拆箱，反编译后为int i=integer.intValue(); Integer i = new Interger(10); Integer i = 10;//替代上面还有一个就是自动拆箱与缓存的问题，Integer如果在[-127,128]间，则 Integer a = 3==Integer b = 3（在Java6中可以通过 java.lang.Integer.IntegerCache.high设置最大值）。所以在这个范围之外就要用equals来比较了。还有一点是for循环中如果存在大量拆装箱的操作都会造成很多不必要的资源浪费。具体样例可以看《Java就业面试大全》第14题。从这里还可以引申出，Byte、Short、Long、Character都有其对应的XxxCache缓存对象。前三者的缓存范围也是 [-128, -127]。而Character范围是 [0, 127]。并且除了上面的Integer外，这四个范围都不能改变。如何正确定义接口的返回值 (boolean/Boolean) 类型及命名 (success/isSuccess or deleted/isDeleted)困扰好久的点终于在这里有了说明，那就是经常遇到的逻辑删除数据库字段 is_delete。阿里规约说不能使用 isDelete吧啦吧啦一大堆只说了可能导致序列化问题，也没说明该定义成什么规范。答案是：is_success用 success、is_deleted用 deleted 。Lombok的@Data用多了发现entity中有一个细节：如果属性是boolean类型的话，它的getter方法 getXxx()会变成 isXxx()。并且如果该属性名是 is开头的话只会保留一个 is，也就是说 isDeleted的getter方法还是跟 deleted的getter一样，都是 isDeleted()，并且 isDelete的set方法会变成 setDelete！这就可能导致序列化的时候出错了。以下是fastJson、jackson、Gson三种序列化的结果：class Model3 implements Serializable { private static final long serialVersionUID = 1836697963736227954L; private boolean isSuccess; public boolean isSuccess() { return isSuccess; } public void setSuccess(boolean success) { isSuccess = success; } public String getHollis(){ return "hollischuang"; } } //result Serializable Result With fastjson :{"hollis":"hollischuang","success":true} Serializable Result With Gson :{"isSuccess":true} Serializable Result With jackson :{"success":true,"hollis":"hollischuang"}从以上可以得出一个结论，就是fastjson和jackson把对象序列化成json时，是通过反射遍历所有getter方法，再根据JavaBeans规则。把isSuccess序列化成success。而Gson则是通过反射遍历类中的所有属性，并把他们序列化成isSuccess。所以，前方高能：我们把 isSuccess用fastjson/jackson进行序列化，再用Gson反序列化，将会得到 isSuccess=falsefalse?!因为序列化的时候根据JavaBeans规范解析成了success，而反序列化时是通过反射找success属性，但是Model中的是isSuccess所以找不到返回默认值false！阿里规约中的这条定义也是从上游卡住了问题的发生，比起下游去处理那些复杂的逻辑更干净省事。所以直接定义成 success/deleted然后可以用MP的 @TableField或者 resultMap处理映射。最后就是用Boolean还是boolean的问题了，这里还是有一些争议的。Boolean的默认值是null，可能出现NPE问题。boolean的默认值是false，若有问题可能无法感知到异常。最终还是统一用《java开发手册》《骂出高效》中声明的，尽量用包装类型Boolean，但是要注意避免NULL值（抛出时最好标明是什么原因抛的NPE）5、异常处理文中把java异常分为两大类：非受检异常（unchecked exception）和受检异常（checked exception）。但是我更偏向于用运行时异常（runtime exception）和一般异常（checked exception）来形容。具体可以看看《Java就业面试大全》的第34-38题异常处理方式一般有两种：1、自己处理2、向上抛，给调用者处理《手册》中也有明确规定说千万不能只捕获而不处理，或者只是简单的e.printStacktrace。能处理就处理，不能处理就往上抛让调用者头疼。try-with-resourcesJava中的IO流、数据库连接的开销都是非常大的，一直打开状态可能会造成内存泄露问题，所以一般要在finally块中调用close()去施放。public void testException(){ BufferedReader br = null; try{ FileReader fileReader = new FileReader("唐宋.txt");//定义文件 br = new BufferedReader(fileReader);//读取文本 while (br.readLine() != null) { System.out.println(br.readLine());//读取一行内容 } } catch (Exception e) { e.printStackTrace(); }finally { try{ if (br != null) { br.close();// 无论如何文件句柄在使用结束后得手动关闭！ } }catch (IOException e){ e.printStackTrace(); } } }可以用Lombok中的 @Cleanup+@SneakyThrows联动：@SneakyThrows public void testException(){ @Cleanup BufferedReader br = null; FileReader fileReader = new FileReader("唐宋.txt");//定义文件 br = new BufferedReader(fileReader);//读取文本 while (br.readLine() != null) { System.out.println(br.readLine());//读取一行内容 } }但是这个方法在业务中还是比较少见的，用起来可能会比较怂。还是推荐下面阿里推荐的方式：public void testException(){ try (BufferedReader br = new BufferedReader(new FileReader("唐宋.txt"))) { while (br.readLine() != null) { System.out.println(br.readLine());//读取一行内容 } } catch (Exception e) { e.printStackTrace(); } }虽然把获取文件的方法都压缩到了try()中，但确实很短好吧，同时还能决定出错时要抛什么异常，更加实用。还有一点就是finally和return的执行顺序，这点在这里有详细说明：《java就业面试大全》第31题6、集合类集合类，简直是面试官的最爱，（在《java...大全》）重复出现的问题这里不再进行讲解：（1）Collection 和 s Collections 区别：第57题（2）Set和List区别（3）ArrayList和LinkedList和Vector的区别：49、50（4）HashMap、HashTable、ConcurrentHashMap区别：51ArrayList 使用了 transient 关键字进行存储优化，而 Vector 没有，为什么？这里先说明一下 transient关键字：transient一般是在序列化中使用。如果一个用户有一些敏感信息（如密码，银行卡号等），为了安全起见，不希望在网络操作（主要涉及到序列化操作，本地序列化缓存也适用）中被传输，这些信息对应的变量就可以加上transient关键字。换句话说，这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化。总之，java 的transient关键字为我们提供了便利，你只需要实现Serilizable接口，将不需要序列化的属性前添加关键字transient，序列化对象的时候，这个属性就不会序列化到指定的目的地中。在使用中主要注意以下几点：1）一旦变量被transient修饰，变量将不再是对象持久化的一部分，该变量内容在序列化后无法获得访问。2）transient关键字只能修饰变量，而不能修饰方法和类。注意，本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量，则该类需要实现Serializable接口。3）被transient关键字修饰的变量不再能被序列化，一个Static静态变量不管是否被transient修饰，均不能被序列化。（反序列化中读取到的值也只是当前JVM中对应static变量的值，而不是通过反序列化得出的）4）若序列化操作是通过Externalizable接口，则没有任何东西可以自动序列化，需要在writeExternal方法中进行手工指定所要序列化的变量，这与是否被transient修饰无关。/** * 将ArrayList的实例状态保存到流中。即，对其实现序列化 * * @serialData 根据数据实例的长度(int)，以适当的顺序写入到Object对象中 */ private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ // 写出元素计数，以及任何隐藏内容 // modCount list被结构（列表大小）修改的次数 int expectedModCount = modCount; // writeObject默认调用的序列化方法，将当前类的非静态和非瞬态字段写入此流 s.defaultWriteObject(); // 写出clone()方法的大小，size即ArrayList中具体存放元素的个数 // 而elementData.length则是ArrayList内部自动扩容数组的长度 s.writeInt(size); // 按照正确的顺序写出所有元素 for (int i=0; i<size; i++) { // 将指定的对象写入ObjectOutputStream // elementData 存储ArrayList的元素的数组缓冲区，大小等于ArrayList的length。 s.writeObject(elementData[i]); } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } }ArrayList 实现了 writeObject 方法，从 s.writeInt(size)可以看到只保存了非 null 的数组位置上的数据。即 list 的 size 个数的 elementData。需要额外注意的一点是，ArrayList 的实现，提供了 fast-fai机制，可以提供弱一致性。/** * 将Vector的实例状态保存到流中。即，对其实现序列化 * 此方法执行同步以确保序列化数据的一致性 */ private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { final java.io.ObjectOutputStream.PutField fields = s.putFields(); final Object[] data; synchronized (this) { // capacityIncrement 容量的大小，当小于写入的容量时自动加倍扩容 fields.put("capacityIncrement", capacityIncrement); // elementCount 数组缓存区的大小 fields.put("elementCount", elementCount); // Vector的elementData中最后一个元素之后的所有数组元素均为null // 所以直接clone()的话后面的null也会被转移进去 data = elementData.clone(); } fields.put("elementData", data); s.writeFields(); }Vector 也实现了 writeObject 方法，但方法并没有像 ArrayList 一样进行优化存储，data = elementData.clone()时会把 null 值也拷贝。通过对比不难得出：保存相同内容的 Vector 与 ArrayList，Vector 的占用的字节比 ArrayList 要多。总结：ArrayList 是非同步实现的一个单线程下较为高效的数据结构（相比 Vector 来说）。ArrayList 只通过一个修改记录字段提供弱一致性，主要用在迭代器里。没有同步方法。即上面提到的 Fast-fail 机制.ArrayList 的存储结构定义为 transient，重写 writeObject来实现自定义的序列化，优化了存储。Vector 是多线程环境下更为可靠的数据结构，所有方法都实现了同步。那么， SynchronizedList和 Vector的差别有是什么呢？两者的数据增长区别跟ArrayList和Vector的区别是一样的，更多是同步代码块(SynchronizedList)和同步方法(Vector)的区别，这使得SynchronizedList 有很好的扩展和兼容功能。他可以将所有的 List 的子类转成线程安全的类。SynchronizedList 中实现的类并没有都使用 synchronized同步代码块。其中有 listIterator 和 listIterator(int index)并没有做同步处理。但是Vector 却对该方法加了方法锁。 所以说，在使用 SynchronizedList 进行遍历的时候要手动加锁。另外，SynchronizedList 可以指定锁定的对象，没有指定的话默认跟Vector一样锁定this对象。（静态代码块可以选择对哪个对象加锁，但是静态方法只能给 this 对象加锁）Set 如何保证元素不重复?在之前58题的时候有稍微提到过一部分，这里再详细说明一下Set体系中主要分为 HashSet、TreeSet。（1）TreeSet：通过二叉树实现的，其中的数据是自动排好序的，不允许放入null值。它的底层是TreeMap的keySet()，是按照key排序的。而 TreeMap 是基于红黑树实现的，红黑树是一种平衡二叉查找树，它能保证任何一个节点的左右子树的高度差不会超过较矮的那棵的一倍。所以TreeSet也是CompareTo()来判断元素重复的。（2）HashSet：底层基本都是通过HashMap实现的，它的底层就是用HashMap存储数据。当向 HashSet 中添加元素的时候，首先计算元素的 hashcode值，然后通过扰动计算和按位与的方式计算出这个元素的存储位置，如果这个位置位空，就将元素添加进去；如果不为空，则用 equals 方法比较元素是否相等，相等就不添加，否则找一个空位添加。这点在51题有提及到。
整理了网上大部分java线程相关的知识，并加入一些自己的理解以及代码示例。线程概述一个Thread类实例只是一个对象，像Java中的任何其他对象一样，具有变量和方法，生死于堆上。Java中，每个线程都有一个调用栈，即使不在程序中创建任何新的线程，线程也在后台运行着。一个Java应用总是从main()方法开始运行，mian()方法运行在一个线程内，它被称为主线程。一旦创建一个新的线程，就产生一个新的调用栈。线程总体分两类：用户线程和守护线程。当所有用户线程执行完毕的时候，JVM自动关闭。但是守候线程却不独立于JVM，守候线程一般是由操作系统或者用户自己创建的，随着JVM关闭也随之结束。用个比较通俗的比如，任何一个守护线程都是整个JVM中所有非守护线程的保姆：只要当前JVM实例中尚存在任何一个非守护线程没有结束，守护线程就全部工作；只有当最后一个非守护线程结束时，守护线程随着JVM一同结束工作。Daemon的作用是为其他线程的运行提供便利服务，守护线程最典型的应用就是 GC (垃圾回收器)，它就是一个很称职的守护者。（后文有守护线程详解）Java线程：创建与启动一、定义线程1、继承（extends）java.lang.Thread类。此类中有个run()方法，应该注意其用法： private Runnable target; @Override public void run() { if (target != null) { target.run(); } }Thread的子类应该重写该方法，否则，该方法不执行任何操作并返回。2、实现（implements）java.lang.Runnable接口。源码如下：@FunctionalInterface public interface Runnable { public abstract void run(); }使用实现接口Runnable的对象创建一个线程时，启动该线程将导致在独立执行的线程中调用对象实现的run方法。方法run的常规协定是，它可能执行任何所需的操作。实现Runnable接口所具有的优势：1、避免Java单继承的问题2、适合多线程处理同一资源3、代码可以被多线程共享，数据独立，很容易实现资源共享二、实例化线程1、如果是扩展java.lang.Thread类的线程，则直接new即可。2、如果是实现了java.lang.Runnable接口的类，则用Thread的构造方法：Thread(Runnable target) Thread(Runnable target, String name) Thread(ThreadGroup group, Runnable target) Thread(ThreadGroup group, Runnable target, String name) Thread(ThreadGroup group, Runnable target, String name, long stackSize)即在xxxDemo的start()中：Thread thread= new Thread(); Thread runnable= new Thread(new Runnable(){ @Override public void run() { ... } );一般用法：Thread thread = new Thread(this,threadName);若继承Thread类或实现Runnable接口，只重写了 run()方法，那么只需要如下这般创建：Thread thread = new ThreadDemo(); Thread runnable = new Thread(new RunnableDemo());三、启动线程通俗来讲，在线程中，把需要处理的代码放到run()方法中，用户自定义的 public void start()会写入Thread的thread.start()方法启动线程，该方法将自动调用run()方法，这个由java的内存机制规定的。并且run()方法必需是public访问权限，返回值类型为void。Thread.start()源码如下：public synchronized void start() { /** * This method is not invoked for the main method thread or "system" * group threads created/set up by the VM. Any new functionality added * to this method in the future may have to also be added to the VM. * * A zero status value corresponds to state "NEW". */ if (threadStatus != 0) throw new IllegalThreadStateException(); /* Notify the group that this thread is about to be started * so that it can be added to the group's list of threads * and the group's unstarted count can be decremented. */ group.add(this); boolean started = false; try { start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { /* do nothing. If start0 threw a Throwable then it will be passed up the call stack */ } } }当然直接调用Thread中的run()方法而不调用start()，程序也可以正常运行，但只是当成类中的方法调用，失去了线程的意义，具体区别如下：Thread.start()：创建了一个新的线程并处于就绪状态，当获得CPU时间片后开始执行，自动执行run()方法（回调机制）；该方法不能重复调用，会抛出 java.lang.IllegalThreadStateException。Thread.run()：直接调用该方法只会顺序执行，相当于调用普通的方法，可重复执行。（没有创建新的线程）四、例子RunnableThreadCallable五、一些常见问题1、线程的名字，一个运行中的线程总是有名字的，名字有两个来源，一个是虚拟机自己给的名字，一个是你自己的定的名字。在没有指定线程名字的情况下，虚拟机总会为线程指定名字，并且主线程的名字总是mian，非主线程的名字不确定。2、线程都可以设置名字，也可以获取线程的名字，连主线程也不例外。thread = new Thread(this,threadName);Thread.currentThread().getName();3、获取当前线程的对象的方法是：Thread.currentThread();4、在上面的代码中，只能保证：每个线程都将启动，每个线程都将运行直到完成。一系列线程以某种顺序启动并不意味着将按该顺序执行。对于任何一组启动的线程来说，调度程序不能保证其执行次序，持续时间也无法保证。（依赖于不同的调度算法）5、当线程目标run()方法结束时该线程完成。6、一旦线程启动，它就永远不能再重新启动（死线程）。只有一个新的线程可以被启动，并且只能一次。7、线程的调度是JVM的一部分，在一个CPU的机器上，实际上一次只能运行一个线程。一次只有一个线程栈执行。JVM线程调度程序决定实际运行哪个处于可运行状态的线程。(宏观并行，微观串行)众多可运行线程中的某一个会被选中做为当前线程。可运行线程被选择运行的顺序是没有保障的。8、尽管通常采用队列形式，但这是没有保障的。队列形式是指当一个线程完成“一轮”时，它移到可运行队列的尾部等待，直到它最终排队到该队列的前端为止，它才能被再次选中。事实上，我们把它称为「可运行池」而不是一个可运行队列，目的是帮助认识线程并不都是以某种有保障的顺序排列成一个一个队列的事实。9、尽管我们无法控制线程调度程序，但可以通过别的方式来影响线程调度的方式。（操作系统中的各种调度算法）(以下涉及到操作系统)经典的调度算法：FIFS先来先服务调度算法、SJF短作业优先调度算法、优先级调度算法（高响应比优先调度算法、时间片轮转调度算法、多级反馈队列调度算法）Java线程：线程栈模型与线程的变量要理解线程调度的原理，以及线程执行过程，必须理解线程栈模型。线程栈是指某时刻时内存中线程调度的栈信息，当前调用的方法总是位于栈顶。线程栈的内容是随着程序的运行动态变化的，因此研究线程栈必须选择一个运行的时刻（实际上指代码运行到什么地方)。下面通过一个示例性的代码说明线程（调用）栈的变化过程。这幅图描述在代码执行到两个不同时刻1、2时候，虚拟机线程调用栈示意图。当程序执行到t.start();时候，程序多出一个分支（增加了一个调用栈B），这样，栈A、栈B并行执行。从这里就可以看出方法调用和线程启动的区别了。Java线程：线程状态的转换一、线程状态死亡态：当线程的run()方法完成时就认为它死去。这个线程对象也许是活的，但是，它已经不是一个单独执行的线程。线程一旦死亡，就不能复生。如果在一个死去的线程上调用start()方法，会抛出 java.lang.IllegalThreadStateException异常。二、阻止线程执行对于线程的阻止，考虑一下三个方面，不考虑IO阻塞的情况：睡眠；等待；因为需要一个对象的锁定而被阻塞。1、睡眠sleep()Thread.sleep(long millis)和 Thread.sleep(long millis, int nanos)静态方法强制当前正在执行的线程休眠（暂停执行），以“减慢线程”。当线程睡眠时，它入睡在某个地方，在苏醒之前不会返回到可运行状态。当睡眠时间到期，则返回到可运行(就绪)状态。线程睡眠的原因：线程执行太快，或者需要强制进入下一轮，因为Java规范不保证合理的轮换。睡眠的位置：为了让其他线程有机会执行，可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程中会睡眠。注意：1、线程睡眠是帮助所有线程获得运行机会的最好方法。2、线程睡眠到期自动苏醒，并返回到可运行(就绪)状态，不是运行状态。sleep()中指定的时间是线程不会运行的最短时间。因此，sleep()方法不能保证该线程睡眠到期后就开始执行。3、sleep()是静态方法，只能控制当前正在运行的线程。2、线程的优先级和线程让步yield()线程的让步是通过 Thread.yield()来实现的。yield()方法的作用是：暂停当前正在执行的线程对象，并执行其他线程。要理解yield()，必须了解线程的优先级的概念。线程总是存在优先级，优先级范围在1~10之间。JVM线程调度程序是基于优先级的抢先调度机制。在大多数情况下，当前运行的线程优先级将大于或等于线程池中任何线程的优先级。但这仅仅是大多数情况。注意：当设计多线程应用程序的时候，一定不要依赖于线程的优先级。因为线程调度优先级操作是没有保障的，只能把线程优先级作用作为一种提高程序效率的方法，但是要保证程序不依赖这种操作。当线程池中线程都具有相同的优先级，调度程序的JVM实现自由选择它喜欢的线程。这时候调度程序的操作有两种可能：一是选择一个线程运行，直到它阻塞或者运行完成为止。二是时间分片，为池内的每个线程提供均等的运行机会。设置线程的优先级：线程默认的优先级是创建它的执行线程的优先级。可以通过 setPriority(int newPriority)更改线程的优先级。例如： Thread t = new MyThread(); t.setPriority(8); t.start();线程优先级为1~10之间的正整数，JVM从不会改变一个线程的优先级。然而，1~10之间的值是没有保证的。一些JVM可能不能识别10个不同的值，而将这些优先级进行每两个或多个合并，变成少于10个的优先级，则两个或多个优先级的线程可能被映射为一个优先级。线程默认优先级是5，Thread类中有三个常量，定义线程优先级范围：static int MAX_PRIORITY 线程可以具有的最高优先级。static int MIN_PRIORITY 线程可以具有的最低优先级。static int NORM_PRIORITY 分配给线程的默认优先级。结论1：优先级高非先运行，只是获得cpu资源的可能性更大。yield()应该做的是让当前运行线程回到可运行状态，以允许具有相同优先级的其他线程获得运行机会。因此，使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是，实际中无法保证yield()达到让步目的，因为让步的线程还有可能被线程调度程序再次选中。结论2：yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下，yield()将导致线程从运行状态转到可运行状态，但有可能没有效果。3、join()方法Thread的非静态方法join()让一个线程B“加入”到另外一个线程A的尾部。在A执行完毕之前，B不能工作。例如： Thread t = new MyThread(); t.start(); t.join();main方法运行thread.join(),这是会让主线程mian等待新的线程thread执行完了，再执行主线程mian下面的代码，thread.join()是然主线程main wait。另外，join()方法还有带超时限制的重载版本。例如 t.join(5000);则让线程等待5000毫秒，如果超过这个时间，则停止等待，变为可运行状态。线程的加入join()对线程栈导致的结果是线程栈发生了变化，当然这些变化都是瞬时的。小结到目前位置，介绍了线程离开运行状态的3种方法：调用Thread.sleep()：使当前线程睡眠至少多少毫秒（尽管它可能在指定的时间之前被中断）。调用Thread.yield()：不能保障太多事情，尽管通常它会让当前运行线程回到可运行性状态，使得有相同优先级的线程有机会执行。调用join()方法：保证当前线程停止执行，直到该线程所加入的线程完成为止。然而，如果它加入的线程没有存活，则当前线程不需要停止。除了以上三种方式外，还有下面几种特殊情况可能使线程离开运行状态：线程的run()方法完成。在对象上调用wait()方法（不是在线程上调用）。线程不能在对象上获得锁定，它正试图运行该对象的方法代码。线程调度程序(策略)可以决定将当前运行状态移动到可运行状态，以便让另一个线程获得运行机会，而不需要任何理由。Java线程：线程的同步与锁以卖票的例子来介绍多线程和资源共享。 卖票是包含一系列动作的过程，有各种操作，例如查询票、收钱、数钱、出票等，其中有一个操作是每次卖掉一张，就将总的票数减去1。有10张票，如果一个人卖票，先做查票、收钱、数钱等各种操作，再将总的票数减去1，效率很低。如果多个人卖票，每个人都是做同样的操作，数钱、检查钱，最后将总的票数减1，这样效率高。但是有一个问题，如果出现两个人同时将总的票数减掉了1，例如，A、B两个人同时读取到票的总数是10，A从中减去1，同时B也从中减去1，总数显示是9，其实票只有8张。导致数据错误。 按照正常逻辑，同一时刻只允许一个人来从总票数中减去1，A读取总票数，再减去1的过程中，B必须等待，等A操作完了，B才能进行。其实票就是共享资源，一次只能由一个人访问。这里就要用到同步机制，即锁机制，使用关键词 synchronized将读取总的票数，并减去1的操作锁定，使得一次只能由一个人访问。每个售票员就是一个线程，多个售票员进行同一项卖票任务。 synchronized原理是，执行synchronized部分代码的时候必须需要对象锁，而一个对象只有一个锁，只有执行完synchronized里面的代码后释放锁，其他线程才可以获得锁，那么就保证了同一时刻只有一个线程访问synchronized里面的代码。使得资源共享的关键是，只有一个实例，synchronized使用的是同一把锁，用实例的锁或者定义一个实例。这就需要使用实现Runnable接口的方式，实现多线程，这样传入的是一个实例。继承Thread的方式，传入的是多个实例，每个实例都有一个锁，那就无法实现控制。 synchronized是Java中的关键字，是一种同步锁。一、同步问题提出线程的同步是为了防止多个线程访问一个数据对象时，对数据造成的破坏（脏数据、脏读、不可重复读、幻读等）。二、同步和锁定1、锁的原理Java中每个对象都有一个内置锁当程序运行到非静态的synchronized同步方法上时，自动获得与正在执行代码类的当前实例（this实例）有关的锁。获得一个对象的锁也称为获取锁、锁定对象、在对象上锁定或在对象上同步。当程序运行到synchronized同步方法或代码块时该对象锁才起作用。一个对象只有一个锁。所以，如果一个线程获得该锁，就没有其他线程可以获得锁，直到第一个线程释放（或返回）锁。这也意味着任何其他线程都不能进入该对象上的synchronized方法或代码块，直到该锁被释放。释放锁是指持锁线程退出了synchronized同步方法或代码块。关于锁和同步，有一下几个要点：只能同步方法，而不能同步变量和类；每个对象只有一个锁；当提到同步时，应该清楚在什么上同步？也就是说，在哪个对象上同步？不必同步类中所有的方法，类可以同时拥有同步和非同步方法。如果两个线程要执行一个类中的synchronized方法，并且两个线程使用相同的实例来调用方法，那么一次只能有一个线程能够执行方法，另一个需要等待，直到锁被释放。也就是说：如果一个线程在对象上获得一个锁，就没有任何其他线程可以进入（该对象的）类中的任何一个同步方法。如果线程拥有同步和非同步方法，则非同步方法可以被多个线程自由访问而不受锁的限制。线程睡眠时，它所持的任何锁都不会释放。线程可以获得多个锁。比如，在一个对象的同步方法里面调用另外一个对象的同步方法，则获取了两个对象的同步锁。同步损害并发性，应该尽可能缩小同步范围。同步不但可以同步整个方法，还可以同步方法中一部分代码块。在使用同步代码块时候，应该指定在哪个对象上同步，也就是说要获取哪个对象的锁。例如：/*同步方法*/ public synchronized int getX() { return x++; }当然，同步方法也可以改写为同步代码块，但功能完全一样的，例如：/*同步代码块*/ public int fix(int y) { synchronized (this) { x = x - y; } return x; }与/*同步代码块 优势：减小锁的粒度，提高性能。 */ public int getX() { synchronized (this) { return x++; } }效果是完全一样的。三、静态方法同步要同步静态方法，需要一个用于整个类对象的锁，这个对象是就是这个类（XXX.class)。例如：public static synchronized int setName(String name){ Xxx.name = name; }等价于public static int setName(String name){ synchronized(Xxx.class){ Xxx.name = name; } }即：synchronized主要有两种方法1.1、synchronized块（对象级）： synchronized(this){}1.2、synchronized块（类级别）：类在堆中存在一个class 对象（注意这里的class是小写），所以所谓的类锁，本质上还是对一个对象，在这里也就是class对象进行同步获取。当然体现出来的效果是在类上进行了同步。 synchronized（类名.class）2、 synchronized方法 public synchronized String setID(){}补充：1.方法锁(同步方法、同步代码块两者的别称)和私有锁，都属于对象锁，但与执行非同步方法不会产生冲突。2.静态同步方法(static+synchronized)，属于类锁。3.类锁和对象锁不发生冲突，但对象锁之间会发生冲突，同一时刻对象只能执行一个同步方法或同步代码块。四、如果线程不能获得锁会怎么样如果线程试图进入同步方法，而其锁已经被占用，则线程在该对象上被阻塞。实质上，线程进入该对象的的一种池中，必须在哪里等待，直到其锁被释放，该线程再次变为可运行或运行为止。当考虑阻塞时，一定要注意哪个对象正被用于锁定：调用同一个对象中非静态同步方法的线程将彼此阻塞。如果是不同对象，则每个线程有自己的对象的锁，线程间彼此互不干预。调用同一个类中的静态同步方法的线程将彼此阻塞，它们都是锁定在相同的Class对象上。静态同步方法和非静态同步方法将永远不会彼此阻塞，因为静态方法锁定在Class对象上，非静态方法锁定在该类的对象上。对于同步代码块，要看清楚什么对象已经用于锁定（synchronized后面括号的内容）。在同一个对象上进行同步的线程将彼此阻塞，在不同对象上锁定的线程将永远不会彼此阻塞。五、何时需要同步在多个线程同时访问互斥（可交换）数据时，应该同步以保护数据，确保两个线程不会同时修改更改它。对于非静态字段中可更改的数据，通常使用非静态方法访问。对于静态字段中可更改的数据，通常使用静态方法访问。六、线程安全类当一个类已经很好的同步以保护它的数据时，这个类就称为“线程安全的”。即使是线程安全类，也应该特别小心，因为操作的线程之间仍然不一定安全。例如：对线程安全的集合进行判断非空和删除所有元素操作的时候，线程一二都查询非空，但是线程一删除元素后线程二再重复执行是错误的。方法：对所有方法增加一个同步锁，这样，当一个线程访问其中一个同步方法时，其他线程只有等待。七、线程死锁（涉及操作系统）所谓死锁是指两个或两个以上的线程在执行过程中，因争夺资源而造成的一种互相等待的现象，若无外力作用，它们都将无法推进下去。DeadLockRiskTest死锁产生的必要条件：（1）互斥（2）不剥夺（3）请求和保持等待（4）循环等待避免死锁：加锁顺序（线程按照一定的顺序加锁）加锁时限（线程尝试获取锁的时候加上一定的时限，超过时限则放弃对该锁的请求，并释放自己占有的锁）死锁检测（LOCK）总结：避免死锁的方式让程序每次至多只能获得一个锁。当然，在多线程环境下，这种情况通常并不现实。设计时考虑清楚锁的顺序，尽量减少嵌在的加锁交互数量。既然死锁的产生是两个线程无限等待对方持有的锁，那么只要等待时间有个上限不就好了。当然synchronized不具备这个功能，但是我们可以使用Lock类中的tryLock方法去尝试获取锁，这个方法可以指定一个超时时限，在等待超过该时限之后便会返回一个失败信息。 我们可以使用 ReentrantLock.tryLock()方法，在一个循环中，如果tryLock()返回失败，那么就释放以及获得的锁，并睡眠一小段时间。这样就打破了死锁的闭环。比如：线程T1持有锁L1并且申请获得锁L2，而线程T2持有锁L2并且申请获得锁L3，而线程T3持有锁L3并且申请获得锁L1。此时如果T3申请锁L1失败，那么T3释放锁L3，并进行睡眠，那么T2就可以获得L3了，然后T2执行完之后释放L2, L3，所以T1也可以获得L2了执行完然后释放锁L1, L2，然后T3睡眠醒来，也可以获得L1, L3了。打破了死锁的闭环。八、线程同步小结线程同步的目的是为了保护多个线程访问一个资源时对资源的破坏。线程同步方法是通过锁来实现，每个对象都有切仅有一个锁，这个锁与一个特定的对象关联，线程一旦获取了对象锁，其他访问该对象的线程就无法再访问该对象的其他同步方法。 注意：方法锁和私有锁都属于对象锁对于静态同步方法，锁是针对这个类的，锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁，当在一个同步方法中访问另外对象上的同步方法时，会获取这两个对象锁。 （对象锁是在一个类的对象上加的的锁，只有一把，不管有几个方法进行了同步。这些同步方法都共有一把锁，只要一个线程获得了这个对象锁，其他的线程就不能访问该对象的任何一个同步方法。）对于同步，要时刻清醒在哪个对象上同步，这是关键。编写线程安全的类，需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断，对“原子”操作做出分析，并保证原子操作期间别的线程无法访问竞争资源。当多个线程等待一个对象锁时，没有获取到锁的线程将发生阻塞。死锁是线程间相互等待锁锁造成的，在实际中发生的概率非常的小。Java线程：线程的交互线程交互的基础知识wait、notify/notifyAll 详解void notify() 唤醒在此对象监视器上等待的单个线程。void notifyAll() 唤醒在此对象监视器上等待的所有线程。void wait() 导致当前的线程等待（前提是先获得锁，即在synchronized同步代码块里使用），直到其他线程调用此对象的 notify()方法或 notifyAll()方法。三个方法都是Object的本地final方法，无法被重写。由于 wait()、notify/notifyAll() 在synchronized 代码块执行，说明当前线程一定是获取了锁的。当线程执行wait()方法时候，会释放当前的锁，然后让出CPU，进入等待状态。只有当 notify/notifyAll() 被执行时候，才会唤醒一个或多个正处于等待状态的线程，然后继续往下执行，直到执行完synchronized 代码块的代码或是中途遇到wait() ，再次释放锁。也就是说，notify/notifyAll() 的执行只是唤醒沉睡的线程，而不会立即释放锁，锁的释放要看代码块的具体执行情况。所以在编程中，尽量在使用了notify/notifyAll() 后立即退出临界区，以唤醒其他线程让其获得锁。wait() 需要被try catch包围，以便发生异常中断也可以使wait等待的线程唤醒。notify 和 notifyAll的区别notify方法只唤醒一个等待（对象的）线程并使该线程开始执行。所以如果有多个线程等待一个对象，这个方法只会唤醒其中一个线程，选择哪个线程取决于操作系统对多线程管理的实现。notifyAll 会唤醒所有等待(对象的)线程，尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒，推荐使用notifyAll 方法。比如在生产者-消费者里面的使用，每次都需要唤醒所有的消费者或是生产者，以判断程序是否可以继续往下执行。public class K { //状态锁 private Object lock; //条件变量 private int now,need; public void produce(int num){ //同步 synchronized (lock){ //当前有的不满足需要，进行等待，直到满足条件 while(now < need){ try { //等待阻塞 lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("我被唤醒了！"); } // 做其他的事情 } } }显然，只有当前值满足需要值的时候，线程才可以往下执行，所以，必须使用while 循环阻塞。注意，wait() 当被唤醒时候，只是让while循环继续往下走.如果此处用if的话，意味着if继续往下走，会跳出if语句块。总结：当等待的事件发生时，需要能够检查notifyAll()通知事件是否已经发生。通常，解决上面问题的最佳方式是利用某种循环，该循环检查某个条件表达式，只有当正在等待的事情还没有发生的情况下，它才继续等待。生产者-消费者问题:生产者-消费者问题代码Java线程：线程的调度-休眠线程休眠的目的是使线程让出CPU的最简单的做法之一，线程休眠时候，会将CPU资源交给其他线程，以便能轮换执行，当休眠一定时间后，线程会苏醒，进入准备状态等待执行。线程休眠的方法是Thread.sleep(long millis)和Thread.sleep(long millis, int nanos)，均为静态方法，哪个线程调用sleep，就休眠哪个线程。Java线程：线程的调度-优先级与线程休眠类似，线程的优先级仍然无法保障线程的执行次序。只不过，优先级高的线程获取CPU资源的概率较大，优先级低的并非没机会执行。线程的优先级用1-10之间的整数表示，数值越大优先级越高，默认的优先级为5。在一个线程中开启另外一个新线程，则新开线程称为该线程的子线程，子线程初始优先级与父线程相同。public class Test { public static void main(String[] args) { Thread t1 = new MyThread1(); Thread t2 = new Thread(new MyRunnable()); t1.setPriority(10); t2.setPriority(1); t2.start(); t1.start(); } } Java线程：线程的调度-让步线程的让步含义就是使当前运行着线程让出CPU资源，但是然给谁不知道，仅仅是让出，线程状态回到可运行状态。线程的让步使用Thread.yield()方法，yield()为静态方法，功能是暂停当前正在执行的线程对象，并执行其他线程。详见：Java线程：线程状态的转换——阻止线程执行Java线程：线程的调度-合并线程的合并的含义就是将几个并行线程的线程合并为一个单线程执行，应用场景是当一个线程必须等待另一个线程执行完毕才能执行时可以使用join方法。详见：Java线程：线程状态的转换——阻止线程执行joinTestJava线程：线程的调度-守护线程只要当前JVM实例中尚存在任何一个非守护线程没有结束，守护线程就全部工作；只有当最后一个非守护线程结束时，守护线程随着JVM一同结束工作。Daemon的作用是为其他线程的运行提供便利服务，守护线程最典型的应用就是 GC (垃圾回收器)，它就是一个很称职的守护者。User和Daemon两者几乎没有区别，唯一的不同之处就在于虚拟机的离开：如果 User Thread已经全部退出运行了，只剩下Daemon Thread存在了，虚拟机也就退出了。因为没有了被守护者，Daemon也就没有工作可做了，也就没有继续运行程序的必要了。daemontestJava线程：线程的同步-同步方法、同步块详见上文：Java线程：线程的同步与锁注意：在使用synchronized关键字时候，应该尽可能避免在synchronized方法或synchronized块中使用sleep或者yield方法，因为synchronized程序块占有着对象锁，你休息那么其他的线程只能一边等着你醒来执行完了才能执行。不但严重影响效率，也不合逻辑。同样，在同步程序块内调用yeild方法让出CPU资源也没有意义，因为你占用着锁，其他互斥线程还是无法访问同步程序块。当然与同步程序块无关的线程可以获得更多的执行时间。同步块Java线程：并发协作-死锁详见Java线程：线程的同步与锁——线程死锁Java线程：volatile关键字Java 语言中的 volatile 变量可以被看作是一种 “程度较轻的 synchronized”；与 synchronized 块相比，volatile 变量所需的编码较少，并且运行时开销也较少，但是它所能实现的功能也仅是 synchronized 的一部分。解释：Volatile 变量具有 synchronized 的可见性特性，但是不具备原子特性。这就是说线程能够自动发现 volatile 变量的最新值。Volatile 变量可用于提供线程安全，但是只能应用于非常有限的一组用例：多个变量之间或者某个变量的当前值与修改后值之间没有约束。因此，单独使用 volatile 还不足以实现计数器、互斥锁或任何具有与多个变量相关的不变式（Invariants）的类（例如 “start <=end”）。出于简易性或可伸缩性的考虑，您可能倾向于使用 volatile 变量而不是锁。当使用 volatile 变量而非锁时，某些习惯用法（idiom）更加易于编码和阅读。此外，volatile 变量不会像锁那样造成线程阻塞，因此也很少造成可伸缩性问题。在某些情况下，如果读操作远远大于写操作，volatile 变量还可以提供优于锁的性能优势。volatile、synchronizedvolatile 功能，声明了volatile的变量，进行写操作的时候，jvm会向处理器发送一条Lock的前缀指令，会把这个变量所在缓存行的数据写回到系统内存。在多处理器的情况下，保证各个处理器缓存一致性的特点，会实现缓存一致性协议。改变了值的volatile变量，在其他行程被指为失效状态其他线程要使用这个变量，需要重新到驻村里面去取。synchronized：可重入锁、互斥性、可见性volatile 可以做到原子性，可见性，不能做到复合操作的原子性比如： volatile int i;i++ 读取i的值，给i加1，把新的值赋值给i，这个i++操作在多线程环境下是无法保证原子性的synchronized后面锁定的对象，可以是一个类对象，也可以是一个成员变量。第一个线程进来调用monitorener 和monitorexit实现，获取对象的监视器，释放对象监视器后续进程过来，如果monitor还没有释放，那么他获取不到这个对象的monitor，会放到一个对列里面：synthronizedQueue。 等到对象的锁释放了，通知synthronizedQueue出队列，获取那个monitor。volatile 的限制——只有在状态真正独立于程序内其他内容时才能使用 volatile详细说明请看此文章：volatile 关键字的最全总结Java线程：新特征-线程池一、固定大小的线程池//创建一个可重用固定线程数的线程池ExecutorService pool = Executors.newFixedThreadPool(2); 二、单任务线程池//创建一个使用单个 worker线程的 Executor，以无界队列方式来运行该线程。ExecutorService pool = Executors.newSingleThreadExecutor();三、可变尺寸的线程池//创建一个可根据需要创建新线程的线程池，但是在以前构造的线程可用时将重用它们。ExecutorService pool = Executors.newCachedThreadPool();四、延迟连接池//创建一个线程池，它可安排在给定延迟后运行命令或者定期地执行。ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);//使用延迟执行风格的方法pool.schedule(t4, 10, TimeUnit.MILLISECONDS); 五、单任务延迟连接池//创建一个单线程执行程序，它可安排在给定延迟后运行命令或者定期地执行。ScheduledExecutorService pool = Executors.newSingleThreadScheduledExecutor();六、自定义线程池//创建等待队列BlockingQueue<Runnable> bqueue = new ArrayBlockingQueue<Runnable>(20);//创建一个单线程执行程序，它可安排在给定延迟后运行命令或者定期地执行。ThreadPoolExecutor pool = new ThreadPoolExecutor(2,3,2,TimeUnit.MILLISECONDS,bqueue);七、代码参考ThreadPoolDIYThreadPoolJava线程：新特征-锁在Java5中，专门提供了锁对象，利用锁可以方便的实现资源的封锁，用来控制对竞争资源并发访问的控制，这些内容主要集中在java.util.concurrent.locks包下面，里面有三个重要的接口Condition、Lock、ReadWriteLock。Condition：Condition将Object监视器方法（wait、notify和 notifyAll）分解成截然不同的对象，以便通过将这些对象与任意Lock实现组合使用，为每个对象提供多个等待 set（wait-set）。Lock：Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。ReadWriteLock：ReadWriteLock维护了一对相关的锁定，一个用于只读操作，另一个用于写入操作。ReentrantReadWriteLock：把它理解成一个数据库的事务锁（拆分成读锁和写锁、写锁之间互斥，读锁不互斥、要获取写锁必须除当前线程外不能存在其他的读锁）。ReadWriteLock lock = new ReentrantReadWriteLock(false); sync = fair ? new FairSync() : new NonfairSync();注意：lock必须lock.unlock()释放，否则如果受保护的代码抛出异常，锁就可能永远得不到释放。Lock lock = new ReentrantLock(); lock.lock(); try { // update object state } finally { lock.unlock(); }关于ReentrantLock：ReenTrantLock可重入锁（和synchronized的区别）总结轻松学习java可重入锁(ReentrantLock)的实现原理在上文中提到了Lock接口以及对象，使用它，很优雅的控制了竞争资源的安全访问，但是这种锁不区分读写，称这种锁为普通锁。为了提高性能，Java提供了读写锁，在读的地方使用读锁，在写的地方使用写锁，灵活控制，在一定程度上提高了程序的执行效率。Java中读写锁有个接口 java.util.concurrent.locks.ReadWriteLock，也有具体的实现在实际开发中，最好能用读写锁的情况下使用读写锁，而不要用普通锁，以求更好的性能。锁相关代码Java线程：新特征-信号量Java的信号量实际上是一个功能完毕的计数器，对控制一定资源的消费与回收有着很重要的意义，信号量常常用于多线程的代码中，并能监控有多少数目的线程等待获取资源，并且通过信号量可以得知可用资源的数目等等，这里总是在强调“数目”二字，但不能指出来有哪些在等待，哪些资源可用。观点：信号量类如果能返回数目，还能知道哪些对象在等待，哪些资源可使用，就非常完美了，仅仅拿到这些概括性的数字，对精确控制意义不是很大。目前还没想到更好的用法。信号量代码Java线程：新特征-阻塞队列阻塞队列是Java5线程新特征中的内容，Java定义了阻塞队列的接口 java.util.concurrent.BlockingQueue，阻塞队列的概念是，一个指定长度的队列，如果队列满了，添加新元素的操作会被阻塞等待，直到有空位为止。同样，当队列为空时候，请求队列元素的操作同样会阻塞等待，直到有可用元素为止。有了这样的功能，就为多线程的排队等候的模型实现开辟了便捷通道，非常有用。java.util.concurrent.BlockingQueue继承了java.util.Queue接口，可以参看API文档。BlockingQueue代码BlockingQueue核心方法public interface BlockingQueue<E> extends Queue<E> { //将给定元素设置到队列中，如果设置成功返回true, 否则返回false。如果是往限定了长度的队列中设置值，推荐使用offer()方法。 boolean add(E e); //将给定的元素设置到队列中，如果设置成功返回true, 否则返回false. e的值不能为空，否则抛出空指针异常。 boolean offer(E e); //将元素设置到队列中，如果队列中没有多余的空间，该方法会一直阻塞，直到队列中有多余的空间。 void put(E e) throws InterruptedException; //将给定元素在给定的时间内设置到队列中，如果设置成功返回true, 否则返回false. boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException; //从队列中获取值，如果队列中没有值，线程会一直阻塞，直到队列中有值，并且该方法取得了该值。 E take() throws InterruptedException; //在给定的时间里，从队列中获取值，时间到了直接调用普通的poll方法，为null则直接返回null。 E poll(long timeout, TimeUnit unit) throws InterruptedException; //获取队列中剩余的空间。 int remainingCapacity(); //从队列中移除指定的值。 boolean remove(Object o); //判断队列中是否拥有该值。 public boolean contains(Object o); //将队列中值，全部移除，并发设置到给定的集合中。 int drainTo(Collection<? super E> c); //指定最多数量限制将队列中值，全部移除，并发设置到给定的集合中。 int drainTo(Collection<? super E> c, int maxElements); }成员详解：ArrayBlockingQueue 基于数组的阻塞队列实现，在ArrayBlockingQueue内部，维护了一个定长数组，以便缓存队列中的数据对象，这是一个常用的阻塞队列，除了一个定长数组外，ArrayBlockingQueue内部还保存着两个整形变量，分别标识着队列的头部和尾部在数组中的位置。 ArrayBlockingQueue在生产者放入数据和消费者获取数据，都是共用同一个锁对象，由此也意味着两者无法真正并行运行，这点尤其不同于LinkedBlockingQueue；按照实现原理来分析，ArrayBlockingQueue完全可以采用分离锁，从而实现生产者和消费者操作的完全并行运行。Doug Lea之所以没这样去做，也许是因为ArrayBlockingQueue的数据写入和获取操作已经足够轻巧，以至于引入独立的锁机制，除了给代码带来额外的复杂性外，其在性能上完全占不到任何便宜。 ArrayBlockingQueue和LinkedBlockingQueue间还有一个明显的不同之处在于，前者在插入或删除元素时不会产生或销毁任何额外的对象实例，而后者则会生成一个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统中，其对于GC的影响还是存在一定的区别。而在创建ArrayBlockingQueue时，我们还可以控制对象的内部锁是否采用公平锁，默认采用非公平锁。LinkedBlockingQueue 基于链表的阻塞队列，同ArrayListBlockingQueue类似，其内部也维持着一个数据缓冲队列（该队列由一个链表构成），当生产者往队列中放入一个数据时，队列会从生产者手中获取数据，并缓存在队列内部，而生产者立即返回；只有当队列缓冲区达到最大值缓存容量时（LinkedBlockingQueue可以通过构造函数指定该值），才会阻塞生产者队列，直到消费者从队列中消费掉一份数据，生产者线程会被唤醒，反之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue之所以能够高效的处理并发数据，还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步，这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据，以此来提高整个队列的并发性能。作为开发者，我们需要注意的是，如果构造一个LinkedBlockingQueue对象，而没有指定其容量大小，LinkedBlockingQueue会默认一个类似无限大小的容量（Integer.MAX_VALUE），这样的话，如果生产者的速度一旦大于消费者的速度，也许还没有等到队列满阻塞产生，系统内存就有可能已被消耗殆尽了。DelayQueue DelayQueue中的元素只有当其指定的延迟时间到了，才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列，因此往队列中插入数据的操作（生产者）永远不会被阻塞，而只有获取数据的操作（消费者）才会被阻塞。使用场景： DelayQueue使用场景较少，但都相当巧妙，常见的例子比如使用一个DelayQueue来管理一个超时未响应的连接队列。PriorityBlockingQueue 基于优先级的阻塞队列（优先级的判断通过构造函数传入的Compator对象来决定），但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者，而只会在没有可消费的数据时，阻塞数据的消费者。因此使用的时候要特别注意，生产者生产数据的速度绝对不能快于消费者消费数据的速度，否则时间一长，会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时，内部控制线程同步的锁采用的是公平锁。SynchronousQueue 一种无缓冲的等待队列，类似于无中介的直接交易，有点像原始社会中的生产者和消费者，生产者拿着产品去集市销售给产品的最终消费者，而消费者必须亲自去集市找到所要商品的直接生产者，如果一方没有找到合适的目标，那么对不起，大家都在集市等待。相对于有缓冲的BlockingQueue来说，少了一个中间经销商的环节（缓冲区），如果有经销商，生产者直接把产品批发给经销商，而无需在意经销商最终会将这些产品卖给那些消费者，由于经销商可以库存一部分商品，因此相对于直接交易模式，总体来说采用中间经销商的模式会吞吐量高一些（可以批量买卖）；但另一方面，又因为经销商的引入，使得产品从生产者到消费者中间增加了额外的交易环节，单个产品的及时响应性能可能会降低。 声明一个SynchronousQueue有两种不同的方式，它们之间有着不太一样的行为。公平模式和非公平模式的区别: 如果采用公平模式：SynchronousQueue会采用公平锁，并配合一个FIFO队列来阻塞多余的生产者和消费者，从而体系整体的公平策略； 但如果是非公平模式（SynchronousQueue默认）：SynchronousQueue采用非公平锁，同时配合一个LIFO队列来管理多余的生产者和消费者，而后一种模式，如果生产者和消费者的处理速度有差距，则很容易出现饥渴的情况，即可能有某些生产者或者是消费者的数据永远都得不到处理。参考链接:https://www.jianshu.com/p/6ba87c7508e4阻塞队列还有更多实现类，用来满足各种复杂的需求：ArrayBlockingQueue, DelayQueue, LinkedBlockingQueue, PriorityBlockingQueue, SynchronousQueue，具体的API差别也很小。Java线程：新特征-阻塞栈对于阻塞栈，与阻塞队列相似。不同点在于栈是“后入先出”的结构，每次操作的是栈顶，而队列是“先进先出”的结构，每次操作的是队列头。这里要特别说明一点的是，阻塞栈是Java6的新特征。BlockingDeque代码Java线程：新特征-条件变量条件变量是Java5线程中很重要的一个概念，顾名思义，条件变量就是表示条件的一种变量。但是必须说明，这里的条件是没有实际含义的，仅仅是个标记而已，并且条件的含义往往通过代码来赋予其含义。这里的条件和普通意义上的条件表达式有着天壤之别。他不是自然语言中的条件概念，而是程序控制的一种手段。条件变量都实现了java.util.concurrent.locks.Condition接口，条件变量的实例化是通过一个Lock对象上调用newCondition()方法来获取的，这样，条件就和一个锁对象绑定起来了。因此，Java中的条件变量只能和锁配合使用，来控制并发程序访问竞争资源的安全。条件变量的出现是为了更精细控制线程等待与唤醒，在Java5之前，线程的等待与唤醒依靠的是Object对象的wait()和notify()/notifyAll()方法，这样的处理不够精细。而在Java5中，一个锁可以有多个条件，每个条件上可以有多个线程等待，通过调用await()方法，可以让线程在该条件下等待。当调用signalAll()方法，又可以唤醒该条件下的等待的线程。条件变方法非条件变量方法Java线程：新特征-原子量所谓的原子量即操作变量的操作是“原子的”，该操作不可再分，因此是线程安全的。为何要使用原子变量呢，原因是多个线程对单个变量操作也会引起一些问题。在Java5之前，可以通过volatile、synchronized关键字来解决并发访问的安全问题，但这样太麻烦。Java5之后，专门提供了用来进行单变量多线程并发安全访问的工具包 java.util.concurrent.atomic，其中的类也很简单。原子量虽然可以保证单个变量在某一个操作过程的安全，但无法保证你整个代码块，或者整个程序的安全性。因此，通常还应该使用锁等同步机制来控制整个程序的安全性。atomicTest原子仅仅是保证变量操作的原子性，但整个程序还需要考虑线程安全的。Java线程：新特征-障碍器Java5中，添加了障碍器类，为了适应一种新的设计需求，比如一个大型的任务，常常需要分配好多子任务去执行，只有当所有子任务都执行完成时候，才能执行主任务，这时候，就可以选择障碍器了。障碍器是多线程并发控制的一种手段。CyclicBarrier栅栏类似于闭锁，它能阻塞一组线程直到某个事件的发生。栅栏与闭锁的关键区别在于，所有的线程必须同时到达栅栏位置，才能继续执行。闭锁用于等待事件，而栅栏用于等待其他线程。CyclicBarrier可以使一定数量的线程反复地在栅栏位置处汇集。当线程到达栅栏位置时将调用await方法，这个方法将阻塞直到所有线程都到达栅栏位置。如果所有线程都到达栅栏位置，那么栅栏将打开，此时所有的线程都将被释放，而栅栏将被重置以便下次使用。CyclicBarrier默认的构造方法是CyclicBarrier(int parties)，其参数表示屏障拦截的线程数量，每个线程使用await()方法告诉CyclicBarrier我已经到达了屏障，然后当前线程被阻塞。CyclicBarrier的另一个构造函数CyclicBarrier(int parties, Runnable barrierAction)，用于线程到达屏障时，优先执行barrierAction，方便处理更复杂的业务场景。调用await方法的线程告诉CyclicBarrier自己已经到达同步点，然后当前线程被阻塞。直到parties个参与线程调用了await方法，CyclicBarrier同样提供带超时时间的await和不带超时时间的await方法。cyclicbarrier代码不可不说的Java“锁”事【基本功】不可不说的Java“锁”事写在最后相关的面试笔试题会另开一篇文章来更新。重新整理了文章的同时也对线程有了更深层次的理解。现在很多博客、公众号只会搬运，很多原文错漏之处全网竟找不到有人修改，现在的乱象就是如此，很多人都太浮躁了静不下心来好好写文章，很多人为了快速掌握都是选择找前人的文章，包括我也如此，所以在此也是惊醒自己，别人的东西要去消化去理解去修正，这样才能变成自己的东西。