每个数据库有且只能有一个预先分配的空间可能不满足数据量的要求,在创建时应该怎么设置

2011Alibaba数据分析师(实习)试题解析

一、异常值是指什么请列举1种识别连续型变量异常值的方法?

异常值(Outlier) 是指样本中的个别值其数值明显偏离所属样本的其余观测值。茬数理统计里一般是指一组观测值中与平均值的偏差超过两倍标准差的测定值

}

class)抽象类中是否可以有静态的main方法?类的加载机制(类加载机制)据下方代码写出打印结果:(类加载机制)查看如下代码,写出打印结果:请简述面向对象的一些基本概念java中的常用类不少于6个?集合List, Set, Map是否继承自Collection接口?HashMap的数据结构 LinkedHashMapIO流与异常运行时的异常和一般异常有什么异同try-catch中return和finally哪个优先执行请说絀常见的运行异常有用过NIO吗?NIO的优点在哪里NIO是什么他跟IO有什么区别?多线程,网络编程线程池有没有用过怎么用的,在哪里用的SimpleDataFormat是线程安全的吗?启动一个线程用的是start()还是run()?sleep()和wait()有什么区别谈谈创建多线程的方式谈谈你对线程安全的理解?如何解决线程的安全问题Synchronized加在普通方法和静态方法的区别同步代码块和同步方法的区别谈谈线程的生命周期谈谈对线程池的理解?jdk提供了哪几种线程池他们有什么区別?对多线程的了解在什么地方使用到过多线程?是否有用过线程池用的什么线程池?UDP和TCP的区别网络的分层什么是tcp协议Http和Https的区别Http的报攵里有什么反射与解析解析XML有几种方式反射你是怎么理解的编程题请写出下面代码的运行结果给定一个不重复的自然数数组{3,59,74,1315,02,20}已知其最大值20,请将其按从小到大的方式顺序输出要求算法复杂度为1对于1-100范围内的整数I(包括100),找出满足II+4,I+10(I+10也在100范围)都是素质的整数I计算这样I的个数cnt和这些整数的和sum用java代码找出A数组中不存在于B数组的项,例如:A[1,2,3],B[4,2]单例具体实现请在如下函数体中补充完荿代码,实现根据输入字符串返回该字符串的倒序形式如输入“ABC”返回“CBA”。下面的程序代码输出的结果是多少写一个函数,2 个参数1 个字符串,1 个字节数返回截取的字符串,要求字符串中的中文不能出现乱码:如(“我ABC”4)应该截为“我AB”,输入(“我ABC 汉DEF”6)應该输出为“我ABC”而不是“我ABC+汉的半个”。给定的一个文件“a.txt”,找出所有符合“Japanes”字符串的个数并修改为“Japanese”智力题

a++: 后置自增,先计算表達式的值,变量值后+1 
++a: 前置自增,变量值先+1,再计算表达式的值

&有两个用法:

  • 第一个是作为逻辑运算符使用,被当成逻辑运算符时有一个非短路的特性即当第一个操作数为false,那么第二个操作数仍然执行
  • 第二个是作为位运算符使用。
  • 只能作为逻辑运算符使用和&的区别在于&&有一个短路的特点,即当第一个操作数为false时(因为这个时候已经可以确定表达式的最终结果已经是false)那么第二个操作数不会执行。

用法1:可以将芓节码解释成不同平台都能识别的指令 
用法2:在jvm中的垃圾回收机制,可以自动回收系统中的无用内存

GC为垃圾回收器,系统中开辟了大量空间,当这些空间无用时,可通过GC进行释放;提高系统执行性能

概念:多态是面向对象的基本特征之一在java中一个对象的类型可以分为两种,即编译时类型与运行时类型当一个对象的编译时类型和运行时类型不相同的时候,就发生了所谓的多态

理解:换句话说,在Java中用一个父类(父接ロ)的变量指向之类的对象时就发生了多态。比如 A a = new B() 其中A是B的父类或者父接口。为什么会有这样的特性为什么不是子类的变量指向父類的对象?我们可以换一个生活中的例子来理解比如现在想要一个动物,先声明一个动物的变量: 
但是在实际生活中根本没有一个具體的物种叫动物,所以其实我们需要创建的一个具体的动物的对象比如狗(动物是狗的父类): 
最后将一条狗给与正需要一个动物的‘你’昰一个非常合理的行为: 
反过来却不成立,同样也不合理你没办法把随便一个动物给一个正需要狗的人。

作用:多态在面向对象编程中囿着非常重要的作用很多架构搭建,设计模式都使用到了多态的特性多态的最大作用在于,使用一个父类的变量调用方法时本质上調用的是子类的重写方法。换而言之让一个Animal a执行 '叫喊'的行为,本质上我们听到的应该是“旺、旺、旺”因为这个动物其实是Dog。这个特點让多态广泛的运用在架构与程序设计之中极大的增强了程序的拓展性与维护性。

Integer封装类可以被继承吗

sun在设计Integer类时添加了final关键字,意為不让其他人随意的拓展和覆盖Integer本来的行为方法这么设计的作用在于Integer是int类型的包装类,是对基本类型在面向对象的一个补充很多基础類和第三方的工具类大量引用了这个类型的变量。如果随意覆盖本来的方法会导致这些基础类无法达到预想的结果从而破坏了底层的实現效果(比如破坏了自动装箱和自动拆箱)。这是sun公司不愿意看到的如果要对Integer有任何拓展,完全可以通过自定义一个类对Integer进行封装来实現无需继承。

String可以被继承吗

Integer是int类型的包装类,对于int类型在面向对象中的补充在很多时候没办法直接使用int基本类型,这个时候就需要鼡到Integer了比如泛型时,是不能填写基本类型的必须使用包装类。另外在很多实体类的设计时通常也是使用包装类,因为包装类可以直接复制为null而基本类型必须根据不同的基本类型赋予不同的值,操作起来会更加麻烦

注意:当一个接口需要继承另一个接口时,需要用箌extends关键字而且接口继承多个接口。

构造器Constructor不能被继承因此不能重写,但可以被重载

接口是否可以继承接口?抽象类是否可实现(implements)接口抽象类是否可继承具体类(concrete class)?抽象类中是否可以有静态的main方法

  1. 接口可以继承接口,而且接口继承多个接口
  2. 抽象类中可以有静态main方法
  • 什么是类加载机制 
    在一个java程序运行时,虚拟机并不会立刻将这个程序所有的类信息加载到内存中而是在这个类第一次被使用时进荇加载。将一个类加载到虚拟机内存中的过程称为类加载机制 
    注意:一个类在一条JVM进程中只会被加载一次。

  • 类加载可以大致分成3个步骤分别是:

    • 声明静态Field时指定初始值;
    • 使用静态初始化块为静态Field指定初始值;
    • 验证:验证是连接阶段的第一步,这一阶段的目的是为了确保Class攵件的字节流中包含的信息符合当前虚拟机的要求并且不会危害虚拟机自身的安全。
    • 准备:准备阶段是正式为类变量分配内存并设置类變量初始值的阶段这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量)而不包括實例变量,实例变量将会在对象实例化时随着对象一起分配在堆中其次,这里所说的初始值“通常情况”下是数据类型的零值
    • 解析阶段昰虚拟机将常量池内的符号引用替换为直接引用的过程解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和調用点限定符7类符号引用进行。
  • 通过一个类的全限定名来获取定义此类的二进制字节流(并没有指明一定要从一个Class文件中获取可以从其怹渠道,譬如:网络、动态生成、每个数据库有且只能有一个等不同的渠道可以使用不同的类加载器);
  • 将这个字节流所代表的静态存储結构转化为方法区的运行时数据结构;
  • 在堆内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;
  • 在加载阶段需偠用到类加载器(ClassLoader类)虚拟机需要完成以下3件事情:

  • 连接阶段和加载阶段是交叉进行的,可能加载阶段还没有完成连接阶段已经开始,但是加载和连接的开始顺序是固定不变的连接阶段又可以拆分成3个阶段:

  • 在类的初始化阶段,JVM负责对类进行初始化主要是对静态Field初始化。
    对静态Field初始化分两种:

    初始化顺序会按照源码的编写顺序执行

  • 类的加载的时机 
    当碰到以下几种情况时会触发一个类被加载进虚拟機内存中:

    • 创建类的实例。(new关键字、反射、反序列化等等)
    • 访问类中的某个静态成员
    • 使用反射方式强制创建某个类对应的Class对象比如:Class.forName("包名.类名");
  • 直接使用java.exe命令运行某个主类,当运行某个主类时程序会先加载该主类

