关于java:并发编程面试题2021最新版

39次阅读

共计 48549 个字符,预计需要花费 122 分钟才能阅读完成。

基础知识

并发编程的优缺点

为什么要应用并发编程(并发编程的长处)

  • 充分利用多核 CPU 的计算能力:通过并发编程的模式能够将多核 CPU 的计算能力施展到极致,性能失去晋升
  • 不便进行业务拆分,晋升零碎并发能力和性能:在非凡的业务场景下,先天的就适宜于并发编程。当初的零碎动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发零碎的根底,利用好多线程机制能够大大提高零碎整体的并发能力以及性能。面对简单业务模型,并行程序会比串行程序更适应业务需要,而并发编程更能吻合这种业务拆分。

并发编程有什么毛病

并发编程的目标就是为了能进步程序的执行效率,进步程序运行速度,然而并发编程并不总是能进步程序运行速度的,而且并发编程可能会遇到很多问题,比方 :内存透露、上下文切换、线程平安、死锁 等问题。

并发编程三要素是什么?在 Java 程序中怎么保障多线程的运行平安?

并发编程三要素(线程的安全性问题体现在):

原子性:原子,即一个不可再被宰割的颗粒。原子性指的是一个或多个操作要么全副执行胜利要么全副执行失败。

可见性:一个线程对共享变量的批改, 另一个线程可能立即看到。(synchronized,volatile)

有序性:程序执行的程序依照代码的先后顺序执行。(处理器可能会对指令进行重排序)

呈现线程平安问题的起因:

  • 线程切换带来的原子性问题
  • 缓存导致的可见性问题
  • 编译优化带来的有序性问题

解决办法:

  • JDK Atomic 结尾的原子类、synchronized、LOCK,能够解决原子性问题
  • synchronized、volatile、LOCK,能够解决可见性问题
  • Happens-Before 规定能够解决有序性问题

并行和并发有什么区别?

  • 并发:多个工作在同一个 CPU 核上,按细分的工夫片轮流 (交替) 执行,从逻辑上来看那些工作是同时执行。
  • 并行:单位工夫内,多个处理器或多核处理器同时解决多个工作,是真正意义上的“同时进行”。
  • 串行:有 n 个工作,由一个线程按程序执行。因为工作、办法都在一个线程执行所以不存在线程不平安状况,也就不存在临界区的问题。

做一个形象的比喻:

并发 = 两个队列和一台咖啡机。

并行 = 两个队列和两台咖啡机。

串行 = 一个队列和一台咖啡机。

什么是多线程,多线程的优劣?

多线程:多线程是指程序中蕴含多个执行流,即在一个程序中能够同时运行多个不同的线程来执行不同的工作。

多线程的益处:

能够进步 CPU 的利用率。在多线程程序中,一个线程必须期待的时候,CPU 能够运行其它的线程而不是期待,这样就大大提高了程序的效率。也就是说容许单个程序创立多个并行执行的线程来实现各自的工作。

多线程的劣势:

  • 线程也是程序,所以线程须要占用内存,线程越多占用内存也越多;
  • 多线程须要协调和治理,所以须要 CPU 工夫跟踪线程;
  • 线程之间对共享资源的拜访会相互影响,必须解决竞用共享资源的问题。

线程和过程区别

什么是线程和过程?

过程

一个在内存中运行的应用程序。每个过程都有本人独立的一块内存空间,一个过程能够有多个线程,比方在 Windows 零碎中,一个运行的 xx.exe 就是一个过程。

线程

过程中的一个执行工作(管制单元),负责以后过程中程序的执行。一个过程至多有一个线程,一个过程能够运行多个线程,多个线程可共享数据。

过程与线程的区别

线程具备许多传统过程所具备的特色,故又称为轻型过程 (Light—Weight Process) 或过程元;而把传统的过程称为重型过程(Heavy—Weight Process),它相当于只有一个线程的工作。在引入了线程的操作系统中,通常一个过程都有若干个线程,至多蕴含一个线程。

基本区别:过程是操作系统资源分配的根本单位,而线程是处理器任务调度和执行的根本单位

资源开销:每个过程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程能够看做轻量级的过程,同一类线程共享代码和数据空间,每个线程都有本人独立的运行栈和程序计数器(PC),线程之间切换的开销小。

蕴含关系:如果一个过程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是过程的一部分,所以线程也被称为轻权过程或者轻量级过程。

内存调配:同一过程的线程共享本过程的地址空间和资源,而过程之间的地址空间和资源是互相独立的

影响关系:一个过程解体后,在保护模式下不会对其余过程产生影响,然而一个线程解体整个过程都死掉。所以多过程要比多线程强壮。

执行过程:每个独立的过程有程序运行的入口、程序执行序列和程序进口。然而线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行管制,两者均可并发执行

什么是上下文切换?

多线程编程中个别线程的个数都大于 CPU 外围的个数,而一个 CPU 外围在任意时刻只能被一个线程应用,为了让这些线程都能失去无效执行,CPU 采取的策略是为每个线程调配工夫片并轮转的模式。当一个线程的工夫片用完的时候就会从新处于就绪状态让给其余线程应用,这个过程就属于一次上下文切换。

概括来说就是:当前任务在执行完 CPU 工夫片切换到另一个工作之前会先保留本人的状态,以便下次再切换回这个工作时,能够再加载这个工作的状态。工作从保留到再加载的过程就是一次上下文切换

上下文切换通常是计算密集型的。也就是说,它须要相当可观的处理器工夫,在每秒几十上百次的切换中,每次切换都须要纳秒量级的工夫。所以,上下文切换对系统来说意味着耗费大量的 CPU 工夫,事实上,可能是操作系统中工夫耗费最大的操作。

Linux 相比与其余操作系统(包含其余类 Unix 零碎)有很多的长处,其中有一项就是,其上下文切换和模式切换的工夫耗费非常少。

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

守护线程和用户线程

  • 用户 (User) 线程:运行在前台,执行具体的工作,如程序的主线程、连贯网络的子线程等都是用户线程
  • 守护 (Daemon) 线程:运行在后盾,为其余前台线程服务。也能够说守护线程是 JVM 中非守护线程的 “佣人”。一旦所有用户线程都完结运行,守护线程会随 JVM 一起完结工作

main 函数所在的线程就是一个用户线程啊,main 函数启动的同时在 JVM 外部同时还启动了好多守护线程,比方垃圾回收线程。

比拟显著的区别之一是用户线程完结,JVM 退出,不论这个时候有没有守护线程运行。而守护线程不会影响 JVM 的退出。

注意事项:

  1. setDaemon(true)必须在 start() 办法前执行,否则会抛出 IllegalThreadStateException 异样
  2. 在守护线程中产生的新线程也是守护线程
  3. 不是所有的工作都能够调配给守护线程来执行,比方读写操作或者计算逻辑
  4. 守护 (Daemon) 线程中不能依附 finally 块的内容来确保执行敞开或清理资源的逻辑。因为咱们下面也说过了一旦所有用户线程都完结运行,守护线程会随 JVM 一起完结工作,所以守护 (Daemon) 线程中的 finally 语句块可能无奈被执行。

如何在 Windows 和 Linux 上查找哪个线程 cpu 利用率最高?

windows 下面用工作管理器看,linux 下能够用 top 这个工具看。

  1. 找出 cpu 耗用厉害的过程 pid,终端执行 top 命令,而后按下 shift+p 查找出 cpu 利用最厉害的 pid 号
  2. 依据下面第一步拿到的 pid 号,top -H -p pid。而后按下 shift+p,查找出 cpu 利用率最厉害的线程号,比方 top -H -p 1328
  3. 将获取到的线程号转换成 16 进制,去百度转换一下就行
  4. 应用 jstack 工具将过程信息打印输出,jstack pid 号 > /tmp/t.dat,比方 jstack 31365 > /tmp/t.dat
  5. 编辑 /tmp/t.dat 文件,查找线程号对应的信息

什么是线程死锁

百度百科:死锁是指两个或两个以上的过程(线程)在执行过程中,因为竞争资源或者因为彼此通信而造成的一种阻塞的景象,若无外力作用,它们都将无奈推动上来。此时称零碎处于死锁状态或零碎产生了死锁,这些永远在相互期待的过程(线程)称为死锁过程(线程)。

多个线程同时被阻塞,它们中的一个或者全副都在期待某个资源被开释。因为线程被无限期地阻塞,因而程序不可能失常终止。

如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会相互期待而进入死锁状态。

上面通过一个例子来阐明线程死锁,代码模仿了上图的死锁的状况 (代码来源于《并发编程之美》):

public class DeadLockDemo {private static Object resource1 = new Object();// 资源 1
    private static Object resource2 = new Object();// 资源 2

