Java基础与面试(一)

1.Java 中 sleep 方法和 wait 方法的区别?

虽然这两个方法都将线程置于等待(waiting)状态,但是他们在行为上大不相同
Thread.sleep(long mills)方法会引入暂停,当调用该方法时,它不做任何事情,并在指定的时间内放弃CPU。当一个线程进入睡眠状态,它可以在睡眠时间过后正常醒来,也可以通过中断来不正常的唤醒它
wait()方法则是用于在线程间通信的。与其相辅相成的是notify()或者notifyAll()方法。通过使用wait()和notify()方法,两个线程可以相互通信,这是解决许多并发性问题的关键,例如:产生消费者问题、进餐哲学家问题、读者和作者问题,以及实现多并发设计。
Java中的所有对象都有一个隐式的锁,该锁也被称为监视器。当我们的线程进入到一个同步方法(即以synchronized 标识),他就会获取锁,以保护临界区(这是一个概念,如果有兴趣的,请查询临界区问题)。举例来说,当我们进入一个同步的实例方法中时,那么线程将会获取当前对象的锁;而当线程进入一个同步的静态方法时,它会获取该类的Class实例的锁。
下面详细讲解二者的不同:
(1)首先,也是最重要的区别是,wait方法必须在同步(synchronized )的上下文中调用,即从同步的方法或Java块中调用。如果调用的方法没有做同步(synchronized ),它将抛出IllegalMonitorStateException。另一方面,调用sleep方法不需要同步(synchronized ),您可以正常调用它
(2)第二个值得注意的是,wait和sleep方法之间的区别是,wait操作在Object类上,而sleep操作定义在 java.lang.Thread
(3)第三个区别是:wait()方法释放它所调用的对象的锁,而sleep只是释放了CPU的占有,并未释放掉锁
(4)wait方法需要在循环中调用,以处理错误的通知,即只有在等待条件正确的情况下才醒来,而对于sleep没有这样的限制。
下面是在Java中调用wait和sleep方法的代码片段

synchronized(monitor)
while(condition == true){ monitor.wait())  //releases monitor lock

Thread.sleep(100); //puts current thread on Sleep

(5)wait和sleep方法之间的另一个不同之处是wait()是一个非静态方法,而sleep()是Java中的一个静态方法

wait()方法应该与notify()或notifyAll()方法一起使用,并打算用于Java中的两个线程之间的通信,而Thread .sleep()方法是在程序或线程执行期间引入短暂停的实用方法。考虑到等待的同步要求,它不应该仅仅用于在Java中引入暂停或休眠。

2. Thread和Runnable

Java中的线程是一个独立的执行路径,用于并行运行两个任务。当两个线程并行运行时,Java中称为多线程.
简而言之,Java的线程可以这样描述“Thread 是Java中的一个类,但也是一种在Java环境中独立执行的方式”。Java中的线程需要一个由这个线程独立执行的任务,这个任务可以是Runnable ,也可以是Callable的。Java中线程和Runnable之间的差异也是Java中一个流行的线程访问问题。

Runnable表示Java中由Thread执行的任务。java.lang.Runnable是一个接口,只有一个方法:run()。当使用Thread.start()来启动线程时,它会去调用在Thread创建时传递进构造器的Runnable对象的run()方法。在run()方法中编写的代码由这个新创建的线程执行。自从start()方法内部调用run()方法以来,Java程序员就一直怀疑为什么不直接调用run()方法。
当您直接调用Runnable接口run()方法时,将不会创建新的线程,并且在run()方法中定义的任务是通过调用线程来执行的。在Java 1.5中添加了另一个接口:Callable,可用于代替Java中Runnable接口。
Callable接口提供了额外的功能,它可以返回计算的结果。

3. 什么是线程局部变量

线程局部变量是只可在线程内部访问的变量,属于线程自身所有,不在多个线程间共享。Java 提供 ThreadLocal 类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。

4.用 Java 写一个线程安全的单例模式(Singleton)