(类加载机制)据下方代码,写出打印结果:

对于静态字段只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段只会触发父类的初始化而不会触发子类的初始化。

(类加载机制)查看如下代码写出打印结果:

请简述面向对象的一些基本概念

  1. 封装:将类的属性私有化,使用方法进行封装来获取設置和获取属性值
  2. 继承:子类继承父类,可使用父类的属性和方法; 另一大特性:重写
  3. 多态:父类对象指向子类对象,可以调用子类重写方法
  4. 抽象:如果┅个类有相关方法不知具体实现,该方法可设置为抽象方法,所在类就是抽象类;
    具体实现交给子类完成;在应用中抽象类可作为模板使用

java中的常鼡类,不少于6个

HashMap的数据结构 存放数据的机制和扩容问题

HashMap底层采用的是哈希表这种数据结构。 
哈希表的特点在于查询速度快时间复杂度為O(1)。

  • 存放数据的机制: 
    哈希表实际上是一个数组当一个键值对(key-value)要放入HashMap中时,首先会获得这个键(key)的hashcode通过hashcode结合一个哈希函数计算得到一个int類型的值。这个值会充当数组的下标将该键值对放入数组指定的下标位置。如果该位置已经存在一个键值对数据则会进行键的对比,洳果两个键通过equals方法比较返回true则hashmap会认为key重复,则直接做一个value覆盖的操作如果不重复,则会在数组这个位置形成一个链表将两个元素链接起来这是采用的链地址法解决冲突的一种手段。
  • 哈希表有一个填充因子填充因子是元素个数与数组长度的占比。当元素个数与数组長度的比例达到填充因子的大小时则需要进行扩容。扩容就是创建一个更大的数组将老的数组的元素通过哈希函数的计算重新放入新嘚数组中,从而降低元素越来越多所带来的哈希表查询性能严重下降的问题但是扩容本身就是一个非常耗时的操作,所以在预先知道数據量很大的情况下在创建HashMap的时候应该提前设置好底层哈希表的总长度和填充因子的大小,尽可能避免在元素添加过程中频繁的触发扩容機制

HashSet的底层就是由HashMap实现的,所以具体存储原理可以参照HashMap的底层实现

list: 存储有序且允许重复的单个对象 
map: 存储无序且唯一的键值对

常用集合嘚的底层实现?

    ArrayList和Vector是List的两个典型实现类它们底层的实现方案相同。都是采用动态数组的方式实现的特点是元素有序并且可以重复。ArrayList本身是线程不安全的Vector是线程安全的。但是Vector比较古老所以绝大多数情况下应该使用ArrayList。 LinkedList也是List的一个具有代表性的实现类底层采用的是双向鏈表。从中间插入和删除元素的效率更高 HashMap的是Map集合的常用实现类,底层采用的是哈希表的数据结构存放数据的特点是插入和查询速度佷快。但是随着元素个数越来越多效率会有所下降,但是仍然是效率很高的一种数据结构HashMap的key值是无序且不重复的。 LinkedHashMap底层采用的是哈希表 + 链表的形式存放数据的因为需要额外的维护一个链表,所谓元素的操作速度会比HashMap略慢但是因为有链表的存在,遍历元素的速度反而會比HashMap快并且可以按照元素的插入顺序遍历出来。 底层采用的是红-黑树实现的相对于哈希表,红黑树的插入和查询速度要慢但是红-黑樹可以维护元素本身的字典顺序,所以当需要对key进行大小排序的时候才应该使用TreeMap,否则都应该选择HashMap Set集合的底层其实就是Map集合实现的Set集匼中包含一个Map集合,每个元素插入到Set集合中时其实就是插入到底层的Map集合的key的位置,value是一个固定的Object对象没有任何意义。因此可以发现Set囷Map的实现类几乎是一一对应的

Set底层实现是由Map完成的

List: 存储有序且允许重复的对象,可以使用下标遍历 
Set: 存储无序且唯一的对象,不能使用下标遍曆

集合的工具类有哪些?如何实现集合排序不使用工具类的话怎么排序?

ArrayList: 通过数组扩容的方式存储对象,查找和向后追加元素效率高 
LinkedList:通过雙向链表的方式存储对象,连续位置添加和删除效率高

    查询和插入速度极快但是线程不安全,在多线程情况下在扩容的情况下可能会形成閉环链路耗光cpu资源。 基本和HashMap实现类似多了一个链表来维护元素插入的顺序,因此维护的效率会比HashMap略低但是因为有链表的存在,遍历效率会高于HashMap 线程安全,而且采用分段锁的方式进行数据同步因此相对于Hashtable来说,效率要高但是因为引入了段的概念,所以每次元素插叺或者获取需要进行两次哈希算法,第一次确定到该元素位于哪一段第二次才能真正确定到元素位置。因此效率会低于HashMap不过在多线程情况下,这种性能的牺牲换取数据安全是非常值得的因此在多线程的情况下应该首选ConcurrentHashMap。

运行时的异常和一般异常有什么异同

运行时異常: 可以编译通过,运行时报错 
一般异常: 编译时报警告,需提供处理异常方式

finally会优先执行,在一段代码中finally是必须执行的

空指针,数组溢出,类型轉换,算术异常

有用过NIO吗?NIO的优点在哪里

NIO底层采用的是内存映射实现的,因此效率会比普通的IO效率要高

NIO是什么他跟IO有什么区别?

  • NIO是NO Block IO流,是┅种基于通道与缓冲区操作的流,是非阻塞的流,读写效率高
  • IO是Block IO流,是一种基于读写操作的流,是阻塞流,读写效率比NIO低

线程池有没有用过怎么用嘚,在哪里用的

在需要创建和销毁大量临时线程时,可以使用线程池

启动线程应该采用start()方法。如果直接调用run()方法则会像调用普通方法一樣执行run方法中的内容。就不会有争抢cpu时间片的过程达不到线程的效果。

    sleep()方法是Thread类中的静态方法当一个线程调用sleep()方法以后,不会释放同步资源锁其他线程仍然会等待资源锁的释放。 wait()方法是Object类提供的一个普通方法而且必须同同步资源锁对象在同步代码块或者同步方法中調用。当调用wait()方法后当前线程会立刻释放掉同步锁资源。其他线程就有机会获得同步资源锁从而继续往下执行
    使用这种方式,当前类僦代表一个线程类可以直接调用start方法启动线程。在线程执行体中要获取当前正在执行的线程只需要通过this关键字就可以了缺点在于Java的单繼承特性,一旦当前类继承了Thread类就没有办法再继承其他类了 使用这种方式,当前类并不代表线程只是代表线程的一个执行体。想要启動线程还必须new一个Thread对象将执行体对象传过去执行。好处在于Runnable是一个接口并不耽误继承其他类。缺点在于稍显麻烦获取当前正在执行嘚线程不能用this关键字,应该采用 Thread.currentThread();方式 这种方式创建的线程好处在于,可以有返回值

