1实现多线程的方式有几种? 其实这个问题并不难,只是在这里做一个总结。一共有三种。
大家可能对前两种已经很清楚了,重点说下第三种。 Callable接口是属于Executor框架中的类,Callable接口与Runnable接口类似,但比后者功能更加强大,主要有三点:
举个例子,此代码在JDK 8 下运行,因为使用了lambda表达式: package exam; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class CallableAndFuture { public static void main(String[] args) { ExecutorService threadPool = Executors.newSingleThreadExecutor(); // 启动线程 Future try { System.out.println('waiting thread to finish.'); System.out.println(future.get()); // 等待线程结束,并获取返回结果 threadPool.shutdown(); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } } } 2volatile关键字的作用 在Java语言中,有时候为了提高程序的运行效率,编译器会做一些优化操作,把经常被访问的变量缓存起来,程序在读取这个变量的时候又有可能直接从寄存器中读取这个值,而不会去内存中读取。这样的好处提高了程序的运行效率,但当遇到多线程编程时,变量的值可能被其他线程改变了,而该缓存的值不会做相应的改变,从而导致应用程序读取的值可能与实际的变量值不一致。关键字volatile正好解决这个问题,被volatile修饰的变量编译器不会做优化,每次都会从内存读取。 3代码中不同属性和方法的执行顺序 经常会遇到一个这样的代码,new一个子类,其子类以及父类每个属性和方法的执行顺序,具体可以看以下例子: ** * Java程序初始化工作可以在许多不同的代码中来完成,它们执行的顺序如下: * 父类静态变量 * 父类静态代码块 * 子类静态变量 * 子类静态代码块 * 父类非静态变量 * 父类非静态代码块 * 父类构造函数 * 子类非静态变量 * 子类非静态代码块 * 子类构造函数 * * * 注意,只有方法具有多态性,属性则没有。 * @author TurtusLi * */ class BaseI { int num = 1; public BaseI() { this.print(); num = 2; } public void print() { System.out.println('Base.num = ' + num); } } public class Example1423 extends BaseI { int num = 3; public Example1423() { this.print(); num = 4; } // 去掉这个复写方法,运行看效果 @Override public void print() { System.out.println('Sub.num = ' + num); } public static void main(String[] args) { BaseI b = new Example1423(); System.out.println(b.num); } } 4switch语句支持String类型的实现原理 在Java 7 以后,switch语句可以用作String类型上。 从本质来讲,switch对字符串的支持,其实也是int类型值的匹配。它的实现原理如下: 通过对case后面的String对象调用hashCode()方法,得到一个int类型的Hash值,然后用这个Hash值来唯一标识着这个case。 那么当匹配的时候,首先调用这个字符串的hashCode()方法,获取一个Hash值(int类型),用这个Hash值来匹配所有的case,如果没有匹配成功,说明不存在;如果匹配成功了,接着会调用字符串的equals()方法进行匹配。 由此看出,String变量不能是null;同时,switch的case子句中使用的字符串也不能为null。 5多线程同步有几种实现方法 Java主要提供了三种实现同步机制的方法。 synchronized关键字。有两种用法,可以是synchronized方法和synchronized代码块。 wait和notify方法。 Lock。Lock接口有一个实现类ReentrantLock,也可以实现多线程的同步。 6在多线程编程的时候有哪些注意事项 如果能用volatile代替synchronized,近可能使用volatile。因为被synchronized修饰的方法或代码块在同一时间只能允许一个线程访问,而volatile没有这个限制,因此使用synchronized会降低并发量。由于volatile无法保证原子性操作,因此在多线程的情况下,只有对变量的操作为原子操作的情况下才可以使用volatile。 尽可能减少synchronized块内的代码。 给每一个线程定义一个名字,这样有利于调试。 尽量使用concurrent容器(ConcurrentHashMap)来代替synchronized容器(Hashtable)。 使用线程池来控制多线程的执行。 7 fail-fast和fail-safe迭代器的区别是什么? 他们的主要区别是fail-safe允许在遍历的过程中对容器的数据进行修改,而fail-fast则不允许。下面分别介绍这两种迭代器的工作原理。 fail-fast:直接在容器上进行遍历,在遍历的过程中,一旦发现容器中的数据被修改了(添加元素、删除或修改元素),就会抛出ConcurrentModificationException异常导致遍历失败。常见的使用fail-fast的容器有HashMap和ArrayList等。 fail-safe:这种遍历是基于容器的克隆。因此,对容器中内容的修改不影响遍历。常见使用fail-safe方式的容器有ConcurrentHashMap和CopyOnWriteArrayList。 8如何能够使JVM中的虚拟机栈、堆内存和方法区发生内存溢出? 关于JVM的知识,有一本非常好的书籍——周志明《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》,里面有非常好的介绍。几乎可以说是Java程序员必读书籍。 虚拟机栈是线程私有的,当创建一个线程时,同时会新建一个虚拟机栈,它描述的是Java方法执行的内存模型。 栈中有一个非常重要的概念——栈帧。栈帧用于保存局部变量表,操作数栈,方法出口等。 其实栈溢出最简单的方式是无限递归。 堆内存是线程共享的,是JVM中内存管理的最大一块内存,它保存所有实例化的对象。 堆内存溢出最简单的方式是不停的new对象,GC来不及回收,直到内存全部耗尽。 方法区也是内存共享的。它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 方法区溢出简单的方式是,调用String类的intern()方法,此方法如果在堆区找不到已经存在的String对象的话,就会往方法区中的常量池放一份,然后返回其引用放在堆区。还有一种办法是不停地加载类。 9在int i =0; i=i++;语句中,i=i++是线程安全的吗?如果不安全,请说明上面操作在JVM中的执行过程,为什么不安全?说出JDK中哪个类能达到以上程序的效果,并且是线程安全且高效的,简述其原理。 语句i=i++的执行过程:先把i的值取出来放到栈顶,可以理解为引入了第三方变量k,此时,k的值为i,然后执行自增操作,于是i的值变为1,最后执行赋值操作i=k(自增前的值),因此,执行结束后,i的值还是0。从上面的分析得知,i=i++语句的执行过程是由多个操作组成,它不是原子操作,因此,不是线程安全的。 想要更多的企业求职加分项目案例,关注Java学习交流群285154486,每天会讲解分享项目,答疑解 |
|
来自: 2017helloworld > 《学术性》