您的位置:首页 > 编程语言 > Java开发

java并发编程实战 java并发编程的艺术 阅读随笔

2016-06-03 21:31 330 查看
java线程池说明 http://www.oschina.net/question/565065_86540
java中断机制 http://ifeve.com/java-interrupt-mechanism/
Ask、现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?

join方法

如果一个线程A执行了thread.join()语句,其含义是当前线程A等待thread线程终止后才从thread.join()返回

join有两个超时特性的方法,如果在超时时间内thread还没有执行结束,则从该超时方法返回

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

java se 5之后,并发包中新增了Lock接口用来实现锁功能,提供与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。虽然缺少了synchronized的便捷性,单拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized不具备的特性。

lock接口在多线程和并发编程中最大的优势是它们为读和写分别提供了锁,它能满足你写像ConcurrentHashMap这样的高性能数据结构和有条件的阻塞。

我们可以分析一下jdk8中的读写锁的源码

在这之前,我们需要了解一下AbstractQueuedSynchronizer队列同步器,是用来构建锁或者其他同步组件的基础框架,它使用一个int成员变量表示同步状态,通过内置的FIFO队列完成资源获取线程的排队工作。

同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法来进行操作,getState()、setState(int newState)、compareAndSetState(int expect,int update),因为它们能保证状态的改变是安全的。

同步器一般是作为子类的内部静态类(待会儿详见读写锁实现),同步器自身没有实现任何同步接口,仅仅定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。

同步器有一些可以重写的方法,比如 tryAcquire独占式获取同步状态 tryRealease独占式释放同步状态 tryAcquireShared共享式获取同步状态 tryRealeaseShared共享式释放同步状态 isHeldExclusively 是否被当前线程所独占

还提供了一些模板方法,独占式获取同步状态、独占式释放同步状态、响应中断的、响应超时的等,还有共享式的一系列模板方法。

这些都是不同类型同步组件的基础。

我们来看一下ReentrantReadWriteLock的源码

public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
private static final long serialVersionUID = -6992448646407690164L;
/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** Performs all synchronization mechanics */
final Sync sync;

读写锁成员有readerLock、writeLock以及sync,都是ReentrantReadWriteLock的内部类

sync就是继承实现了同步器中的 tryAcquire、tryRealease、tryAcquireShared、tryRealeaseShared等方法,分别用于readerLock、writeLock使用

abstract static class Sync extends AbstractQueuedSynchronizer

protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}

以readLock为例

public static class ReadLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -5992448646407690164L;
private final Sync sync;

protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}

public void lock() {
sync.acquireShared(1);
}

public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}

public boolean tryLock() {
return sync.tryReadLock();
}

public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

public void unlock() {
sync.releaseShared(1);
}

public Condition newCondition() {
throw new UnsupportedOperationException();
}

public String toString() {
int r = sync.getReadLockCount();
return super.toString() +
"[Read locks = " + r + "]";
}
}


以上,我们可以了解,要实现高效缓存,多人读,一人写,就可以用ReentrantReadWriteLock,读取用读锁,写用写锁

既然读的时候可以多人访问,那么为什么还要加读锁呢?当然要加锁了,否则在写时去读,可能不正确-(写的时候不能去读)

读写锁的作用为,当我们加上写锁时,其他线程被阻塞,只有一个写操作在执行,当我们加上读锁后,它是不会限制多个读线程去访问的。也就是get和put之间是互斥的,put与任何线程均为互斥,但是get与get线程间并不是互斥的。其实加读写锁的目的是同一把锁的读锁既可以与写锁互斥,读锁之间还可以共享。

Ask、在java中wait和sleep方法的不同?

sleep()方法,属于Thread类中的。而wait()方法,则是属于Object类中的。

在调用sleep()方法的过程中,线程不会释放对象锁。

而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备

Wait通常被用于线程间交互,sleep通常被用于暂停执行

Ask、用Java实现阻塞队列