线程安全的单例是指一个Singleton类,它返回完全相同的实例,即使暴露在多个线程中。Java中的Singleton是一种经典的设计模式,就像工厂方法模式或装饰设计模式一样,甚至在JDK中也使用了很多,比如java.lang.Runtime是一个Singleton的示例。单例模式确保了在任何时候,该类只有一个实例留在Java程序中。
在Java 5之前那,使用双重检查锁定机制,在Java中创建线程安全的单例模式,如果一个线程没有同时看到其他线程创建的实例,那么最终您将得到多个单例类实例。从Java 5开始,volatile变量保证可以使用双重检查锁定模式来编写线程安全的单例。我个人不喜欢这种方式,因为有许多其他更简单的替代方法来编写线程安全的singleton,比如使用静态字段初始化singleton实例,或者在Java中使用Enum作为singleton。让我们来看看在Java中创建线程安全的单例模式的两种方法。
1.使用Enum实现线程安全的单例模式
使用Enum创建单例是在Java中创建线程安全Singleton的最简单和有效的方法,因为线程安全是由Java编程语言本身提供保证的。你不需要为线程安全问题操心。由于枚举实例在Java中是默认的,所以它也为序列化提供了安全性。
值得记住的一点是,当我们讨论线程安全的Singleton时,我们讨论的是指Singleton 类的实例创建时的线程安全,而并不是表示我们任何对于单例对象的方法的调用也是线程安全的。如果单例类维护任何状态,并包含修改该状态的方法,则需要编写代码以避免和线程安全、同步的问题。这里的任何方法都是在Java中使用Enum创建线程安全单例的代码示例。

 public enum Singleton{
    INSTANCE;

    public void show(){
        System.out.println("Singleton using Enum in Java");
    }
}

//使用Singleton.INSTANCE来访问到这个单例对象
Singleton.INSTANCE.show();

如果这符合您的需要,那么这是在Java中编写线程安全的单例的最简单方法。使用Enum作为单例也提供了更多的好处,您可以从中发现为什么枚举单例在Java中更好。

2.使用静态字段初始化的线程安全单例。
你可以通过在在类加载期间创建单例实例,来实现线程安全的Singleton。静态字段在类装入时被初始化,类加载器将保证实例在其完全创建之前是不可见的。下面是在Java中使用静态工厂方法创建线程安全单例的示例。使用静态字段实现单例模式的唯一缺点是,这不是一个惰性的初始化,并且在任何客户端调用getInstance()方法之前,Singleton都是已经初始化了的。

public class Singleton{
    private static final Singleton INSTANCE = new Singleton();

    private Singleton(){ }

    public static Singleton getInstance(){
        return INSTANCE;
    }
    public void show(){
        System.out.println("Singleon using static initialization in Java");
    }
}
//Here is how to access this Singleton class
Singleton.getInstance().show();

这里我们不是在getInstance()方法中创建单例实例,而是由类加载器创建。另外,私有构造函数不可能创建另一个实例。当然也有例外,您仍然可以通过反射访问私有构造函数,并调用setAccess (true)。顺便说一下,通过这种方式,您仍然可以通过从构造函数中抛出异常来防止创建另一个Singleton实例。
3. 双重检查锁定机制

public class SingleTon {
    private volatile static SingleTon singleTon = null;
    private SingleTon() {}
    public static SingleTon getInstance() {
        if( singleTon == null ) {
            synchronized( SingleTon.class ) {
                if( singleTon == null ) {
                    singleTon = new SingleTon();
                }
            }

        }
        return singleTon;
    }

}

请注意变量以volatile修饰,该关键字可以保证单利模式的线程安全。
需要提示的是,这属于懒汉式单例模式
4.登记式单例模式

public class RegSingleton {
     /**
      * 登记薄,用来存放所有登记的实例
      */
     private static Map m_registry = new HashMap();
     //在类加载的时候添加一个实例到登记薄
     static {
         RegSingleton x = new RegSingleton();
         m_registry.put(x.getClass().getName(), x);
     }

     /**
      * 受保护的默认构造方法
      */
     protected RegSingleton() {}

