java面试小结——并发与多线程

Java线程的状态及如何转换

这里写图片描述


多个线程之间如何协调?

wait()、notify()、notifyAll():这三个方法用于协调多个线程对共享数据的存取,所以必须在同步语句块内使用。wait方法要等待notify/notifyAll的线程释放锁后才能开始继续往下执行。

// 等待方
synchronized(lockObj){
    while(condition is false){
        lockObj.wait();
    }

    // do business
}
// 通知方
synchronized(lockObj){
    // change condition

    lockObj.notifyAll();
}

说说Java的线程池是如何实现的?

  • 创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。为了避免这些问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。
  • maximumPoolSize和corePoolSize的区别:这个概念很重要,maximumPoolSize为线程池最大容量,也就是说线程池最多能起多少Worker。corePoolSize是核心线程池的大小,当corePoolSize满了时,同时workQueue full(ArrayBolckQueue是可能满的) 那么此时允许新建Worker去处理workQueue中的Task,但是不能超过maximumPoolSize。超过corePoolSize之外的线程会在空闲超时后终止。可以通过beforeExecute和afterExecute实现线程池的监听;

这里写图片描述
这里写图片描述

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

关于BlockingQueue和TransferQueue的异同。

  • TransferQueue继承了BlockingQueue并扩展了一些新方法。BlockingQueue是Java 5中加入的接口,它是指这样的一个队列:当生产者向队列添加元素但队列已满时,生产者会被阻塞;当消费者从队列移除元素但队列为空时,消费者会被阻塞。
  • TransferQueue则更进一步,生产者会一直阻塞直到所添加到队列的元素被某一个消费者所消费(不仅仅是添加到队列里就完事),新添加的transfer方法用来实现这种约束。顾名思义,阻塞就是发生在元素从一个线程transfer到另一个线程的过程中,它有效地实现了元素在线程之间的传递(以建立Java内存模型中的happens-before关系的方式)。
  • TransferQueue还包括了其他的一些方法:两个tryTransfer方法,一个是非阻塞的,另一个带有timeout参数设置超时时间的。还有两个辅助方法hasWaitingConsumer()和getWaitingConsumerCount()。
  • TransferQueue相比SynchronousQueue用处更广、更好用,因为你可以决定是使用BlockingQueue的方法(例如put方法)还是确保一次传递完成(即transfer方法)。在队列中已有元素的情况下,调用transfer方法,可以确保队列中被传递元素之前的所有元素都能被处理。Doug Lea说从功能角度来讲,LinkedTransferQueue实际上是ConcurrentLinkedQueue、SynchronousQueue(公平模式)和LinkedBlockingQueue的超集。而且LinkedTransferQueue更好用,因为它不仅仅综合了这几个类的功能,同时也提供了更高效的实现。

谈谈HashMap的实现

  • 从结构实现来讲,HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下如所示。

这里写图片描述

从源码可知,HashMap类中有一个非常重要的字段,就是 Node[] table,即哈希桶数组;
Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。
HashMap就是使用哈希表来存储的。为解决冲突,Java中HashMap采用了链地址法,简单来说,就是数组加链表的结合。在每个数组元素上都一个链表结构,当数据被Hash后,得到数组下标,把数据放在对应下标元素的链表上。
Node[] table的初始化长度默认值是16,Load factor为负载因子(默认值是0.75),threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。threshold = length * Load factor。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。

  • 确定哈希桶数组索引位置:取key的hashCode值、高位运算(通过hashCode()的高16位异或低16位实现的)、取模运算。
    这里写图片描述
  • HashMap的put方法执行过程可以通过下图来理解。
    这里写图片描述
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 步骤①:tab为空则创建
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 步骤②:计算index,并对null做处理 
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 步骤③:节点key存在,直接覆盖value
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 步骤④:判断该链为红黑树
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 步骤⑤:该链为链表
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //链表长度大于8转换为红黑树进行处理
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // key已经存在直接覆盖value
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 步骤⑥:超过最大容量 就扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
  • 扩容机制:扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组。在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”。这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            // 超过最大值就不再扩充了,就只好随你碰撞去吧
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 没超过最大值,就扩充为原来的2倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }

        // 计算新的resize上限
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            // 把每个bucket都移动到新的buckets中
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            // 原索引
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            // 原索引+oldCap
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);

                        // 原索引放到bucket里
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        // 原索引+oldCap放到bucket里
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