    public static void main(String[] args) {new Thread(() -> {synchronized (resource1) {System.out.println(Thread.currentThread() + "get resource1");
                try {Thread.sleep(1000);
                } catch (InterruptedException e) {e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 1").start();

        new Thread(() -> {synchronized (resource2) {System.out.println(Thread.currentThread() + "get resource2");
                try {Thread.sleep(1000);
                } catch (InterruptedException e) {e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "线程 2").start();}
}

输入后果

Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1

线程 A 通过 synchronized (resource1) 取得 resource1 的监视器锁,而后通过Thread.sleep(1000);让线程 A 休眠 1s 为的是让线程 B 失去 CPU 执行权,而后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠完结了都开始希图申请获取对方的资源,而后这两个线程就会陷入相互期待的状态,这也就产生了死锁。下面的例子合乎产生死锁的四个必要条件。

造成死锁的四个必要条件是什么

  1. 互斥条件:线程 (过程) 对于所调配到的资源具备排它性,即一个资源只能被一个线程 (过程) 占用,直到被该线程 (过程) 开释
  2. 申请与放弃条件:一个线程 (过程) 因申请被占用资源而产生阻塞时,对已取得的资源放弃不放。
  3. 不剥夺条件:线程 (过程) 已取得的资源在末应用完之前不能被其余线程强行剥夺,只有本人应用结束后才开释资源。
  4. 循环期待条件:当产生死锁时,所期待的线程 (过程) 必定会造成一个环路(相似于死循环),造成永恒阻塞

如何防止线程死锁

咱们只有毁坏产生死锁的四个条件中的其中一个就能够了。

毁坏互斥条件

这个条件咱们没有方法毁坏,因为咱们用锁原本就是想让他们互斥的(临界资源须要互斥拜访)。

毁坏申请与放弃条件

一次性申请所有的资源。

毁坏不剥夺条件

占用局部资源的线程进一步申请其余资源时,如果申请不到,能够被动开释它占有的资源。

毁坏循环期待条件

靠按序申请资源来预防。按某一程序申请资源,开释资源则反序开释。毁坏循环期待条件。

咱们对线程 2 的代码批改成上面这样就不会产生死锁了。

new Thread(() -> {synchronized (resource1) {System.out.println(Thread.currentThread() + "get resource1");
        try {Thread.sleep(1000);
        } catch (InterruptedException e) {e.printStackTrace();
        }
        System.out.println(Thread.currentThread() + "waiting get resource2");
        synchronized (resource2) {System.out.println(Thread.currentThread() + "get resource2");
        }
    }
}, "线程 2").start();

输入后果

Thread[线程 1,5,main]get resource1
Thread[线程 1,5,main]waiting get resource2
Thread[线程 1,5,main]get resource2
Thread[线程 2,5,main]get resource1
Thread[线程 2,5,main]waiting get resource2
Thread[线程 2,5,main]get resource2

咱们剖析一下下面的代码为什么防止了死锁的产生?

线程 1 首先取得到 resource1 的监视器锁,这时候线程 2 就获取不到了。而后线程 1 再去获取 resource2 的监视器锁,能够获取到。而后线程 1 开释了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就能够执行了。这样就毁坏了毁坏循环期待条件,因而防止了死锁。

创立线程的四种形式

创立线程有哪几种形式?

创立线程有四种形式:

  • 继承 Thread 类;
  • 实现 Runnable 接口;
  • 实现 Callable 接口;
  • 应用 Executors 工具类创立线程池

继承 Thread 类

步骤

  1. 定义一个 Thread 类的子类,重写 run 办法,将相干逻辑实现,run()办法就是线程要执行的业务逻辑办法
  2. 创立自定义的线程子类对象
  3. 调用子类实例的 star()办法来启动线程
public class MyThread extends Thread {

    @Override
    public void run() {System.out.println(Thread.currentThread().getName() + "run()办法正在执行...");
    }

}
public class TheadTest {public static void main(String[] args) {MyThread myThread = new MyThread();     
        myThread.start();
        System.out.println(Thread.currentThread().getName() + "main()办法执行完结");
    }

}

运行后果

main main()办法执行完结
Thread-0 run()办法正在执行...

实现 Runnable 接口

步骤

  1. 定义 Runnable 接口实现类 MyRunnable,并重写 run()办法
  2. 创立 MyRunnable 实例 myRunnable,以 myRunnable 作为 target 创立 Thead 对象,该 Thread 对象才是真正的线程对象
  3. 调用线程对象的 start()办法
public class MyRunnable implements Runnable {

    @Override
    public void run() {System.out.println(Thread.currentThread().getName() + "run()办法执行中...");
    }

}
public class RunnableTest {public static void main(String[] args) {MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
        System.out.println(Thread.currentThread().getName() + "main()办法执行实现");
    }

}

执行后果

main main()办法执行实现
Thread-0 run()办法执行中...

实现 Callable 接口

步骤

  1. 创立实现 Callable 接口的类 myCallable
  2. 以 myCallable 为参数创立 FutureTask 对象
  3. 将 FutureTask 作为参数创立 Thread 对象
  4. 调用线程对象的 start()办法
public class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() {System.out.println(Thread.currentThread().getName() + "call()办法执行中...");
        return 1;
    }

}
public class CallableTest {public static void main(String[] args) {FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable());
        Thread thread = new Thread(futureTask);
        thread.start();

        try {Thread.sleep(1000);
            System.out.println("返回后果" + futureTask.get());
        } catch (InterruptedException e) {e.printStackTrace();
        } catch (ExecutionException e) {e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "main()办法执行实现");
    }

}

执行后果

Thread-0 call()办法执行中...
返回后果 1
main main()办法执行实现

应用 Executors 工具类创立线程池

Executors 提供了一系列工厂办法用于创先线程池,返回的线程池都实现了 ExecutorService 接口。

次要有 newFixedThreadPool,newCachedThreadPool,newSingleThreadExecutor,newScheduledThreadPool,后续具体介绍这四种线程池

public class MyRunnable implements Runnable {

    @Override
    public void run() {System.out.println(Thread.currentThread().getName() + "run()办法执行中...");
    }

}
public class SingleThreadExecutorTest {public static void main(String[] args) {ExecutorService executorService = Executors.newSingleThreadExecutor();
        MyRunnable runnableTest = new MyRunnable();
        for (int i = 0; i < 5; i++) {executorService.execute(runnableTest);
        }

        System.out.println("线程工作开始执行");
        executorService.shutdown();}

}

执行后果

线程工作开始执行
pool-1-thread-1 is running...
pool-1-thread-1 is running...
pool-1-thread-1 is running...
pool-1-thread-1 is running...
pool-1-thread-1 is running...

说一下 runnable 和 callable 有什么区别?

相同点

  • 都是接口
  • 都能够编写多线程程序
  • 都采纳 Thread.start()启动线程

次要区别

  • Runnable 接口 run 办法无返回值;Callable 接口 call 办法有返回值,是个泛型,和 Future、FutureTask 配合能够用来获取异步执行的后果
  • Runnable 接口 run 办法只能抛出运行时异样,且无奈捕捉解决;Callable 接口 call 办法容许抛出异样,能够获取异样信息

:Callalbe 接口反对返回执行后果,须要调用 FutureTask.get() 失去,此办法会阻塞主过程的持续往下执行,如果不调用不会阻塞。

线程的 run()和 start()有什么区别?

每个线程都是通过某个特定 Thread 对象所对应的办法 run()来实现其操作的,run()办法称为线程体。通过调用 Thread 类的 start()办法来启动一个线程。

start() 办法用于启动线程,run() 办法用于执行线程的运行时代码。run() 能够反复调用,而 start() 只能调用一次。

start()办法来启动一个线程,真正实现了多线程运行。调用 start()办法无需期待 run 办法体代码执行结束,能够间接继续执行其余的代码;此时线程是处于就绪状态,并没有运行。而后通过此 Thread 类调用办法 run()来实现其运行状态,run()办法运行完结,此线程终止。而后 CPU 再调度其它线程。

run()办法是在本线程里的,只是线程里的一个函数,而不是多线程的。如果间接调用 run(),其实就相当于是调用了一个一般函数而已,间接待用 run()办法必须期待 run()办法执行结束能力执行上面的代码,所以执行门路还是只有一条,基本就没有线程的特色,所以在多线程执行时要应用 start()办法而不是 run()办法。

为什么咱们调用 start() 办法时会执行 run() 办法,为什么咱们不能间接调用 run() 办法?

这是另一个十分经典的 java 多线程面试问题,而且在面试中会常常被问到。很简略,然而很多人都会答不上来!

new 一个 Thread,线程进入了新建状态。调用 start() 办法,会启动一个线程并使线程进入了就绪状态,当调配到工夫片后就能够开始运行了。start() 会执行线程的相应筹备工作,而后主动执行 run() 办法的内容,这是真正的多线程工作。

而间接执行 run() 办法,会把 run 办法当成一个 main 线程下的一般办法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结:调用 start 办法方可启动线程并使线程进入就绪状态,而 run 办法只是 thread 的一个一般办法调用,还是在主线程里执行。

什么是 Callable 和 Future?

Callable 接口相似于 Runnable,从名字就可以看进去了,然而 Runnable 不会返回后果,并且无奈抛出返回后果的异样,而 Callable 性能更弱小一些,被线程执行后,能够返回值,这个返回值能够被 Future 拿到,也就是说,Future 能够拿到异步执行工作的返回值。

Future 接口示意异步工作,是一个可能还没有实现的异步工作的后果。所以说 Callable 用于产生后果,Future 用于获取后果。

什么是 FutureTask

FutureTask 示意一个异步运算的工作。FutureTask 外面能够传入一个 Callable 的具体实现类,能够对这个异步运算的工作的后果进行期待获取、判断是否曾经实现、勾销工作等操作。只有当运算实现的时候后果能力取回,如果运算尚未实现 get 办法将会阻塞。一个 FutureTask 对象能够对调用了 Callable 和 Runnable 的对象进行包装,因为 FutureTask 也是 Runnable 接口的实现类,所以 FutureTask 也能够放入线程池中。

线程的状态和基本操作

说说线程的生命周期及五种根本状态?

  1. 新建(new):新创建了一个线程对象。
  2. 可运行 (runnable):线程对象创立后,当调用线程对象的 start() 办法,该线程处于就绪状态,期待被线程调度选中,获取 cpu 的使用权。
  3. 运行 (running):可运行状态(runnable) 的线程取得了 cpu 工夫片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的惟一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
  4. 阻塞 (block):处于运行状态中的线程因为某种原因,临时放弃对 CPU 的使用权,进行执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。阻塞的状况分三种:(一). 期待阻塞:运行状态中的线程执行 wait() 办法,JVM 会把该线程放入期待队列 (waitting queue) 中,使本线程进入到期待阻塞状态;(二). 同步阻塞:线程在获取 synchronized 同步锁失败 (因为锁被其它线程所占用),,则 JVM 会把该线程放入锁池(lock pool) 中,线程会进入同步阻塞状态;(三). 其余阻塞: 通过调用线程的 sleep()或 join()或收回了 I/O 申请时,线程会进入到阻塞状态。当 sleep()状态超时、join()期待线程终止或者超时、或者 I/O 处理完毕时,线程从新转入就绪状态。
  5. 死亡 (dead):线程 run()、main() 办法执行完结,或者因异样退出了 run()办法,则该线程完结生命周期。死亡的线程不可再次复活。

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

计算机通常只有一个 CPU,在任意时刻只能执行一条机器指令,每个线程只有取得 CPU 的使用权能力执行指令。所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流取得 CPU 的使用权,别离执行各自的工作。在运行池中,会有多个处于就绪状态的线程在期待 CPU,JAVA 虚拟机的一项工作就是负责线程的调度,线程调度是指依照特定机制为多个线程调配 CPU 的使用权。

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

分时调度模型是指让所有的线程轮流取得 cpu 的使用权,并且平均分配每个线程占用的 CPU 的工夫片这个也比拟好了解。

Java 虚拟机采纳抢占式调度模型,是指优先让可运行池中优先级高的线程占用 CPU,如果可运行池中的线程优先级雷同,那么就随机抉择一个线程,使其占用 CPU。处于运行状态的线程会始终运行,直至它不得不放弃 CPU。

线程的调度策略

线程调度器抉择优先级最高的线程运行,然而,如果产生以下状况,就会终止线程的运行:

(1)线程体中调用了 yield 办法让出了对 cpu 的占用权力

(2)线程体中调用了 sleep 办法使线程进入睡眠状态

(3)线程因为 IO 操作受到阻塞

(4)另外一个更高优先级线程呈现

(5)在反对工夫片的零碎中,该线程的工夫片用完

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

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

工夫分片是指将可用的 CPU 工夫调配给可用的 Runnable 线程的过程。调配 CPU 工夫能够基于线程优先级或者线程期待的工夫。

线程调度并不受到 Java 虚拟机管制,所以由应用程序来管制它是更好的抉择(也就是说不要让你的程序依赖于线程的优先级)。

请说出与线程同步以及线程调度相干的办法。

(1)wait():使一个线程处于期待(阻塞)状态,并且开释所持有的对象的锁;

(2)sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此办法要解决 InterruptedException 异样;

(3)notify():唤醒一个处于期待状态的线程,当然在调用此办法的时候,并不能确切的唤醒某一个期待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;

(4)notityAll():唤醒所有处于期待状态的线程,该办法并不是将对象的锁给所有线程,而是让它们竞争,只有取得锁的线程能力进入就绪状态;

sleep() 和 wait() 有什么区别?

两者都能够暂停线程的执行

  • 类的不同:sleep() 是 Thread 线程类的静态方法,wait() 是 Object 类的办法。
  • 是否开释锁:sleep() 不开释锁;wait() 开释锁。
  • 用处不同:Wait 通常被用于线程间交互 / 通信,sleep 通常被用于暂停执行。
  • 用法不同:wait() 办法被调用后,线程不会主动昏迷,须要别的线程调用同一个对象上的 notify() 或者 notifyAll() 办法。sleep() 办法执行实现后,线程会主动昏迷。或者能够应用 wait(long timeout)超时后线程会主动昏迷。

你是如何调用 wait() 办法的?应用 if 块还是循环?为什么?

处于期待状态的线程可能会收到谬误警报和伪唤醒,如果不在循环中查看期待条件,程序就会在没有满足完结条件的状况下退出。

wait() 办法应该在循环调用,因为当线程获取到 CPU 开始执行的时候,其余条件可能还没有满足,所以在解决前,循环检测条件是否满足会更好。上面是一段规范的应用 wait 和 notify 办法的代码:

synchronized (monitor) {
    // 判断条件谓词是否失去满足
    while(!locked) {
        // 期待唤醒
        monitor.wait();}
    // 解决其余的业务逻辑
}

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

Java 中,任何对象都能够作为锁,并且 wait(),notify()等办法用于期待对象的锁或者唤醒线程,在 Java 的线程中并没有可供任何对象应用的锁,所以任意对象调用办法肯定定义在 Object 类中。

wait(), notify()和 notifyAll()这些办法在同步代码块中调用

有的人会说,既然是线程放弃对象锁,那也能够把 wait()定义在 Thread 类外面啊,新定义的线程继承于 Thread 类,也不须要从新定义 wait()办法的实现。然而,这样做有一个十分大的问题,一个线程齐全能够持有很多锁,你一个线程放弃锁的时候,到底要放弃哪个锁?当然了,这种设计并不是不能实现,只是治理起来更加简单。

综上所述,wait()、notify()和 notifyAll()办法要定义在 Object 类中。

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

当一个线程须要调用对象的 wait()办法的时候,这个线程必须领有该对象的锁,接着它就会开释这个对象锁并进入期待状态直到其余线程调用这个对象上的 notify()办法。同样的,当一个线程须要调用对象的 notify()办法时,它会开释这个对象的锁,以便其余在期待的线程就能够失去这个对象锁。因为所有的这些办法都须要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步办法或者同步块中被调用。

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

使以后线程从执行状态(运行状态)变为可执行态(就绪状态)。

以后线程到了就绪状态,那么接下来哪个线程会从就绪状态变成执行状态呢?可能是以后线程,也可能是其余线程,看零碎的调配了。

为什么 Thread 类的 sleep()和 yield ()办法是动态的?

Thread 类的 sleep()和 yield()办法将在以后正在执行的线程上运行。所以在其余处于期待状态的线程上调用这些办法是没有意义的。这就是为什么这些办法是动态的。它们能够在以后正在执行的线程中工作,并防止程序员谬误的认为能够在其余非运行线程调用这些办法。

线程的 sleep()办法和 yield()办法有什么区别?

(1)sleep()办法给其余线程运行机会时不思考线程的优先级,因而会给低优先级的线程以运行的机会;yield()办法只会给雷同优先级或更高优先级的线程以运行的机会;

(2)线程执行 sleep()办法后转入阻塞(blocked)状态,而执行 yield()办法后转入就绪(ready)状态;

(3)sleep()办法申明抛出 InterruptedException,而 yield()办法没有申明任何异样;

(4)sleep()办法比 yield()办法(跟操作系统 CPU 调度相干)具备更好的可移植性,通常不倡议应用 yield()办法来管制并发线程的执行。

如何进行一个正在运行的线程?

在 java 中有以下 3 种办法能够终止正在运行的线程:

  1. 应用退出标记,使线程失常退出,也就是当 run 办法实现后线程终止。
  2. 应用 stop 办法强行终止,然而不举荐这个办法,因为 stop 和 suspend 及 resume 一样都是过期作废的办法。
  3. 应用 interrupt 办法中断线程。

Java 中 interrupted 和 isInterrupted 办法的区别?

interrupt:用于中断线程。调用该办法的线程的状态为将被置为”中断”状态。

留神:线程中断仅仅是置线程的中断状态位,不会进行线程。须要用户本人去监督线程的状态为并做解决。反对线程中断的办法(也就是线程中断后会抛出 interruptedException 的办法)就是在监督线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异样。

interrupted:是静态方法,查看以后中断信号是 true 还是 false 并且革除中断信号。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和前面的就返回 false 了。

isInterrupted:查看以后中断信号是 true 还是 false

什么是阻塞式办法?

阻塞式办法是指程序会始终期待该办法实现期间不做其余事件,ServerSocket 的 accept()办法就是始终期待客户端连贯。这里的阻塞是指调用后果返回之前,以后线程会被挂起,直到失去后果之后才会返回。此外,还有异步和非阻塞式办法在工作实现前就返回。

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

首先,wait()、notify() 办法是针对对象的,调用任意对象的 wait()办法都将导致线程阻塞,阻塞的同时也将开释该对象的锁,相应地,调用任意对象的 notify()办法则将随机解除该对象阻塞的线程,但它须要从新获取该对象的锁,直到获取胜利能力往下执行;

其次,wait、notify 办法必须在 synchronized 块或办法中被调用,并且要保障同步块或办法的锁对象与调用 wait、notify 办法的对象是同一个,如此一来在调用 wait 之前以后线程就曾经胜利获取某对象的锁,执行 wait 阻塞后以后线程就将之前获取的对象锁开释。

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

如果线程调用了对象的 wait()办法,那么线程便会处于该对象的期待池中,期待池中的线程不会去竞争该对象的锁。

notifyAll() 会唤醒所有的线程,notify() 只会唤醒一个线程。

notifyAll() 调用后,会将全副线程由期待池移到锁池,而后参加锁的竞争,竞争胜利则继续执行,如果不胜利则留在锁池期待锁被开释后再次参加竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机管制。

如何在两个线程间共享数据?

在两个线程间共享变量即可实现共享。

一般来说,共享变量要求变量自身是线程平安的,而后在线程内应用的时候,如果有对共享变量的复合操作,那么也得保障复合操作的线程安全性。

Java 如何实现多线程之间的通信和合作?

能够通过中断 和 共享变量的形式实现线程间的通信和合作

比如说最经典的生产者 - 消费者模型:当队列满时,生产者须要期待队列有空间能力持续往里面放入商品,而 在期待的期间内,生产者必须开释对临界资源(即队列)的占用权 。因为生产者如果不开释对临界资源的占用权,那么 消费者就无奈生产队列中的商品,就不会让队列有空间,那么生产者就会始终有限期待上来。因而,个别状况下,当队列满时,会让生产者交出对临界资源的占用权,并进入挂起状态。而后期待消费者生产了商品,而后消费者告诉生产者队列有空间了。同样地,当队列空时,消费者也必须期待,期待生产者告诉它队列中有商品了。这种相互通信的过程就是线程间的合作。

Java 中线程通信合作的最常见的两种形式:

一.syncrhoized 加锁的线程的 Object 类 的 wait()/notify()/notifyAll()

二.ReentrantLock 类加锁的线程的Condition 类的await()/signal()/signalAll()

线程间间接的数据交换:

三. 通过管道进行线程间通信:1)字节流;2)字符流

同步办法和同步块,哪个是更好的抉择?

同步块是更好的抉择,因为它不会锁住整个对象(当然你也能够让它锁住整个对象)。同步办法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们进行执行并须要期待取得这个对象上的锁。

同步块更要合乎凋谢调用的准则,只在须要锁住的代码块锁住相应的对象,这样从侧面来说也能够防止死锁。

请晓得一条准则:同步的范畴越小越好。

什么是线程同步和线程互斥,有哪几种实现形式?

当一个线程对共享的数据进行操作时,应使之成为一个”原子操作“,即在没有实现相干操作之前,不容许其余线程打断它,否则,就会毁坏数据的完整性,必然会失去谬误的处理结果,这就是线程的同步。

在多线程利用中,思考不同线程之间的数据同步和避免死锁。当两个或多个线程之间同时期待对方开释资源的时候就会造成线程之间的死锁。为了避免死锁的产生,须要通过同步来实现线程平安。

线程互斥是指对于共享的过程系统资源,在各单个线程拜访时的排它性。当有若干个线程都要应用某一共享资源时,任何时刻最多只容许一个线程去应用,其它要应用该资源的线程必须期待,直到占用资源者开释该资源。线程互斥能够看成是一种非凡的线程同步。

线程间的同步办法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用零碎内核对象的单一性来进行同步,应用时须要切换内核态与用户态,而用户模式就是不须要切换到内核态,只在用户态实现操作。

用户模式下的办法有:原子操作(例如一个繁多的全局变量),临界区。内核模式下的办法有:事件,信号量,互斥量。

实现线程同步的办法

  • 同步代码办法:sychronized 关键字润饰的办法
  • 同步代码块:sychronized 关键字润饰的代码块
  • 应用非凡变量域 volatile 实现线程同步:volatile 关键字为域变量的拜访提供了一种免锁机制
  • 应用重入锁实现线程同步:reentrantlock 类是可冲入、互斥、实现了 lock 接口的锁他与 sychronized 办法具备雷同的根本行为和语义

在监视器 (Monitor) 外部,是如何做线程同步的?程序应该做哪种级别的同步?

在 java 虚拟机中,每个对象 (Object 和 class) 通过某种逻辑关联监视器, 每个监视器和一个对象援用相关联,为了实现监视器的互斥性能,每个对象都关联着一把锁。

一旦办法或者代码块被 synchronized 润饰,那么这个局部就放入了监视器的监督区域,确保一次只能有一个线程执行该局部的代码,线程在获取锁之前不容许执行该局部的代码

另外 java 还提供了显式监视器 (Lock) 和隐式监视器 (synchronized) 两种锁计划

如果你提交工作时,线程池队列已满,这时会产生什么

这里辨别一下:

(1)如果应用的是无界队列 LinkedBlockingQueue,也就是无界队列的话,没关系,持续增加工作到阻塞队列中期待执行,因为 LinkedBlockingQueue 能够近乎认为是一个无穷大的队列,能够有限寄存工作

(2)如果应用的是有界队列比方 ArrayBlockingQueue,工作首先会被增加到 ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会依据 maximumPoolSize 的值减少线程数量,如果减少了线程数量还是解决不过去,ArrayBlockingQueue 持续满,那么则会应用回绝策略 RejectedExecutionHandler 解决满了的工作,默认是 AbortPolicy

什么叫线程平安?servlet 是线程平安吗?

线程平安是编程中的术语,指某个办法在多线程环境中被调用时,可能正确地解决多个线程之间的共享变量,使程序性能正确实现。

Servlet 不是线程平安的,servlet 是单实例多线程的,当多个线程同时拜访同一个办法,是不能保障共享变量的线程安全性的。

Struts2 的 action 是多实例多线程的,是线程平安的,每个申请过去都会 new 一个新的 action 调配给这个申请,申请实现后销毁。

SpringMVC 的 Controller 是线程平安的吗?不是的,和 Servlet 相似的解决流程。

Struts2 益处是不必思考线程平安问题;Servlet 和 SpringMVC 须要思考线程平安问题,然而性能能够晋升不必解决太多的 gc,能够应用 ThreadLocal 来解决多线程的问题。

在 Java 程序中怎么保障多线程的运行平安?

  • 办法一:应用安全类,比方 java.util.concurrent 下的类,应用原子类 AtomicInteger
  • 办法二:应用主动锁 synchronized。
  • 办法三:应用手动锁 Lock。

手动锁 Java 示例代码如下:

Lock lock = new ReentrantLock();
lock. lock();
try {System. out. println("取得锁");
} catch (Exception e) {// TODO: handle exception} finally {System. out. println("开释锁");
    lock. unlock();}

你对线程优先级的了解是什么?

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

Java 的线程优先级调度会委托给操作系统去解决,所以与具体的操作系统优先级无关,如非特地须要,个别无需设置线程优先级。

线程类的构造方法、动态块是被哪个线程调用的

这是一个十分刁钻和刁滑的问题。请记住:线程类的构造方法、动态块是被 new 这个线程类所在的线程所调用的,而 run 办法外面的代码才是被线程本身所调用的。

如果说下面的说法让你感到困惑,那么我举个例子,假如 Thread2 中 new 了 Thread1,main 函数中 new 了 Thread2,那么:

(1)Thread2 的构造方法、动态块是 main 线程调用的,Thread2 的 run()办法是 Thread2 本人调用的

(2)Thread1 的构造方法、动态块是 Thread2 调用的,Thread1 的 run()办法是 Thread1 本人调用的

Java 中怎么获取一份线程 dump 文件?你如何在 Java 中获取线程堆栈?

Dump 文件是过程的内存镜像。能够把程序的执行状态通过调试器保留到 dump 文件中。

在 Linux 下,你能够通过命令 kill -3 PID(Java 过程的过程 ID)来获取 Java 利用的 dump 文件。

在 Windows 下,你能够按下 Ctrl + Break 来获取。这样 JVM 就会将线程的 dump 文件打印到规范输入或谬误文件中,它可能打印在控制台或者日志文件中,具体位置依赖利用的配置。

一个线程运行时产生异样会怎么?

如果异样没有被捕捉该线程将会进行执行。Thread.UncaughtExceptionHandler 是用于解决未捕捉异样造成线程忽然中断状况的一个内嵌接口。当一个未捕捉异样将造成线程中断的时候,JVM 会应用 Thread.getUncaughtExceptionHandler()来查问线程的 UncaughtExceptionHandler 并将线程和异样作为参数传递给 handler 的 uncaughtException()办法进行解决。

Java 线程数过多会造成什么异样?

  • 线程的生命周期开销十分高
  • 耗费过多的 CPU 资源如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量闲暇的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争 CPU 资源时还将产生其余性能的开销。
  • 升高稳定性 JVM 在可创立线程的数量上存在一个限度,这个限度值将随着平台的不同而不同,并且接受着多个因素制约,包含 JVM 的启动参数、Thread 构造函数中申请栈的大小,以及底层操作系统对线程的限度等。如果毁坏了这些限度,那么可能抛出 OutOfMemoryError 异样。

并发实践

Java 内存模型

Java 中垃圾回收有什么目标?什么时候进行垃圾回收?

垃圾回收是在内存中存在没有援用的对象或超过作用域的对象时进行的。

垃圾回收的目标是辨认并且抛弃利用不再应用的对象来开释和重用资源。

如果对象的援用被置为 null,垃圾收集器是否会立刻开释对象占用的内存?

不会,在下一个垃圾回调周期中,这个对象将是被可回收的。

也就是说并不会立刻被垃圾收集器立即回收,而是在下一次垃圾回收时才会开释其占用的内存。

finalize()办法什么时候被调用?析构函数 (finalization) 的目标是什么?

1)垃圾回收器(garbage colector)决定回收某对象时,就会运行该对象的 finalize()办法;finalize 是 Object 类的一个办法,该办法在 Object 类中的申明 protected void finalize() throws Throwable {} 在垃圾回收器执行时会调用被回收对象的 finalize()办法,能够笼罩此办法来实现对其资源的回收。留神:一旦垃圾回收器筹备开释对象占用的内存,将首先调用该对象的 finalize()办法,并且下一次垃圾回收动作产生时,才真正回收对象占用的内存空间

2)GC 原本就是内存回收了,利用还须要在 finalization 做什么呢?答案是大部分时候,什么都不必做(也就是不须要重载)。只有在某些很非凡的状况下,比方你调用了一些 native 的办法(个别是 C 写的),能够要在 finaliztion 里去调用 C 的开释函数。

重排序与数据依赖性

为什么代码会重排序?

在执行程序时,为了提供性能,处理器和编译器经常会对指令进行重排序,然而不能随便重排序,不是你想怎么排序就怎么排序,它须要满足以下两个条件:

  • 在单线程环境下不能扭转程序运行的后果;
  • 存在数据依赖关系的不容许重排序

须要留神的是:重排序不会影响单线程环境的执行后果,然而会毁坏多线程的执行语义。

as-if-serial 规定和 happens-before 规定的区别

  • as-if-serial 语义保障单线程内程序的执行后果不被扭转,happens-before 关系保障正确同步的多线程程序的执行后果不被扭转。
  • as-if-serial 语义给编写单线程程序的程序员发明了一个幻境:单线程程序是按程序的程序来执行的。happens-before 关系给编写正确同步的多线程程序的程序员发明了一个幻境:正确同步的多线程程序是按 happens-before 指定的程序来执行的。
  • as-if-serial 语义和 happens-before 这么做的目标,都是为了在不扭转程序执行后果的前提下,尽可能地进步程序执行的并行度。

并发关键字

synchronized

synchronized 的作用?

在 Java 中,synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,管制 synchronized 代码段不被多个线程同时执行。synchronized 能够润饰类、办法、变量。

另外,在 Java 晚期版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都须要操作系统帮忙实现,而操作系统实现线程之间的切换时须要从用户态转换到内核态,这个状态之间的转换须要绝对比拟长的工夫,工夫老本绝对较高,这也是为什么晚期的 synchronized 效率低的起因。庆幸的是在 Java 6 之后 Java 官网对从 JVM 层面对 synchronized 较大优化,所以当初的 synchronized 锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁打消、锁粗化、偏差锁、轻量级锁等技术来缩小锁操作的开销。

说说本人是怎么应用 synchronized 关键字,在我的项目中用到了吗

synchronized 关键字最次要的三种应用形式:

  • 润饰实例办法: 作用于以后对象实例加锁,进入同步代码前要取得以后对象实例的锁
  • 润饰静态方法: 也就是给以后类加锁,会作用于类的所有对象实例,因为动态成员不属于任何一个实例对象,是类成员(static 表明这是该类的一个动态资源,不论 new 了多少个对象,只有一份)。所以如果一个线程 A 调用一个实例对象的非动态 synchronized 办法,而线程 B 须要调用这个实例对象所属类的动态 synchronized 办法,是容许的,不会产生互斥景象,因为拜访动态 synchronized 办法占用的锁是以后类的锁,而拜访非动态 synchronized 办法占用的锁是以后实例对象锁
  • 润饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要取得给定对象的锁。

总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例办法上是给对象实例上锁。尽量不要应用 synchronized(String a) 因为 JVM 中,字符串常量池具备缓存性能!

上面我以一个常见的面试题为例解说一下 synchronized 关键字的具体应用。

面试中面试官常常会说:“单例模式理解吗?来给我手写一下!给我解释一下双重测验锁形式实现单例模式的原理呗!”

双重校验锁实现对象单例(线程平安)

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {}

    public static Singleton getUniqueInstance() {
       // 先判断对象是否曾经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            // 类对象加锁
            synchronized (Singleton.class) {if (uniqueInstance == null) {uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

另外,须要留神 uniqueInstance 采纳 volatile 关键字润饰也是很有必要。

uniqueInstance 采纳 volatile 关键字润饰也是很有必要的,uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

  1. 为 uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. 将 uniqueInstance 指向调配的内存地址

然而因为 JVM 具备指令重排的个性,执行程序有可能变成 1->3->2。指令重排在单线程环境下不会呈现问题,然而在多线程环境下会导致一个线程取得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因而返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

应用 volatile 能够禁止 JVM 的指令重排,保障在多线程环境下也能失常运行。

说一下 synchronized 底层实现原理?

synchronized 是 Java 中的一个关键字,在应用的过程中并没有看到显示的加锁和解锁过程。因而有必要通过 javap 命令,查看相应的字节码文件。

synchronized 同步语句块的状况

public class SynchronizedDemo {public void method() {synchronized (this) {System.out.println("synchronized 代码块");
        }
    }
}

通过 JDK 反汇编指令 javap -c -v SynchronizedDemo

能够看出在执行同步代码块之前之后都有一个 monitor 字样,其中后面的是 monitorenter,前面的是来到 monitorexit,不难想象一个线程也执行同步代码块,首先要获取锁,而获取锁的过程就是 monitorenter,在执行完代码块之后,要开释锁,开释锁就是执行 monitorexit 指令。

为什么会有两个 monitorexit 呢?

这个次要是避免在同步代码块中线程因异样退出,而锁没有失去开释,这必然会造成死锁(期待的线程永远获取不到锁)。因而最初一个 monitorexit 是保障在异常情况下,锁也能够失去开释,防止死锁。仅有 ACC_SYNCHRONIZED 这么一个标记,该标记表明线程进入该办法时,须要 monitorenter,退出该办法时须要 monitorexit。

synchronized 可重入的原理

重入锁是指一个线程获取到该锁之后,该线程能够持续取得该锁。底层原理保护一个计数器,当线程获取该锁时,计数器加一,再次取得该锁时持续加一,开释锁时,计数器减一,当计数器值为 0 时,表明该锁未被任何线程所持有,其它线程能够竞争获取锁。

什么是自旋

很多 synchronized 外面的代码只是一些很简略的代码,执行工夫十分快,此时期待的线程都加锁可能是一种不太值得的操作,因为线程阻塞波及到用户态和内核态切换的问题。既然 synchronized 外面的代码执行得十分快,无妨让期待锁的线程不要被阻塞,而是在 synchronized 的边界做忙循环,这就是自旋。如果做了屡次循环发现还没有取得锁,再阻塞,这样可能是一种更好的策略。

多线程中 synchronized 锁降级的原理是什么?

synchronized 锁降级原理:在锁对象的对象头外面有一个 threadid 字段,在第一次拜访的时候 threadid 为空,jvm 让其持有偏差锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 统一,如果统一则能够间接应用此对象,如果不统一,则降级偏差锁为轻量级锁,通过自旋循环肯定次数来获取锁,执行肯定次数之后,如果还没有失常获取到要应用的对象,此时就会把锁从轻量级降级为重量级锁,此过程就形成了 synchronized 锁的降级。

锁的降级的目标:锁降级是为了减低了锁带来的性能耗费。在 Java 6 之后优化 synchronized 的实现形式,应用了偏差锁降级为轻量级锁再降级到重量级锁的形式,从而减低了锁带来的性能耗费。

线程 B 怎么晓得线程 A 批改了变量

(1)volatile 润饰变量

(2)synchronized 润饰批改变量的办法

(3)wait/notify

(4)while 轮询

当一个线程进入一个对象的 synchronized 办法 A 之后,其它线程是否可进入此对象的 synchronized 办法 B?

不能。其它线程只能拜访该对象的非同步办法,同步办法则不能进入。因为非静态方法上的 synchronized 修饰符要求执行办法时要取得对象的锁,如果曾经进入 A 办法阐明对象锁曾经被取走,那么试图进入 B 办法的线程就只能在等锁池(留神不是期待池哦)中期待对象的锁。

synchronized、volatile、CAS 比拟

(1)synchronized 是乐观锁,属于抢占式,会引起其余线程阻塞。

(2)volatile 提供多线程共享变量可见性和禁止指令重排序优化。

(3)CAS 是基于冲突检测的乐观锁(非阻塞)

synchronized 和 Lock 有什么区别?

  • 首先 synchronized 是 Java 内置关键字,在 JVM 层面,Lock 是个 Java 类;
  • synchronized 能够给类、办法、代码块加锁;而 lock 只能给代码块加锁。
  • synchronized 不须要手动获取锁和开释锁,应用简略,产生异样会主动开释锁,不会造成死锁;而 lock 须要本人加锁和开释锁,如果使用不当没有 unLock()去开释锁就会造成死锁。
  • 通过 Lock 能够晓得有没有胜利获取锁,而 synchronized 却无奈办到。

synchronized 和 ReentrantLock 区别是什么?

synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比 synchronized 更多更灵便的个性,能够被继承、能够有办法、能够有各种各样的类变量

synchronized 晚期的实现比拟低效,比照 ReentrantLock,大多数场景性能都相差较大,然而在 Java 6 中对 synchronized 进行了十分多的改良。

相同点:两者都是可重入锁

两者都是可重入锁。“可重入锁”概念是:本人能够再次获取本人的外部锁。比方一个线程取得了某个对象的锁,此时这个对象锁还没有开释,当其再次想要获取这个对象的锁的时候还是能够获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器降落为 0 时能力开释锁。

次要区别如下:

  • ReentrantLock 应用起来比拟灵便,然而必须有开释锁的配合动作;
  • ReentrantLock 必须手动获取与开释锁,而 synchronized 不须要手动开释和开启锁;
  • ReentrantLock 只实用于代码块锁,而 synchronized 能够润饰类、办法、变量等。
  • 二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的 park 办法加锁,synchronized 操作的应该是对象头中 mark word

Java 中每一个对象都能够作为锁,这是 synchronized 实现同步的根底:

  • 一般同步办法,锁是以后实例对象
  • 动态同步办法,锁是以后类的 class 对象
  • 同步办法块,锁是括号外面的对象

volatile

volatile 关键字的作用

对于可见性,Java 提供了 volatile 关键字来保障可见性和禁止指令重排。volatile 提供 happens-before 的保障,确保一个线程的批改能对其余线程是可见的。当一个共享变量被 volatile 润饰时,它会保障批改的值会立刻被更新到主存,当有其余线程须要读取时,它会去内存中读取新值。

从实际角度而言,volatile 的一个重要作用就是和 CAS 联合,保障了原子性,具体的能够参见 java.util.concurrent.atomic 包下的类,比方 AtomicInteger。

volatile 罕用于多线程环境下的单次操作(单次读或者单次写)。

Java 中能创立 volatile 数组吗?

能,Java 中能够创立 volatile 类型数组,不过只是一个指向数组的援用,而不是整个数组。意思是,如果扭转援用指向的数组,将会受到 volatile 的爱护,然而如果多个线程同时扭转数组的元素,volatile 标示符就不能起到之前的爱护作用了。

volatile 变量和 atomic 变量有什么不同?

volatile 变量能够确保后行关系,即写操作会产生在后续的读操作之前, 但它并不能保障原子性。例如用 volatile 润饰 count 变量,那么 count++ 操作就不是原子性的。

而 AtomicInteger 类提供的 atomic 办法能够让这种操作具备原子性如 getAndIncrement()办法会原子性的进行增量操作把以后值加一,其它数据类型和援用变量也能够进行类似操作。

volatile 能使得一个非原子操作变成原子操作吗?

关键字 volatile 的次要作用是使变量在多个线程间可见,但无奈保障原子性,对于多个线程拜访同一个实例变量须要加锁进行同步。

尽管 volatile 只能保障可见性不能保障原子性,但用 volatile 润饰 long 和 double 能够保障其操作原子性。

所以从 Oracle Java Spec 外面能够看到:

  • 对于 64 位的 long 和 double,如果没有被 volatile 润饰,那么对其操作能够不是原子的。在操作的时候,能够分成两步,每次对 32 位操作。
  • 如果应用 volatile 润饰 long 和 double,那么其读写都是原子操作
  • 对于 64 位的援用地址的读写,都是原子操作
  • 在实现 JVM 时,能够自由选择是否把读写 long 和 double 作为原子操作
  • 举荐 JVM 实现为原子操作

volatile 修饰符的有过什么实际?

单例模式

是否 Lazy 初始化:是

是否多线程平安:是

实现难度:较简单

形容:对于 Double-Check 这种可能呈现的问题(当然这种概率曾经十分小了,但毕竟还是有的嘛~),解决方案是:只须要给 instance 的申明加上 volatile 关键字即可 volatile 关键字的一个作用是禁止指令重排,把 instance 申明为 volatile 之后,对它的写操作就会有一个内存屏障(什么是内存屏障?),这样,在它的赋值实现之前,就不必会调用读操作。留神:volatile 阻止的不是 singleton = newSingleton()这句话外部 [1-2-3] 的指令重排,而是保障了在一个写操作([1-2-3])实现之前,不会调用读操作(if (instance == null))。

public class Singleton7 {

    private static volatile Singleton7 instance = null;

    private Singleton7() {}

    public static Singleton7 getInstance() {if (instance == null) {synchronized (Singleton7.class) {if (instance == null) {instance = new Singleton7();
                }
            }
        }

        return instance;
    }

}

synchronized 和 volatile 的区别是什么?

synchronized 示意只有一个线程能够获取作用对象的锁,执行代码,阻塞其余线程。

volatile 示意变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保障多线程环境下变量的可见性;禁止指令重排序。

区别

  • volatile 是变量修饰符;synchronized 能够润饰类、办法、变量。
  • volatile 仅能实现变量的批改可见性,不能保障原子性;而 synchronized 则能够保障变量的批改可见性和原子性。
  • volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
  • volatile 标记的变量不会被编译器优化;synchronized 标记的变量能够被编译器优化。
  • volatile 关键字 是线程同步的 轻量级实现 ,所以volatile 性能必定比 synchronized 关键字要好。然而volatile 关键字只能用于变量而 synchronized 关键字能够润饰办法以及代码块。synchronized 关键字在 JavaSE1.6 之后进行了次要包含为了缩小取得锁和开释锁带来的性能耗费而引入的偏差锁和轻量级锁以及其它各种优化之后执行效率有了显著晋升, 理论开发中应用 synchronized 关键字的场景还是更多一些

final

什么是不可变对象,它对写并发利用有什么帮忙?

不可变对象 (Immutable Objects) 即对象一旦被创立它的状态(对象的数据,也即对象属性值)就不能扭转,反之即为可变对象(Mutable Objects)。

不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中蕴含许多不可变类,如 String、根本类型的包装类、BigInteger 和 BigDecimal 等。

只有满足如下状态,一个对象才是不可变的;

  • 它的状态不能在创立后再被批改;
  • 所有域都是 final 类型;并且,它被正确创立(创立期间没有产生 this 援用的逸出)。

不可变对象保障了对象的内存可见性,对不可变对象的读取不须要进行额定的同步伎俩,晋升了代码执行效率。

Lock 体系

Lock 简介与初识 AQS

Java Concurrency API 中的 Lock 接口 (Lock interface) 是什么?比照同步它有什么劣势?

Lock 接口比同步办法和同步块提供了更具扩展性的锁操作。他们容许更灵便的构造,能够具备齐全不同的性质,并且能够反对多个相干类的条件对象。

它的劣势有:

(1)能够使锁更偏心

(2)能够使线程在期待锁的时候响应中断

(3)能够让线程尝试获取锁,并在无奈获取锁的时候立刻返回或者期待一段时间

(4)能够在不同的范畴,以不同的程序获取和开释锁

整体上来说 Lock 是 synchronized 的扩大版,Lock 提供了无条件的、可轮询的 (tryLock 办法)、定时的(tryLock 带参办法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 办法) 锁操作。另外 Lock 的实现类根本都反对非偏心锁 (默认) 和偏心锁,synchronized 只反对非偏心锁,当然,在大部分状况下,非偏心锁是高效的抉择。

乐观锁和乐观锁的了解及如何实现,有哪些实现形式?

乐观锁:总是假如最坏的状况,每次去拿数据的时候都认为他人会批改,所以每次在拿数据的时候都会上锁,这样他人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比方行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比方 Java 外面的同步原语 synchronized 关键字的实现也是乐观锁。

乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为他人不会批改,所以不会上锁,然而在更新的时候会判断一下在此期间他人有没有去更新这个数据,能够应用版本号等机制。乐观锁实用于多读的利用类型,这样能够进步吞吐量,像数据库提供的相似于 write_condition 机制,其实都是提供的乐观锁。在 Java 中 java.util.concurrent.atomic 包上面的原子变量类就是应用了乐观锁的一种实现形式 CAS 实现的。

乐观锁的实现形式:

1、应用版本标识来确定读到的数据与提交时的数据是否统一。提交后批改版本标识,不统一时能够采取抛弃和再次尝试的策略。

2、java 中的 Compare and Swap 即 CAS,当多个线程尝试应用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并能够再次尝试。CAS 操作中蕴含三个操作数 —— 须要读写的内存地位(V)、进行比拟的预期原值(A)和拟写入的新值(B)。如果内存地位 V 的值与预期原值 A 相匹配,那么处理器会主动将该地位值更新为新值 B。否则处理器不做任何操作。

什么是 CAS

CAS 是 compare and swap 的缩写,即咱们所说的比拟替换。

cas 是一种基于锁的操作,而且是乐观锁。在 java 中锁分为乐观锁和乐观锁。乐观锁是将资源锁住,等一个之前取得锁的线程开释锁之后,下一个线程才能够拜访。而乐观锁采取了一种宽泛的态度,通过某种形式不加锁来解决资源,比方通过给记录加 version 来获取数据,性能较乐观锁有很大的进步。

CAS 操作蕴含三个操作数 —— 内存地位(V)、预期原值(A)和新值(B)。如果内存地址外面的值和 A 的值是一样的,那么就将内存外面的值更新成 B。CAS 是通过有限循环来获取数据的,若果在第一轮循环中,a 线程获取地址外面的值被 b 线程批改了,那么 a 线程须要自旋,到下次循环才有可能机会执行。

java.util.concurrent.atomic 包下的类大多是应用 CAS 操作来实现的(AtomicInteger,AtomicBoolean,AtomicLong)。

CAS 的会产生什么问题?

1、ABA 问题:

比如说一个线程 one 从内存地位 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,而后 two 又将 V 地位的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中依然是 A,而后 one 操作胜利。只管线程 one 的 CAS 操作胜利,但可能存在潜藏的问题。从 Java1.5 开始 JDK 的 atomic 包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。

2、循环工夫长开销大:

对于资源竞争重大(线程抵触重大)的状况,CAS 自旋的概率会比拟大,从而节约更多的 CPU 资源,效率低于 synchronized。

3、只能保障一个共享变量的原子操作:

当对一个共享变量执行操作时,咱们能够应用循环 CAS 的形式来保障原子操作,然而对多个共享变量操作时,循环 CAS 就无奈保障操作的原子性,这个时候就能够用锁。

什么是死锁?

当线程 A 持有独占锁 a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的状况下,就会产生 AB 两个线程因为相互持有对方须要的锁,而产生的阻塞景象,咱们称为死锁。

产生死锁的条件是什么?怎么避免死锁?

产生死锁的必要条件:

1、互斥条件:所谓互斥就是过程在某一时间内独占资源。

2、申请与放弃条件:一个过程因申请资源而阻塞时,对已取得的资源放弃不放。

3、不剥夺条件:过程已取得资源,在末应用完之前,不能强行剥夺。

4、循环期待条件:若干过程之间造成一种头尾相接的循环期待资源关系。

这四个条件是死锁的必要条件,只有零碎产生死锁,这些条件必然成立,而只有上述条件之 一不满足,就不会产生死锁。

了解了死锁的起因,尤其是产生死锁的四个必要条件,就能够最大可能地防止、预防和 解除死锁。

避免死锁能够采纳以下的办法:

  • 尽量应用 tryLock(long timeout, TimeUnit unit)的办法(ReentrantLock、ReentrantReadWriteLock),设置超时工夫,超时能够退出避免死锁。
  • 尽量应用 Java. util. concurrent 并发类代替本人手写锁。
  • 尽量升高锁的应用粒度,尽量不要几个性能用同一把锁。
  • 尽量减少同步的代码块。

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

死锁:是指两个或两个以上的过程(或线程)在执行过程中,因抢夺资源而造成的一种相互期待的景象,若无外力作用,它们都将无奈推动上来。

活锁:工作或者执行者没有被阻塞,因为某些条件没有满足,导致始终反复尝试,失败,尝试,失败。

活锁和死锁的区别在于,处于活锁的实体是在一直的扭转状态,这就是所谓的“活”,而处于死锁的实体体现为期待;活锁有可能自行解开,死锁则不能。

饥饿:一个或者多个线程因为种种原因无奈取得所须要的资源,导致始终无奈执行的状态。

Java 中导致饥饿的起因:

1、高优先级线程吞噬所有的低优先级线程的 CPU 工夫。

2、线程被永恒梗塞在一个期待进入同步块的状态,因为其余线程总是能在它之前继续地对该同步块进行拜访。

3、线程在期待一个自身也处于永恒期待实现的对象(比方调用这个对象的 wait 办法),因为其余线程总是被继续地取得唤醒。

多线程锁的降级原理是什么?

在 Java 中,锁共有 4 种状态,级别从低到高顺次为:无状态锁,偏差锁,轻量级锁和重量级锁状态,这几个状态会随着竞争状况逐步降级。锁能够降级但不能降级。

AQS(AbstractQueuedSynchronizer)详解与源码剖析

AQS 介绍

AQS 的全称为(AbstractQueuedSynchronizer),这个类在 java.util.concurrent.locks 包上面。

AQS 是一个用来构建锁和同步器的框架,应用 AQS 能简略且高效地结构出利用宽泛的大量的同步器,比方咱们提到的 ReentrantLock,Semaphore,其余的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等皆是基于 AQS 的。当然,咱们本人也能利用 AQS 十分轻松容易地结构出合乎咱们本人需要的同步器。

AQS 原理剖析

上面大部分内容其实在 AQS 类正文上曾经给出了,不过是英语看着比拟吃力一点,感兴趣的话能够看看源码。

AQS 原理概览

AQS 核心思想是,如果被申请的共享资源闲暇,则将以后申请资源的线程设置为无效的工作线程,并且将共享资源设置为锁定状态。如果被申请的共享资源被占用,那么就须要一套线程阻塞期待以及被唤醒时锁调配的机制,这个机制 AQS 是用 CLH 队列锁实现的,行将临时获取不到锁的线程退出到队列中。

CLH(Craig,Landin,and Hagersten)队列是一个虚构的双向队列(虚构的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条申请共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的调配。

看个 AQS(AbstractQueuedSynchronizer)原理图:

AQS 应用一个 int 成员变量来示意同步状态,通过内置的 FIFO 队列来实现获取资源线程的排队工作。AQS 应用 CAS 对该同步状态进行原子操作实现对其值的批改。

private volatile int state;// 共享变量,应用 volatile 润饰保障线程可见性

状态信息通过 protected 类型的 getState,setState,compareAndSetState 进行操作

// 返回同步状态的以后值
protected final int getState() {return state;}
 // 设置同步状态的值
protected final void setState(int newState) {state = newState;}
// 原子地(CAS 操作)将同步状态值设置为给定值 update 如果以后同步状态的值等于 expect(期望值)protected final boolean compareAndSetState(int expect, int update) {return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

AQS 对资源的共享形式

AQS 定义两种资源共享形式

  • Exclusive(独占):只有一个线程能执行,如 ReentrantLock。又可分为偏心锁和非偏心锁:

    • 偏心锁:依照线程在队列中的排队程序,先到者先拿到锁
    • 非偏心锁:当线程要获取锁时,忽视队列程序间接去抢锁,谁抢到就是谁的
  • Share(共享):多个线程可同时执行,如 Semaphore/CountDownLatch。Semaphore、CountDownLatch、CyclicBarrier、ReadWriteLock 咱们都会在前面讲到。

ReentrantReadWriteLock 能够看成是组合式,因为 ReentrantReadWriteLock 也就是读写锁容许多个线程同时对某一资源进行读。

不同的自定义同步器争用共享资源的形式也不同。自定义同步器在实现时只须要实现共享资源 state 的获取与开释形式即可,至于具体线程期待队列的保护(如获取资源失败入队 / 唤醒出队等),AQS 曾经在顶层实现好了。

AQS 底层应用了模板办法模式

同步器的设计是基于模板办法模式的,如果须要自定义同步器个别的形式是这样(模板办法模式很经典的一个利用):

  1. 使用者继承 AbstractQueuedSynchronizer 并重写指定的办法。(这些重写办法很简略,无非是对于共享资源 state 的获取和开释)
  2. 将 AQS 组合在自定义同步组件的实现中,并调用其模板办法,而这些模板办法会调用使用者重写的办法。

这和咱们以往通过实现接口的形式有很大区别,这是模板办法模式很经典的一个使用。

AQS 应用了模板办法模式,自定义同步器时须要重写上面几个 AQS 提供的模板办法:

isHeldExclusively()// 该线程是否正在独占资源。只有用到 condition 才须要去实现它。tryAcquire(int)// 独占形式。尝试获取资源,胜利则返回 true,失败则返回 false。tryRelease(int)// 独占形式。尝试开释资源,胜利则返回 true,失败则返回 false。tryAcquireShared(int)// 共享形式。尝试获取资源。正数示意失败;0 示意胜利,但没有残余可用资源;负数示意胜利,且有残余资源。tryReleaseShared(int)// 共享形式。尝试开释资源,胜利则返回 true,失败则返回 false。

默认状况下,每个办法都抛出 UnsupportedOperationException。这些办法的实现必须是外部线程平安的,并且通常应该简短而不是阻塞。AQS 类中的其余办法都是 final,所以无奈被其余类应用,只有这几个办法能够被其余类应用。

以 ReentrantLock 为例,state 初始化为 0,示意未锁定状态。A 线程 lock()时,会调用 tryAcquire()独占该锁并将 state+1。尔后,其余线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即开释锁)为止,其它线程才有机会获取该锁。当然,开释锁之前,A 线程本人是能够反复获取此锁的(state 会累加),这就是可重入的概念。但要留神,获取多少次就要开释如许次,这样能力保障 state 是能回到零态的。

再以 CountDownLatch 以例,工作分为 N 个子线程去执行,state 也初始化为 N(留神 N 要与线程个数统一)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次,state 会 CAS(Compare and Swap)减 1。等到所有子线程都执行完后 (即 state=0),会 unpark() 主调用线程,而后主调用线程就会从 await()函数返回,持续后余动作。

一般来说,自定义同步器要么是独占办法,要么是共享形式,他们也只需实现 tryAcquire-tryReleasetryAcquireShared-tryReleaseShared 中的一种即可。但 AQS 也反对自定义同步器同时实现独占和共享两种形式,如ReentrantReadWriteLock

ReentrantLock(重入锁)实现原理与偏心锁非偏心锁区别

什么是可重入锁(ReentrantLock)?

ReentrantLock 重入锁,是实现 Lock 接口的一个类,也是在理论编程中应用频率很高的一个锁,反对重入性,示意可能对共享资源可能反复加锁,即以后线程获取该锁再次获取不会被阻塞。

在 java 关键字 synchronized 隐式反对重入性,synchronized 通过获取自增,开释自减的形式实现重入。与此同时,ReentrantLock 还反对偏心锁和非偏心锁两种形式。那么,要想完完全全的弄懂 ReentrantLock 的话,次要也就是 ReentrantLock 同步语义的学习:1. 重入性的实现原理;2. 偏心锁和非偏心锁。

重入性的实现原理

要想反对重入性,就要解决两个问题:1. 在线程获取锁的时候,如果曾经获取锁的线程是以后线程的话则间接再次获取胜利;2. 因为锁会被获取 n 次,那么只有锁在被开释同样的 n 次之后,该锁才算是齐全开释胜利

ReentrantLock 反对两种锁:偏心锁 非偏心锁 何谓公平性,是针对获取锁而言的,如果一个锁是偏心的,那么锁的获取程序就应该合乎申请上的相对工夫程序,满足 FIFO

读写锁 ReentrantReadWriteLock 源码剖析

ReadWriteLock 是什么

首先明确一下,不是说 ReentrantLock 不好,只是 ReentrantLock 某些时候有局限。如果应用 ReentrantLock,可能自身是为了避免线程 A 在写数据、线程 B 在读数据造成的数据不统一,但这样,如果线程 C 在读数据、线程 D 也在读数据,读数据是不会扭转数据的,没有必要加锁,然而还是加锁了,升高了程序的性能。因为这个,才诞生了读写锁 ReadWriteLock。

ReadWriteLock 是一个读写锁接口,读写锁是用来晋升并发程序性能的锁拆散技术,ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写的拆散,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,晋升了读写的性能。

而读写锁有以下三个重要的个性:

(1)偏心选择性:反对非偏心(默认)和偏心的锁获取形式,吞吐量还是非偏心优于偏心。

(2)重进入:读锁和写锁都反对线程重进入。

(3)锁降级:遵循获取写锁、获取读锁再开释写锁的秩序,写锁可能降级成为读锁。

Condition 源码剖析与期待告诉机制

LockSupport 详解

并发容器

并发容器之 ConcurrentHashMap 详解 (JDK1.8 版本) 与源码剖析

什么是 ConcurrentHashMap?

ConcurrentHashMap 是 Java 中的一个 线程平安且高效的 HashMap 实现。平时波及高并发如果要用 map 构造,那第一工夫想到的就是它。绝对于 hashmap 来说,ConcurrentHashMap 就是线程平安的 map,其中利用了锁分段的思维进步了并发度。

那么它到底是如何实现线程平安的?

JDK 1.6 版本要害因素:

  • segment 继承了 ReentrantLock 充当锁的角色,为每一个 segment 提供了线程平安的保障;
  • segment 保护了哈希散列表的若干个桶,每个桶由 HashEntry 形成的链表。

JDK1.8 后,ConcurrentHashMap 摈弃了原有的Segment 分段锁,而采纳了 CAS + synchronized 来保障并发安全性

Java 中 ConcurrentHashMap 的并发度是什么?

ConcurrentHashMap 把理论 map 划分成若干局部来实现它的可扩展性和线程平安。这种划分是应用并发度取得的,它是 ConcurrentHashMap 类构造函数的一个可选参数,默认值为 16,这样在多线程状况下就能防止争用。

在 JDK8 后,它摒弃了 Segment(锁段)的概念,而是启用了一种全新的形式实现, 利用 CAS 算法。同时退出了更多的辅助变量来进步并发度,具体内容还是查看源码吧。

什么是并发容器的实现?

何为同步容器:能够简略地了解为通过 synchronized 来实现同步的容器,如果有多个线程调用同步容器的办法,它们将会串行执行。比方 Vector,Hashtable,以及 Collections.synchronizedSet,synchronizedList 等办法返回的容器。能够通过查看 Vector,Hashtable 等这些同步容器的实现代码,能够看到这些容器实现线程平安的形式就是将它们的状态封装起来,并在须要同步的办法上加上关键字 synchronized。

并发容器应用了与同步容器齐全不同的加锁策略来提供更高的并发性和伸缩性,例如在 ConcurrentHashMap 中采纳了一种粒度更细的加锁机制,能够称为分段锁,在这种锁机制下,容许任意数量的读线程并发地拜访 map,并且执行读操作的线程和写操作的线程也能够并发的拜访 map,同时容许肯定数量的写操作线程并发地批改 map,所以它能够在并发环境下实现更高的吞吐量。

Java 中的同步汇合与并发汇合有什么区别?

同步汇合与并发汇合都为多线程和并发提供了适合的线程平安的汇合,不过并发汇合的可扩展性更高。在 Java1.5 之前程序员们只有同步汇合来用且在多线程并发的时候会导致争用,妨碍了零碎的扩展性。Java5 介绍了并发汇合像 ConcurrentHashMap,不仅提供线程平安还用锁拆散和外部分区等古代技术进步了可扩展性。

SynchronizedMap 和 ConcurrentHashMap 有什么区别?

SynchronizedMap 一次锁住整张表来保障线程平安,所以每次只能有一个线程来访为 map。

ConcurrentHashMap 应用分段锁来保障在多线程下的性能。

ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将 hash 表分为 16 个桶,诸如 get,put,remove 等罕用操作只锁以后须要用到的桶。

这样,原来只能一个线程进入,当初却能同时有 16 个写线程执行,并发性能的晋升是不言而喻的。

另外 ConcurrentHashMap 应用了一种不同的迭代形式。在这种迭代形式中,当 iterator 被创立后汇合再产生扭转就不再是抛出 ConcurrentModificationException,取而代之的是在扭转时 new 新的数据从而不影响原有的数据,iterator 实现后再将头指针替换为新的数据,这样 iterator 线程能够应用原来老的数据,而写线程也能够并发的实现扭转。

并发容器之 CopyOnWriteArrayList 详解

CopyOnWriteArrayList 是什么,能够用于什么利用场景?有哪些优缺点?

CopyOnWriteArrayList 是一个并发容器。有很多人称它是线程平安的,我认为这句话不谨严,短少一个前提条件,那就是非复合场景下操作它是线程平安的。

CopyOnWriteArrayList(免锁容器)的益处之一是当多个迭代器同时遍历和批改这个列表时,不会抛出 ConcurrentModificationException。在 CopyOnWriteArrayList 中,写入将导致创立整个底层数组的正本,而源数组将保留在原地,使得复制的数组在被批改时,读取操作能够平安地执行。

CopyOnWriteArrayList 的应用场景

通过源码剖析,咱们看出它的优缺点比拟显著,所以应用场景也就比拟显著。就是适合读多写少的场景。

CopyOnWriteArrayList 的毛病

  1. 因为写操作的时候,须要拷贝数组,会耗费内存,如果原数组的内容比拟多的状况下,可能导致 young gc 或者 full gc。
  2. 不能用于实时读的场景,像拷贝数组、新增元素都须要工夫,所以调用一个 set 操作后,读取到数据可能还是旧的,尽管 CopyOnWriteArrayList 能做到最终一致性, 然而还是没法满足实时性要求。
  3. 因为理论应用中可能没法保障 CopyOnWriteArrayList 到底要搁置多少数据,万一数据略微有点多,每次 add/set 都要从新复制数组,这个代价切实太昂扬了。在高性能的互联网利用中,这种操作分分钟引起故障。

CopyOnWriteArrayList 的设计思维

  1. 读写拆散,读和写离开
  2. 最终一致性
  3. 应用另外开拓空间的思路,来解决并发抵触

并发容器之 ThreadLocal 详解

ThreadLocal 是什么?有哪些应用场景?

ThreadLocal 是一个本地线程正本变量工具类,在每个线程中都创立了一个 ThreadLocalMap 对象,简略说 ThreadLocal 就是一种以空间换工夫的做法,每个线程能够拜访本人外部 ThreadLocalMap 对象内的 value。通过这种形式,防止资源在多线程间共享。

原理:线程局部变量是局限于线程外部的变量,属于线程本身所有,不在多个线程间共享。Java 提供 ThreadLocal 类来反对线程局部变量,是一种实现线程平安的形式。然而在治理环境下(如 web 服务器)应用线程局部变量的时候要特地小心,在这种状况下,工作线程的生命周期比任何利用变量的生命周期都要长。任何线程局部变量一旦在工作实现后没有开释,Java 利用就存在内存泄露的危险。

经典的应用场景是为每个线程调配一个 JDBC 连贯 Connection。这样就能够保障每个线程的都在各自的 Connection 上进行数据库的操作,不会呈现 A 线程关了 B 线程正在应用的 Connection;还有 Session 治理 等问题。

ThreadLocal 应用例子:

public class TestThreadLocal {
    
    // 线程本地存储变量
    private static final ThreadLocal<Integer> THREAD_LOCAL_NUM 
        = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {return 0;}
    };
 
    public static void main(String[] args) {for (int i = 0; i <3; i++) {// 启动三个线程
            Thread t = new Thread() {
                @Override
                public void run() {add10ByThreadLocal();
                }
            };
            t.start();}
    }
    
    /** * 线程本地存储变量加 5 */
    private static void add10ByThreadLocal() {for (int i = 0; i <5; i++) {Integer n = THREAD_LOCAL_NUM.get();
            n += 1;
            THREAD_LOCAL_NUM.set(n);
            System.out.println(Thread.currentThread().getName() + ": ThreadLocal num=" + n);
        }
    }
    
}

打印后果:启动了 3 个线程,每个线程最初都打印到“ThreadLocal num=5”,而不是 num 始终在累加直到值等于 15

Thread-0 : ThreadLocal num=1
Thread-1 : ThreadLocal num=1
Thread-0 : ThreadLocal num=2
Thread-0 : ThreadLocal num=3
Thread-1 : ThreadLocal num=2
Thread-2 : ThreadLocal num=1
Thread-0 : ThreadLocal num=4
Thread-2 : ThreadLocal num=2
Thread-1 : ThreadLocal num=3
Thread-1 : ThreadLocal num=4
Thread-2 : ThreadLocal num=3
Thread-0 : ThreadLocal num=5
Thread-2 : ThreadLocal num=4
Thread-2 : ThreadLocal num=5
Thread-1 : ThreadLocal num=5

什么是线程局部变量?

线程局部变量是局限于线程外部的变量,属于线程本身所有,不在多个线程间共享。Java 提供 ThreadLocal 类来反对线程局部变量,是一种实现线程平安的形式。然而在治理环境下(如 web 服务器)应用线程局部变量的时候要特地小心,在这种状况下,工作线程的生命周期比任何利用变量的生命周期都要长。任何线程局部变量一旦在工作实现后没有开释,Java 利用就存在内存泄露的危险。

ThreadLocal 内存透露剖析与解决方案

ThreadLocal 造成内存透露的起因?

ThreadLocalMap 中应用的 key 为 ThreadLocal 的弱援用, 而 value 是强援用。所以,如果 ThreadLocal 没有被内部强援用的状况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会呈现 key 为 null 的 Entry。如果咱们不做任何措施的话,value 永远无奈被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中曾经思考了这种状况,在调用 set()get()remove() 办法的时候,会清理掉 key 为 null 的记录。应用完 ThreadLocal办法后 最好手动调用 remove() 办法

ThreadLocal 内存透露解决方案?

  • 每次应用完 ThreadLocal,都调用它的 remove()办法,革除数据。
  • 在应用线程池的状况下,没有及时清理 ThreadLocal,不仅是内存透露的问题,更重大的是可能导致业务逻辑呈现问题。所以,应用 ThreadLocal 就跟加锁完要解锁一样,用完就清理。

并发容器之 BlockingQueue 详解

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

阻塞队列(BlockingQueue)是一个反对两个附加操作的队列。

这两个附加的操作是:在队列为空时,获取元素的线程会期待队列变为非空。当队列满时,存储元素的线程会期待队列可用。

阻塞队列罕用于生产者和消费者的场景,生产者是往队列里增加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者寄存元素的容器,而消费者也只从容器里拿元素。

JDK7 提供了 7 个阻塞队列。别离是:

ArrayBlockingQueue:一个由数组构造组成的有界阻塞队列。

LinkedBlockingQueue:一个由链表构造组成的有界阻塞队列。

PriorityBlockingQueue:一个反对优先级排序的无界阻塞队列。

DelayQueue:一个应用优先级队列实现的无界阻塞队列。

SynchronousQueue:一个不存储元素的阻塞队列。

LinkedTransferQueue:一个由链表构造组成的无界阻塞队列。

LinkedBlockingDeque:一个由链表构造组成的双向阻塞队列。

Java 5 之前实现同步存取时,能够应用一般的一个汇合,而后在应用线程的合作和线程同步能够实现生产者,消费者模式,次要的技术就是用好,wait,notify,notifyAll,sychronized 这些关键字。而在 java 5 之后,能够应用阻塞队列来实现,此形式大大简少了代码量,使得多线程编程更加容易,平安方面也有保障。

BlockingQueue 接口是 Queue 的子接口,它的主要用途并不是作为容器,而是作为线程同步的的工具,因而他具备一个很显著的个性,当生产者线程试图向 BlockingQueue 放入元素时,如果队列已满,则线程被阻塞,当消费者线程试图从中取出一个元素时,如果队列为空,则该线程会被阻塞,正是因为它所具备这个个性,所以在程序中多个线程交替向 BlockingQueue 中放入元素,取出元素,它能够很好的控制线程之间的通信。

阻塞队列应用最经典的场景就是 socket 客户端数据的读取和解析,读取数据的线程一直将数据放入队列,而后解析线程一直从队列取数据解析。

并发容器之 ConcurrentLinkedQueue 详解与源码剖析

并发容器之 ArrayBlockingQueue 与 LinkedBlockingQueue 详解

线程池

Executors 类创立四种常见线程池

什么是线程池?有哪几种创立形式?

池化技术相比大家曾经不足为奇了,线程池、数据库连接池、Http 连接池等等都是对这个思维的利用。池化技术的思维次要是为了缩小每次获取资源的耗费,进步对资源的利用率。

在面向对象编程中,创立和销毁对象是很费时间的,因为创立一个对象要获取内存资源或者其它更多资源。在 Java 中更是如此,虚拟机将试图跟踪每一个对象,以便可能在对象销毁后进行垃圾回收。所以进步服务程序效率的一个伎俩就是尽可能减少创立和销毁对象的次数,特地是一些很耗资源的对象创立和销毁,这就是”池化资源”技术产生的起因。

线程池顾名思义就是当时创立若干个可执行的线程放入一个池(容器)中,须要的时候从池中获取线程不必自行创立,应用结束不须要销毁线程而是放回池中,从而缩小创立和销毁线程对象的开销。Java 5+ 中的 Executor 接口定义一个执行线程的工具。它的子类型即线程池接口是 ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很分明的状况下,因而在工具类 Executors 面提供了一些动态工厂办法,生成一些罕用的线程池,如下所示:

(1)newSingleThreadExecutor:创立一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有工作。如果这个惟一的线程因为异样完结,那么会有一个新的线程来代替它。此线程池保障所有工作的执行程序依照工作的提交程序执行。

(2)newFixedThreadPool:创立固定大小的线程池。每次提交一个工作就创立一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会放弃不变,如果某个线程因为执行异样而完结,那么线程池会补充一个新线程。如果心愿在服务器上应用线程池,倡议应用 newFixedThreadPool 办法来创立线程池,这样能取得更好的性能。

(3)newCachedThreadPool:创立一个可缓存的线程池。如果线程池的大小超过了解决工作所须要的线程,那么就会回收局部闲暇(60 秒不执行工作)的线程,当工作数减少时,此线程池又能够智能的增加新线程来解决工作。此线程池不会对线程池大小做限度,线程池大小齐全依赖于操作系统(或者说 JVM)可能创立的最大线程大小。

(4)newScheduledThreadPool:创立一个大小有限的线程池。此线程池反对定时以及周期性执行工作的需要。

线程池有什么长处?

  • 升高资源耗费:重用存在的线程,缩小对象创立销毁的开销。
  • 进步响应速度。可无效的管制最大并发线程数,进步系统资源的使用率,同时防止过多资源竞争,防止梗塞。当工作达到时,工作能够不须要的等到线程创立就能立刻执行。
  • 进步线程的可管理性。线程是稀缺资源,如果无限度的创立,不仅会耗费系统资源,还会升高零碎的稳定性,应用线程池能够进行对立的调配,调优和监控。
  • 附加性能:提供定时执行、定期执行、单线程、并发数管制等性能。

综上所述应用线程池框架 Executor 能更好的治理线程、提供系统资源使用率。

线程池都有哪些状态?

  • RUNNING:这是最失常的状态,承受新的工作,解决期待队列中的工作。
  • SHUTDOWN:不承受新的工作提交,然而会持续解决期待队列中的工作。
  • STOP:不承受新的工作提交,不再解决期待队列中的工作,中断正在执行工作的线程。
  • TIDYING:所有的工作都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子办法 terminated()。
  • TERMINATED:terminated()办法完结后,线程池的状态就会变成这个。

什么是 Executor 框架?为什么应用 Executor 框架?

Executor 框架是一个依据一组执行策略调用,调度,执行和管制的异步工作的框架。

每次执行工作创立线程 new Thread()比拟耗费性能,创立一个线程是比拟耗时、耗资源的,而且无限度的创立线程会引起应用程序内存溢出。

所以创立一个线程池是个更好的的解决方案,因为能够限度线程的数量并且能够回收再利用这些线程。利用 Executors 框架能够十分不便的创立一个线程池。

在 Java 中 Executor 和 Executors 的区别?

  • Executors 工具类的不同办法依照咱们的需要创立了不同的线程池,来满足业务的需要。
  • Executor 接口对象能执行咱们的线程工作。
  • ExecutorService 接口继承了 Executor 接口并进行了扩大,提供了更多的办法咱们能取得工作执行的状态并且能够获取工作的返回值。
  • 应用 ThreadPoolExecutor 能够创立自定义线程池。
  • Future 示意异步计算的后果,他提供了查看计算是否实现的办法,以期待计算的实现,并能够应用 get()办法获取计算的后果。

线程池中 submit() 和 execute() 办法有什么区别?

接管参数:execute()只能执行 Runnable 类型的工作。submit()能够执行 Runnable 和 Callable 类型的工作。

返回值:submit()办法能够返回持有计算结果的 Future 对象,而 execute()没有

异样解决:submit()不便 Exception 解决

什么是线程组,为什么在 Java 中不举荐应用?

ThreadGroup 类,能够把线程归属到某一个线程组中,线程组中能够有线程对象,也能够有线程组,组中还能够有线程,这样的组织构造有点相似于树的模式。

线程组和线程池是两个不同的概念,他们的作用齐全不同,前者是为了不便线程的治理,后者是为了治理线程的生命周期,复用线程,缩小创立销毁线程的开销。

为什么不举荐应用线程组?因为应用有很多的安全隐患吧,没有具体查究,如果须要应用,举荐应用线程池。

线程池之 ThreadPoolExecutor 详解

Executors 和 ThreaPoolExecutor 创立线程池的区别

《阿里巴巴 Java 开发手册》中强制线程池不容许应用 Executors 去创立,而是通过 ThreadPoolExecutor 的形式,这样的解决形式让写的同学更加明确线程池的运行规定,躲避资源耗尽的危险

Executors 各个办法的弊病:

  • newFixedThreadPool 和 newSingleThreadExecutor: 次要问题是沉积的申请解决队列可能会消耗十分大的内存,甚至 OOM。
  • newCachedThreadPool 和 newScheduledThreadPool: 次要问题是线程数最大数是 Integer.MAX_VALUE,可能会创立数量十分多的线程,甚至 OOM。

ThreaPoolExecutor 创立线程池形式只有一种,就是走它的构造函数,参数本人指定

你晓得怎么创立线程池吗?

创立线程池的形式有多种,这里你只须要答 ThreadPoolExecutor 即可。

ThreadPoolExecutor() 是最原始的线程池创立,也是阿里巴巴 Java 开发手册中明确标准的创立线程池的形式。

ThreadPoolExecutor 构造函数重要参数剖析

ThreadPoolExecutor 3 个最重要的参数:

  • corePoolSize:外围线程数,线程数定义了最小能够同时运行的线程数量。
  • maximumPoolSize:线程池中容许存在的工作线程的最大数量
  • workQueue:当新工作来的时候会先判断以后运行的线程数量是否达到外围线程数,如果达到的话,工作就会被寄存在队列中。

ThreadPoolExecutor其余常见参数:

  1. keepAliveTime:线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的工作提交,外围线程外的线程不会立刻销毁,而是会期待,直到期待的工夫超过了 keepAliveTime才会被回收销毁;
  2. unitkeepAliveTime 参数的工夫单位。
  3. threadFactory:为线程池提供创立新线程的线程工厂
  4. handler:线程池工作队列超过 maxinumPoolSize 之后的回绝策略

ThreadPoolExecutor 饱和策略

ThreadPoolExecutor 饱和策略定义:

如果以后同时运行的线程数量达到最大线程数量并且队列也曾经被放满了任时,ThreadPoolTaskExecutor 定义一些策略:

  • ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来回绝新工作的解决。
  • ThreadPoolExecutor.CallerRunsPolicy:调用执行本人的线程运行工作。您不会工作申请。然而这种策略会升高对于新工作提交速度,影响程序的整体性能。另外,这个策略喜爱减少队列容量。如果您的应用程序能够接受此提早并且你不能工作抛弃任何一个工作申请的话,你能够抉择这个策略。
  • ThreadPoolExecutor.DiscardPolicy:不解决新工作,间接抛弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy:此策略将抛弃最早的未解决的工作申请。

举个例子:Spring 通过 ThreadPoolTaskExecutor 或者咱们间接通过 ThreadPoolExecutor 的构造函数创立线程池的时候,当咱们不指定 RejectedExecutionHandler 饱和策略的话来配置线程池的时候默认应用的是 ThreadPoolExecutor.AbortPolicy。在默认状况下,ThreadPoolExecutor 将抛出 RejectedExecutionException 来回绝新来的工作,这代表你将失落对这个工作的解决。对于可伸缩的应用程序,倡议应用 ThreadPoolExecutor.CallerRunsPolicy。当最大池被填满时,此策略为咱们提供可伸缩队列。(这个间接查看 ThreadPoolExecutor 的构造函数源码就能够看出,比较简单的起因,这里就不贴代码了)

一个简略的线程池 Demo:Runnable+ThreadPoolExecutor

线程池实现原理

为了让大家更分明下面的面试题中的一些概念,我写了一个简略的线程池 Demo。

首先创立一个 Runnable 接口的实现类(当然也能够是 Callable 接口,咱们下面也说了两者的区别。)

import java.util.Date;

/** * 这是一个简略的 Runnable 类,须要大概 5 秒钟来执行其工作。*/
public class MyRunnable implements Runnable {

    private String command;

    public MyRunnable(String s) {this.command = s;}

    @Override
    public void run() {System.out.println(Thread.currentThread().getName() + "Start. Time =" + new Date());
        processCommand();
        System.out.println(Thread.currentThread().getName() + "End. Time =" + new Date());
    }

    private void processCommand() {
        try {Thread.sleep(5000);
        } catch (InterruptedException e) {e.printStackTrace();
        }
    }

    @Override
    public String toString() {return this.command;}
}

编写测试程序,咱们这里以阿里巴巴举荐的应用 ThreadPoolExecutor 构造函数自定义参数的形式来创立线程池。

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExecutorDemo {

    private static final int CORE_POOL_SIZE = 5;
    private static final int MAX_POOL_SIZE = 10;
    private static final int QUEUE_CAPACITY = 100;
    private static final Long KEEP_ALIVE_TIME = 1L;
    public static void main(String[] args) {

        // 应用阿里巴巴举荐的创立线程池的形式
        // 通过 ThreadPoolExecutor 构造函数自定义参数创立
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy());

        for (int i = 0; i < 10; i++) {
            // 创立 WorkerThread 对象(WorkerThread 类实现了 Runnable 接口)Runnable worker = new MyRunnable("" + i);
            // 执行 Runnable
            executor.execute(worker);
        }
        // 终止线程池
        executor.shutdown();
        while (!executor.isTerminated()) { }
        System.out.println("Finished all threads");
    }
}

能够看到咱们下面的代码指定了:

  1. corePoolSize: 外围线程数为 5。
  2. maximumPoolSize:最大线程数 10
  3. keepAliveTime : 等待时间为 1L。
  4. unit: 等待时间的单位为 TimeUnit.SECONDS。
  5. workQueue:工作队列为 ArrayBlockingQueue,并且容量为 100;
  6. handler: 饱和策略为 CallerRunsPolicy

Output:

pool-1-thread-2 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-5 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-4 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-1 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-3 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-5 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-3 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-2 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-4 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-1 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-2 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-1 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-4 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-3 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-5 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-2 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-3 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-4 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-5 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-1 End. Time = Tue Nov 12 20:59:54 CST 2019

线程池之 ScheduledThreadPoolExecutor 详解

FutureTask 详解

原子操作类

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

原子操作(atomic operation)意为”不可被中断的一个或一系列操作”。

处理器应用基于对缓存加锁或总线加锁的形式来实现多处理器之间的原子操作。在 Java 中能够通过锁和循环 CAS 的形式来实现原子操作。CAS 操作——Compare & Set,或是 Compare & Swap,当初简直所有的 CPU 指令都反对 CAS 的原子操作。

原子操作是指一个不受其余操作影响的操作工作单元。原子操作是在多线程环境下防止数据不统一必须的伎俩。

int++ 并不是一个原子操作,所以当一个线程读取它的值并加 1 时,另外一个线程有可能会读到之前的值,这就会引发谬误。

为了解决这个问题,必须保障减少操作是原子的,在 JDK1.5 之前咱们能够应用同步技术来做到这一点。到 JDK1.5,java.util.concurrent.atomic 包提供了 int 和 long 类型的原子包装类,它们能够主动的保障对于他们的操作是原子的并且不须要应用同步。

java.util.concurrent 这个包外面提供了一组原子类。其根本的个性就是在多线程环境下,当有多个线程同时执行这些类的实例蕴含的办法时,具备排他性,即当某个线程进入办法,执行其中的指令时,不会被其余线程打断,而别的线程就像自旋锁一样,始终等到该办法执行实现,才由 JVM 从期待队列中抉择另一个线程进入,这只是一种逻辑上的了解。

原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

原子数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

原子属性更新器:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater

解决 ABA 问题的原子类:AtomicMarkableReference(通过引入一个 boolean 来反映两头有没有变过),AtomicStampedReference(通过引入一个 int 来累加来反映两头有没有变过)

说一下 atomic 的原理?

Atomic 包中的类根本的个性就是在多线程环境下,当有多个线程同时对单个(包含根本类型及援用类型)变量进行操作时,具备排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能胜利,而未胜利的线程能够向自旋锁一样,持续尝试,始终等到执行胜利。

AtomicInteger 类的局部源码:

// setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比拟并替换”的作用)private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
    try {
        valueOffset = unsafe.objectFieldOffset
        (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) {throw new Error(ex); }
}

private volatile int value;

AtomicInteger 类次要利用 CAS (compare and swap) + volatile 和 native 办法来保障原子操作,从而防止 synchronized 的高开销,执行效率大为晋升。

CAS 的原理是拿冀望的值和本来的一个值作比拟,如果雷同则更新成新的值。UnSafe 类的 objectFieldOffset() 办法是一个本地办法,这个办法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个 volatile 变量,在内存中可见,因而 JVM 能够保障任何时刻任何线程总能拿到该变量的最新值。

并发工具

并发工具之 CountDownLatch 与 CyclicBarrier

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

CountDownLatch 与 CyclicBarrier 都是用于管制并发的工具类,都能够了解成保护的就是一个计数器,然而这两者还是各有不同侧重点的:

  • CountDownLatch 个别用于某个线程 A 期待若干个其余线程执行完工作之后,它才执行;而 CyclicBarrier 个别用于一组线程相互期待至某个状态,而后这一组线程再同时执行;CountDownLatch 强调一个线程等多个线程实现某件事情。CyclicBarrier 是多个线程互等,等大家都实现,再携手共进。
  • 调用 CountDownLatch 的 countDown 办法后,以后线程并不会阻塞,会持续往下执行;而调用 CyclicBarrier 的 await 办法,会阻塞以后线程,直到 CyclicBarrier 指定的线程全副都达到了指定点的时候,能力持续往下执行;
  • CountDownLatch 办法比拟少,操作比较简单,而 CyclicBarrier 提供的办法更多,比方可能通过 getNumberWaiting(),isBroken()这些办法获取以后多个线程的状态,并且 CyclicBarrier 的构造方法能够传入 barrierAction,指定当所有线程都达到时执行的业务性能;
  • CountDownLatch 是不能复用的,而 CyclicLatch 是能够复用的。

并发工具之 Semaphore 与 Exchanger

Semaphore 有什么作用

Semaphore 就是一个信号量,它的作用是限度某段代码块的并发数。Semaphore 有一个构造函数,能够传入一个 int 型整数 n,示意某段代码最多只有 n 个线程能够拜访,如果超出了 n,那么请期待,等到某个线程执行结束这段代码块,下一个线程再进入。由此能够看出如果 Semaphore 构造函数中传入的 int 型整数 n=1,相当于变成了一个 synchronized 了。

Semaphore(信号量)- 容许多个线程同时拜访: synchronized 和 ReentrantLock 都是一次只容许一个线程拜访某个资源,Semaphore(信号量)能够指定多个线程同时拜访某个资源。

什么是线程间替换数据的工具 Exchanger

Exchanger 是一个用于线程间合作的工具类,用于两个线程间替换数据。它提供了一个替换的同步点,在这个同步点两个线程可能替换数据。替换数据是通过 exchange 办法来实现的,如果一个线程先执行 exchange 办法,那么它会同步期待另一个线程也执行 exchange 办法,这个时候两个线程就都达到了同步点,两个线程就能够替换数据。

罕用的并发工具类有哪些?

  • Semaphore(信号量)- 容许多个线程同时拜访: synchronized 和 ReentrantLock 都是一次只容许一个线程拜访某个资源,Semaphore(信号量)能够指定多个线程同时拜访某个资源。
  • CountDownLatch(倒计时器): CountDownLatch 是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程期待,它能够让某一个线程期待直到倒计时完结,再开始执行。
  • CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 十分相似,它也能够实现线程间的技术期待,然而它的性能比 CountDownLatch 更加简单和弱小。次要利用场景和 CountDownLatch 相似。CyclicBarrier 的字面意思是可循环应用(Cyclic)的屏障(Barrier)。它要做的事件是,让一组线程达到一个屏障(也能够叫同步点)时被阻塞,直到最初一个线程达到屏障时,屏障才会开门,所有被屏障拦挡的线程才会持续干活。CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数示意屏障拦挡的线程数量,每个线程调用 await()办法通知 CyclicBarrier 我曾经达到了屏障,而后以后线程被阻塞。

作者:ThinkWon
起源:https://thinkwon.blog.csdn.ne…

正文完
 0