阻塞队列是一个支持两个附加操作的队列,即支持阻塞的插入和移除方法

java最新jdk中目前有如下几种阻塞队列

ArrayBlockingQueue 一个由数组结构组成的有界阻塞队列,按照FIFO原则对元素进行排序

LinkedBlockingQueue 一个由链表结构组成的有界阻塞队列,FIFO

PriorityBlockingQueue 一个支持优先级排序的无界阻塞队列,可以自定义类实现compareTo()方法指定元素排序规则,或者促使或PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序

DelayQueue 一个使用优先级队列实现的无界阻塞队列,使用PriorityQueue实现,队列中元素必须实现Delayed接口,创建元素时可以指定多久才能从队列中获取当前元素,只有在延迟期满才能从队列中提取元素。用于缓存系统的设计(保存缓存元素的有效期)、定时任务调度(保存当天将会执行的任务以及执行时间,一旦从DelayQueue中获取到任务就开始执行。TimerQueue就是使用DelayQueue实现的)

SynchronousQueue 一个不存储元素的阻塞队列,每个put操作必须等待一个take操作,否则不能继续添加元素。支持公平访问队列,默认情况下线程采用非公平策略访问队列,构造时可以通过构造参数指定公平访问

LinkedTransferQueue 一个由链表结构组成的无界阻塞队列,多了tryTransfer和transfer方法。

transfer方法,如果当前有消费者正在等待接收元素,transfer方法可以把生产者传入的元素立刻transfer给消费者。如果没有消费者在等待,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回。

tryTransfer方法,用来试探生产者传入的元素是否能直接传给消费者

LinkedBlockingDeque 一个由链表结构组成的双向阻塞队列。队列两端都可以插入和移除元素,双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。初始化时可以设置容量防止其过度膨胀。

自己实现阻塞队列时,可以用Object的wait()方法、notify()方法或者Lock中Condition的await()、signal()方法,他们都可以实现等待/通知模式

wait()和notify()必须在synchronized的代码块中使用 因为只有在获取当前对象的锁时才能进行这两个操作 否则会报异常

而await()和signal()一般与Lock()配合使用(Condition con = lock.newCondition(); lock.lock();con.await() ),也必须先lock.lock()或者lock.lockInterruptibly()获取锁之后,才能await或者signal,否则会报异常

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

与阻塞队列类似,也可以直接用阻塞队列来实现

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

原子操作的描述是: 多个线程执行一个操作时,其中任何一个线程要么完全执行完此操作,要么没有执行此操作的任何步骤 ,那么这个操作就是原子的。

Java中的原子操作包括:

1)除long和double之外的基本类型的赋值操作

2)所有引用reference的赋值操作

3)java.concurrent.Atomic.* 包中所有类的一切操作。

但是java对long和double的赋值操作是非原子操作!!long和double占用的字节数都是8,也就是64bits。在32位操作系统上对64位的数据的读写要分两步完成,每一步取32位数据。这样对double和long的赋值操作就会有问题:如果有两个线程同时写一个变量内存,一个进程写低32位,而另一个写高32位,这样将导致获取的64位数据是失效的数据。因此需要使用volatile关键字来防止此类现象。volatile本身不保证获取和设置操作的原子性,仅仅保持修改的可见性。但是java的内存模型保证声明为volatile的long和double变量的get和set操作是原子的,具体后面再分析。(from http://www.iteye.com/topic/213794

jdk1.5开始提供atomic包,里面有13个原子操作类,4中类型,基本都是使用Unsafe实现的包装类。Unsafe是jni方法

原子更新基本类型类 AtomicBoolean 原子更新布尔类型 AtomicInteger 原子更新整型 AtomicLong 原子更新长整型

原子更新数组 AtomicIntegerArray 原子更新整型数组里的元素 AtomicLongArray 原子更新长整型数组里的元素 AtomicReferenceArray 原子更新引用类型数组里的元素

原子更新引用类型 AtomicReference 原子更新引用类型 AtomicReferenceFieldUpdater 原子更新引用类型里的字段 AtomicMarkableReference 原则更新带有标记位的引用类型

原子更新字段类 AtomicIntegerFieldUpdater 原子更新整型的字段的更新器 AtomicLongFieldUpdater 原子更新长整型的字段的更新器 AtomicStampedUpdater 原子更新带有版本号的引用类型

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

如果一个字段被声明为volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。保证了共享变量的可见性,当一个线程修改一个共享变量时,另外一个线程能读到这个修改后的值。

JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程读/写共享变量的副本。(本地内存是JMM的一个抽象概念,并不真实存在,涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化)

JMM通过控制主内存与每个线程的本地内存之间的交互,来提供内存可见性。

执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。从java源代码到最终实际执行的指令序列,会分别经历3种重排序

1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

2)指令级并行的重排序。现代处理器采用指令级并行技术ILP,将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,使得加载和存储操作看上去可能是在乱序执行。