谈谈线程安全的ConcurrentHashMap的实现原理。

  • ConcurrentHashMap在jdk1.8中主要做了2方面的改进:改进一是取消segments字段,直接采用transient volatile HashEntry[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率;改进二是将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构,对于hash表来说,最核心的能力在于将key hash之后能均匀的分布在数组中,如果hash之后散列的很均匀,那么table数组中的每个队列长度主要为0或者1。但实际情况并非总是如此理想,虽然ConcurrentHashMap类默认的加载因子为0.75,但是在数据量过大或者运气不佳的情况下,还是会存在一些队列长度过长的情况,如果还是采用单向列表方式,那么查询某个节点的时间复杂度为O(n);因此,对于个数超过8(默认值)的列表,jdk1.8中采用了红黑树的结构,那么查询的时间复杂度可以降低到O(logN),可以改进性能。
  • TreeNode类:树节点类,另外一个核心的数据结构。当链表长度过长的时候,会转换为TreeNode。但是与HashMap不相同的是,它并不是直接转换为红黑树,而是把这些结点包装成TreeNode放在TreeBin对象中,由TreeBin完成对红黑树的包装。而且TreeNode在ConcurrentHashMap继承自Node类,而并非HashMap中的集成自LinkedHashMap.Entry。
  • 二叉查找树,也称有序二叉树(ordered binary tree),是指一棵空树或者具有下列性质的二叉树:
1.若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
2.若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
3.任意节点的左、右子树也分别为二叉查找树。
4.没有键值相等的节点(no duplicate nodes)。
  • 红黑树虽然本质上是一棵二叉查找树,但它在二叉查找树的基础上增加了着色和相关的性质使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n)。但它是如何保证一棵n个结点的红黑树的高度始终保持在logn的呢?这就引出了红黑树的5个性质:
1.每个结点要么是红的要么是黑的。  
2.根结点是黑的。  
3.每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的。  
4.如果一个结点是红的,那么它的两个儿子都是黑的。  
5.对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点。

什么是一致性哈希

  • 环形Hash空间:按照常用的hash算法来将对应的key哈希到一个具有2^32次方个桶的空间中,即0~(2^32)-1的数字空间中。现在我们可以将这些数字头尾相连,想象成一个闭合的环形;
  • 把数据通过一定的hash算法处理后映射到环上;
  • 将机器通过hash算法映射到环上(一般情况下对机器的hash计算是采用机器的IP或者机器唯一的别名作为输入值),然后以顺时针的方向计算,将所有对象存储到离自己最近的机器中;
  • 机器的删除与添加:普通hash求余算法最为不妥的地方就是在有机器的添加或者删除之后会照成大量的对象存储位置失效,这样就大大的不满足单调性了。通过对节点的添加和删除的分析,一致性哈希算法在保持了单调性的同时,还是数据的迁移达到了最小,这样的算法对分布式集群来说是非常合适的,避免了大量数据迁移,减小了服务器的的压力。
  • 平衡性:在一致性哈希算法中,为了尽可能的满足平衡性,其引入了虚拟节点。它实际上是节点在hash空间的复制品,一实际个节点对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在hash空间中以hash值排列。
    这里写图片描述

Java有哪些实现锁的方式

  • synchronized同步锁:它无法中断一个正在等候获得锁的线程,也无法通过投票得到锁,如果不想等下去,也就没法得到锁。但除非对锁的某个高级特性有明确的需要,或者有明确的证据表明在特定情况下,同步已经成为瓶颈,否则还是应当继续使用synchronized。
  • volatile是比synchronized更轻量,因为它没有上下文切换;其实现是通过lock指令将缓存行数据写到系统内存且让其他缓存数据无效;
  • ReentrantLock可以支持公平锁,当然公平锁性能会有影响,默认为非公平的;
// 对于非公平锁,会执行该方法:
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();//获取状态变量
    if (c == 0) {//表明没有线程占有该同步状态
        if (compareAndSetState(0, acquires)) {//以原子方式设置该同步状态
            setExclusiveOwnerThread(current);//该线程拥有该FairSync同步状态
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {//当前线程已经拥有该同步状态
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);//重复设置状态变量(锁的可重入特性)
        return true;
    }

    return false;
}
// 而对于公平锁,该方法则是这样:
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        //先判断该线程节点是否是队列的头结点
        //是则以原子方式设置同步状态,获取锁
        //否则失败返回
        if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {//重入
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }

    return false;
}
  • AQS管理的FIFO等待队列,获取锁状态失败的线程会被放入该队列,等待再次尝试获取锁。而state成员变量,代表着锁的同步状态,一个线程成功获得锁,这个行为的实质就是该线程成功的设置了state变量的状态。