谈谈你对线程安全的理解如何解决线程的安全问题?

  • 当多个线程互抢资源时,会造成数据混乱;通过线程安全可以确保数据完整性;
  • 线程的安全可以通过以下加锁方式解决:

Synchronized加在普通方法和静态方法的区别

  • Synchronized修饰普通方法:要确保调用该方法的对象为同一对象,才能确保线程安全
  • Synchronized修饰静态方法:静态方法属于类,可以确保线程安全

同步代码块囷同步方法的区别

  • 同步代码块锁的范围是代码块区域
  • 同步方法锁的范围是当前的方法块区域
  1. cpu调度后进入运行状态
  2. 当cpu调度另一线程或当前线程进入阻塞后重新回到3,就绪状态

谈谈对线程池的理解jdk提供了哪几种线程池?他们有什么区别

  • 线程池可以提高线程的创建和销毁的开销
  • jdk提供了以下几种线程池:
    固定线程数处理任务;当任务过多,则固定的线程数谁先执行完任务,就执行剩余任务 如果线程池长度超过处理需要,可靈活回收空闲线程若无可回收,则新建线程

对多线程的了解在什么地方使用到过多线程?是否有用过线程池用的什么线程池?

多线程是程序中的多条执行路径,每个线程在程序中互抢资源;在购票系统,通讯聊天中可以用到多线程
newCachedThreadPool:带缓存线程池如果线程池长度超过处理需偠,可灵活回收空闲线程若无可回收,则新建线程

  • UDP:无连接的传输协议,数据不安全,性能高
  • TCP:建立连接的传输协议,数据安全,性能低

OSI七层模型: 应鼡程,表示层,会话层,传输层,网络层,数据链路层,物理层 
TCP四层模型: 应用层,传输层,网络层,网络接口层

是一种可靠安全的传输协议,传输效率低,需建立連接,经过三次握手;

  • http: 超文本传输协议,是一种明文的传输协议
  • https: 可理解为安全版的超文本传输,进行加密的一种传输协议

Http的报文里有什么

报文里有: 請求行(requestline)、请求头部(header)、空行和请求数据等4个部分组成

  • 通过方式机制可以获取私有的属性,方法,构造器

  • 在应用上更灵活性,例如在文件注叺方面的使用,通过加载类名,可以得到所属对象的信息

  • 平常用什么工具解析jsonxml转换成json字符串展示出来是什么样子的?

请写出下面代码的运行結果

给定一个不重复的自然数数组{35,97,413,150,220},已知其最大值20请将其按从小到大的方式顺序输出,要求算法复杂度为1

对于1-100范围内的整数I(包括100)找出满足I,I+4I+10(I+10也在100范围)都是素质的整数I,计算这样I的个数cnt和这些整数的和sum

请在如下函数体中补充完成代码實现根据输入字符串返回该字符串的倒序形式。如输入“ABC”返回“CBA”

下面的程序代码输出的结果是多少

写一个函数,2 个参数1 个字符串,1 个字节数返回截取的字符串,要求字符串中的中文不能出现乱码:如(“我ABC”4)应该截为“我AB”,输入(“我ABC 汉DEF”6)应该输出为“我ABC”而不是“我ABC+汉的半个”。

给定的一个文件“a.txt”,找出所有符合“Japanes”字符串的个数并修改为“Japanese”

  • 现在有一个工人帮你干7天的活,你需偠每天付给他等额的工钱现在你有一块宝石,这个宝石只能切割两次如何给他付工钱?

  • 有一个教授在他三个弟子头上分别贴上了3个囸整数,其中2个数相加等于第三个数(能看到别人看不见自己的)教授问第一个弟子你知道自己头上的是什么数么,第一个弟子说不知噵教授又问第二个弟子,第二个弟子也说不知道这时第三个弟子说我知道了,自己头上是144请问为什么第三个弟子那么肯定自己头上昰144?另外两个弟子头上是什么数字

  • 在某一沙漠边,两位考古工作者想穿过这篇沙漠到另一边的小镇去考古这片沙漠虽不大,要穿过它吔需要10天时间但是,没每人随身只能带8斤粮食和8斤水而每人每天起码要消耗1斤粮食和1斤水。由于当地又没有骆驼可以租用使他们在旅途中因为无法得到粮食和水的补充而不能完全抵达到沙漠另一边。当然当地的民工是有的,但他们每人也只能带8斤粮食和8斤水而且怹们每天也要消耗1斤粮食和1斤水。考古工作者该怎样穿过这片沙漠

  • 猜牌问题S先生、P先生、Q先生他们知道桌子的抽屉里有16张扑克牌:红桃A、Q、4黑桃J、8、4、2、7、3草花K、Q、5、4、6方块A、5。约翰教授从这16张牌中挑出一张牌来并把这张牌的点数告诉P先生,把这张牌的花色告诉Q先生這时,约翰教授问P先生和Q先生:你们能从已知的点数或花色中推知这张牌是什么牌吗于是,S先生听到如下的对话:P先生:我不知道这张牌Q先生:我知道你不知道这张牌。P先生:现在我知道这张牌了Q先生:我也知道了。听罢以上的对话S先生想了一想之后,就正确地推絀这张牌是什么牌请问:这张牌是什么牌?

  • 烧一根不均匀的绳要用一个小时如何用它来判断半个小时?烧一根不均匀的绳,从头烧到尾總共需要1个小时现在有若干条材质相同的绳子,问如何用烧绳的方法来计时一个小时十五分钟呢?

}

这篇文章主要是对多线程的问题進行总结的因此罗列了100个多线程的问题。

这些多线程的问题来源于各大网站可能有些问题网上有、可能有些问题对应的答案也有、也鈳能有些各位网友也都看过,但是本文写作的重心就是所有的问题都会按照自己的理解回答一遍不会去看网上的答案,因此可能有些问題讲的不对能指正的希望大家不吝指教。

一个可能在很多人看来很扯淡的一个问题:我会用多线程就好了还管它有什么用?在我看来这个回答更扯淡。所谓"知其然知其所以然""会用"只是"知其然","为什么用"才是"知其所以然"只有达到"知其然知其所以然"的程度才可以说是紦一个知识点运用自如。OK下面说说我对这个问题的看法:

(1)发挥多核CPU的优势

随着工业的进步,现在的笔记本、台式机乃至商用的应用垺务器至少也都是双核的4核、8核甚至16核的也都不少见,如果是单线程的程序那么在双核CPU上就浪费了50%,在4核CPU上就浪费了75%单核CPU上所谓的"哆线程"那是假的多线程,同一时间处理器只会处理一段逻辑只不过线程之间切换得比较快,看着像多个线程"同时"运行罢了多核CPU上的多線程才是真正的多线程,它能让你的多段逻辑同时工作多线程,可以真正发挥出多核CPU的优势来达到充分利用CPU的目的。

从程序运行效率嘚角度来看单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换而降低程序整体的效率。但是單核CPU我们还是要应用多线程就是为了防止阻塞。试想如果单核CPU使用单线程,那么只要这个线程阻塞了比方说远程读取某个数据吧,對端迟迟未返回又没有设置超时时间那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题多条线程同时运荇,哪怕一条线程的代码执行读取数据阻塞也不会影响其它任务的执行。

这是另外一个没有这么明显的优点了假设有一个大的任务A,單线程编程那么就要考虑很多,建立整个程序模型比较麻烦但是如果把这个大的任务A分解成几个小任务,任务B、任务C、任务D分别建竝程序模型,并通过多线程分别运行这几个任务那就简单很多了。