对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序。对于处理器,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障指令,来禁止特定类型的处理器重排序。

happens-before规则中有一条

volatile变量规则:对于一个volatile域的写,happens-before于任意后续对这个volatile域的读。

注:两个操作之间具有happens-before关系,并不意味着前一个操作要在后一个操作之前执行!仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。

即JMM允许的重排序可以发生在两个happens-before操作上。

理解volatile特性,可以把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。即get与set方法都加上synchronized

锁的happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性。这意味着,对一个volatile变量的读,总是能看到任意线程对这个volatile变量最后的写入。

所得语义决定了临界区代码的执行具有原子性,这意味着,即使是64位的long型和double型变量,只要是volatile变量,对该变量的读/写就具有原子性。

简而言之,volatile具有如下特性

1)可见性,对一个volatile变量的读,总是能看到任意线程对这个volatile变量最后的写入

2)原子性,对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性

jdk5开始,volatile写与锁的释放有相同内存语义,volatile读与锁的获取有相同内存语义。

volatile写的内存语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存

volatile读的内存语义:当读一个valatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。(内存屏障,一组处理器指令,用于实现对内存操作的顺序限制)

每个volatile写操作前面插入一个StoreStore屏障,保证在volatile写之前,其前面的所有普通写操作已经对任意处理器可见,因为StoreStore屏障会将上面所有的普通写在volatile写之前刷新到主内存。

每个volatile写操作后面插入一个StoreLoad屏障,此屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序

每个volatile读操作后面插入一个LoadLoad屏障,用来禁止处理器把上面的volatile读与下面的普通读重排序。

每个volatile读操作后面插入一个LoadStore屏障,用来禁止处理器把上面的volatile读与下面的普通写重排序。

Ask、什么是竞争条件?你怎样发现和解决竞争?

多个线程或者进程在读写一个共享数据时结果依赖于它们执行的相对时间,这种情形叫做竞争。

竞争条件发生在当多个进程或者线程在读写数据时,其最终的的结果依赖于多个进程的指令执行顺序。

举一个例子:

我们平常编程经常遇到的修改某个字段,这个操作在库存那里尤为突出,当两个单子同时修改库存的时候,这时就形成了竞争条件,如果不做同步处理,这里十有八九就是错误的了,因为如果两个单子同时出库,而出库的数量刚好大于库存数量,这里就会出现问题。(当然,还有几种情况会出现问题,我们这里只是为了举一个竞争条件的例子)

再比如多个线程操作A账户往B账户转账,如果没做同步处理,最后会发现,钱总账对不上。

发现:有共享变量时会发生竞争

解决:进行同步处理,原子操作

Ask、你将如何使用thread dump?你将如何分析Thread dump?

在UNIX中你可以使用kill -3,然后thread dump将会打印日志,在windows中你可以使用”CTRL+Break”。非常简单和专业的线程面试问题,但是如果他问你怎样分析它,就会很棘手。

dump 文件里,值得关注的线程状态有:

死锁,Deadlock(重点关注)

执行中,Runnable

