多线程知识点

什么是Java内存模型?

Java 内存模型是通过各种操作来定义,包括对变量的读/写操作,监视器的加锁和释放操作,以及线程启动和合并操作。JMM为程序中所有的操作定义了一个偏序关系,称之为Happens-Before。要想保证操作B的线程能看到操作A的结果(无论A和B是否在同一个线程中执行),那么在A和B之间必须满足Happens-Before关系。如果两个操作之间缺乏Happens-Before关系,那么JVM可以对它们任意地排序。

Java内存模型说明了某个线程的内存操作在哪些情况下对于其他线程是可见的

在正确同步的程序中不存在数据竞争,并会表现出串行一致性,这意味着程序中的所有操作都会按照一种固定和全局的顺序执行。

Happens-Before规则包括:

程序顺序规则:如果程序中操作A在操作B之前,那么线程中操作A将在B操作之前执行。

监视器规则:在监视器锁上的解锁操作,必须在同一个监视器锁上的加锁操作之前执行。

volatile 变量规则:对 volatile 变量的写入操作必须在对该变量的读操作之前执行。原子变量与 volatile 变量在读操作和写操作上有着相同的内存语义。

线程启动规则:在线程上对 Thread.start() 的调用必须在该线程中执行任何操作之前执行。

线程结束规则:线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行。

中断规则规则:当一个线程在另一个线程上调用 interrupt 时,必须在被中断线程检测到 interrupt调用之前执行。

终结器规则:对象的构造函数必须在启动该对象的终结器之前执行完成。

传递性规则:如果操作A在操作B之前执行,并且操作B在操作C之前执行,那么操作A必须在操作C之前执行。

进程和线程有什么不同?

一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用。

线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而线程可以共享进程中的内存空间。每个线程都拥有单独的栈内存用来存储本地数据。

多线程编程的好处是什么?

在多线程程序中,多个线程被并发的执行以提高程序的效率,CPU不会因为某个线程需要等待资源而进入空闲状态。多个线程共享堆内存,因此创建多个线程去执行一些任务会比创建多个进程更好。举个例子,Servlets比CGI更好,是因为Servlets支持多线程而CGI不支持。

什么是原子操作?在Java Concurrency API中有哪些原子类(atomic classes)?

原子操作是指一个不受其它操作影响的操作任务单元。原子操作是在多线程环境下避免数据不一致必须的手段。

int++并不是一个原子操作,需要经历“读取-赋值-写入”,所以当一个线程读取它的值并加1时,另外一个线程有可能会读到之前的值,这就会引发错误。

什么是CAS?ABA?

CAS(Compare and Swap),从内存领域来说这是乐观锁,因为它在对共享变量更新之前会先比较当前值是否与更新前的值一致,如果是,则更新,如果不是,则无限循环执行(称为自旋),直到当前值与更新前的值一致为止,才执行更新。
简单的来说,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则返回V。

在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现。

ABA:在某些算法中如果V中值先由A变为B,再由B变为A,那么仍然认为发生了变化,并需要重新执行算法中的某些步骤。

为了解决ABA问题,一个解决方案是:不只是更新内存V中的值,而是更新两个值,包含一个引用和一个版本号。即使值A->B->A,版本号也将不同,无需重复执行。

什么是死锁(Deadlock)?如何分析和避免死锁?

死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

死锁的发生必须满足以下四个条件:

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

避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁。

分析死锁,需要查看Java应用程序的线程栈。需要找出那些状态为BLOCKED的线程和它们等待的资源。每个资源都有一个唯一的id,用这个id可以找出哪些线程已经拥有了它的对象锁。

避免嵌套锁,只在需要的地方使用锁和避免无限期等待是避免死锁的通常办法。如何分析死锁

什么是竞态条件和临界区?

当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。

导致竞态条件发生的代码区称作临界区。在临界区中使用适当的同步就可以避免竞态条件。

什么是线程优先级?