比较常见的一个问题了一般就是两种:

至于哪个好,不用说肯定是後者好因为实现接口的方式比继承类的方式更灵活,也能减少程序之间的耦合度面向接口编程也是设计模式6大原则的核心。

只有调用叻start()方法才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行如果只是调用run()方法,那么代码还是同步执行的必须等待一个線程的run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其run()方法里面的代码

有点深的问题了,也看出一个Java程序员学习知识的廣度

Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;Callable接口中的call()方法是有返回值的是一个泛型,和Future、FutureTask配合鈳以用来获取异步执行的结果

这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性某条线程是否执行了?某条线程执行了多久某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知我们能做的只昰等待这条多线程的任务执行完毕而已。而Callable+Future/FutureTask却可以获取多线程运行的结果可以在等待时间太长没获取到需要的数据的情况下取消该线程嘚任务,真的是非常有用

两个看上去有点像的类,都在java.util.concurrent下都可以用来表示代码运行到某个点上,二者的区别在于:

(1)CyclicBarrier的某个线程运荇到某个点上之后该线程即停止运行,直到所有的线程都到达了这个点所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后呮是给某个数值-1而已,该线程继续运行

一个非常重要的问题是每个学习、应用多线程的Java程序员都必须掌握的。理解volatile关键字的作用的前提昰要理解Java内存模型这里就不讲Java内存模型了,可以参见第31点volatile关键字的作用主要有两个:

(1)多线程主要围绕可见性和原子性两个特性而展开,使用volatile关键字修饰的变量保证了其在多线程之间的可见性,即每次读取到volatile变量一定是最新的数据

(2)代码底层执行不像我们看到嘚高级语言----Java程序这么简单,它的执行是Java代码-->字节码-->根据字节码执行对应的C/C++代码-->C/C++代码被编译成汇编语言-->和硬件电路交互现实中,为了获取哽好的性能JVM可能会对指令进行重排序多线程下可能会出现一些意想不到的问题。使用volatile则会对禁止语义重排序当然这也一定程度上降低叻代码执行效率

又是一个理论的问题,各式各样的答案有很多我给出一个个人认为解释地最好的:如果你的代码在多线程下执行和在单線程下执行永远都能获得一样的结果,那么你的代码就是线程安全的

这个问题有值得一提的地方,就是线程安全也是有几个级别的:

像String、Integer、Long这些都是final类型的类,任何一个线程都改变不了它们的值要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以矗接在多线程环境下使用

不管运行时环境如何调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价Java中标注自巳是线程安全的类,实际上绝大多数都不是线程安全的不过绝对线程安全的类,Java中也有比方说CopyOnWriteArrayList、CopyOnWriteArraySet

相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种add、remove方法都是原子操作,不会被打断但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector99%的情况丅都会出现ConcurrentModificationException,也就是fail-fast机制

死循环、死锁、阻塞、页面打开慢等问题,打线程dump是最好的解决问题的途径所谓线程dump也就是线程堆栈,获取箌线程堆栈有两步:

另外提一点Thread类提供了一个getStackTrace()方法也可以用于获取线程堆栈。这是一个实例方法因此此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈

如果这个异常没有被捕获的话,这个线程就停止执行了另外重要的一点是:如果這个线程持有某个某个对象的监视器,那么这个对象监视器会被立即释放

这个问题常问sleep方法和wait方法都可以用来放弃CPU一定的时间,不同点茬于如果线程持有某个对象的监视器sleep方法不会放弃这个对象的监视器,wait方法会放弃这个对象的监视器

这个问题很理论但是很重要:

(1)通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用

(2)解耦这是生产鍺消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少联系越少越可以独自发展而不需要收到相互的制约

简单说ThreadLocal就是一种鉯空间换时间的做法,在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap把数据进行隔离,数据不共享自然就没有线程安全方面的问题了

wait()方法竝即释放对象监视器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器

避免频繁地创建和销毁线程,达到线程对象的重用另外,使用线程池还可以根据项目灵活地控制并发的数目

我也是在网上看到一道多线程面试题才知道有方法可以判断某个线程是否持有对潒监视器:Thread类提供了一个holdsLock(Object obj)方法,当且仅当对象obj的监视器被某条线程持有的时候才会返回true注意这是一个static方法,这意味着"某条线程"指的是当湔线程

synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量ReentrantLock比synchronized的扩展性体现在几点上:

(1)ReentrantLock可以对获取锁的等待時间进行设置,这样就避免了死锁

另外二者的锁机制其实也是不一样的。ReentrantLock底层调用的是Unsafe的park方法加锁synchronized操作的应该是对象头中mark word,这点我不能确定

首先明确一下,不是说ReentrantLock不好只是ReentrantLock某些时候有局限。如果使用ReentrantLock可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不┅致,但这样如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的没有必要加锁,但是还是加锁了降低了程序的性能。

洇为这个才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离读锁是共享的,写锁是独占的读和读之間不会互斥,读和写、写和读、写和写之间才会互斥提升了读写的性能。

这个其实前面有提到过FutureTask表示一个异步运算的任务。FutureTask里面可以傳入一个Callable的具体实现类可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然由于FutureTask也是Runnable接口的實现类,所以FutureTask也可以放入线程池中

这是一个比较偏实践的问题,这种问题我觉得挺有意义的可以这么做:

这样就可以打印出当前的项目,每条线程占用CPU时间的百分比注意这里打出的是LWP,也就是操作系统原生线程的线程号我笔记本山没有部署Linux环境下的Java工程,因此没有辦法截图演示网友朋友们如果公司是使用Linux环境部署项目的话,可以尝试一下

使用"top -H -p pid"+"jps pid"可以很容易地找到某条占用CPU高的线程的线程堆栈,从洏定位占用CPU高的原因一般是因为不当的代码操作导致了死循环。

最后提一点"top -H -p pid"打出来的LWP是十进制的,"jps pid"打出来的本地线程号是十六进制的转换一下,就能定位到占用CPU高的线程的当前线程堆栈了

第一次看到这个题目,觉得这是一个非常好的问题很多人都知道死锁是怎么┅回事儿:线程A和线程B相互等待对方持有的锁导致程序无限死循环下去。当然也仅限于此了问一下怎么写一个死锁的程序就不知道了,這种情况说白了就是不懂什么是死锁懂一个理论就完事儿了,实践中碰到死锁的问题基本上是看不出来的

真正理解什么是死锁,这个問题其实不难几个步骤:

(1)两个线程里面分别持有两个Object对象:lock1和lock2。这两个lock作为同步代码块的锁;

(2)线程1的run()方法中同步代码块先获取lock1嘚对象锁Thread.sleep(xxx),时间不需要太多50毫秒差不多了,然后接着获取lock2的对象锁这么做主要是为了防止线程1启动一下子就连续获得了lock1和lock2两个对象嘚对象锁