     /**
      * 静态工厂方法,返回指定登记对象的唯一实例;
      * 对于已登记的直接取出返回,对于还未登记的,先登记,然后取出返回
      * @param name
      * @return RegSingleton
      */
     public static RegSingleton getInstance(String name) {
         if (name == null) {
             name = "RegSingleton";
         }
         if (m_registry.get(name) == null) {
             try {
                 m_registry.put(name, (RegSingleton) Class.forName(name).newInstance());
             } catch (InstantiationException e) {
                 e.printStackTrace();
             } catch (IllegalAccessException e) {
                 e.printStackTrace();
             } catch (ClassNotFoundException e) {
                 e.printStackTrace();
             }
         }
         return m_registry.get(name);
     }

     /**
      * 一个示意性的商业方法
      * @return String
      */
     public String about() {
         return "Hello,I am RegSingleton!";
     }
}

5.生产者-消费者模式

生产者消费者模式的优点:
它确实是一种有用的设计模式,在编写多线程或并发代码时最常用。
1)生产者-消费者模式简单开发。您可以独立地、并发地对生产者和消费者进行编码,他们只需要知道共享对象。
2)生产者不需要知道谁是消费者,也不需要知道有多少消费者。消费者也是如此。 3)生产者和消费者可以以不同的速度工作。不存在消费者消费半成品的风险。事实上,通过监测消费者的速度,可以引入更多的消费者来更好地利用。
4)分离生产者和消费者的功能会导致更干净、更易读和易于管理的代码。
多线程的生产者消费者问题:
生产者-消费者问题也是一个流行的java面试问题,在这个问题中,面试官要求设计生产者–消费者模式,以便生产者在队列或桶是满的情况下等待,如果队列或桶是空的,则消费者应该等待。
这个问题可以用Java的不同方式来实现或解决,经典的方法是使用wait和notify方法,在生产者和消费者线程之间进行通信,并在满队列和空队列等单个条件下阻塞每个线程。由于在Java 5中引入了BlockingQueue数据结构,它现在变得更简单了,因为BlockingQueue通过引入阻塞方法put() 和take(),隐式地提供了这种控制。
现在您不需要使用wait和notify来在生产者和消费者之间进行通信。如果队列是满的,BlockingQueue的put()方法将阻塞,如果队列是空的,则take()将阻塞。

使用BlockingQueue实现生产者消费者模式:

BlockingQuue是一个接口,Java 5提供了不同的实现,如ArrayBlockingQueue和LinkedBlockingQueue,都实现了FIFO原理,而ArrayLinkedQueue在自然LinkedBlockingQueue中是有限制的。这里是一个完整的代码示例,它包含有BlockingQueue的生产者消费者模式。与经典的等待通知代码相比,它更简单易懂。

//生产者
import java.util.concurrent.BlockingQueue;

public class Producer  implements Runnable{

    private final BlockingQueue sharedQueue;

    public Producer(BlockingQueue sharedQueue) {
        this.sharedQueue = sharedQueue;
    }

    @Override
    public void run() {
        for(int i=0; i<10; i++){
            try {
                System.out.println("Produced: " + i);
                sharedQueue.put(i);
            } catch (InterruptedException ex) {

            }
        }
    }

//消费者
public class Consumer  implements Runnable{
    private final BlockingQueue sharedQueue;

    public Consumer (BlockingQueue sharedQueue) {
        this.sharedQueue = sharedQueue;
    }