每一个线程都有优先级,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现和操作系统相关。可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个int变量(从1-10),1代表最低优先级,10代表最高优先级。

什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing)?

线程调度器是一个操作系统服务,它负责为Runnable状态的线程分配CPU时间。一旦创建一个线程并启动它,它的执行便依赖于线程调度器的实现。

时间分片是指将可用的CPU时间分配给可用的Runnable线程的过程。分配CPU时间可以基于线程优先级或者线程等待的时间。线程调度并不受到Java虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让程序依赖于线程的优先级)。

什么是上下文切换(context-switching)?

CPU给每个任务都服务一定的时间,然后把当前任务的状态保存下来,在加载下一任务的状态后,继续服务下一任务。任务的状态保存及再加载,这段过程就叫做上下文切换。

上下文切换是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。上下文切换是多任务操作系统和多线程环境的基本特征。

如何确保线程安全?

在Java中可以有很多方法来保证线程安全:同步(synchronized、lock、ReentrantLock等),原子类(atomic concurrent classes),实现并发锁,使用volatile关键字,使用不变类和线程安全类。线程安全教程

有哪些不同的线程生命周期?

新建一个线程时,它的状态是New调用线程的start()方法时,状态被改变为Runnable。线程调度器会为Runnable线程池中的线程分配CPU时间并且将它们的状态改变为Running。其它的线程状态还有Waiting,Blocked 和Dead。读这篇文章可以了解更多关于线程生命周期的知识。

Java语言定义了5种线程状态,在任意一个时间点,一个线程只能有且只有其中的一种状态,这5种状态分别如下。

  • 新建(New):创建后尚未启动的线程处于这种状态。
  • 运行(Runable):Runable包括了操作系统线程状态中的Running和Ready,也就是处于此状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执行时间。
  • 无限期等待(Waiting):处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显式地唤醒。以下方法会让线程陷入无限期的等待状态:
  1. 没有设置Timeout参数的Object.wait()方法。
  2. 没有设置Timeout参数的Thread.join()方法。
  3. LockSupport.park()方法。
  • 限期等待(Timed Waiting):处于这种状态的线程也不会被分配CPU执行时间,不过无须等待被其他线程显式地唤醒,在一定时间之后它们会由系统自动唤醒。以下方法会让线程进入限期等待状态:
  1. Thread.sleep()方法。
  2. 设置了Timeout参数的Object.wait()方法。
  3. 设置了Timeout参数的Thread.join()方法。
  4. LockSupport.parkNanos()方法。
  5. LockSupport.parkUntil()方法。
  • 阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是:“阻塞状态”在等待着获取到一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。
  • 结束(Terminated):已终止线程的线程状态,线程已经结束执行。

用户线程和守护线程有什么区别?

在Java程序中创建一个线程,它就被称为用户线程。

守护线程是在后台执行并且不会阻止JVM终止的线程。当没有用户线程在运行的时候,JVM关闭程序并且退出。一个守护线程创建的子线程依然是守护线程。

守护线程最典型的应用就是 GC (垃圾回收器)。

如何创建一个线程?

有两种创建线程的方法:一是实现Runnable接口,然后将它传递给Thread的构造函数,创建一个Thread对象;二是直接继承Thread类。Java中创建线程

用Runnable还是Thread?

Java不支持类的多重继承,但允许实现多个接口。所以如果要继承其它类,实现Runnable接口更佳。

如何创建守护线程?

使用Thread类的setDaemon(true)方法可以将线程设置为守护线程,需要注意的是,需要在调用start()方法前调用这个方法,否则会抛出IllegalThreadStateException异常。

怎么检测一个线程是否拥有锁?

在java.lang.Thread中有一个方法叫holdsLock(),如果当且仅当当前线程拥有某个具体对象的锁,它返回true。

可以直接调用Thread类的run()方法么?

当然可以,但是如果调用Thread的run()方法,它的行为就会和普通的方法一样,只会是在原来的线程中调用,没有新的线程启动。