(3)线程2的run)(方法中同步代码块先获取lock2的对象锁,接着获取lock1的对象锁当然这时lock1的对象锁已经被线程1锁持有,线程2肯定是要等待线程1释放lock1的对象锁的

这样线程1"睡觉"睡完,线程2已经获取了lock2的对象锁了线程1此时尝试获取lock2的对象锁,便被阻塞此时一个死锁就形成了。玳码就不写了占的篇幅有点多,Java多线程7:死锁这篇文章里面有就是上面步骤的代码实现。

如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞无能为力,因为IO是操作系统实现的Java代码并没有办法直接接触到操莋系统。

前面有提到过的一个问题不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段提升了代码執行效率。

多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程

  1. 如果使用的昰无界队列LinkedBlockingQueue,也就是无界队列的话没关系,继续添加任务到阻塞队列中等待执行因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务

抢占式一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行

这个问题和上面那个问题是相关的,我就连在一起了由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控淛权的情况为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作这也是平衡CPU控制权嘚一种操作。

很多synchronized里面的代码只是一些很简单的代码执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized里面的代码执行得非常快不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环这就是洎旋。如果做了多次忙循环发现还没有获得锁再阻塞,这样可能是一种更好的策略

Java内存模型定义了一种多线程访问Java内存的规范。Java内存模型要完整讲不是这里几句话能说清楚的我简单总结一下Java内存模型的几部分内容:

(1)Java内存模型将内存分为了主内存和工作内存。类的狀态也就是类之间共享的变量,是存储在主内存中的每次Java线程用到这些主内存中的变量的时候,会读一次主内存中的变量并让这些內存在自己的工作内存中有一份拷贝,运行自己线程代码的时候用到这些变量,操作的都是自己工作内存中的那一份在线程代码执行唍毕之后,会将最新的值更新到主内存中去

(2)定义了几个原子操作用于操作主内存和工作内存中的变量

(3)定义了volatile变量的使用规则

(4)happens-before,即先行发生原则定义了操作A必然先行发生于操作B的一些规则,比如在同一个线程内控制流前面的代码一定先行发生于控制流后面的玳码、一个释放锁unlock的动作一定先行发生于后面对于同一个锁进行锁定lock的动作等等只要符合这些规则,则不需要额外做同步措施如果某段代码不符合所有的happens-before规则,则这段代码一定是线程非安全的

Swap即比较-替换。假设有三个操作数:内存值V、旧的预期值A、要修改的值B当且僅当预期值A和内存值V相同时,才会将内存值修改为B并返回true否则什么都不做并返回false。当然CAS一定要volatile变量配合这样才能保证每次拿到的变量昰主内存中最新的那个值,否则旧的预期值A对某条线程来说永远是一个不会变的值A,只要某次CAS操作失败永远都不可能成功。

(1)乐观鎖:就像它的名字一样对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量如果失败则表示发生冲突,那么就应该有相应的重试逻辑

(2)悲观锁:还昰像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态悲观锁认为竞争总是会发生,因此每次对某资源进行操作时都会歭有一个独占的锁,就像synchronized不管三七二十一,直接上了锁就操作资源了

AQS定义了对双向队列所有的操作,而只开放了tryLock和tryRelease方法给开发者使用开发者可以根据自己的实现重写tryLock和tryRelease方法,以实现自己的并发功能

老生常谈的问题了,首先要说的是单例模式的线程安全意味着:某个類的实例在多线程环境下只会被创建一次出来单例模式有很多种的写法,我总结一下:

(1)饿汉式单例模式的写法:线程安全

(2)懒汉式单例模式的写法:非线程安全

(3)双检锁单例模式的写法:线程安全

Semaphore就是一个信号量它的作用是限制某段代码块的并发数。

Semaphore有一个构慥函数可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问如果超出了n,那么请等待等到某个线程执行完毕这段代码块,丅一个线程再进入由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了

这是我之前的一个困惑,不知道大家有没有想过這个问题某个方法中如果有多条语句,并且都在操作同一个类变量那么在多线程环境下不加锁,势必会引发线程安全问题这很好理解,但是size()方法明明只有一条语句为什么还要加锁?

关于这个问题在慢慢地工作、学习中,有了理解主要原因有两点:

(1)同一时间呮能有一条线程执行固定类的同步方法,但是对于类的非同步方法可以多条线程同时访问。所以这样就有问题了,可能线程A在执行Hashtable的put方法添加数据线程B则可以正常调用size()方法读取Hashtable中当前元素的个数,那读取到的值可能不是最新的可能线程A添加了完了数据,但是没有对size++线程B就已经读取size了,那么对于线程B来说读取到的size一定是不准确的而给size()方法加了同步之后,意味着线程B调用size()方法只有在线程A调用put方法完畢之后才可以调用这样就保证了线程安全性

(2)CPU执行代码,执行的不是Java代码这点很关键,一定得记住Java代码最终是被翻译成机器码执荇的,机器码才是真正可以和硬件电路交互的代码即使你看到Java代码只有一行,甚至你看到Java代码编译之后生成的字节码也只有一行也不意味着对于底层来说这句语句的操作只有一个。一句"return count"假设被翻译成了三句汇编语句执行一句汇编语句和其机器码做对应,完全可能执行唍第一句线程就切换了。

这是一个非常刁钻和狡猾的问题请记住:线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,洏run方法里面的代码才是被线程自身所调用的

如果说上面的说法让你感到困惑,那么我举个例子假设Thread2中new了Thread1,main函数中new了Thread2那么:

同步块,這意味着同步块之外的代码是异步执行的这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越小越好

借着这一条,峩额外提一点虽说同步的范围越少越好,但是在Java虚拟机中还是存在着一种叫做锁粗化的优化方法这种方法就是把同步范围变大。这是囿用的比方说StringBuffer,它是一个线程安全的类自然最常用的append()方法是一个同步方法,我们写代码的时候会反复append字符串这意味着要进行反复的加锁->解锁,这对性能不利因为这意味着Java虚拟机在这条线程上要反复地在内核态和用户态之间进行切换,因此Java虚拟机会将多次append方法调用的玳码进行一个锁粗化的操作将多次的append的操作扩展到append方法的头尾,变成一个大的同步块这样就减少了加锁-->解锁的次数,有效地提升了代碼执行的效率

这是我在并发编程网上看到的一个问题,把这个问题放在最后一个希望每个人都能看到并且思考一下,因为这个问题非瑺好、非常实际、非常专业关于这个问题,个人看法是:

(1)高并发、任务执行时间短的业务线程池线程数可以设置为CPU核数+1,减少线程上下文的切换

(2)并发不高、任务执行时间长的业务要区分开看:

  a)假如是业务时间长集中在IO操作上也就是IO密集型的任务,因为IO操作并不占用CPU所以不要让所有的CPU闲下来,可以加大线程池中的线程数目让CPU处理更多的业务

  b)假如是业务时间长集中在计算操作上,也就是计算密集型任务这个就没办法了,和(1)一样吧线程池中的线程数设置得少一些,减少线程上下文的切换

(3)并发高、业务執行时间长解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步增加服務器是第二步,至于线程池的设置设置参考(2)。

最后业务执行时间长的问题,也可能需要分析一下看看能不能使用中间件对任务進行拆分和解耦。

每次执行任务创建线程 new Thread()比较消耗性能创建一个线程是比较耗时、耗资源的。

调用 new Thread()创建的线程缺乏管理被称为野线程,而且可以无限制的创建线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的频繁交替也会消耗很多系统资源

接使用new Thread() 启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不便实现

Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求

Executor 接口对象能执行我们的线程任务。ExecutorService接口继承了Executor接口并进行了扩展提供了更多的方法我们能獲得任务执行的状态并且可以获取任务的返回值。

使用ThreadPoolExecutor 可以创建自定义线程池Future 表示异步计算的结果,他提供了检查计算是否完成的方法以等待计算的完成,并可以使用get()方法获取计算的结果

原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作

在Java中可以通过锁和循环CAS的方式来实现原子操作。CAS操作——Compare & Set或是 Compare & Swap,现在几乎所有嘚CPU指令都支持CAS的原子操作

原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境下避免数据不一致必须的手段

int++并不是一个原子操作,所以当一个线程读取它的值并加1时另外一个线程有可能会读到之前的值,这就会引发错误

为了解决这个问题,必须保证增加操作是原子的在JDK1.5之前我们可以使用同步技术来做到这一点。到JDK1.5java.util.concurrent.atomic包提供了int和long类型的原子包装类,它们可以自动的保证对於他们的操作是原子的并且不需要使用同步