等待资源,Waiting on condition(重点关注)

等待获取监视器,Waiting on monitor entry(重点关注)

暂停,Suspended

对象等待中,Object.wait() 或 TIMED_WAITING

阻塞,Blocked(重点关注)

停止,Parked

Ask、为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?

这是另一个非常经典的java多线程面试问题。这也是我刚开始写线程程序时候的困惑。现在这个问题通常在电话面试或者是在初中级Java面试的第一轮被问到。这个问题的回答应该是这样的,当你调用start()方法时你将创建新的线程,并且执行在run()方法里的代码。但是如果你直接调用run()方法,它不会创建新的线程也不会执行调用线程的代码。阅读我之前写的《start与run方法的区别》这篇文章来获得更多信息。

1) start:
  用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里方法 run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。
2) run:
  run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。这两个方法应该都比较熟悉,把需要并行处理的代码放在run()方法中,start()方法启动线程将自动调用 run()方法,这是由jvm的内存机制规定的。并且run()方法必须是public访问权限,返回值类型为void.。

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

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

Ask、在Java中CycliBarriar和CountdownLatch有什么区别?

这个线程问题主要用来检测你是否熟悉JDK5中的并发包。这两个的区别是CyclicBarrier可以重复使用已经通过的障碍,而CountdownLatch不能重复使用。

等待多线程完成的CountdownLatch

允许一个或多个线程等待其他线程完成操作。JDK1.5之后的并发包中提供的CountdownLatch可以实现join的功能,并且比join的功能更多。

比如定义一个CountDownLatch c = new CountDownLatch(n);

n可以代表n个线程,每个线程执行的最后加上c.countDown(),n会减一。

另一个线程需要等待这n个线程执行结束,就加上c.await(),则该线程阻塞,直到n变成0,即n个线程都执行完毕。如果不想让该线程阻塞太长时间,则可以通过await(long time,TimeUnit unit)方法指定时间,等待特定时间后,就不再阻塞。

一个线程调用countDown方法happens-before另外一个线程调用await方法

注:计数器必须大于等于0,只是等于0时,计数器就是零,调用await方法时不会阻塞当前线程。CountDownLatch不可能重新初始化或者修改内部计数器的值。

同步屏障CyclicBarrier

字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续进行。

CyclicBarrier默认的构造方法CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程阻塞。

CyclicBarrier还提供一个更高级的构造函数CyclicBarrier(int parties,Runnable barrierAction),用于在线程到达屏障时,优先执行barrierAction就方便处理更复杂的业务场景。比如分别处理每个文件中的数据,最后通过barrierAction来对数据进行汇总。

区别:CountDownLatch的计数器只能使用一次,而CyclicBarrier得计数器可以使用reset()方法重置,所以,CyclicBarrier能处理更为复杂的业务场景。比如,如果计算发生错误,可以重置计数器,并让线程重新执行一次。

CyclicBarrier还提供其他有用的方法,用来获得阻塞的线程数量以及了解阻塞的线程是否被中断等。

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

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

不可变对象(immutable objects),后面文章我将使用immutable objects来代替不可变对象!

那么什么是immutable objects?什么又是mutable Objects呢?

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

举个例子:String和StringBuilder,String是immutable的,每次对于String对象的修改都将产生一个新的String对象,而原来的对象保持不变,而StringBuilder是mutable,因为每次对于它的对象的修改都作用于该对象本身,并没有产生新的对象。

但有的时候String的immutable特性也会引起安全问题,这就是密码应该存放在字符数组中而不是String中的原因!

immutable objects 比传统的mutable对象在多线程应用中更具有优势,它不仅能够保证对象的状态不被改变,而且还可以不使用锁机制就能被其他线程共享。

实际上JDK本身就自带了一些immutable类,比如String,Integer以及其他包装类。为什么说String是immutable的呢?比如:java.lang.String 的trim,uppercase,substring等方法,它们返回的都是新的String对象,而并不是直接修改原来的对象。