调用start()方法才会启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。

可以多次调用Thread的start()方法吗?

调用start方法后线程状态不再是新建状态(NEW),而是变成运行态,所以如果对一个线程对象多次调用start方法的话,会产生:IllegalThreadStateException异常。

/**
 * Causes this thread to begin execution; the Java Virtual Machine
 * calls the run method of this thread.
 *
 * The result is that two threads are running concurrently: the
 * current thread (which returns from the call to the start method)
 * and the other thread (which executes its run method).
 * 
 * It is never legal to start a thread more than once.
 * In particular, a thread may not be restarted once it has completed
 * execution.
 *
 * @exception  IllegalThreadStateException  if the thread was already
 *             started.
 * @see        #run()
 * @see        #stop()
 */
public synchronized void start() {
    /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
     */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
     * so that it can be added to the group's list of threads
     * and the group's unstarted count can be decremented. */
    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}

private native void start0();

如何确保main()方法所在的线程是Java程序最后结束的线程?

调用某线程A的join()方法,将该线程与当前线程B合并,即等待该线程A执行完毕后再继续执行线程B。

当线程B调用线程A的join方法时,线程B必须等待线程A执行完毕,线程B才能继续往下执行。join方法主要用来将大问题分解成小问题,当小问题计算完成时,大问题才能继续往下执行,这时候我们就可以利用join方法了(这种设计思想在开发复杂的程序时一定要掌握)。

可以使用Thread类的join()方法来确保所有程序创建的线程在main()方法退出前结束。Thread类的join()方法

线程之间是如何通信的?

当线程间是可以共享资源时,线程间通信是协调它们的重要的手段。Object类中wait()\notify()\notifyAll()方法可以用于线程间通信关于资源的锁的状态。

为什么线程通信的方法wait()、notify()和notifyAll()被定义在Object类里?

很明显的原因是Java提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。

Java的每个对象中都有一个锁(monitor,也可以称为监视器) ,并且wait()、notify()、notifyAll()等方法用于等待对象的锁或者通知其它线程对象的监视器可用。在Java的线程中并没有可供任何对象使用的锁和同步器。这就是为什么这些方法是Object类的一部分,这样Java的每一个类都有用于线程间通信的基本方法。

为什么wait()、notify()和notifyAll()必须在同步方法或者同步块中被调用?

当一个线程需要调用对象的wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其它线程调用这个对象上的notify()方法。同样的,当一个线程需要调用对象的notify()方法时,它会释放这个对象的锁,以便其它在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以它们只能在同步方法或者同步块中被调用,否则会抛出IllegalMonitorStateException异常。

notify() 和 notifyAll()有什么区别?

notify()方法不能唤醒某个具体的线程,所以只有一个线程在等待的时候它才有用武之地。

而notifyAll()唤醒所有线程并允许它们争夺锁,确保了至少有一个线程能继续运行。

1)notify()和notifyAll()都是Object对象用于通知处在等待该对象的线程的方法。

2)void notify():唤醒一个正在等待该对象的线程不包括当前执行notify()的线程

3)void notifyAll():唤醒所有正在等待该对象的线程,让他们去竞争获取锁。

两者的最大区别在于:

notifyAll()唤醒所有线程,使所有原来在该对象上等待被notify的线程统统退出wait的状态,变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。

notify()只是选择一个wait状态线程进行通知,并使它获得该对象上的锁,但不惊动其他同样在等待被该对象notify的线程们。

如何暂停正在运行的线程?

可以使用Thread类的Sleep()方法让线程暂停一段时间。需要注意的是,这并不会让线程终止,一旦从休眠中唤醒线程,线程的状态将会被改变为Runnable,并且根据线程调度,它将得到执行。

如何停止一个线程?

当不阻塞时候设置一个标志位,让代码块正常运行结束并停止线程。如果发生了阻塞,用interrupt()方法,Thread.interrupt()方法不会中断一个正在运行的线程。这一方法实际上完成的是,在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态。