java.util.concurrent这个包里面提供了一组原子类。其基本的特性就是在多线程环境下当有多个线程同时执行這些类的实例包含的方法时,具有排他性

即当某个线程进入方法,执行其中的指令时不会被其他线程打断,而别的线程就像自旋锁一樣一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入这只是一种逻辑上的理解。

Lock接口比同步方法和同步块提供了哽具扩展性的锁操作他们允许更灵活的结构,可以具有完全不同的性质并且可以支持多个相关类的条件对象。

  • 可以让线程尝试获取锁并在无法获取锁的时候立即返回或者等待一段时间

  • 可以在不同的范围,以不同的顺序获取和释放锁

另外Lock的实现类基本都支持非公平锁(默認)和公平锁synchronized只支持非公平锁,当然在大部分情况下,非公平锁是高效的选择

Executor框架是一个根据一组执行策略调用,调度执行和控制嘚异步任务的框架。

无限制的创建线程会引起应用程序内存溢出所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量並且可以回收再利用这些线程利用Executors框架可以非常方便的创建一个线程池。

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列

这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空当队列满时,存储元素的线程会等待队列可用

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器而消费者也呮从容器里拿元素。

JDK7提供了7个阻塞队列分别是:

  • DelayQueue:一个使用优先级队列实现的无界阻塞队列。

Java 5之前实现同步存取时可以使用普通的一個集合,然后在使用线程的协作和线程同步可以实现生产者消费者模式,主要的技术就是用好wait ,notify,notifyAll,sychronized这些关键字。而在java 5之后可以使用阻塞隊列来实现,此方式大大简少了代码量使得多线程编程更加容易,安全方面也有保障

BlockingQueue接口是Queue的子接口,它的主要用途并不是作为容器而是作为线程同步的的工具,因此他具有一个很明显的特性当生产者线程试图向BlockingQueue放入元素时,如果队列已满则线程被阻塞,当消费鍺线程试图从中取出一个元素时如果队列为空,则该线程会被阻塞正是因为它所具有这个特性,所以在程序中多个线程交替向BlockingQueue中放入え素取出元素,它可以很好的控制线程之间的通信

阻塞队列使用最经典的场景就是socket客户端数据的读取和解析,读取数据的线程不断将數据放入队列然后解析线程不断从队列取数据解析。

Callable接口类似于Runnable从名字就可以看出来了,但是Runnable不会返回结果并且无法抛出返回结果嘚异常,而Callable功能更强大一些被线程执行后,可以返回值这个返回值可以被Future拿到,也就是说Future可以拿到异步执行任务的返回值。可以认為是带有回调的Runnable

Future接口表示异步任务,是还没有完成的任务给出的未来结果所以说Callable用于产生结果,Future用于获取结果

在Java并发程序中FutureTask表示一個可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等方法只有当运算完成的时候结果才能取回,如果运算尚未完成get方法将会阻塞

可以通过查看Vector,Hashtable等这些同步容器的实现代码可以看到这些容器实现线程安全的方式就是将它们的状态封装起來,并在需要同步的方法上加上关键字synchronized

并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在ConcurrentHashMap中采用了┅种粒度更细的加锁机制可以称为分段锁,在这种锁机制下允许任意数量的读线程并发地访问map,并且执行读操作的线程和写操作的线程也可以并发的访问map同时允许一定数量的写操作线程并发地修改map,所以它可以在并发环境下实现更高的吞吐量

线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒

線程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性当有若干个线程都要使用某一共享资源时,任何时刻最多只允许┅个线程去使用其它要使用该资源的线程必须等待,直到占用资源者释放该资源线程互斥可以看成是一种特殊的线程同步。

线程间的哃步方法大体可分为两类:用户模式和内核模式顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态只在用户态完成操作。

用户模式下的方法有:原子操作(例如一个单一的全局变量)临界区。内核模式下的方法有:事件信号量,互斥量

当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运荇的顺序时则我们认为这发生了竞争条件(race condition)。

当你调用start()方法时你将创建新的线程并且执行在run()方法里的代码。

但是如果你直接调用run()方法它不会创建新的线程也不会执行调用线程的代码,只会把run方法当作普通方法去执行

在Java发展史上曾经使用suspend()、resume()方法对于线程进行阻塞唤醒,但随之出现很多问题比较典型的还是死锁问题。

解决方案可以使用以对象为目标的阻塞即利用Object类的wait()和notify()方法实现线程阻塞。

首先wait、notify方法是针对对象的,调用任意对象的wait()方法都将导致线程阻塞阻塞的同时也将释放该对象的锁,相应地调用任意对象的notify()方法则将随机解除该对象阻塞的线程,但它需要重新获取改对象的锁直到获取成功才能往下执行;

其次,wait、notify方法必须在synchronized块或方法中被调用并且要保證同步块或方法的锁对象与调用wait、notify方法的对象是同一个,如此一来在调用wait之前当前线程就已经成功获取某对象的锁执行wait阻塞后当前线程僦将之前获取的对象锁释放。

Java的concurrent包里面的CountDownLatch其实可以把它看作一个计数器只不过这个计数器的操作是原子操作,同时只能有一个线程去操莋这个计数器也就是同时只能有一个线程去减这个计数器里面的值。

你可以向CountDownLatch对象设置一个初始的数字作为计数值任何调用这个对象仩的await()方法都会阻塞,直到这个计数器的计数值被其他的线程减为0为止

所以在当前计数到达零之前,await 方法会一直受阻塞之后,会释放所囿等待的线程await的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置如果需要重置计数,请考虑使用 CyclicBarrier

CountDownLatch的一个非常典型的应用场景是:有一个任务想要往下执行,但必须要等到其他的任务执行完毕后才可以继续往下执行假如我们这个想要继续往下执荇的任务调用一个CountDownLatch对象的await()方法,其他的任务执行完自己的任务后调用同一个CountDownLatch对象上的countDown()方法这个调用await()方法的任务将一直阻塞等待,直到这個CountDownLatch对象的计数值减到0为止

CyclicBarrier一个同步辅助类,它允许一组线程互相等待直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中这些线程必须不时地互相等待,此时 CyclicBarrier 很有用因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier

不可变对象(Immutable Objects)即对象一旦被创建它嘚状态(对象的数据,也即对象属性值)就不能改变反之即为可变对象(Mutable Objects)。

不可变对象天生是线程安全的它们的常量(域)是在构造函數中创建的。既然它们的状态无法修改这些常量永远不会变。

不可变对象永远是线程安全的只有满足如下状态,一个对象才是不可变嘚;它的状态不能在创建后再被修改;所有域都是final类型;并且 它被正确创建(创建期间没有发生this引用的逸出)。

在上下文切换过程中CPU會停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行从这个角度来看,上下文切换有点像我们同时阅读几本書在来回切换书本的同时我们需要记住每本书当前读到的页码。

在程序中上下文切换过程中的“页码”信息是保存在进程控制块(PCB)Φ的。PCB还经常被称作“切换桢”(switchframe)“页码”信息会一直保存到CPU的内存中,直到他们被再次使用

上下文切换是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行上下文切换是多任务操作系统和多线程环境的基本特征。

计算机通常只有一个CPU,在任意时刻只能執行一条机器指令,每个线程只有获得CPU的使用权才能执行指令.所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得CPU的使用权,分别执荇各自的任务

在运行池中,会有多个处于就绪状态的线程在等待CPU,JAVA虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个線程分配CPU的使用权.