如何在Java中写出Immutable的类?

要写出这样的类,需要遵循以下几个原则:

1)immutable对象的状态在创建之后就不能发生改变,任何对它的改变都应该产生一个新的对象。

2)Immutable类的所有的属性都应该是final的。

3)对象必须被正确的创建,比如:对象引用在对象创建过程中不能泄露(leak)。

4)对象应该是final的,以此来限制子类继承父类,以避免子类改变了父类的immutable特性。

5)如果类中包含mutable类对象,那么返回给客户端的时候,返回该对象的一个拷贝,而不是该对象本身(该条可以归为第一条中的一个特例)

当然不完全遵守上面的原则也能够创建immutable的类,比如String的hashcode就不是final的,但它能保证每次调用它的值都是一致的,无论你多少次计算这个值,它都是一致的,因为这些值的是通过计算final的属性得来的!

另外,如果你的Java类中存在很多可选的和强制性的字段,你也可以使用建造者模式来创建一个immutable的类。

下面是一个例子:

public final class Contacts {


private final String name;


private final String mobile;


public Contacts(String name, String mobile) {


this.name = name;
this.mobile = mobile;


}


public String getName(){


return name;


}


public String getMobile(){


return mobile;


}


}


我们为类添加了final修饰,从而避免因为继承和多态引起的immutable风险。

上面是最简单的一种实现immutable类的方式,可以看到它的所有属性都是final的。

有时候你要实现的immutable类中可能包含mutable的类,比如java.util.Date,尽管你将其设置成了final的,但是它的值还是可以被修改的,为了避免这个问题,我们建议返回给用户该对象的一个拷贝,这也是Java的最佳实践之一。下面是一个创建包含mutable类对象的immutable类的例子:

public final class ImmutableReminder{


private final Date remindingDate;


public ImmutableReminder (Date remindingDate) {


if(remindingDate.getTime() < System.currentTimeMillis()){


throw new IllegalArgumentException("Can not set reminder” +
“ for past time: " + remindingDate);


}


this.remindingDate = new Date(remindingDate.getTime());


}


public Date getRemindingDate() {


return (Date) remindingDate.clone();


}


}


上面的getRemindingDate()方法可以看到,返回给用户的是类中的remindingDate属性的一个拷贝,这样的话如果别人通过getRemindingDate()方法获得了一个Date对象,然后修改了这个Date对象的值,那么这个值的修改将不会导致ImmutableReminder类对象中remindingDate值的修改。

使用Immutable类的好处:
1)Immutable对象是线程安全的,可以不用被synchronize就在并发环境中共享

2)Immutable对象简化了程序开发,因为它无需使用额外的锁机制就可以在线程间共享

3)Immutable对象提高了程序的性能,因为它减少了synchroinzed的使用

4)Immutable对象是可以被重复使用的,你可以将它们缓存起来重复使用,就像字符串字面量和整型数字一样。你可以使用静态工厂方法来提供类似于valueOf()这样的方法,它可以从缓存中返回一个已经存在的Immutable对象,而不是重新创建一个。

immutable也有一个缺点就是会制造大量垃圾,由于他们不能被重用而且对于它们的使用就是”用“然后”扔“,字符串就是一个典型的例子,它会创造很多的垃圾,给垃圾收集带来很大的麻烦。当然这只是个极端的例子,合理的使用immutable对象会创造很大的价值。

看完以上的分析之后,多次提到final

对于final域,编译器和处理器遵守两个重排序规则。

1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

2)初次读一个final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

final域的重排序规则可以确保:在引用变量为任意线程可见之前,该引用变量指向的对象的final域已经在构造函数中被正确初始化了。其实,要得到这个效果,还需要一个保证:在构造函数内部,不能让这个被构造对象的引用为其他线程所见,也就是对象引用不能再构造函数中“溢出”。

因为在构造函数返回前,被构造对象的引用不能为其他线程所见,因为此时的final域可能还没有被初始化。在构造函数返回后,任意线程都将保证能看到final域正确初始化之后的值。