Java提供了很丰富的API但没有为停止线程提供API。JDK 1.0本来有一些像stop(), suspend() 和 resume()的控制方法但是由于潜在的死锁威胁因此在后续的JDK版本中他们被弃用了,之后Java API的设计者就没有提供一个兼容且线程安全的方法来停止一个线程。当run() 或者 call() 方法执行完的时候线程会自动结束,如果要手动结束一个线程,可以用volatile 布尔变量来退出run()方法的循环或者是取消任务来中断线程。

为什么Thread类的sleep()和yield()方法是静态的?

Thread类的sleep()和yield()方法在当前正在执行的线程上运行。所以在其它处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其它非运行线程调用这些方法。

wait()和sleep()的区别?

  • 这两个方法来自不同的类分别是Thread和Object;
  • 最主要是sleep方法仅仅释放CPU资源或者让当前线程停止执行一段时间,没有释放锁;而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法;
  • wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用synchronized(x){ x.notify() //或者wait() },而sleep可以在任何地方使用;
  • sleep必须捕获异常,wait可以捕获异常InterruptedException,在wait执行前以防其他线程中断当前正在执行的线程,而notify和notifyAll不需要捕获异常。

为什么应该在循环中检查等待条件?

处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。因此,当一个等待线程醒来时,不能认为它原来的等待状态仍然是有效的,在notify()方法调用之后和等待线程醒来之前这段时间它可能会改变。这就是在循环中使用wait()方法效果更好的原因。

interrupt()、interrupted() 和 isInterrupted()方法的区别?

https://my.oschina.net/clopopo/blog/121933

http://www.cnblogs.com/onlywujun/p/3565082.html

一个线程运行时发生异常会怎样?

简单的说,如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候JVM会使用Thread.getUncaughtExceptionHandler()来查询线程的UncaughtExceptionHandler并将线程和异常作为参数传递给handler的uncaughtException()方法进行处理。

无论同步块是正常还是异常退出的,里面的线程都会释放锁。

Thread类中的yield方法有什么作用?

yield()方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。

volatile关键字的作用?

1、volatile 保证可见性:volatile去修饰成员变量的时候,会强制刷新内存,所以线程都会直接读取该变量并且不缓存它。volatile变量可以保证下一个读取操作会在前一个写操作之后发生,遵循volatile变量规则。这就确保了线程读取到的变量是同内存中是一致的。

2、volatile 禁止指令重排优化。

volatile毕竟不是锁,不适合大量并发写操作。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:

1) 对变量的写操作不依赖于当前值;

2) 该变量没有包含在具有其他变量的不变式中。

什么是同步锁synchronized?

在Java中,最基本的互斥同步手段就是synchronized关键字,synchronized关键字经过编译之后,会在同步块的入口位置和退出位置分别形成monitorentermonitorexit这两个字节码指令这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。如果Java程序中的synchronized明确指定了对象参数,那就是这个对象的reference;如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或Class对象来作为锁对象。

根据虚拟机规范的要求,在执行monitorenter指令时,首先要尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁就被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。

在虚拟机规范对monitorenter和monitorexit的行为描述中,有两点是需要特别注意的。首先,synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。其次,同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间。对于代码简单的同步块(如被synchronized修饰的getter()或setter()方法),状态转换消耗的时间有可能比用户代码执行的时间还要长。所以synchronized是Java语言中一个重量级(Heavyweight)的操作,有经验的程序员都会在确实必要的情况下才使用这种操作。而虚拟机本身也会进行一些优化,譬如在通知操作系统阻塞线程之前加入一段自旋等待过程,避免频繁地切入到核心态之中。

简而言之,synchronized会引起线程上下文切换,造成程序性能的严重损耗。

什么是Lock接口?对比同步它有什么优势?

Lock接口比同步方法和同步块提供了更具扩展性的锁操作。它们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。

ReentrantLock表现为API层面的互斥锁(lock()和unlock()配合try/finally使用),而synchronized表现为原生语法层面的互斥锁。