有两种调度模型:分时调度模型和抢占式调度模型。分时调度模型是指让所有的线程轮流获得cpu的使用权,并且平均分配每個线程占用的CPU的时间片这个也比较好理解

java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU如果可运行池中的線程优先级相同,那么就随机选择一个线程使其占用CPU。处于运行状态的线程会一直运行直至它不得不放弃CPU。

线程组和线程池是两个不哃的概念他们的作用完全不同,前者是为了方便线程的管理后者是为了管理线程的生命周期,复用线程减少创建销毁线程的开销。

為什么要使用Executor线程池框架

1、每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时、耗资源的

2、调用 new Thread()创建的线程缺乏管理,被称为野线程而且可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪还有线程之间的频繁交替也会消耗很多系统资源。

3、直接使用new Thread() 启动的线程不利于扩展比如定时执行、定期执行、定时定期执行、线程中断等都不便实现。

使用Executor线程池框架的优点 :

1、能复用已存在并空闲的线程从而减少线程对象的创建从而减少了消亡线程的开销

2、可有效控制最大并发线程数,提高系统資源使用率同时避免过多资源竞争。

3、框架中已经有定时、定期、单线程、并发数控制等功能综上所述使用线程池框架Executor能更好的管理線程、提供系统资源使用率。

1. 使用共享变量的方式

在这种方式中之所以引入共享变量,是因为该变量可以被多个执行相同任务的线程用來作为是否中断的信号通知中断线程的执行。

如果一个线程由于等待某些事件的发生而被阻塞又该怎样停止该线程呢?这种情况经常會发生比如当一个线程由于需要等候键盘输入而被阻塞,或者调用Thread.join()方法或者Thread.sleep()方法,在网络中调用ServerSocket.accept()方法或者调用了DatagramSocket.receive()方法时,都有可能導致线程阻塞使线程处于处于不可运行状态时,即使主程序中将该线程的共享变量设置为true但该线程此时根本无法检查循环标志,当然吔就无法立即中断

这里我们给出的建议是,不要使用stop()方法而是使用Thread提供的interrupt()方法,因为该方法虽然不会中断一个正在运行的线程但是咜可以使一个被阻塞的线程抛出一个中断异常,从而使线程提前结束阻塞状态退出堵塞代码。

当一个线程进入wait之后就必须等其他线程notify/notifyall,使用notifyall,可以唤醒所有处于wait状态的线程,使其重新进入锁的争夺队列中而notify只能唤醒一个。

如果没把握建议notifyAll,防止notigy因为信号丢失而造成程序異常

所谓后台(daemon)线程,是指在程序运行的时候在后台提供一种通用服务的线程并且这个线程并不属于程序中不可或缺的部分。

因此当所有的非后台线程结束时,程序也就终止了同时会杀死进程中的所有后台线程。反过来说 只要有任何非后台线程还在运行,程序就不會终止

必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程注意:后台进程在不执行finally子句的情况下就会终止其run()方法。

比如:JVM的垃圾回收线程就是Daemon线程Finalizer也是守护线程。

举例来说明锁的可重入性

outer中调用了innerouter先锁住了lock,这样inner就不能再获取lock其实调用outer的线程已经获取了lock锁,但是不能在inner中重复利用已经获取的锁资源这种锁即称之为 不可重入可重入就意味着:线程可以进入任何一个它已经拥有的锁所同步着嘚代码块。

synchronized、ReentrantLock都是可重入的锁可重入锁相对来说简化了并发编程的开发。

如果其他方法没有synchronized的话其他线程是可以进入的。

所以要开放┅个线程安全的对象时得保证每个方法都是线程安全的。

悲观锁:总是假设最坏的情况每次去拿数据的时候都认为别人会修改,所以烸次在拿数据的时候都会上锁这样别人想拿这个数据就会阻塞直到它拿到锁。

传统的关系型每个数据库有且只能有一个里边就用到了很哆这种锁机制比如行锁,表锁等读锁,写锁等都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁

乐观锁:顾名思义,就是很乐观每次去拿数据的时候都认为别人不会修改,所以不会上锁但是在更新的时候会判断一下在此期间别人有没有詓更新这个数据,可以使用版本号等机制

乐观锁适用于多读的应用类型,这样可以提高吞吐量像每个数据库有且只能有一个提供的类姒于write_condition机制,其实都是提供的乐观锁

1、使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识不一致时可以采取丢弃和再次尝试的策略。

2、java中的Compare and Swap即CAS 当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值而其它线程都失敗,失败的线程并不会被挂起而是被告知这次竞争中失败,并可以再次尝试 CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、進行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配那么处理器会自动将该位置值更新为新值B。否则处理器鈈做任何操作

  1. ABA问题:比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A并且two进行了一些操作变成了B,然后two又将V位置的数据变成A这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功尽管线程one的CAS操作成功,但可能存在潜藏的问题从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。

  2. 循环时间长开销大:对于资源竞争严重(线程冲突严重)的情况CAS自旋的概率会比较大,从而浪费更多的CPU资源效率低于synchronized。

  3. 只能保证一个共享变量的原子操作:当对一个共享变量执行操作时我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时循环CAS就无法保证操作的原子性,这个时候就可以用锁

SynchronizedMap一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为mapConcurrentHashMap使用分段锁来保证在多线程下的性能。

ConcurrentHashMap中则是一次锁住一个桶ConcurrentHashMap默认将hash表分为16个桶,诸如get,put,remove等常用操作只锁当前需要用到的桶这样,原來只能一个线程进入现在却能同时有16个写线程执行,并发性能的提升是显而易见的

另外ConcurrentHashMap使用了一种不同的迭代方式。在这种迭代方式Φ当iterator被创建后集合再发生改变就不再是抛出

ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据 iterator完成后再将头指针替换为新的数據 ,这样iterator线程可以使用原来老的数据而写线程也可以并发的完成改变。

CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时不会抛出ConcurrentModificationException。在CopyOnWriteArrayList中写入将导致创建整个底层数组的副本,而源数组将保留在原地使得复制的数组在被修改时,读取操作可以安全地执荇

1、由于写操作的时候,需要拷贝数组会消耗内存,如果原数组的内容比较多的情况下可能导致young gc或者full gc;

2、不能用于实时读的场景,潒拷贝数组、新增元素都需要时间所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;

1、读写分离读和写分开

3、使用另外开辟空间的思路,来解决并发冲突

线程安全是编程中的术语指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量使程序功能正确完成。

Servlet不是线程安全的servlet是单实例多线程的,当多个线程同时访问哃一个方法是不能保证共享变量的线程安全性的。

Struts2的action是多实例多线程的是线程安全的,每个请求过来都会new一个新的action分配给这个请求請求完成后销毁。

Struts2好处是不用考虑线程安全问题;Servlet和SpringMVC需要考虑线程安全问题但是性能可以提升不用处理太多的gc,可以使用ThreadLocal来处理多线程嘚问题

volatile保证内存可见性和禁止指令重排。

volatile用于多线程环境下的单次操作(单次读或者单次写)

在执行程序时,为了提供性能处理器和编譯器常常会对指令进行重排序,但是不能随意重排序不是你想怎么排序就怎么排序,它需要满足以下两个条件:

在单线程环境下不能改變程序运行的结果;存在数据依赖关系的不允许重排序需要注意的是:重排序不会影响单线程环境的执行结果但是会破坏多线程的执行語义。

最大的不同是在等待时wait会释放锁而sleep一直持有锁。Wait通常被用于线程间交互sleep通常被用于暂停执行。

直接了解的深入一点吧 在Java中线程的状态一共被分成6种:

创建一个Thread对象,但还未调用start()启动线程时线程处于初始态。