    @Override
    public void run() {
        while(true){
            try {
                System.out.println("Consumed: "+ sharedQueue.take());
            } catch (InterruptedException ex) {
            }
        }
    }


}
//Tets
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class ProducerConsumerPattern {

    public static void main(String args[]) {

        //创建共享对象
        BlockingQueue sharedQueue = new LinkedBlockingQueue();

        //创建Producer 和 Consumer 线程对象
        Thread prodThread = new Thread(new Producer(sharedQueue));
        Thread consThread = new Thread(new Consumer(sharedQueue));

        //启动 producer 和 Consumer 线程
        prodThread.start();
        consThread.start();
    }


输出

Produced: 0
Produced: 1
Produced: 2
Produced: 3
Consumed: 0
Produced: 4
Consumed: 1
Produced: 5
Produced: 6
Consumed: 2
Produced: 7
Consumed: 3
Produced: 8
Produced: 9
Consumed: 4
Consumed: 5
Consumed: 6
Consumed: 7
Consumed: 8
Consumed: 9

使用wait()和notify()实现生产者-消费者

生产者
import java.util.Vector;

public class Producer implements Runnable{

    private final Vector sharedQueue;
    private final int SIZE;

    public Producer(Vector sharedQueue, int size) {
        this.sharedQueue = sharedQueue;
        this.SIZE = size;
    }

    @Override
    public void run() {
        for (int i = 0; i < 7; i++) {
            System.out.println("Produced: " + i);
            try {
                produce(i);
            } catch (InterruptedException ex) {
            }

        }
    }

    private void produce(int i) throws InterruptedException {

        //wait if queue is full
        while (sharedQueue.size() == SIZE) {
            synchronized (sharedQueue) {
                System.out.println("Queue is full " + Thread.currentThread().getName()
                        + " is waiting , size: " + sharedQueue.size());

                sharedQueue.wait();
            }
        }

        //producing element and notify consumers
        synchronized (sharedQueue) {
            sharedQueue.add(i);
            sharedQueue.notifyAll();
        }
    }
//消费者
import java.util.Vector;

public class Consumer implements Runnable{

    private final Vector sharedQueue;
    private final int SIZE;

    public Consumer(Vector sharedQueue, int size) {
        this.sharedQueue = sharedQueue;
        this.SIZE = size;
    }

    @Override
    public void run() {
        while (true) {
            try {
                System.out.println("Consumed: " + consume());
                Thread.sleep(50);
            } catch (InterruptedException ex) {
            }

        }
    }

    private int consume() throws InterruptedException {
        while (sharedQueue.isEmpty()) {
            synchronized (sharedQueue) {
                System.out.println("Queue is empty " + Thread.currentThread().getName()
                        + " is waiting , size: " + sharedQueue.size());

                sharedQueue.wait();
            }
        }

        //Otherwise consume element and notify waiting producer
        synchronized (sharedQueue) {
            sharedQueue.notifyAll();
            return (Integer) sharedQueue.remove(0);
        }
    }

//Test
public class ProducerConsumerSolution {

    public static void main(String args[]) {
        Vector sharedQueue = new Vector();
        int size = 4;
        Thread prodThread = new Thread(new Producer(sharedQueue, size), "Producer");
        Thread consThread = new Thread(new Consumer(sharedQueue, size), "Consumer");
        prodThread.start();
        consThread.start();
    }

输出

Produced: 0
Produced: 1
Produced: 2
Produced: 3
Produced: 4
Queue is full Producer is waiting , size: 4
Consumed: 0
Produced: 5
Queue is full Producer is waiting , size: 4
Produced: 6
Queue is full Producer is waiting , size: 4
Consumed: 1
Consumed: 2
Consumed: 3
Consumed: 4
Consumed: 5
Consumed: 6
Queue is empty Consumer is waiting , size: 0

Java 中应该使用什么数据类型来代表价格

如果不是特别关心内存和性能的话,使用BigDecimal,否则使用预定义精度的 double 类型。

怎么将 byte 转换为 String?

1)使用JDK的API:
String str = new String(bytes, “UTF-8”);
需要注意的点是要使用的正确的编码,否则会使用平台默认编码,这个编码可能跟原来的编码相同,也可能不同。
2)如果您从任何文本文件(例如XML文档、HTML文件或二进制文件)读取字节数组,您可以使用Apache Commons IO库直接将FileInputStream转换为字符串。该方法还可以在内部缓冲输入,因此不需要使用另一个BufferedInputStream来包装
String fromStream = IOUtils.toString(fileInputStream, “UTF-8”);
为了正确地将这些字节数组转换成字符串,您必须首先通过读取元数据来发现正确的字符编码,例如Content-Type,

我们能将 int 强制转换为 byte 类型的变量吗?如果该值大于 byte 类型的范围,将会出现什么现象?

是的,我们可以做强制转换,但是 Java 中 int 是 32 位的,而 byte 是 8 位的,所以,如果强制转化是,int 类型的高 24 位将会被丢弃,byte 类型的范围是从 -128 到 128。

        int source = 444;
        //444 = 110111100

        byte target = (byte) source;
        //target = 截取8位   10111100(高位是符号位)  取反 11000011  +1   1000100  即得到-68

        System.out.println("-->" + target);

哪个类包含 clone 方法?是 Cloneable 还是 Object?

java.lang.Cloneable 是一个标示性接口,不包含任何方法,clone 方法在 object 类中定义。并且需要知道 clone() 方法是一个本地方法,这意味着它是由 c 或 c++ 或 其他本地语言实现的。

Java 中 ++ 操作符是线程安全的吗?

不是线程安全的操作。它涉及到多个指令,如读取变量值,增加,然后存储回内存,这个过程可能会出现多个线程交差。

+= 和 = 区别

int a = 10;
byte b = 2;
b = (byte)(b + a)
b += a

通过代码即可理解,对于+操作符,如果两这个整型相加,如 byte、short 或者 int,首先会将它们提升到 int 类型,然后在执行加法操作。
而对于+=操作符,会隐式的将结果转换为持有者的类型
我们看到+操作符做了强制转换,如果不转换会报错的。

3 * 0.1 == 0.3?

flase,因为有些浮点数不能完全精确的表示出来。

64 位 JVM 中,int 的长度是多数?

Java的数据类型不会根据平台的不同而改变长度,都是 32 位

Serial 与 Parallel GC之间的不同之处?

垃圾收集的工作方法是采用一些GC算法,例如:标记和扫描。Java中有不同类型的垃圾回收器可以收集堆内存的不同区域,比如Java中有串行、并行和并发的垃圾收集器。
JDK 1.7中还引入了一个名为G1(Garbage first)的新收集器。了解GC的第一步是了解对象何时成为垃圾收集的合格对象?由于JVM提供内存管理,Java开发人员只关心创建对象,他们不关心清理,这是由垃圾收集器完成的,但是它只能收集没有强引用的对象,或者没有任何线程访问的对象。
如果一个对象,它应该被收集,但由于无意的强引用而仍然存在于内存中,那么它就被称为Java的内存泄漏。Java web应用程序中的ThreadLocal变量很容易导致内存泄漏。

Java中垃圾回收的关注点

1)对象是在Java堆中创建的,不管它们的范围是什么,例如本地变量或成员变量。值得注意的是,类变量或静态成员是在Java内存空间的方法区中创建,而堆和方法区可以在不同的线程之间共享。
2)垃圾收集是由Java虚拟机提供的一种机制,可以从符合垃圾回收资格的对象中回收堆空间
3)垃圾回收从内存管理中解放了Java程序员,而这是c++编程的一个重要部分,并提供了更多的时间来关注业务逻辑。
4)Java中的垃圾回收是由一个称为垃圾收集器的守护线程执行的
5)在将对象从内存中删除之前,垃圾收集线程调用该对象的finalize()方法,提供了执行任何所需要的清理的机会
6)您作为Java程序员不能强迫Java进行垃圾收集;只有基于堆空间的情况,JVM认为它需要一个垃圾收集。
7)有一些方法,比如System.gc()和Runtime.gc(),用来将垃圾收集的请求发送到JVM,但是不能保证垃圾回收会发生。
8)如果堆中已经没有足够的空间可以用来创建新的对象,那么JVM就会抛出OutOfMemoryError
9)J2SE 5(Java 2标准版)添加了一个称为人机工程学目标的新功能,即用最少的命令行调优JVM使其性能更强悍。

