遇到面试题总结

本文深入解析Java中的关键概念,包括volatile关键字的线程安全性、MANIFEST.MF文件的应用、Lock与synchronized的区别、多线程实现方式、native关键字的含义、stack/deque/queue的区别、双向链表的实现、内存泄漏的类型与处理,以及单例模式的优化。

1、描述Java 关键字volatile?

  用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。

public class VolatileTest extends Thread {
    
    volatile boolean flag = false;
    int i = 0;
    
    public void run() {
        while (!flag) {
            i++;
        }
    }
    
    public static void main(String[] args) throws Exception {
        VolatileTest vt = new VolatileTest();
        vt.start();
        Thread.sleep(2000);
        vt.flag = true;
        System.out.println("stope" + vt.i);
    }
}

在flag前面加上volatile关键字,强制线程每次读取该值的时候都去“主内存”中取值

2、Java MANIFEST.MF文件的用途

可以用的上的有:

    1. Main-Class 指定程序的入口,这样可以直接用java -jar xxx.jar来运行程序。

    2. Class-Path 指定jar包的依赖关系,class loader会依据这个路径来搜索class。

我平时用mf文件一般说来都是让jar成为可以直接运行这一目的。虽然目的这么简单,但是每次打包还是很容易出现各种各样的问题让人头痛。mf总是没有被应用,或者是出错了。

首先给出一个简单而标准的例子:

Manifest-Version: 1.0
Main-Class: ui.JFrameMain
Class-Path: jtds-1.2.jar c3p0-0.9.1.2.jar 
esri_mo20.jar esri_mo20res.jar jsde83_sdk.jar

解释一下:

Manifest-Version mf文件版本号,这行照抄

Main-Class 包含main函数的类,程序的入口

Class-Path 用到的一些包吧 

3、Lock和synchronized的区别和使用

 1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;

  2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

4、JAVA多线程实现的四种方式

     1、继承Thread类创建线程

     2、实现Runnable接口创建线程

     3、实现Callable接口通过FutureTask包装器来创建Thread线程

    4、使用ExecutorService、Callable、Future实现有返回结果的线程

5、Java native关键字

 一. 什么是Native Method
   简单地讲,一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。这个特征并非java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern "C"告知C++编译器去调用一个C的函数。
   "A native method is a java method whose implementation is provided by non-java code."
   在定义一个native method时,并不提供实现体(有些像定义一个java interface),因为其实现体是由非java语言在外面实现的。

 

6、stack,deque,queue对比

1.stack堆栈,没有迭代器,支持push()方法。后进先出,top()返回最顶端的元素,pop()剔除最顶元素,后进先出(LIFO);

2.deque双端队列,支持迭代器,有push_back()方法,跟vector差不多,比vector多了个pop_front,push_front方法;

3.queue队列,先进先出,不支持迭代器。队列通常(但并非一定)以 FIFO(先进先出)的方式排序各个元素。Queue使用时要尽量避免Collection的add()和remove()方法而是要使用offer()来加入元素,使用poll()来获取并移出元素。它们的优点是通过返回值可以判断成功与否,add()和remove()方法在失败的时候会抛出异常。 如果要使用前端而不移出该元素,使用element()或者peek()方法。

7、双向链表的java实现

package structure;

import java.util.Arrays;
import java.util.Scanner;
import static net.mindview.util.Print.*;

/**
 * 双向链表的操作
 * @author Tom Tao
 *
 */
class Node {
    public int value;
    public Node(int n) {
        this.value = n;
    }
    Node pre;
    Node next;
}

public class ReLinkedList {
    public static Node head;
    public static int nodeNum;
    
    /*初始化*/
    public static void init(int n[]) {
        head = new Node(n[0]);
        Node temp = null;
        head.pre = null;
        Node p = head;
        nodeNum ++;
        for(int i = 1 ; n[i] != -1 ; i ++) {
            nodeNum ++;
            temp = new Node(n[i]);
            p.next = temp;
            temp.pre = p;
            p = temp;
        }
        p.next = null;
    }
    
    /*打印*/
    public static void printList(Node h) {
        while(h != null) {
            printnb(h.value + " ");
            h = h.next;
        }
    }
    