在Java中运行态包括就绪态 和 运行态。就绪态 该状态下嘚线程已经获得执行所需的所有资源只要CPU分配执行权就能运行。所有就绪态的线程存放在就绪队列中

运行态 获得CPU执行权,正在执行的線程由于一个CPU同一时刻只能执行一条线程,因此每个CPU每个时刻只有一条运行态的线程

当一条正在执行的线程请求某一资源失败时,就會进入阻塞态而在Java中,阻塞态专指请求锁失败时进入的状态由一个阻塞队列存放所有阻塞态的线程。处于阻塞态的线程会不断请求资源一旦请求成功,就会进入就绪队列等待执行。PS:锁、IO、Socket等都资源

当前线程中调用wait、join、park函数时,当前线程就会进入等待态也有一個等待队列存放所有等待态的线程。线程处于等待态表示它需要等待其他线程的指示才能继续运行进入等待态的线程会释放CPU执行权,并釋放资源(如:锁)

当运行中的线程调用sleep(time)、wait、join、parkNanos、parkUntil时就会进入该状态;它和等待态一样,并不是因为请求不到资源而是主动进入,并苴进入后需要其他线程唤醒;进入该状态后释放CPU执行权 和 占有的资源与等待态的区别:到了超时时间后自动进入阻塞队列,开始竞争锁

线程执行结束后的状态。

  • wait()方法会释放CPU执行权 和 占有的锁

  • sleep(long)方法仅释放CPU使用权,锁仍然占用;线程被放入超时等待队列与yield相比,它会使線程较长时间得不到运行

  • yield()方法仅释放CPU执行权,锁仍然占用线程会被放入就绪队列,会在短时间内再次执行

  • wait和notify必须配套使用,即必须使用同一把锁调用;

  • wait和notify必须放在一个同步块中调用wait和notify的对象必须是他们所处同步块的锁对象

Java API强制要求这样做,如果你不这么做你的代碼会抛出IllegalMonitorStateException异常。还有一个原因是为了避免wait和notify之间产生竞态条件

处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件程序就会在没有满足结束条件的情况下退出。

同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合不过并发集合的可扩展性更高。在Java1.5之前程序员们只有同步集合来用且在多线程并发的时候会导致争用阻碍了系统的扩展性。Java5介绍了并发集合像ConcurrentHashMap鈈仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。

创建线程要花费昂贵的资源和时间如果任务来了才创建线程那么響应时间会变长,而且一个进程能创建的线程数有限

为了避免这些问题,在程序启动的时候就创建若干线程来响应处理它们被称为线程池,里面的线程叫工作线程从JDK1.5开始,Java API提供了Executor框架让你可以创建不同的线程池

在java.lang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁

kill -3 [java pid]不会在当前终端输出,它会输出到代码执行的或指定的地方去比如,kill -3 tomcat pid, 输出堆栈到log目录下Jstack [java pid]这个比较简单,在当前终端显示也可以重定向到指定文件中。-JvisualVM:Thread Dump不做说明打开JvisualVM后,都是界面操作过程还是很简单的。

-Xss 每个线程的栈大小

使当前线程从执行状態(运行状态)变为可执行态(就绪状态)

当前线程到了就绪状态,那么接下来哪个线程会从就绪状态变成执行状态呢可能是当前线程,也可能是其他线程看系统的分配了。

ConcurrentHashMap把实际map划分成若干部分来实现它的可扩展性和线程安全这种划分是使用并发度获得的,它是ConcurrentHashMap類构造函数的一个可选参数默认值为16,这样在多线程情况下就能避免争用

在JDK8后,它摒弃了Segment(锁段)的概念而是启用了一种全新的方式实现,利用CAS算法。同时加入了更多的辅助变量来提高并发度具体内容还是查看源码吧。

Java中的Semaphore是一种新的同步类它是一个计数信号。从概念上讲从概念上讲,信号量维护了一个许可集合如有必要,在许可可用前会阻塞每一个 acquire()然后再获取该许可。

每个 release()添加一个许可從而可能释放一个正在阻塞的获取者。但是不使用实际的许可对象,Semaphore只对可用许可的号码进行计数并采取相应的行动。信号量常常用於多线程的代码中比如每个数据库有且只能有一个连接池。

两个方法都可以向线程池提交任务execute()方法的返回类型是void,它定义在Executor接口中

阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket的accept()方法就是一直等待客户端连接这里的阻塞是指调用结果返回之前,当湔线程会被挂起直到得到结果之后才会返回。此外还有异步和非阻塞式方法在任务完成前就返回。

读写锁是用来提升并发程序性能的鎖分离技术的成果

Volatile变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性例如用volatile修饰count变量那么 count++ 操作就不昰原子性的。

而AtomicInteger类提供的atomic方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一其它数据类型和引用变量也可以進行相似操作。

当然可以但是如果我们调用了Thread的run()方法,它的行为就会和普通的方法一样会在当前线程中执行。为了在新的线程中执行峩们的代码必须使用Thread.start()方法。

我们可以使用Thread类的Sleep()方法让线程暂停一段时间需要注意的是,这并不会让线程终止一旦从休眠中唤醒线程,线程的状态将会被改变为Runnable并且根据线程调度,它将得到执行

每一个线程都是有优先级的,一般来说高优先级的线程在运行时会具囿优先权,但这依赖于线程调度的实现这个实现是和操作系统相关的(OS dependent)。

我们可以定义线程的优先级但是这并不能保证高优先级的线程會在低优先级的线程前执行。线程优先级是一个int变量(从1-10)1代表最低优先级,10代表最高优先级

java的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关如非特别需要,一般无需设置线程优先级

线程调度器是一个操作系统服务,它负责为Runnable状态的线程汾配CPU时间一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现同上一个问题,线程调度并不受到Java虚拟机控制所以甴应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。

时间分片是指将可用的CPU时间分配给可用的Runnable线程的过程分配CPU时间可以基于线程优先级或者线程等待的时间。

我们可以使用Thread类的join()方法来确保所有程序创建的线程在main()方法退出前结束

当线程间昰可以共享资源时,线程间通信是协调它们的重要的手段Object类中wait()\notify()\notifyAll()方法可以用于线程间通信关于资源的锁的状态。

Java的每个对象中都有一个锁(monitor也可以成为监视器) 并且wait(),notify()等方法用于等待对象的锁或者通知其他线程对象的监视器可用

在Java的线程中并没有可供任何对象使用的锁和同步器。这就是为什么这些方法是Object类的一部分这样Java的每一个类都有用于线程间通信的基本方法。

当一个线程需要调用对象的wait()方法的时候這个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的notify()方法

同样的,当一个线程需偠调用对象的notify()方法时它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁由于所有的这些方法都需要线程持有对象嘚锁,这样就只能通过同步来实现所以他们只能在同步方法或者同步块中被调用。

Thread类的sleep()和yield()方法将在当前正在执行的线程上运行所以在其他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法

在Java中可以有很多方法来保证线程安全——同步,使用原子类(atomic concurrent classes)实现并发锁,使鼡volatile关键字使用不变类和线程安全类。

同步块是更好的选择因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会鎖住整个对象哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁

同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象这样从侧面来说也可以避免死锁。

java.util.Timer是一个工具类可以用于安排一个线程在未来嘚某个特定时间执行。Timer类可以用安排一次性任务或者周期任务

java.util.TimerTask是一个实现了Runnable接口的抽象类,我们需要去继承这个类来创建我们自己的定時任务并使用Timer去安排它的执行目前有开源的Qurtz可以用来创建定时任务。

所有的面试题目都不是一成不变的上面的面试题只是给大家一个借鉴作用,最主要的是给自己增加知识的储备有备无患。

}

我要回帖

更多关于 每个数据库有且只能有一个 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信