当对象有资格进行垃圾收集时

如果对象它不能从任何活动线程或任何静态引用访问到,则该对象将成为垃圾收集或GC的合格对象。换句话说,如果对一个对象的所有引用都为null,那么它就有资格进行垃圾收集。循环依赖项不作为参考依据,所以如果对象A引用对象B,而对象B引用对象A,并且它们没有任何其他的实时引用,那么A和B都有资格进行垃圾收集。
一般情况下,在Java中,一个对象可以在以下情况下获得垃圾回收的资格:
1)所有对该对象的引用都显式地设置为null,例如object = null。
2)对象是在一个块中创建的,一旦控制退出该块,引用就会被排除。
3)如果对象保存对另一个对象的引用,我们将父对象设置为null,还有将容器对象的引用设置为null时候,子对象以及容器被的对象就可以进行垃圾回收。
4)如果对象仅通过WeakHashMap以弱引用的形式存在,那么它将有资格进行垃圾收集

在Java中使用堆进行垃圾收集

Java对象是在堆中创建的,为了在Java中对其进行垃圾收集,将它分为三个部分或三个代,他们被称为年轻代/新生代、老年代、永久代。新生代又分为三个部分:Eden space(伊甸区)、Survivor 1(幸存区1)和Survivor 2 (幸存区2)。当一个对象在堆中第一次创建时,它在新生代的Eden space中创建,在后续的Minor GC之后,如果一个对象存活下来,它将被转移到survivor 1(幸存区1),而在Major GC,则会移动到老年代或者永久代。
这里写图片描述