旧的内存模型中一个缺陷就是final域的值会改变,JDK5之后,增强了final的语义,增加了写和读重排序规则,可以为java程序员提供初始化安全保证:只要对象是正确构造的(被构造对象的引用在构造函数中没有溢出),那么不需要使用同步,就可以保证任意线程都能看到这个final域在构造函数中被初始化之后的值。

Ask、你在多线程环境中遇到的常见的问题是什么?你是怎么解决它的?

多线程和并发程序中常遇到的有Memory-interface、竞争条件、死锁、活锁和饥饿。问题是没有止境的,如果你弄错了,将很难发现和调试。这是大多数基于面试的,而不是基于实际应用的Java线程问题。

Ask、在java中绿色线程和本地线程区别?

1.什么是绿色线程?

绿色线程(Green Thread)是一个相对于操作系统线程(Native Thread)的概念。
操作系统线程(Native Thread)的意思就是,程序里面的线程会真正映射到操作系统的线程,线程的运行和调度都是由操作系统控制的
绿色线程(Green Thread)的意思是,程序里面的线程不会真正映射到操作系统的线程,而是由语言运行平台自身来调度。
当前版本的Python语言的线程就可以映射到操作系统线程。当前版本的Ruby语言的线程就属于绿色线程,无法映射到操作系统的线程,因此Ruby语言的线程的运行速度比较慢。
难道说,绿色线程要比操作系统线程要慢吗?当然不是这样。事实上,情况可能正好相反。Ruby是一个特殊的例子。线程调度器并不是很成熟。
目前,线程的流行实现模型就是绿色线程。比如,stackless Python,就引入了更加轻量的绿色线程概念。在线程并发编程方面,无论是运行速度还是并发负载上,都优于Python。
另一个更著名的例子就是ErLang(爱立信公司开发的一种开源语言)。
ErLang的绿色线程概念非常彻底。ErLang的线程不叫Thread,而是叫做Process。这很容易和进程混淆起来。这里要注意区分一下。
ErLang Process之间根本就不需要同步。因为ErLang语言的所有变量都是final的,不允许变量的值发生任何变化。因此根本就不需要同步。
final变量的另一个好处就是,对象之间不可能出现交叉引用,不可能构成一种环状的关联,对象之间的关联都是单向的,树状的。因此,内存垃圾回收的算法效率也非常高。这就让ErLang能够达到Soft Real Time(软实时)的效果。这对于一门支持内存垃圾回收的语言来说,可不是一件容易的事情

2.Java世界中的绿色线程

所谓绿色线程更多的是一个逻辑层面的概念,依赖于虚拟机来实现。操作系统对于虚拟机内部如何进行线程的切换并不清楚,从虚拟机外部来看,或者说站在操作系统的角度看,这些都是不可见的。可以把虚拟机看作一个应用程序,程序的代码本身来建立和维护针对不同线程的堆栈,指令计数器和统计信息等等。这个时候的线程仅仅存在于用户级别的应用程序中,不需要进行系统级的调用,也不依赖于操作系统为线程提供的具体功能。绿色线程主要是为了移植方便,但是会增加虚拟机的复杂度。总的来说,它把线程的实现对操作系统屏蔽,处在用户级别的实现这个层次上。绿色线程模型的一个特点就是多CPU也只能在某一时刻仅有一个线程运行。
本机线程简单地说就是和操作系统的线程对应,操作系统完全了解虚拟机内部的线程。对于windows操作系统,一个java虚拟机的线程对应一个本地线程,java线程调度依赖于操作系统线程。对于solaris,复杂一些,因为后者本身提供了用户级和系统级两个层次的线程库。依赖于操作系统增加了对于平台的依赖性,但是虚拟机实现相对简单些,而且可以充分利用多CPU实现多线程同时处理。

Ask、线程与进程的区别?

Ask、 什么是多线程中的上下文切换?

即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程同时执行,时间片一般为几十毫秒ms。

CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片之后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。

Ask、死锁与活锁的区别,死锁与饥饿的区别?

活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。 活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。
活锁可以认为是一种特殊的饥饿。 下面这个例子在有的文章里面认为是活锁。实际上这只是一种饥饿。因为没有体现出“活”的特点。 假设事务T2再不断的重复尝试获取锁R,那么这个就是活锁。
如果事务T1封锁了数据R,事务T2又请求封锁R,于是T2等待。T3也请求封锁R,当T1释放了R上的封锁后,系统首先批准了T3的请求,T2仍然等待。然后T4又请求封锁R,当T3释放了R上的封锁之后,系统又批准了T4的请求......T2可能永远等待。
活锁应该是一系列进程在轮询地等待某个不可能为真的条件为真。活锁的时候进程是不会blocked,这会导致耗尽CPU资源。
解决协同活锁的一种方案是调整重试机制。

比如引入一些随机性。例如如果检测到冲突,那么就暂停随机的一定时间进行重试。这回大大减少碰撞的可能性。 典型的例子是以太网的CSMA/CD检测机制。
另外为了避免可能的死锁,适当加入一定的重试次数也是有效的解决办法。尽管这在业务上会引起一些复杂的逻辑处理。
比如约定重试机制避免再次冲突。 例如自动驾驶的防碰撞系统(假想的例子),可以根据序列号约定检测到相撞风险时,序列号小的飞机朝上飞, 序列号大的飞机朝下飞。

死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

死锁发生的条件

互斥条件:线程对资源的访问是排他性的,如果一个线程对占用了某资源,那么其他线程必须处于等待状态,直到资源被释放。

请求和保持条件:线程T1至少已经保持了一个资源R1占用,但又提出对另一个资源R2请求,而此时,资源R2被其他线程T2占用,于是该线程T1也必须等待,但又对自己保持的资源R1不释放。

不剥夺条件:线程已获得的资源,在未使用完之前,不能被其他线程剥夺,只能在使用完以后由自己释放。

环路等待条件:在死锁发生时,必然存在一个“进程-资源环形链”,即:{p0,p1,p2,...pn},进程p0(或线程)等待p1占用的资源,p1等待p2占用的资源,pn等待p0占用的资源。(最直观的理解是,p0等待p1占用的资源,而p1而在等待p0占用的资源,于是两个进程就相互等待)

避免死锁方法:一次封锁法和 顺序封锁法。

一次封锁法要求每个事务必须一次将所有要使用的数据全部加锁,否则就不能继续执行。
一次封锁法虽然可以有效地防止死锁的发生,但也存在问题,一次就将以后要用到的全部数据加锁,势必扩大了封锁的范围,从而降低了系统的并发度。

顺序封锁法是预先对数据对象规定一个封锁顺序,所有事务都按这个顺序实行封锁。
顺序封锁法可以有效地防止死锁,但也同样存在问题。事务的封锁请求可以随着事务的执行而动态地决定,很难事先确定每一个事务要封锁哪些对象,因此也就很难按规定的顺序去施加封锁。

什么是活锁
活锁:是指线程1可以使用资源,但它很礼貌,让其他线程先使用资源,线程2也可以使用资源,但它很绅士,也让其他线程先使用资源。这样你让我,我让你,最后两个线程都无法使用资源。
避免活锁的简单方法是采用先来先服务的策略。当多个事务请求封锁同一数据对象时,封锁子系统按请求封锁的先后次序对事务排队,数据对象上的锁一旦释放就批准申请队列中第一个事务获得锁。
什么是饥饿
饥饿:是指如果线程T1占用了资源R,线程T2又请求封锁R,于是T2等待。T3也请求资源R,当T1释放了R上的封锁后,系统首先批准了T3的请求,T2仍然等待。然后T4又请求封锁R,当T3释放了R上的封锁之后,系统又批准了T4的请求......,T2可能永远等待。

Ask、Java中用到的线程调度算法是什么?