Lock的优势有:

    • 等待可中断(线程等待锁的时候可以选择放弃等待)
    • 可实现公平锁(底层AQS-CLH队列实现公平锁,公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间先后顺序来依次获得锁,即先判断该线程是否位于CLH队列的头部,是则获取锁)
    • 锁可以绑定多个条件(结合Condition对象使用,细化锁的粒度)
    • 可以在不同的范围,以不同的顺序获取和释放锁

注意: Lock 必须在 finally 块中释放,否则如果受保护的代码抛出异常,锁就有可能永远得不到释放!

关于锁的例子

什么是AQS(AbstractQueuedSynchronizer)?

AbstractQueuedSynchronizer中文称作抽象队列同步器,主要就是维护了一个 state 属性、一个 FIFO 队列(叫做CLH队列)和线程的阻塞与解除阻塞操作。

AQS用一个32位的整型 state 表示同步状态,它是用volatile修饰的:private volatile int state; 通过CAS原子操作修改该变量的值,修改成功的线程表示获取到该锁,没有修改成功,或者发现状态state已经是加锁状态,则通过一个Waiter对象封装线程,添加到等待队列中,并挂起等待被唤醒。在互斥锁中 state 表示着线程是否已经获取了锁,0未获取,1已经获取了,大于1表示重入数。

CLH 队列是一个 FIFO 的双向链表,每个节点里面都有一个 prev 和 next ,具备进出队列快、无锁、检查是否有线程在等待也非常容易等特点。

CLH 队列锁中,每个节点(Node)代表着一个需要获取锁的线程。该Node中有两个常量SHARE、EXCLUSIVE。其中SHARE代表着共享模式,EXCLUSIVE代表着独占模式。

什么是可重入锁(ReentrantLock)?

ReentrantLock 类实现了 Lock 接口,属于独占锁。ReentrantLock 锁意味着什么呢?简单来说,它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。这模仿了 synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续)synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁。

ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入CLH队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果:

  • 非公平锁:如果同时还有另一个线程进来尝试获取锁,那么有可能会让这个线程抢先获取;
  • 公平锁:如果同时还有另一个线程进来尝试获取锁,当它发现自己不是在CLH队列队首的话,就会排到队尾,由队首的线程获取到锁。

非公平锁的性能是公平锁性能的几十倍以上,这和公平锁每次试图占有锁时,都必须先要进等待队列,按照FIFO的顺序去获取锁。使用公平锁的线程可能会进行频繁切换,而频繁切换线程,性能必然会下降的厉害,因此在实际的开发过程中,如果需要使用公平锁,务必要考虑线程的切换频率。

同步方法和同步块,哪个是更好的选择?

同步块是更好的选择,因为它不会锁住整个对象(当然也可以让它锁住整个对象)。

同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致它们停止执行并需要等待获得这个对象上的锁。

什么是ThreadLocal?

ThreadLocal用于创建线程自己的变量,彻底消除竞争条件

ThreadLocal将线程与变量关联起来,当线程终止后,变量会作为垃圾回收。ThreadLocal例子

什么是Semaphore?

Java中的Semaphore是一种新的同步类,它是一个计数信号。从概念上讲,从概念上讲,信号量维护了一个许可集合。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release()添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore只对可用许可的号码进行计数,并采取相应的行动。信号量常常用于多线程的代码中,比如数据库连接池。

什么是读写锁(ReentrantReadWriteLock)?

一般而言,读写锁是用来提升并发程序性能的锁分离技术的成果。一个ReadWriteLock维护一对关联的锁,一个用于只读操作一个用于写操作。读写锁使用的场合是一个共享资源被大量读取操作,而只有少量的写操作(修改数据)。

在没有写线程的情况下,一个读锁可能会同时被多个读线程持有,支持并发读;而写锁是独占的。

JDK中的ReentrantReadWriteLock最多支持65535个写锁和65535个读锁。