堆的区域有点特殊,它被用来存储与JVM中类和方法相关的元数据,它也托管由JVM提供的字符串池

Java中的垃圾收集器类型

Java在运行时(J2SE 5)提供了Java中各种类型的垃圾收集,您可以根据应用程序的性能需求进行选择。除了串行垃圾收集器,Java 5添加了三个额外的垃圾收集器。每一个都是分代垃圾收集器,它已经被实现来增加应用程序的吞吐量,或者减少垃圾收集的暂停时间。
1)Throughput Garbage Collector
2)Concurrent low pause Collector
3)The Incremental (Sometimes called train) low pause collector

Java中垃圾收集的JVM参数

垃圾回收调优是一项长期的工作,需要大量的应用程序分析和耐心才能使其正确。
例如,如果一个应用程序有太多的声明周期很短的对象,那么使Eden空间足够大或更大,将减少Minor GC的频率。您还可以使用JVM参数来配置年轻代和老年代的大小,比如-XX:NewRatio=3 意味着年轻代和老年代的比例是1:3,你们要注意这一代人的规模。
如果设置的年轻代/新生代太大,那么使得年老带变小,二者会导致Mijor GC发生的更频繁,而Mirror GC会暂停应用程序的线程,而这会导致吞吐量降低。NewSize 和MaxNewSize可以用来配置新生代。

Java中Full GC和并发垃圾收集

在java中,并发的垃圾收集器使用了一个与应用程序线程并发运行的垃圾收集器线程,目标是在老年代变满之前完成对其的回收。在正常的操作中,并发的垃圾收集器可以在应用程序线程仍在运行的情况下完成大部分工作,因此应用程序线程只能看到短暂的暂停。
作为一个回退,如果并发的垃圾收集器无法在老年代变满之前完成,那么应用程序就会暂停,他所收集的所有应用程序线程都停止了。此类带有应用程序停止的回收被称为Full GC,并表明需要对并发垃圾回收的参数进行一些调整。
总是尽量避免或最小化Full GC,因为这会影响Java应用程序的性能

JVM总结

1)Java堆被划分为3代,用于垃圾收集。分别是年轻代/新生代,老年代和永久代
2)新对象是在年轻代中创建的,随后被转移到老年代上。
3)字符串常量池池是在堆的永久带创建的,垃圾收集可以在永久代中发生,但是依赖于JVM到JVM。自从JDK 1.7更新,字符串常量池池被移动到创建对象的堆区域中。
4)Mirror GC用于将对象从Eden区移动到幸存区1和幸存区2。而Magor GC则用于将年轻代的幸存对象转移到老年代。
5)当Major GC发生时,应用程序线程在此期间停止,这将降低应用程序的性能和吞吐量。
6)JVM命令行选项-Xmx和-Xms用于设置Java堆的启动和最大大小。根据我的经验,这个参数的理想比例是1:1或1:1.5,例如,您可以将xmx和-Xms都设置为1GB或-Xms 为1.2 GB和-Xms 为1.8 GB。
7)在Java中没有手动的垃圾收集方法,但是您可以使用各种引用类,例如:WeakReference 或SoftReference 来帮助垃圾收集器

猜你喜欢

转载自blog.csdn.net/qq_31179577/article/details/79416052