public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    // 这个if分支其实是一种优化:CAS操作失败的话才进入enq中的循环。
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
} 
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }

            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
  • ReentrantReadWriteLock对重入锁再进一步分离为读锁和写锁,在读多写少的场景下能显著提升性能。
  • ReadLock可以被多个线程持有并且在作用时排斥任何的WriteLock,而WriteLock则是完全的互斥。
  • 写线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级的特性。读取锁是不能直接升级为写入锁的。因为获取一个写入锁需要释放所有读取锁,所以如果有两个读取锁视图获取写入锁而都不释放读取锁时就会发生死锁。
  • 如果读取执行情况很多,写入很少的情况下,使用ReentrantReadWriteLock可能会使写入线程遭遇饥饿问题,也就是写入线程吃吃无法竞争到锁定而一直处于等待状态。
  • StampedLock控制锁有三种模式(写,读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问。在读锁上分为悲观锁和乐观锁。所谓的乐观读模式,也就是若读的操作很多,写的操作很少的情况下,你可以乐观地认为,写入与读取同时发生几率很少,因此不悲观地使用完全的读取锁定,程序可以查看读取资料之后,是否遭到写入执行的变更,再采取后续的措施(重新读取变更信息,或者抛出异常),这一个小小改进,可大幅度提高程序的吞吐量!
class Point {
   private double x, y;
   private final StampedLock sl = new StampedLock();
   void move(double deltaX, double deltaY) { // an exclusively locked method
     long stamp = sl.writeLock();
     try {
       x += deltaX;
       y += deltaY;
     } finally {
       sl.unlockWrite(stamp);
     }
   }

   //下面看看乐观读锁案例
   double distanceFromOrigin() { // A read-only method
     long stamp = sl.tryOptimisticRead(); //获得一个乐观读锁
     double currentX = x, currentY = y; //将两个字段读入本地局部变量
     if (!sl.validate(stamp)) { //检查发出乐观读锁后同时是否有其他写锁发生?
        stamp = sl.readLock(); //如果没有,我们再次获得一个读悲观锁
        try {
          currentX = x; // 将两个字段读入本地局部变量
          currentY = y; // 将两个字段读入本地局部变量
        } finally {
           sl.unlockRead(stamp);
        }
     }
     return Math.sqrt(currentX * currentX + currentY * currentY);
   }
    //下面是悲观读锁案例
   void moveIfAtOrigin(double newX, double newY) { // upgrade
     // Could instead start with optimistic, not read mode
     long stamp = sl.readLock();
     try {
       while (x == 0.0 && y == 0.0) { //循环,检查当前状态是否符合
         long ws = sl.tryConvertToWriteLock(stamp); //将读锁转为写锁
         if (ws != 0L) { //这是确认转为写锁是否成功
           stamp = ws; //如果成功 替换票据
           x = newX; //进行状态改变
           y = newY; //进行状态改变
           break;
         }
         else { //如果不能成功转换为写锁
           sl.unlockRead(stamp); //我们显式释放读锁
           stamp = sl.writeLock(); //显式直接进行写锁 然后再通过循环再试
         }
       }
     } finally {
       sl.unlock(stamp); //释放读锁或写锁
     }
   }
 }

AtomicLong、LongAdder和LongAccumulator的实现有何不同?

  • AtomicLong是Java 5引入的基于CAS的无锁的操作长整形值的工具类;
  • LongAdder是Java 8提供的累加器,基于Striped64实现。它常用于状态采集、统计等场景。AtomicLong也可以用于这种场景,但在线程竞争激烈的情况下,LongAdder要比AtomicLong拥有更高的吞吐量,但会耗费更多的内存空间。
  • Striped64的设计核心思路就是通过内部的分散计算来避免竞争(比如多线程CAS操作时的竞争)。Striped64内部包含一个基础值和一个单元哈希表。没有竞争的情况下,要累加的数会累加到这个基础值上;如果有竞争的话,会将要累加的数累加到单元哈希表中的某个单元里面。所以整个Striped64的值包括基础值和单元哈希表中所有单元的值的总和。
/** 
 * 存放Cell的hash表,大小为2的幂。 
 */  
transient volatile Cell[] cells;  
/** 
 * 基础值,没有竞争时会使用(更新)这个值,同时做为初始化竞争失败的回退方案。 
 * 原子更新。 
 */  
transient volatile long base;  
/** 
 * 自旋锁,通过CAS操作加锁,用于保护创建或者扩展Cell表。 
 */  
transient volatile int cellsBusy;
  • LongAccumulator和LongAdder类似,也基于Striped64实现。但要比LongAdder更加灵活(要传入一个函数接口),LongAdder相当于是LongAccumulator的一种特例。

CompletableFuture对比Future有哪些改进,怎么用

  • Future对象代表一个尚未完成异步操作的结果。从Java 5以来,JUC包一直提供着最基本的Future,不过它太鸡肋了,除了get、cancel、isDone和isCancelled方法之外就没有其他的操作了,对于结果的获取很不方便,只能通过阻塞或者轮询的方式得到任务的结果。阻塞的方式显然和我们的异步编程的初衷相违背,轮询的方式又会耗费无谓的CPU资源,而且也不能及时地得到计算结果,这样很不方便。
  • 好在Java 8中引入了具有函数式风格的CompletableFuture,支持一系列的函数式的组合、运算操作,非常方便,可以写出函数式风格的代码而摆脱callback hell。
  • 主动完成计算:CompletableFuture类实现了CompletionStage和Future接口,所以你还是可以像以前一样通过阻塞或者轮询的方式获得结果,尽管这种方式不推荐使用。

主要的API如下所示:

supplyAsync/runAsync -- 创建CompletableFuture对象;
whenComplete/whenCompleteAsync/exceptionally -- 计算完成或者抛出异常的时可以执行特定的Action;
thenApply/thenApplyAsync -- 对数据进行一些处理或变换;
thenAccept/thenAcceptAsync -- 纯消费,不返回新的计算值;
thenAcceptBoth/thenAcceptBothAsync/runAfterBoth -- 当两个CompletionStage都正常完成计算的时候,就会执行提供的Action;
thenCompose/thenComposeAsync -- 这个Function的输入是当前的CompletableFuture的计算值,返回结果将是一个新的CompletableFuture。
记住,thenCompose返回的对象并不一是函数fn返回的对象,如果原来的CompletableFuture还没有计算出来,
它就会生成一个新的组合后的CompletableFuture。可以用来实现异步pipline;
thenCombine/thenCombineAsync - 并行执行的,它们之间并没有先后依赖顺序,和thenAcceptBoth的区别在于有返回值;
allOf/anyOf -- 所有的/其中一个CompletableFuture都执行完后执行计算;

来源:http://ginobefunny.com/post/java_concurrent_interview_questions/


volatile 与 synchronized 作用与区别

1,volatile
它所修饰的变量不保留拷贝,直接访问主内存中的。
在Java内存模型中,有main memory,每个线程也有自己的memory (例如寄存器)。为了性能,一个线程会在自己的memory中保持要访问的变量的副本。这样就会出现同一个变 量在某个瞬间,在一个线程的memory中的值可能与另外一个线程memory中的值,或者main memory中的值不一致的情况。 一个变量声明为volatile,就意味着这个变量是随时会被其他线程修改的,因此不能将它cache在线程memory中。

2,synchronized
当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。

 一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

 二、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。

 三、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。

 四、当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。

 五、以上规则对其它对象锁同样适用.

区别:

一、volatile是变量修饰符,而synchronized则作用于一段代码或方法。

二、volatile只是在线程内存和“主”内存间同步某个变量的值;而synchronized通过锁定和解锁某个监视器同步所有变量的值。显然synchronized要比volatile消耗更多资源


现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?

thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。
比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
想要更深入了解,建议看一下join的源码,也很简单的,使用wait方法实现的。

t.join(); //调用join方法,等待线程t执行完毕
t.join(1000); //等待 t 线程,等待时间是1000毫秒。

public static void main(String[] args) {
        method01();
        method02();
    }

    /**
     * 第一种实现方式,顺序写死在线程代码的内部了,有时候不方便
     */
    private static void method01() {
        Thread t1 = new Thread(new Runnable() {
            @Override public void run() {
                System.out.println("t1 is finished");
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override public void run() {
                try {
                    t1.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2 is finished");
            }
        });
        Thread t3 = new Thread(new Runnable() {
            @Override public void run() {
                try {
                    t2.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t3 is finished");
            }
        });

        t3.start();
        t2.start();
        t1.start();
    }


    /**
     * 第二种实现方式,线程执行顺序可以在方法中调换
     */
    private static void method02(){
        Runnable runnable = new Runnable() {
            @Override public void run() {
                System.out.println(Thread.currentThread().getName() + "执行完成");
            }
        };
        Thread t1 = new Thread(runnable, "t1");
        Thread t2 = new Thread(runnable, "t2");
        Thread t3 = new Thread(runnable, "t3");
        try {
            t1.start();
            t1.join();
            t2.start();
            t2.join();
            t3.start();
            t3.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

在Java中Lock接口比synchronized块的优势是什么?你需要实现一个高效的缓存,它允许多个用户读,但只允许一个用户写,以此来保持它的完整性,你会怎样去实现它?

Lock接口 和 ReadWriteLock接口 如下:

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

整体上来说Lock是synchronized的扩展版,Lock提供了无条件的、可轮询的(tryLock方法)、定时的(tryLock带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition方法)锁操作。另外Lock的实现类基本都支持非公平锁(默认)和公平锁,synchronized只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。

ReadWriteLock是对Lock的运用,具体的实现类是 ReentrantReadWriteLock ,下面用这个类来实现读写类型的高效缓存:

import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 用ReadWriteLock读写锁来实现一个高效的Map缓存
 * Created by LEO on 2017/10/30.
 */
public class ReaderAndWriter<K, V> {
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();
    private final Lock writeLock = lock.writeLock();
    private final Map<K, V> map;

    public ReaderAndWriter(Map<K, V> map) {
        this.map = map;
    }

    /*************   这是用lock()方法写的   ********************/
//    public V put(K key, V value){
//        writeLock.lock();
//        try {
//            return map.put(key, value);
//        }finally {
//            writeLock.unlock();
//        }
//    }
//    public V get(K key){
//        readLock.lock();
//        try {
//            return map.get(key);
//        }finally {
//            readLock.unlock();
//        }
//    }

    /*************   这是用tryLock()方法写的   ********************/
    public V put(K key, V value){
        while (true){
            if(writeLock.tryLock()){
                try {
                    System.out.println("put "+ key +" = " + value);
                    return map.put(key, value);
                }finally {
                    writeLock.unlock();
                }
            }
        }
    }
    public V get(K key){
        while (true){
            if (readLock.tryLock()) {
                try {
                    V v = map.get(key);
                    System.out.println("get "+ key +" = " + v);
                    return v;
                } finally {
                    readLock.unlock();
                }
            }
        }
    }


    /********************    下面是测试区       *********************************/
    public static void main(String[] args) {
        final ReaderAndWriter<String, Integer> rw = new ReaderAndWriter<>(new HashMap<>());
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i < 100; i++) {
            exec.execute(new TestRunnable(rw));
        }
        exec.shutdown();
    }

    static class TestRunnable implements Runnable{
        private final ReaderAndWriter<String, Integer> rw;
        private final String KEY = "x";

        TestRunnable(ReaderAndWriter<String, Integer> rw) {
            this.rw = rw;
        }

        @Override
        public void run() {
            Random random = new Random();
            int r = random.nextInt(100);
            //生成随机数,小于30的写入缓存,大于等于30则读取数字
            if (r < 30){
                rw.put(KEY, r);
            } else {
                rw.get(KEY);
            }
        }
    }
}

在java中wait和sleep方法的不同

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

此处我想理一下Java多线程的基础知识:
- Java的多线程锁是挂在对象上的,并不是在方法上的。即每个对象都有一个锁,当遇到类似synchronized的同步需要时,就会监视(monitor)每个想使用本对象的线程按照一定的规则来访问,规则也就是在同一时间内只能有一个线程能访问此对象。
- Java中获取锁的单位是线程。当线程A获取了对象B的锁,也就是对象B的持有标记上写的是线程A的唯一标识,在需要同步的情况下的话,只有线程A能访问对象B。
- Thread常用方法有:start/stop/yield/sleep/interrupt/join等,他们是线程级别的方法,所以并不会太关心锁的具体逻辑。
- Object的线程有关方法是:wait/wait(事件参数)/notify/notifyAll,他们是对象的方法,所以使用的时候就有点憋屈了,必须当前线程获取了本对象的锁才能使用,否则会报异常。但他们能更细粒度的控制锁,可以释放锁。


用Java实现阻塞队列

这是一个相对艰难的多线程面试问题,它能达到很多的目的。第一,它可以检测侯选者是否能实际的用Java线程写程序;第二,可以检测侯选者对并发场景的理解,并且你可以根据这个问很多问题。如果他用wait()和notify()方法来实现阻塞队列,你可以要求他用最新的Java 5中的并发类来再写一次。

下面是实现了阻塞的take和put方法的阻塞队列(分别用synchronized 和 wait/notify 实现):

import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 实现了阻塞的take和put方法的阻塞队列
 *      分别用synchronized 和 wait/notify 实现
 * @author xuexiaolei
 * @version 2017年11月01日
 */
public class MyBlocingQueue<E> {
    private final List list;
    private final int limit;//有大小限制的

    public MyBlocingQueue(int limit) {
        this.limit = limit;
        this.list = new LinkedList<E>();
    }

    //这是用synchronized写的,在list空或者满的时候效率会低,因为会一直轮询
//    public void put(E e){
//        while(true){
//            synchronized (list){
//                if (list.size() < limit) {
//                    System.out.println("list : " + list.toString());
//                    System.out.println("put : " + e);
//                    list.add(e);
//                    return;
//                }
//            }
//        }
//    }
//    public E take(){
//        while (true) {
//            synchronized (list) {
//                if (list.size() > 0){
//                    System.out.println("list : " + list.toString());
//                    E remove = (E) list.remove(0);
//                    System.out.println("take : " + remove);
//                    return remove;
//                }
//            }
//        }
//    }

    //用wait,notify写的,在list空或者满的时候效率会高一点,因为wait释放锁,然后等待唤醒
    public synchronized void put(E e){
        while (list.size() == limit){
            try {
                wait();
            } catch (InterruptedException e1) {
                e1.printStackTrace();
            }
        }
        System.out.println("list : " + list.toString());
        System.out.println("put : " + e);
        list.add(e);
        notifyAll();
    }
    public synchronized E take() {
        while (list.size() == 0){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("list : " + list.toString());
        E remove = (E) list.remove(0);
        System.out.println("take : " + remove);
        notifyAll();
        return remove;
    }


    /********************    下面是测试区       *********************************/
    public static void main(String[] args) {
        final MyBlocingQueue<Integer> myBlocingQueue = new MyBlocingQueue(10);
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i < 100; i++) {
            exec.execute(new TestRunnable(myBlocingQueue));
        }
        exec.shutdown();
    }

    static class TestRunnable implements Runnable{
        private final MyBlocingQueue<Integer> myBlocingQueue;

        TestRunnable(MyBlocingQueue<Integer> myBlocingQueue) {
            this.myBlocingQueue = myBlocingQueue;
        }

        @Override
        public void run() {
            Random random = new Random();
            int r = random.nextInt(100);
            //生成随机数,按照一定比率读取或者放入,可以更改!!!
            if (r < 30){
                myBlocingQueue.put(r);
            } else {
                myBlocingQueue.take();
            }
        }
    }
}

BlockingQueue介绍

Java5中提供了BlockingQueue的方法,并且有几个实现,在此介绍一下。

BlockingQueue 具有 4 组不同的方法用于插入、移除以及对队列中的元素进行检查。如果请求的操作不能得到立即执行的话,每个方法的表现也不同。这些方法如下:

Throws exceptionSpecial valueBlocksTimes out
add(e)offer(e)put(e)offer(Object, long, TimeUnit)
remove()poll()take()poll(long, TimeUnit)
element()peek()

- Throws exception 抛异常:如果试图的操作无法立即执行,抛一个异常。
- Special value 特定值:如果试图的操作无法立即执行,返回一个特定的值(常常是 true / false)
- Blocks 阻塞:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。
- Times out 超时:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功(典型的是true / false)。

BlockingQueue 的实现类
- ArrayBlockingQueue:ArrayBlockingQueue 是一个有界的阻塞队列,其内部实现是将对象放到一个数组里。有界也就意味着,它不能够存储无限多数量的元素。它有一个同一时间能够存储元素数量的上限。你可以在对其初始化的时候设定这个上限,但之后就无法对这个上限进行修改了(译者注:因为它是基于数组实现的,也就具有数组的特性:一旦初始化,大小就无法修改)。
- DelayQueue:DelayQueue 对元素进行持有直到一个特定的延迟到期。注入其中的元素必须实现 java.util.concurrent.Delayed 接口。
- LinkedBlockingQueue:LinkedBlockingQueue 内部以一个链式结构(链接节点)对其元素进行存储。如果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。
- PriorityBlockingQueue:PriorityBlockingQueue 是一个无界的并发队列。它使用了和类 java.util.PriorityQueue 一样的排序规则。你无法向这个队列中插入 null 值。所有插入到 PriorityBlockingQueue 的元素必须实现 java.lang.Comparable 接口。因此该队列中元素的排序就取决于你自己的 Comparable 实现。
- SynchronousQueue:SynchronousQueue 是一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素。据此,把这个类称作一个队列显然是夸大其词了。它更多像是一个汇合点。

BlocingQueue的实现大多是通过 lock锁的多条件(condition)阻塞控制,下面我们自己写一个简单版:

import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 模仿ArrayBlockingQueue实现阻塞队列
 * @author xuexiaolei
 * @version 2017年11月01日
 */
public class MyBlocingQueue2<E> {
    private final List list;
    private final int limit;//有大小限制的
    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    public MyBlocingQueue2(int limit) {
        this.limit = limit;
        this.list = new LinkedList<E>();
    }

    public void put(E e) throws InterruptedException {
        lock.lock();
        try {
            while (list.size() == limit){
                notFull.await();
            }
            list.add(e);
            notEmpty.signalAll();
        }finally {
            lock.unlock();
        }
    }
    public E take() throws InterruptedException {
        lock.lock();
        try {
            while (list.size() == 0){
                notEmpty.await();
            }
            E remove = (E) list.remove(0);
            notFull.signalAll();
            return remove;
        }finally {
            lock.unlock();
        }
    }
}

用Java写代码来解决生产者——消费者问题

与上面的问题很类似,但这个问题更经典,有些时候面试都会问下面的问题。在Java中怎么解决生产者——消费者问题,当然有很多解决方法,我已经分享了一种用阻塞队列实现的方法。有些时候他们甚至会问怎么实现哲学家进餐问题。

生产者、消费者有很多的实现方法:
- 用wait() / notify()方法
- 用Lock的多Condition方法
- BlockingQueue阻塞队列方法

可以发现在上面实现阻塞队列题中,BlockingQueue的实现基本都用到了类似的实现,将BlockingQueue的实现方式稍微包装一下就成了一个生产者-消费者模式了。

import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

/**
 * 用阻塞队列快速实现生产者-消费者
 * @author xuexiaolei
 * @version 2017年11月01日
 */
public class ProduceAndConsumer {
    public static void main(String[] args) {
        final BlockingQueue<Integer> list = new ArrayBlockingQueue<Integer>(10);
        Procude procude = new Procude(list);
        Consumer consumer = new Consumer(list);
        procude.start();
        consumer.start();
    }

    static class Procude extends Thread{
        private final BlockingQueue<Integer> list;
        Procude(BlockingQueue<Integer> list) {
            this.list = list;
        }
        @Override public void run() {
            while(true){
                try {
                    Integer take = list.take();
                    System.out.println("消费数据:" + take);
//                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    static class Consumer extends Thread{
        private final BlockingQueue<Integer> list;
        Consumer(BlockingQueue<Integer> list) {
            this.list = list;
        }
        @Override public void run() {
            while (true){
                try {
                    int i = new Random().nextInt(100);
                    list.put(i);
                    System.out.println("生产数据:" + i);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

用Java写一个会导致死锁的程序,你将怎么解决?

/**
 * 简单死锁程序
 *      lockA、lockB分别是两个资源,线程A、B必须同是拿到才能工作
 *      但A线程先拿lockA、再拿lockB
 *      线程先拿lockB、再拿lockA
 * @author xuexiaolei
 * @version 2017年11月01日
 */
public class SimpleDeadLock {
    public static void main(String[] args) {
        Object lockA = new Object();
        Object lockB = new Object();
        A a = new A(lockA, lockB);
        B b = new B(lockA, lockB);
        a.start();
        b.start();
    }

    static class A extends Thread{
        private final Object lockA;
        private final Object lockB;
        A(Object lockA, Object lockB) {
            this.lockA = lockA;
            this.lockB = lockB;
        }

        @Override public void run() {
            synchronized (lockA){
                try {
                    Thread.sleep(1000);
                    synchronized (lockB){
                        System.out.println("Hello A");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    static class B extends Thread{
        private final Object lockA;
        private final Object lockB;
        B(Object lockA, Object lockB) {
            this.lockA = lockA;
            this.lockB = lockB;
        }

        @Override public void run() {
            synchronized (lockB){
                try {
                    Thread.sleep(1000);
                    synchronized (lockA){
                        System.out.println("Hello B");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

如何避免死锁?
- 从死锁的四个必要条件来看,破坏其中的任意一个条件就可以避免死锁。但互斥条件是由资源本身决定的,不剥夺条件一般无法破坏,要实现的话得自己写更多的逻辑。
- 避免无限期的等待:用Lock.tryLock(),wait/notify等方法写出请求一定时间后,放弃已经拥有的锁的程序。
- 注意锁的顺序:以固定的顺序获取锁,可以避免死锁。
- 开放调用:即只对有请求的进行封锁。你应当只想你要运行的资源获取封锁,比如在上述程序中我在封锁的完全的对象资源。但是如果我们只对它所属领域中的一个感兴趣,那我们应当封锁住那个特殊的领域而并非完全的对象。
- 最后,如果能避免使用多个锁,甚至写出无锁的线程安全程序是再好不过了。


什么是原子操作,Java中的原子操作是什么?

  • 原子操作是不可分割的操作,一个原子操作中间是不会被其他线程打断的,所以不需要同步一个原子操作。
  • 多个原子操作合并起来后就不是一个原子操作了,就需要同步了。
  • i++不是一个原子操作,它包含 读取-修改-写入 操作,在多线程状态下是不安全的。
  • 另外,java内存模型允许将64位的读操作或写操作分解为2个32位的操作,所以对long和double类型的单次读写操作并不是原子的,注意使用volitile使他们成为原子操作。

Java中的volatile关键是什么作用?怎样使用它?在Java中它跟synchronized方法有什么不同?

自从Java 5和Java内存模型改变以后,基于volatile关键字的线程问题越来越流行。应该准备好回答关于volatile变量怎样在并发环境中确保可见性。

volatile关键字的作用是:保证变量的可见性。
在java内存结构中,每个线程都是有自己独立的内存空间(此处指的线程栈)。当需要对一个共享变量操作时,线程会将这个数据从主存空间复制到自己的独立空间内进行操作,然后在某个时刻将修改后的值刷新到主存空间。这个中间时间就会发生许多奇奇怪怪的线程安全问题了,volatile就出来了,它保证读取数据时只从主存空间读取,修改数据直接修改到主存空间中去,这样就保证了这个变量对多个操作线程的可见性了。换句话说,被volatile修饰的变量,能保证该变量的 单次读或者单次写 操作是原子的。

但是线程安全是两方面需要的 原子性(指的是多条操作)和可见性。volatile只能保证可见性,synchronized是两个均保证的。
volatile轻量级,只能修饰变量;synchronized重量级,还可修饰方法。
volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞。


什么是竞争条件(race condition)?你怎样发现和解决的?

这是一道出现在多线程面试的高级阶段的问题。大多数的面试官会问最近你遇到的竞争条件,以及你是怎么解决的。有些时间他们会写简单的代码,然后让你检测出代码的竞争条件。可以参考我之前发布的关于Java竞争条件的文章。在我看来这是最好的java线程面试问题之一,它可以确切的检测候选者解决竞争条件的经验。关于这方面最好的书是《java并发编程实战》。

竞争条件,在《java并发编程实战》叫做竞态条件:指设备或系统出现不恰当的执行时序,而得到不正确的结果。

下面是个最简单的例子,是一个单例模式实现的错误示范:

@NotThreadSafe
public class LazyInitRace {
    private ExpensiveObject instance = null;

    public ExpensiveObject getInstance() {
        if (instance == null)
            instance = new ExpensiveObject();
        return instance;
    }
}

在上述例子中,表现一种很常见的竞态条件类型:“先检查后执行”。根据某个检查结果来执行进一步的操作,但很有可能这个检查结果是失效的!还有很常见的竞态条件“读取-修改-写入”三连,在多线程条件下,三个小操作并不一定会放在一起执行的。

如何对待竞态条件?
首先,警惕复合操作,当多个原子操作合在一起的时候,并不一定仍然是一个原子操作,此时需要用同步的手段来保证原子性。
另外,使用本身是线程安全的类,这样在很大程度上避免了未知的风险。


Java中你怎样唤醒一个阻塞的线程

这是个关于线程和阻塞的棘手的问题,它有很多解决方法。如果线程遇到了IO阻塞,我并且不认为有一种方法可以中止线程。如果线程因为调用wait()、sleep()、或者join()方法而导致的阻塞,你可以中断线程,并且通过抛出InterruptedException来唤醒它。我之前写的《How to deal with blocking methods in java》有很多关于处理线程阻塞的信息。

这个我们先简单粗暴地对某些阻塞方法进行分类:
- 会抛出InterruptedException的方法:wait、sleep、join、Lock.lockInterruptibly等,针对这类方法,我们在线程内部处理好异常(要不完全内部处理,要不把这个异常抛出去),然后就可以实现唤醒。
- 不会抛InterruptedException的方法:Socket的I/O,同步I/O,Lock.lock等。对于I/O类型,我们可以关闭他们底层的通道,比如Socket的I/O,关闭底层套接字,然后抛出异常处理就好了;比如同步I/O,关闭底层Channel然后处理异常。对于Lock.lock方法,我们可以改造成Lock.lockInterruptibly方法去实现。


什么是不可变对象,它对写并发应用有什么帮助

另一个多线程经典面试问题,并不直接跟线程有关,但间接帮助很多。这个java面试问题可以变的非常棘手,如果他要求你写一个不可变对象,或者问你为什么String是不可变的。

immutable Objects(不可变对象)就是那些一旦被创建,它们的状态就不能被改变的Objects,每次对他们的改变都是产生了新的immutable的对象,而mutable Objects(可变对象)就是那些创建后,状态可以被改变的Objects.

如何在Java中写出Immutable的类?
1. immutable对象的状态在创建之后就不能发生改变,任何对它的改变都应该产生一个新的对象。
2. immutable类的所有的属性都应该是final的。
3. 对象必须被正确的创建,比如:对象引用在对象创建过程中不能泄露(leak)。
4. 对象应该是final的,以此来限制子类继承父类,以避免子类改变了父类的immutable特性。
5. 如果类中包含mutable类对象,那么返回给客户端的时候,返回该对象的一个拷贝,而不是该对象本身(该条可以归为第一条中的一个特例)

使用Immutable类的好处:
1. Immutable对象是线程安全的,可以不用被synchronize就在并发环境中共享
2.Immutable对象简化了程序开发,因为它无需使用额外的锁机制就可以在线程间共享
3. Immutable对象提高了程序的性能,因为它减少了synchroinzed的使用
4. Immutable对象是可以被重复使用的,你可以将它们缓存起来重复使用,就像字符串字面量和整型数字一样。你可以使用静态工厂方法来提供类似于valueOf()这样的方法,它可以从缓存中返回一个已经存在的Immutable对象,而不是重新创建一个。

/**
 * 不可变对象
 * @author xuexiaolei
 * @version 2017年11月03日
 */
public class ImmutableObjectPerson {
    private final String name;
    private final String sex;

    public ImmutableObjectPerson(String name, String sex) {
        this.name = name;
        this.sex = sex;
    }

    public String getName() {
        return name;
    }
    public String getSex() {
        return sex;
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值