独占锁ReentrantLock中有一个state,共享锁中也有一个state,其中独占锁中的state为0或者1,如果有重入,则表示重入的次数,共享锁中state表示的持有锁的数量。

而在ReadWriteLock中则不同,由于ReadWriteLock中存在两个锁--读锁和写锁,它们之间有联系但是也有差异,所以需要有两个state来分别表它们。于是ReentrantReadWriteLock就将state一分二位,高16位表示共享锁的数量,低16位表示独占锁的数量。2^16 – 1 = 65535。这就是读取锁和写入锁的数量最大分别只能是65535的原因。

什么是Callable和Future?

Java 5在concurrency包中引入了java.util.concurrent.Callable 接口,它和Runnable接口很相似,但它的 call() 方法可以返回值和抛出异常。Callable可以返回装载有计算结果的Future对象。

Callable接口使用泛型去定义它的返回类型。Executors类提供了一些有用的方法去在线程池中执行Callable内的任务。由于Callable任务是并行的,必须等待它返回的结果。

java.util.concurrent.Future对象可以获取Callable返回的结果。在线程池提交Callable任务后会返回一个Future对象,使用它可以知道Callable任务的状态和得到Callable返回的执行结果。Future提供了get()方法可以等待Callable结束并获取它的执行结果。

什么是FutureTask?

FutureTask表示一个可以取消的异步运算。它有启动运算、取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成get()方法将会阻塞。一个FutureTask对象可以对调用了Callable和Runnable的对象进行包装,由于FutureTask也是调用了Runnable接口所以它可以提交给Executor来执行。Java FutureTask例子

什么是Java Timer类?如何创建一个有特定时间间隔的任务?

java.util.Timer是一个工具类,可以用于安排一个线程在未来的某个特定时间执行。Timer类可以用安排一次性任务或者周期任务。

java.util.TimerTask是一个实现了Runnable接口的抽象类,需要去继承这个类来创建定时任务并使用Timer去安排它的执行。Java Timer的例子

CyclicBarrier 和 CountDownLatch有什么不同?

CyclicBarrier 和 CountDownLatch 都可以用来让一组线程等待其它线程。

使用CyclicBarrier可以在多个关口处将多个线程执行结果汇总,而CountDownLatch是在各线程执行完毕后向总线程汇报结果。

CyclicBarrier是一个同步辅助工具类,它允许一组线程相互等待,直到到达一个公共的栏栅点。CyclicBarriers对于那些包含一组固定大小线程,并且这些线程必须不时地相互等待的程序非常有用。之所以将其称之为循环的Barrier是因为该Barrier在等待的线程释放之后可以重用。 
CyclicBarrier 支持一个可选的 Runnable 命令,在一组线程中的最后一个线程到达之后(但在释放所有线程之前),该命令只在每个屏障点运行一次。若在继续所有参与线程之前更新共享状态,此屏障操作很有用。 

	/**
     * Updates state on barrier trip and wakes up everyone.
     * Called only while holding lock.
     */// 实现到达栏栅点后开始下一轮循环
    private void nextGeneration() {
        // signal completion of last generation
        trip.signalAll();
        // set up next generation
        count = parties;
        generation = new Generation();
    }

什么是线程池?参数意义?拒绝策略?如何配置参数?

创建线程要花费昂贵的资源和时间,而且无限制的创建线程会引起应用程序栈内存溢出。所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。

一个线程池管理了一组工作线程,同时它还包括了一个用于放置等待执行的任务的阻塞队列。

java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池。

什么是阻塞队列?如何使用阻塞队列来实现生产者-消费者模型?

java.util.concurrent.BlockingQueue的特性是:当队列是空的时,从队列中获取或删除元素的操作将会被阻塞,或者当队列是满时,往队列里添加元素的操作会被阻塞。

阻塞队列不接受空值,当尝试向队列中添加空值的时候,它会抛出NullPointerException。

