阿里面经之解答 by cmershen(3)——String/StringBuffer/StringBuilder,Java序列化,线程安全,线程同步,ThreadLocal

11.String,StringBuffer,StringBuilder的区别

(1)都是final的,不能被继承。
(2)String长度不可变,另外两个长度是可变的(例如StringBuffer有append方法)
(3)StringBuffer是线程同步的,里面的每一个API都添加了synchronized修饰,而StringBuilder不是线程同步的,因此拥有更好的性能。

12.String有重写Object的hashCode()和toString()方法吗?如果重写equals不重写hashcode会怎么样?
有。Object.java的hashCode是native方法,应该是调用了C++的代码。

    /**
     * Returns a hash code for this string. The hash code for a
     * {@code String} object is computed as
     * <blockquote><pre>
     * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
     * </pre></blockquote>
     * using {@code int} arithmetic, where {@code s[i]} is the
     * <i>i</i>th character of the string, {@code n} is the length of
     * the string, and {@code ^} indicates exponentiation.
     * (The hash value of the empty string is zero.)
     *
     * @return  a hash code value for this object.
     */
    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

toString()方法返回的是该对象本身。
重写equals不重写hashCode会怎样?
还是看HashMap.java的代码片段:

 while ((e = e.next) != null) {
    if (e.hash == h &&
        ((ek = e.key) == key || (ek != null && key.equals(ek))))
        return e.val;
}

所以底层判断的原则是先看hashcode是不是相等,再看equals是不是相等。如果重写equals不重写hashcode,就有可能出现两个对象的hashCode不相等,但equals相等,这时HashMap就get不出对应的value,虽然用户重写equals,用自己的逻辑已经判定是“同一对象”。
(如果你认为这两个对象不相同,那么你重写equals令其相同做什么?)

13.Java的序列化
(1)什么是Java的序列化?有什么用?
将一个对象转换成与平台无关的二进制流储存在外存储器中,或在网络中传输。其他程序一旦获得这个二进制流(从文件、从数据库、从网络等)就可以将其转化为Java对象进行操作。
举例:Java的远程方法调用(RMI)就是序列化的具体应用。RMI可以让一个JVM上的对象调用其他JVM上对象的方法。RMI是J2EE的基础。
也可以将一个JavaBean序列化后存储在数据库中。
(2)怎么实现Java的序列化和反序列化:
让一个类实现Serializable接口:

public class Student implements Serializable{
    private int id;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public Student(int id) {
        this.id = id;
    }
}

然后使用ObjectOutputStream流写入这个类:

    public static void main(String[] args) {
        try {
            ObjectOutputStream s = new ObjectOutputStream(new FileOutputStream("E:\\1.txt"));
            Student s1 = new Student(1);
            s.writeObject(s1);
            s.close();
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }

运行程序,发现生成了E:\1.txt文件,其中存储的就是s1对象。

接下来反序列化:

    public static void main(String[] args) {
        try {
            ObjectInputStream s = new ObjectInputStream(new FileInputStream("E:\\1.txt"));
            Student s1 = (Student)s.readObject();
            System.out.println(s1.getId());
            s.close();
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }

运行程序,发现s1的学号1已经被读出。
自定义序列化:在特殊情况下,我们有时候不能按照java底层的默认机制对一个对象序列化。这时我们需要在序列化的类中添加readObject()和writeObject()方法,按照自己的逻辑进行序列化和反序列化。如果还需要引用默认序列化方法,则分别调用defaultReadObject()和defaultWriteObject().
有时候我们不能将所有属性都序列化(例如密码等敏感信息),这时需要在不想序列化的属性前面添加transient关键字。
注意:(1)static修饰的属性不能被序列化;(2)如果被序列化对象的属性里面有对象(有点绕),要保证这个对象也是可序列化的。(3)对象的类名,属性会被序列化,而方法不会被序列化。(4)反序列化时要有序列化对象的.class文件,否则强转时会报错。
(5)最好显示声明serialVersionUID:private static final long serialVersionUID = 7247714666080613254L;
因为不同JVM有可能对同一个类生成的serialVersionUID不同,也可能该类的属性改变,这都会导致反序列化不回去,但如果人为指定了serialVersionUID,就不存在上述情况。
常见的序列化协议:XML,JSON

14.Java如何实现多线程?
这个基本上是必问的了,有3种方式:继承Thread类重写run函数,实现Runnable接口,实现Callable接口。
三种方式有什么区别?
继承Thread类,重写run方法,并new这个类调用start方法。(因为java没有多继承,所以这种方式用的很少)
实现Runnable接口,也重写run方法,并使用new Thread(MyThread).start()执行线程。
实现Callable接口和Runnable差不多,实现的是call方法,但可以有返回值。
15. 线程安全

什么是线程安全?如果一个类在多线程访问的情况下,其行为永远与预期一致,就称为线程安全。
反例:如果一个ArrayList类的addItem方法如下实现:

public void addItem(int item) {
    items[Size]=item;
    Size++;
}

那么假设有两个线程1和2并发调用这个方法,假设此时数组为空,Size=0。
线程1执行到items[Size]=item;的时候系统调度到线程2工作,线程1暂停,那么线程2也执行items[Size]=item;语句,那么就会覆盖掉线程1加入的那个数据(因为此时Size都是0),而执行后,Size变成2,数组中却只有1个元素,这就造成了混乱。
如何保证线程安全?
可以对变量使用volatile修饰;也可以对程序段或方法加synchronized修饰。
非线程安全的类(如ArrayList、HashMap等),不能在多线程中共享,但可以在多线程环境中作为某个线程独享的属性。
16.多线程环境中如何进行信息交互?Object类中的wait(),notify(),notifyAll()方法都是干什么用的?
关于这三个方法,JavaAPI是这么解释的(节选自Object.java):

    /**
     * Causes the current thread to wait until another thread invokes the
     * {@link java.lang.Object#notify()} method or the
     * {@link java.lang.Object#notifyAll()} method for this object.
     * In other words, this method behaves exactly as if it simply
     * performs the call {@code wait(0)}.
     * <p>
     * The current thread must own this object's monitor. The thread
     * releases ownership of this monitor and waits until another thread
     * notifies threads waiting on this object's monitor to wake up
     * either through a call to the {@code notify} method or the
     * {@code notifyAll} method. The thread then waits until it can
     * re-obtain ownership of the monitor and resumes execution.
     */
    public final void wait() throws InterruptedException {
        wait(0);
    }
        /**
     * Wakes up a single thread that is waiting on this object's
     * monitor. If any threads are waiting on this object, one of them
     * is chosen to be awakened. The choice is arbitrary and occurs at
     * the discretion of the implementation. A thread waits on an object's
     * monitor by calling one of the {@code wait} methods.
     * <p>
     * The awakened thread will not be able to proceed until the current
     * thread relinquishes the lock on this object. The awakened thread will
     * compete in the usual manner with any other threads that might be
     * actively competing to synchronize on this object; for example, the
     * awakened thread enjoys no reliable privilege or disadvantage in being
     * the next thread to lock this object.
     * @throws  IllegalMonitorStateException  if the current thread is not
     *               the owner of this object's monitor.
     * @see        java.lang.Object#notifyAll()
     * @see        java.lang.Object#wait()
     */
    public final native void notify();
        /**
     * Wakes up all threads that are waiting on this object's monitor. A
     * thread waits on an object's monitor by calling one of the
     * {@code wait} methods.
     * <p>
     * The awakened threads will not be able to proceed until the current
     * thread relinquishes the lock on this object. The awakened threads
     * will compete in the usual manner with any other threads that might
     * be actively competing to synchronize on this object; for example,
     * the awakened threads enjoy no reliable privilege or disadvantage in
     * being the next thread to lock this object.
     * <p>
     * This method should only be called by a thread that is the owner
     * of this object's monitor. See the {@code notify} method for a
     * description of the ways in which a thread can become the owner of
     * a monitor.
     */
    public final native void notifyAll();

这堆E文翻译过来大概就是说,wait()方法使得持有该对象的锁的线程阻塞掉,而notify()则是唤醒一个等待该对象的线程,notifyAll()是唤醒所有等待该对象的线程。
调用wait()方法必须在同步块或者同步方法中进行(synchronized块或者synchronized方法)。
这里要注意一点:notify()和notifyAll()方法只是唤醒等待该对象的monitor的线程,并不决定哪个线程能够获取到monitor。而如果有多个线程等待该对象,则notify()方法唤醒的具体是哪一个则由JVM底层的进程调度决定。
17. 多线程共用一个变量需注意什么?
如果我们实例化了一个实现Runnable接口的类的对象代表一个线程,这个类中定义了全局变量且run方法会修改变量时,如果有多个线程同时修改这个变量,就会出现异常情况。(因为全局信息被并行的修改会造成错误。)
而ThreadLocal解决了这个问题。ThreadLocal类可以封装一个对象进去,被ThreadLocal封装的对象对每个线程来说是独享的,也就是说即使 被ThreadLocal封装的对象是全局的,它也会保证在各个线程间独立。
接下来我们简单的研究一下ThreadLocal.java的源码。
ThreadLocal提供了三个API,get(),set(),remove()。分别用于取出线程本地变量、设置、清空。
ThreadLocal底层维护了一个ThreadLocalMap,它是Thread类的一个属性。所以每个ThreadLocalMap均与当前线程一一对应,再里面则是一个Entry[]数组,数组下标为当前线程的Hash值,对应的Entry对象里面封装了Object value。

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

所以get方法的具体实现逻辑是,先获得当前线程,再把当前的ThreadLocal对象本身(其中包含被封装对象在每个线程中的版本)传进去,如果当前线程的Hash值命中,且对应下标中存有Entry对象,则返回这个对象,再取出封装在里面的value,强制转换并返回。
set方法的实现细节是:

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

这个跟get差不多,如果当前线程中有ThreadLocalMap,则把value放进对应的下标中去。当然如果这个下标有可能在数组中不存在或者出现重复,这就要rehash了,在这里不做讨论。
ThreadLocalMap的set实现细节如下:

        private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

接下来是remove方法:

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

哈哈,写remove的时候好像作者都变懒了,把get和set里面的前两行压缩到了一行。最后调用了map的remove方法,再看看map的remove方法怎么实现的:

        private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

挺简单的,就是Hash命中了以后取消对封装的那个value的引用,然后rehash一次。

接下来还有一个volatile关键字要介绍。用volatile修饰的变量,线程在每次使用变量的时候,都会去内存中读取一下该变量最后的值。这样不同的线程访问同一变量,每次都看到的是最后的值。

阅读更多
版权声明:完整版Leetcode题解请出门左转https://github.com/cmershen1/leetcode/tree/master/docs https://blog.csdn.net/cmershen/article/details/51799040
文章标签: 阿里 线程
个人分类: Java
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