    /*插入*/
    public static void insert(int index, int v) {
        Node ne = new Node(v);
        if(index == 0) {
            ne.next = head;
            ne.pre = null;
            head = ne;
            return;
        }
        Node temp = head;
        Node t = temp;
        for(int i = 0 ; i < index ; i ++) {
            t = temp;
            temp = temp.next;
        }
        if(index == nodeNum) {
            t.next = ne;
            ne.pre = t;
            ne.next = null;
            return;
        }
        ne.next = temp;
        ne.pre = t;
        temp.pre = ne;
        t.next = ne;
    }
    
    /*删除*/
    public static void delete(int index) {
        if(index == 0){
            head = head.next;
            head.pre = null;
            return;
        }
        Node temp = head;
        Node t = temp;
        for(int i = 0 ; i < index ; i ++) {
            t = temp;
            temp = temp.next;
        }
        if(index == nodeNum) {
            t.next = null;
            return;
        }
        t.next = temp.next;
        temp.next.pre = t;
    }
    
    /*查找*/
    public static int find(int index) {
        Node temp = head;
        for(int i = 0 ; i < index ; i ++) {
            temp = temp.next;
        }
        return temp.value;
    }
    
    /*test*/
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        print("please input the nums to init:");
        int[] n = new int[100];
        Arrays.fill(n, -1);
        int val;
        int count = 0;
        while((val = sc.nextInt()) >= 0) {
            n[count++] = val;
        }
        init(n);
        while(true){
            printList(head);
            print("\nplease input the index and num to insert");
            int index = sc.nextInt();
            val = sc.nextInt();
            insert(index, val);
            printList(head);
            print("\nplease input the index to delete");
            index = sc.nextInt();
            delete(index);
            printList(head);
            print("\nplease input the index to find");
            index = sc.nextInt();
            print(find(index));
            print("----------------------------------------------------");
        }
    }
}

8、java内存泄漏与处理

内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;

内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

memory leak会最终会导致out of memory!

以发生的方式来分类,内存泄漏可以分为4类:

  1. 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
  2. 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
  3. 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
  4. 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。

从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到

一、Java内存回收机制
不论哪种语言的内存分配方式,都需要返回所分配内存的真实地址,也就是返回一个指针到内存块的首地址。Java中对象是采用new或者反射的方法创建的,这些对象的创建都是在堆(Heap)中分配的,所有对象的回收都是由Java虚拟机通过垃圾回收机制完成的。GC为了能够正确释放对象,会监控每个对象的运行状况,对他们的申请、引用、被引用、赋值等状况进行监控,Java会使用有向图的方法进行管理内存,实时监控对象是否可以达到,如果不可到达,则就将其回收,

二、Java内存泄露引起原因
内存泄露是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成的内存空间的浪费称为内存泄露。内存泄露有时不严重且不易察觉,这样开发者就不知道存在内存泄露,但有时也会很严重,会提示你Out of memory。

那么,Java内存泄露根本原因是什么呢?长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。具体主要有如下几大类:
1、静态集合类引起内存泄露:
像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。

Static Vector v = new Vector(10);
for (int i = 1; i<100; i++)
{
Object o = new Object();
v.add(o);
o = null;
}//
复制代码
在这个例子中,循环申请Object 对象,并将所申请的对象放入一个Vector 中,如果仅仅释放引用本身(o=null),那么Vector 仍然引用该对象,所以这个对象对GC 来说是不可回收的。因此,如果对象加入到Vector 后,还必须从Vector 中删除,最简单的方法就是将Vector对象设置为null。----在循环外面添加v=null,这样就可以将vector里面的内存进行回收了。

案例2
public static void main(String[] args) {
if (true) {
ArrayList

在局部变量的作用域之外,al这个引用变成了空指针,但是new ArrayList< String >()这个对象仍可能存在,它的实际地址值也就是这个对象在内存中的地址还是保存在栈里面的,gc无法把它给回收

public static void main(String[] args) {
if (true) {
ArrayList

9、单列

考虑性能问题。synchronized关键字修饰getInstance()方法,会导致所有调用getInstance()方法的线程都要竞争同一把锁,即使在单例对象已经生成好的情况下。这里使用double check的方式。另外,还需要注意instance在初始化过程中,可能已经不为NULL,这样有可能会导致程序崩溃,因此这里加了个临时变量t,确保初始化完成之后再赋值给instance。

public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null){
Singleton t = new Singleton();
instance = t;
}
}
}
return instance;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值