阻塞队列的实现都是线程安全的,所有的查询方法都是原子的并且使用了内部锁或者其它形式的并发控制。

BlockingQueue 接口是Java collections框架的一部分,它主要用于实现生产者-消费者问题。

什么是并发容器?

Java集合类都是快速失败的,这就意味着当集合被改变且一个线程在使用迭代器遍历集合的时候,迭代器的next()方法将抛出ConcurrentModificationException异常。

并发容器支持并发的遍历和并发的更新。

主要的类有ConcurrentHashMap, CopyOnWriteArrayList 和CopyOnWriteArraySet。如何避免ConcurrentModificationException

ConcurrentHashMap

ConcurrentHashMap一次锁住一个桶。ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用操作只锁当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。前面说到的16个线程指的是写线程,而读操作大部分时候都不需要用到锁。只有在resize()扩容等操作时才需要锁住整个hash表。
在迭代方面,ConcurrentHashMap使用了一种不同的迭代方式。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据 ,iterator完成后再将头指针替换为新的数据 ,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。

CopyOnWriteArrayList

CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出ConcurrentModificationException。

利用高并发往往是读多写少的特性,对读操作不加锁;对写操作加锁,先复制一份新的集合,在新的集合上面修改,然后将新集合赋值给旧的引用,并通过volatile 保证其可见性。

1. 读:不变性(array数组内部不会发生修改)

2. 写:重入锁

CopyOnWriteArrayList适合多读少些的场景,否则不停地复制数组性能较差。

CopyOnWriteArraySet

基于CopyOnWriteArrayList实现,其唯一的不同是在add时调用的是CopyOnWriteArrayList的addIfAbsent()方法,其遍历当前Object数组,如Object数组中已有当前元素,则直接返回,如果没有则放入Object数组的尾部,并返回。

什么是fork join框架?

fork join框架是JDK7中出现的一款高效的工具,Java开发人员可以通过它充分利用现代服务器上的多处理器。它是专门为了那些可以递归划分成许多子模块设计的,目的是将所有可用的处理能力用来提升程序的性能。fork join框架一个巨大的优势是它使用了工作窃取算法,可以完成更多任务的工作线程可以从其它线程中窃取任务来执行。

实现生产者-消费者?

三个线程T1,T2,T3,怎么确保它们按顺序执行?

Java 多线程是指在一个程序中可以同时执行多个线程,并且每个线程独立运行,互不干扰。Java 多线程是Java 平台提供的重要特性,常用于提高程序并发性能和解决复杂问题。 Java 多线程的重点知识点包括以下几个方面: 1. 线程的创建和启动:可以通过继承 Thread 类或实现 Runnable 接口来创建线程,并使用 start() 方法启动线程。 2. 线程的生命周期:线程的生命周期包括新建、就绪、运行、阻塞和死亡等阶段,可以通过调用 sleep()、wait()、join() 等方法来控制线程的状态转换。 3. 线程同步:多个线程之间可能会共享资源,为了保证数据的一致性和避免冲突,需要使用同步机制,如 synchronized 关键字、Lock 接口等。 4. 线程间通信:多个线程之间可以通过共享内存或者消息传递的方式进行通信,如使用 wait()、notify()、notifyAll() 等方法。 5. 线程池:线程池可以管理和复用线程,减少线程的创建和销毁开销,提高系统的性能。 6. 线程安全性:在多线程环境下,存在资源竞争和线程安全性问题,需要采取相应的措施来保证线程的安全性,如使用 synchronized 关键字、volatile 关键字等。 7. 线程调度:Java 提供了线程调度器来控制线程的执行顺序和优先级,可以使用 yield()、join()、setPriority() 等方法进行调度。 8. 线程异常处理:在多线程环境下,线程的异常需要处理和捕获,可以使用 try-catch 块来捕获异常。 综上所述,Java 多线程是Java 平台提供的重要特性,掌握多线程的概念和相关知识点可以帮助开发者提高程序的并发性能和解决复杂问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值