JVM调度的模式有两种:分时调度和抢占式调度。

分时调度是所有线程轮流获得CPU使用权,并平均分配每个线程占用CPU的时间;

抢占式调度是根据线程的优先级别来获取CPU的使用权。JVM的线程调度模式采用了抢占式模式。既然是抢占调度,那么我们就能通过设置优先级来“有限”的控制线程的运行顺序,注意“有限”一次。

Ask、 在Java中什么是线程调度?

1、首先简单说下java内存模型:Java中所有变量都储存在主存中,对于所有线程都是共享的(因为在同一进程中),每个线程都有自己的工作内存或本地内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,而线程之间无法相互直接访问,变量传递均需要通过主存完成,但是在程序内部可以互相调用(通过对象方法),所有线程间的通信相对简单,速度也很快。



java内存模型

2、进程间的内部数据和状态都是相互完全独立的,因此进程间通信大多数情况是必须通过网络实现。线程本身的数据,通常只有寄存器数据,以及一个程序执行时使用的堆栈,所以线程的切换比进程切换的负担要小。

3、CPU对于各个线程的调度是随机的(分时调度),在Java程序中,JVM负责线程的调度。 线程调度是指按照特定的机制为多个线程分配CPU的使用权,也就是实际执行的时候是线程,因此CPU调度的最小单位是线程,而资源分配的最小单位是进程。

Ask、在线程中你怎么处理不可捕捉异常?

在java多线程程序中,所有线程都不允许抛出未捕获的checked exception,也就是说各个线程需要自己把自己的checked exception处理掉。这一点是通过java.lang.Runnable.run()方法声明(因为此方法声明上没有throw exception部分)进行了约束。但是线程依然有可能抛出unchecked exception,当此类异常跑抛出时,线程就会终结,而对于主线程和其他线程完全不受影响,且完全感知不到某个线程抛出的异常(也是说完全无法catch到这个异常)。JVM的这种设计源自于这样一种理念:“线程是独立执行的代码片断,线程的问题应该由线程自己来解决,而不要委托到外部。”基于这样的设计理念,在Java中,线程方法的异常(无论是checked还是unchecked exception),都应该在线程代码边界之内(run方法内)进行try catch并处理掉.

但如果线程确实没有自己try catch某个unchecked exception,而我们又想在线程代码边界之外(run方法之外)来捕获和处理这个异常的话,java为我们提供了一种线程内发生异常时能够在线程代码边界之外处理异常的回调机制,即Thread对象提供的setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)方法。

通过该方法给某个thread设置一个UncaughtExceptionHandler,可以确保在该线程出现异常时能通过回调UncaughtExceptionHandler接口的public void uncaughtException(Thread t, Throwable e) 方法来处理异常,这样的好处或者说目的是可以在线程代码边界之外(Thread的run()方法之外),有一个地方能处理未捕获异常。但是要特别明确的是:虽然是在回调方法中处理异常,但这个回调方法在执行时依然还在抛出异常的这个线程中!另外还要特别说明一点:如果线程是通过线程池创建,线程异常发生时UncaughtExceptionHandler接口不一定会立即回调。

比之上述方法,还有一种编程上的处理方式可以借鉴,即,有时候主线程的调用方可能只是想知道子线程执行过程中发生过哪些异常,而不一定会处理或是立即处理,那么发起子线程的方法可以把子线程抛出的异常实例收集起来作为一个Exception的List返回给调用方,由调用方来根据异常情况决定如何应对。不过要特别注意的是,此时子线程早以终结。

Ask、 什么是线程组,为什么在Java中不推荐使用?

Ask、为什么使用Executor框架比使用应用创建和管理线程好?

Ask、 在Java中Executor和Executors的区别?

Ask、 如何在Windows和Linux上查找哪个线程使用的CPU时间最长?

windows上面用任务管理器看,linux下可以用top 这个工具看。

当然如果你要查找具体的进程,可以用ps命令,比如查找java:
ps -ef |grep java
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: