Java中锁有哪些:
1、面试题:线程、程序、进程的基本概念和之间的关系?
1、程序
是含有指令和数据的⽂件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
2、进程
是程序的⼀次执行 过程,是系统运⾏程序的基本单位,因此进程是动态的。
系统运⾏⼀个程序即是⼀个进程从创建,运⾏到消亡的过程。
简单来说,⼀个进程就是⼀个执⾏中的程序,它在计算机中⼀个指令接着⼀个指令地执⾏着,同时,每个进程还占有某些系统资源如 CPU 时间,内存空间,⽂件,输⼊输出设备的使⽤权等等。换句话说,当程序在执⾏时,将会被操作系统载⼊内存中。
3、线程
与进程相似,但线程是⼀个⽐进程更⼩的执⾏单位。⼀个进程在其执⾏的过程中可以产⽣多个线程。与进程不同的是同类的多个线程共享同⼀块内存空间和⼀组系统资源,所以系统在产⽣⼀个线程,或是在各个线程之间作切换⼯作时,负担要⽐进程⼩得多,也正因为如此,线程也被称为轻量级进程。
4、关系:
线程是进程划分成的更⼩的运⾏单位。
线程和进程最⼤的不同在于基本上各进程是独立的,⽽各线程则不⼀定,因为同⼀进程中的线程极有可能会相互影响。
从另⼀⻆度来说,进程属于操作系统的范畴,主要是同⼀段时间内,可以同时执⾏⼀个以上的程序,⽽线程则是在同⼀程序内⼏乎同时执⾏⼀个以上的程序段。
进程和线程的区别?
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
线程是进程的一个实体,是CPU调度和分派的基本单位,是比进程更小的能独立运行的基本单位。
1、进程是运行中的程序,线程是进程的内部的一个执行序列
2、进程是资源分配的单元,线程是执行单元
3、进程间切换代价大,线程间切换代价小
4、进程拥有资源多,线程拥有资源少
5、多个线程共享进程的资源
面试题2:并发和并行的区别?
并发:同一时间段,多个任务都在执行(但单位时间内不移动同时执行)
并行:单位时间内,多个任务同时执行
面试题3:为什么要使用多线程?
1、从计算机底层:线程可看作轻量级进程,是程序执行的最小单位,线程间的切换调度成本远低于进程。另外,多核CPU时代意味着多个线程可以同时进行,减少了线程上下文切换的开销。
2、从当代互联网发展趋势:现在系统需要高并发量,利用好多线程机制可以大大提高并发能力及性能。
**3、单核时代:**多线程主要是为了提高CPU和IO设备的综合利用率。
**4、多核时代:**为了提高CPU的利用率。
4、使用多线程可能带来的问题:
内存泄漏、上下文切换、死锁
5、什么是上下文切换?
多线程编程中⼀般线程的个数都⼤于 CPU 核心的个数,而⼀个 CPU 核心在任意时刻只能被⼀个线程使⽤,为了让这些线程都能得到有效执行, CPU 采取的策略是为每个线程分配时间片并轮转的形式。当⼀个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于⼀次上下文切换。
当前任务在执行完 CPU 时间片切换到另⼀个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。
任务从保存到再加载的过程就是⼀次上下文切换。
上下文切换对系统来说意味着消耗大量的CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
Linux系统相较于其它系统,上下文切换和模式切换的时间消耗非常少。
6、什么是线程死锁?如何避免死锁?
线程死锁描述:
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期阻塞,因此程序不可能正常的终止。
产生死锁的四个必要条件:
1、互斥条件:该资源任意一个时刻只由一个线程占用。
2、请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
3、不剥夺条件:线程已获得的资源在未使用完之前不能被其它线程强行剥夺,只有使用完毕才释放。
4、循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
如何避免线程死锁:
只要破坏产生死锁的四个条件中的一个就可以。
1、破坏互斥条件:不能被破坏,因为使用锁就是想让他们互斥。
2、破坏请求与保持条件:一次性申请所有的资源。
3、破坏不剥夺条件:占用部分资源的线程进一步申请其它资源时,如果申请不到,可以主动释放它本身占有的资源。
4、破坏循环等待条件:按序申请资源进行预防,按照某一顺序申请资源,反序释放资源。
一、使用线程
有三种使用线程的方法:1、实现Runnable接口 2、实现Callable接口 3、继承Thread类
实现Runnable和Callable接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过Thread来调用。可以理解为任务是通过线程驱动而执行的。
1、实现Runnable接口
需要实现接口中的run()方法。
使用Runnable实例再创建一个Thread实例,然后再调用Thread实例的start()方法来启动线程。
public class MyRunnable implements Runnable {
@Override
public void run() {//实现Runnable接口中的run()方法
// ...
}
}
public static void main(String[] args) {
MyRunnable instance = new MyRunnable();//new一个MyRunnable实例
Thread thread = new Thread(instance);//使用Runnable实例创建一个Thread实例
thread.start();
}
2、实现Callable接口
与Runnable相比,Callable可以有返回值,返回值通过FutureTask进行封装。
public class MyCallable implements Callable<Integer> {
public Integer call() {
return 123;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);//将返回值通过FutureTask进行封装
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
3、继承Thread类
同样也需要实现run()方法,因为Thread类也实现了Runnable接口。
当调用start()方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的run()方法。
public class MyThread extends Thread {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}
4、实现接口VS继承Thread
实现接口会更好。因为
1、Java不支持多重继承,因此继承了Thread类就无法继承其它类,但是可以实现多个接口。
2、类可能只需要要求可执行,继承整个Thread类开销过大。
面试题:实现Runnable接口和Callable接口的区别?
Runnable接口不会返回结果或者抛出检查异常,但是JDK1.5引入的Callable可以。
⼯具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。
(Executors.callable(Runnable task )或 Executors.callable(Runnable task, Object resule))。
面试题:调用start() 方法会执行run()方法,为什么不能直接调用run()方法?
new一个Thread,线程进入初始(NEW)状态。
调用start()方法,会启动一个线程并使线程进入就绪(READY)状态,当分配到时间片后可以开始运行。
start() 方法会执行线程的相应准备工作,然后自动执行run()方法的内容。
但是直接执行run()方法,会把run()方法当成一个main线程下的普通方法执行,并不会在某个线程中执行它,因此并不是多线程工作。
调用start() 方法可启动线程并使线程进入就绪状态,直接执行run()方法不会以多线程的方式执行。
面试题:为什么要使用线程池?如何创建线程池?
池化技术的思想主要是为了减少每次获取资源的消耗,提⾼对资源的利⽤率。
线程池提供了⼀种限制和管理资源(包括执行⼀个任务)。 每个线程池还维护⼀些基本统计信息,例如已完成任务的数量。
1、降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
2、提高响应速度。当任务到达时,可以不需要等待线程创建就能立即执行。
3、提高线程的可管理性。线程池可以对线程进行统一的分配、调优和监控。
创建:
1、通过构造方法实现:ThreadPoolExecutor
2、通过Executor框架的工具类Executors实现—三种ThreadPoolExecutor
二、基础线程机制
1、Executor
Executor 管理多个异步任务的执行,无需程序员显式地管理线程的生命周期。
这里异步任务指多个任务的执行互不干扰,不需要进行同步操作。
三种ThreadPoolExecutor:
1、CachedThreadPool:一个任务创建一个线程。
2、FixedThreadPool:所有任务只能使用固定大小的线程。
3、SingleThreadExecutor:相当于大小为1的FixedThreadPool
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();//一个任务创建一个线程
for (int i = 0; i < 5; i++) {
executorService.execute(new MyRunnable());//五个线程互不干扰
}
executorService.shutdown();
}
execute的工作原理:
2、Daemon
守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。
当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。
main()属于非守护线程。
在线程启动之前使用setDaemon()方法可以将一个线程设置为守护线程。
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.setDaemon(true);
}
3、sleep()
Thread.sleep(millisec)方法会休眠当前正在执行的线程,millisec单位为毫秒。
sleep()可能会抛出InterruptedException,因为异常不能跨线程传播回main()中,因此必须在本地进行处理。线程中抛出的其他异常也同样需要在本地进行处理。
public void run() {
try {
Thread.sleep(3000);//使用try..catch..抛出异常
} catch (InterruptedException e) {
e.printStackTrace();
}
}
4、yield()
对静态方法Thread.yield()的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其他线程可以运行。
public void run() {
Thread.yield();
}
面试题:执行execute()方法和submit()方法的区别?
1、execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否。
2、submit() 方法用于提交需要返回值的任务。线程池会返回一个Future类型的对象,可以判断任务是否执行成功。
三、中断
一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。
1、InterruptedException
通过调用一个线程的interrupt()来中断线程,若线程处于阻塞、限期等待或无限期等待,就会抛出InterruptedException异常,提前结束该线程。
但是不能中断I/O阻塞和synchronized锁阻塞。
public class InterruptExample {
private static class MyThread1 extends Thread {
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println("Thread run");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new MyThread1();
thread1.start();
thread1.interrupt();
System.out.println("Main run");//打印
}
main()中启动了一个线程然后中断它,线程中调用了sleep()方法–即中断线程时,线程限期等待—会抛出InterruptedException,提前结束线程,不会执行后面的语句(不会打印Thread run)。
结果:
Main run
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at InterruptExample.lambda$main$0(InterruptExample.java:5)
at InterruptExample$$Lambda$1/713338599.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
2、interrupted()
如果一个线程的run()方法执行一个无限循环,并且没有执行如sleep()等操作让线程抛出InterruptedException异常提前结束线程,那么调用线程的interrupt() 方法就无法使线程提前结束。
但是调用interrupt()方法会设置线程的中断标记,此时调用**interrupted()**方法会返回true。
因此可以在循环体中使用interrupted()方法来判断线程是否处于中断状态,从而提前结束线程。
public class InterruptExample {
private static class MyThread2 extends Thread {
@Override
public void run() {
while (!interrupted()) {//在循环体中进行是否处于中断状态判断--提前结束线程
// ..
}
System.out.println("Thread end");
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread2 = new MyThread2();
thread2.start();
thread2.interrupt();//会打印Thread end
}
3、Executor的中断操作
调用Executor的shutdown()方法会等待线程都执行完毕之后再关闭。
但如果调用的是shutdownNow()方法,相当于调用每个线程的interrupt()方法。
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
//使用Lambda创建线程,相当于创建了一个匿名内部线程
executorService.execute(() -> {
try {
Thread.sleep(2000);
System.out.println("Thread run");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
executorService.shutdownNow();//对这个线程执行interrupt()方法
System.out.println("Main run");//因此结果是先打印,然后抛出异常
}
结果:
Main run
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at ExecutorInterruptExample.lambda$main$0(ExecutorInterruptExample.java:9)
at ExecutorInterruptExample$$Lambda$1/1160460865.run(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
如果只想中断Executor中的一个线程,可以通过使用**submit()**方法提交一个线程,会返回一个Future<?>对象,通过调用该对象的cancel(true)方法就可以中断线程。
Future<?> future = executorService.submit(() -> {
// ..
});
future.cancel(true);
四、互斥同步
Java提供了两种锁机制来控制多个线程对共享资源的互斥访问。
一是JVM事项的synchronized,另一个是JDK实现的ReentrantLock。
面试题:构造方法能使用synchronized关键字修饰吗?
构造方法不能使用synchronized关键字修饰。
构造⽅法本身就属于线程安全的,不存在同步的构造⽅法⼀说。
synchronized关键字的底层原理?
本质上都是对对象监视器monitor的获取。
Monitor 是基于 C++实现的,由ObjectMonitor实现的。每个对象中都内置了⼀个 ObjectMonitor 对象。
wait/notify 等⽅法也依赖于 monitor 对象,这就是为什么只有在同步的块或者⽅法中才能调⽤ wait/notify 等⽅法。
1、synchronized同步代码块的实现使用 monitorenter 和 monitorexit 指令,分别指向同步代码块开始和结束位置。
2、synchronized修饰方法使用ACC_SYNCHRONIZED标识,该标识表明是一个同步方法。
1、synchronized
1、同步一个代码块
指定加锁对象,对给定对象/类加锁。
public void func() {
synchronized (this) {
// ...
}
}
只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。
public class SynchronizedExample {
public void func1() {
//同步
synchronized (this) {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}
}
public static void main(String[] args) {
//使用ExecutorService执行两个线程,调用同一个对象的同步代码块,两个线程会进行同步
//一个线程进入同步语句块时,另一个线程必须等待
SynchronizedExample e1 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func1());
executorService.execute(() -> e1.func1());
}
//---结果:0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
public static void main(String[] args) {
//两个线程调用不同对象的同步代码块,这两个线程不需要同步。两个线程交叉执行。
SynchronizedExample e1 = new SynchronizedExample();
SynchronizedExample e2 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func1());
executorService.execute(() -> e2.func1());
}
//---结果:0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9---结果不一定
2、修饰实例方法
对当前对象实例加锁,进入同步代码前要获得当前对象实例的锁。
public synchronized void func () {
// ...
}
和同步代码块一样,作用于同一个对象。
3、同步一个类
public void func() {
synchronized (SynchronizedExample.class) {
// ...
}
}
作用于整个类,两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步。
public class SynchronizedExample {
public void func2() {
//同步
synchronized (SynchronizedExample.class) {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}
}
public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
SynchronizedExample e2 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func2());
executorService.execute(() -> e2.func2());
}
//---结果:0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
4. 修饰静态方法
给当前的类加锁,会作用于类的所有对象实例,进入同步代码前要获得当前class的锁。
因为静态成员不属于任何一个实例对象,是类成员。
所以,如果A线程调用一个实例对象的非静态synchronized方法,而B线程需要调用该实例对象所属类的静态synchronized方法,是允许的,不会发生互斥现象。
因为访问静态synchronized方法占用的锁是当前类的锁,而访问非静态synchronized方法占用当前实例对象的锁。
public synchronized static void fun() {
// ...
}
作用于整个类。
2、ReentrantLock可重入锁
例:
state 初始化为 0,表示未锁定状态。
A 线程 lock()时,会调⽤tryAcquire()独占该锁并将 state+1。
此后,其他线程再 tryAcquire()时就会失败,直到 A 线程unlock()到 state=0(即释放锁)为⽌,其它线程才有机会获取该锁。
当然,释放锁之前, A 线程⾃⼰是可以重复获取此锁的(state 会累加),这就是可重⼊的概念。
但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。
是java.util.concurrent(JUC)包中的锁。
public class LockExample {
private Lock lock = new ReentrantLock();//new一个重入锁
public void func() {
lock.lock();//上锁
try {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
} finally {
lock.unlock(); // 确保释放锁,从而避免发生死锁。
}
}
}
public static void main(String[] args) {
LockExample lockExample = new LockExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> lockExample.func());
executorService.execute(() -> lockExample.func());
}
//---结果:0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
3、比较
1、锁的实现:synchronized是JVM实现的,ReentrantLock是JDK实现的。
2、性能:新版本java对synchronized 进行了很多优化,例如自旋锁等,与 ReentrantLock 大致相同。
3、等待可中断:ReentrantLock可中断,synchronized不行。
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,先处理其他事情。
4、公平锁:synchronized锁是非公平的,ReentrantLock默认情况下是非公平的,但也可以是公平的。
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序依次获得锁。
5、锁绑定多个条件:一个ReentrantLock可以同时绑定多个Condition对象。
4、使用选择
除非需要使用ReentrantLock的高级功能,否则优先使用synchronized。
因为synchronized是JVM实现的一种锁机制,JVM原生地支持它,而ReentrantLock不是所有JDK版本都支持。同时使用synchronized不用担心没有释放锁导致的死锁问题,因为JVM会保证锁的释放。
五、线程之间的协作
当多个线程需要一起工作去解决某个问题的时候,如果某些部分必须在其他部分之前完成,那么就需要对线程进行协调。
1、join()
在某一线程中调用另一个线程的join()方法,会将当前线程挂起,而不是等待,直到目标线程(调用join的线程)结束。
public class JoinExample {
private class A extends Thread {
@Override
public void run() {
System.out.println("A");
}
}
private class B extends Thread {
private A a;
B(A a) {
this.a = a;
}
@Override
public void run() {
try {
a.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B");
}
}
//虽然b线程先启动,但是b线程中调用了a线程的join()方法,b线程会等待a线程结束才继续执行。
//因此结果是a线程的输出先于b线程的输出。
public void test() {
A a = new A();
B b = new B(a);
b.start();
a.start();
}
}
public static void main(String[] args) {
JoinExample example = new JoinExample();
example.test();
}
//结果:
A
B
2、wait()、notify()、notifyAll()
调用wait() 使线程等待某个条件满足,线程在等待时会被挂起,
当其它线程的运行使得这个条件满足时,其它线程会调用notify()或者notifyAll()来唤醒挂起的线程。
都属于Object的一部分,而不属于Thread。
只能在同步方法或者同步控制块中使用,否则会在运行时抛出IllegalMonitorStateException。
使用wait()挂起期间,线程会释放锁。
因为如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行notify方法唤醒挂起的线程,造成死锁。
public class WaitNotifyExample {
public synchronized void before() {
System.out.println("before");
notifyAll();
}
public synchronized void after() {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("after");
}
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
WaitNotifyExample example = new WaitNotifyExample();
executorService.execute(() -> example.after());
executorService.execute(() -> example.before());
}
//结果:
before
after
面试题:wait() 和sleep() 的区别和共同点:
1、最主要的区别是:sleep() 方法没有释放锁,wait()释放了锁
2、wait()是Object的方法,sleep()是Thread的静态方法。
3、wait()通常被用于线程间的交互/通信,sleep()通常被用于暂停线程的执行。
4、wait()方法被调用后,线程不会自动苏醒,需要notify()或notifyAll()唤醒。
sleep()方法执行完成后,线程会自动苏醒
二者都可以暂停线程的执行。
3、await()、signal()、signalAll()
JUC 类库中提供了Condition类实现线程之间的协调,可以在Condition上调用await()方法使线程等待,其它线程调用signal()或signalAll()方法唤醒等待的线程。
相比wait(),await()可以指定等待的条件,更加灵活。
public class AwaitSignalExample {
//使用Lock获取一个Condition对象
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void before() {
lock.lock();
try {
System.out.println("before");
condition.signalAll();
} finally {
lock.unlock();
}
}
public void after() {
lock.lock();
try {
condition.await();
System.out.println("after");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
AwaitSignalExample example = new AwaitSignalExample();
executorService.execute(() -> example.after());
executorService.execute(() -> example.before());
}
//结果:
before
after
六、线程状态
一个线程只能处于一种状态,并且这里的线程状态特指Java虚拟机的线程状态,不能反映线程在特定操作系统下的状态。
1、新建(NEW)
创建后尚未启动。
2、可运行(RUNABLE)
正在Java虚拟机中运行。但是在操作系统层面,可能处于运行状态,也可能等待资源调度(例如处理器资源),资源调度完成就进入运行状态。
所以该状态的可运行是指可以被运行,具体有没有运行要看低层操作系统的资源调度。
3、阻塞(BLOCKED)
请求获取monitor lock从而进入synchronized函数或者代码块,但是其它线程已经占用了该monitor lock,所以处于阻塞状态。
要结束该状态进入RUNABLE需要其它线程释放monitor lock。
4、无限期等待(WAITING)
等待其它线程显式地唤醒。
阻塞和等待的区别在于,阻塞是被动的,在等待获取monitor lock。而等待是主动的,通过调用Object.wait()等方法进入。
5、限期等待(TIMED_WAITING)
无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。
调用Thread.sleep()方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。
调用Object.wait()方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。
睡眠和挂起用来描述行为,阻塞和等待用来描述状态。
6、死亡(TERMINATED)
可以是线程结束任务之后自己结束,或者产生了异常而结束。
七、JUC - AQS
java.util.concurrent(JUC)的核心AbstractQueuedSynchronizer(AQS)
AQS 是⼀个⽤来构建锁和同步器的框架,使⽤ AQS 能简单且⾼效地构造出应⽤⼴泛的⼤量的同
步器。
AQS原理:
核心思想是,
如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态。
如果被请求的共享资源被占用,那么就需要一套线程阻塞等待及被唤醒时锁分配的机制,AQS中通过CLH队列锁实现,将暂时获取不到锁的线程加到队列中。
CLH队列是一个虚拟的双向队列。
AQS将每条请求共享资源的线程封装成CLH锁队列中的一个节点实现锁的分配。
AQS 使⽤⼀个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队⼯作。
AQS 使⽤ CAS 对该同步状态进⾏原⼦操作实现对其值的修改。
通过 protected 类型的 getState, setState, compareAndSetState 进⾏操作。
ps.
AQS有独占和共享两种资源共享方式。
AQS底层使用了模板方法模式。
面试题:如何实现三个线程的有序唤醒?
方式1: 用join实现三个线程有序执行
方式2: 用CountDownLatch实现三个线程有序执行
创建线程类的时候,将上一个计数器和本线程计数器传入。运行前业务执行上一个计数器.await,执行后本计数器.countDown。
方式3:用Condition中的await()方法进行条件唤醒实现有序
组件1、CountDownLatch(倒计时器)
用于控制一个或者多个线程等待多个线程,协调多个线程之间的同步。
CountDownLatch 的作⽤就是 允许 count 个线程阻塞在⼀个地方,直⾄所有线程的任务都执行完毕。
维护了一个计数器cnt,每次调用countDown()方法会让计数器的值减一,减到0的时候,那些因为调用await()方法而在等待的线程就会被唤醒。
public class CountdownLatchExample {
public static void main(String[] args) throws InterruptedException {
final int totalThread = 10;
CountDownLatch countDownLatch = new CountDownLatch(totalThread);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalThread; i++) {
executorService.execute(() -> {
System.out.print("run..");
countDownLatch.countDown();
});
}
countDownLatch.await();
System.out.println("end");
executorService.shutdown();
}
}
//---结果:run..run..run..run..run..run..run..run..run..run..end
组件2、CyclicBarrier(循环屏障)
用来控制多个线程互相等待,当多个线程都到达时,这些线程才会继续执行。
和CountDownLatch相似,都是维护计数器实现的。线程执行await()方法之后计数器减一,并进行等待,直到计数器为0,所有调用await()方法而等待的线程才能继续执行。
CyclicBarrier和CountDownLatch的一个区别是,CyclicBarrier的计数器通过调用reset()方法可以循环使用,所以叫循环屏障。
CyclicBarrier 有两个构造函数,其中 parties 指示计数器的初始值,barrierAction 在所有线程都到达屏障的时候会执行一次。
//parties表示计数器的初始值,barrierAction在所有线程都到达屏障时执行一次
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
public CyclicBarrier(int parties) {
this(parties, null);
}
public class CyclicBarrierExample {
public static void main(String[] args) {
final int totalThread = 10;
CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);//计数器初始值为10
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalThread; i++) {
executorService.execute(() -> {
System.out.print("before..");
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.print("after..");
});
}
executorService.shutdown();
}
}
//十个线程都到达时再继续执行线程,------等一等后面的线程,像是加了同步
//结果:---打印10个before,打印10个after
before..before..before..before..before..before..before..before..before..before..after..after..after..after..after..after..after..after..after..after..
组件3、Semaphore(信号量)
允许多个线程同时访问。
类似操作系统中的信号量,可以控制对互斥资源的访问线程数。
public class SemaphoreExample {
public static void main(String[] args) {
final int clientCount = 3;
final int totalRequestCount = 10;
//定义了10个请求,但是Semaphore命令使只能有3个客户端同时访问
Semaphore semaphore = new Semaphore(clientCount);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalRequestCount; i++) {
executorService.execute(()->{
try {
semaphore.acquire();
System.out.print(semaphore.availablePermits() + " ");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
});
}
executorService.shutdown();
}
}
2 1 2 2 2 2 2 1 2 2
八、JUC其它组件
1、FutureTask
Callable接口实现线程可以有返回值,返回值通过FutureTask进行封装。
FutureTask实现了RunnableFuture接口,该接口继承自Runnable和Future接口,因此FutureTask可以当做一个任务执行也可以有返回值。
//FutureTask实现了RunnableFuture接口
public class FutureTask<V> implements RunnableFuture<V>
//RunnableFuture接口继承自Runnable和Future<V>接口
public interface RunnableFuture<V> extends Runnable, Future<V>
FutureTask可用于异步获取执行结果或取消执行任务的场景。
当一个计算任务需要执行很长时间,可以用FutureTask封装这个任务,主线程在完成自己的任务后再去获取结果。
public class FutureTaskExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int result = 0;
for (int i = 0; i < 100; i++) {
Thread.sleep(10);
result += i;
}
return result;
}
});
Thread computeThread = new Thread(futureTask);
computeThread.start();
Thread otherThread = new Thread(() -> {
System.out.println("other task is running...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
otherThread.start();
System.out.println(futureTask.get());
}
}
//这个例子中computeThread 需要计算一段时间,使用FutureTask封装,等otherThread 执行完再去获取结果
other task is running...
4950
2、BlockingQueue
java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:
1、FIFO队列:先进先出,LinkedBlockingQueue、ArrayBlockingQueue(固定长度)
2、优先级队列:PriorityBlockingQueue
提供了阻塞的take()和put()方法:
如果队列为空,take()将阻塞,直至队列中有内容;
如果队列为满,put()将阻塞,直至队列中有闲置位置。
生产者-消费者问题:
public class ProducerConsumer {
//new一个固定长度的阻塞队列
private static BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);
private static class Producer extends Thread {//生产者
@Override
public void run() {
try {
queue.put("product");//调用put方法
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("produce..");
}
}
private static class Consumer extends Thread {//消费者
@Override
public void run() {
try {
String product = queue.take();//调用take方法
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("consume..");
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
Producer producer = new Producer();
producer.start();
}
for (int i = 0; i < 5; i++) {
Consumer consumer = new Consumer();
consumer.start();
}
for (int i = 0; i < 3; i++) {
Producer producer = new Producer();
producer.start();
}
}
//先生产两个,打印两个produce,然后消费两个打印两个consume,此时阻塞队列为空,take()阻塞,生产一个消费一个
//--结果:produce..produce..consume..consume..produce..consume..produce..consume..produce..consume..
3、ForkJoin
主要用于并行计算中,原理类似MapReduce,把大的计算任务拆分成多个小任务并行计算。
public class ForkJoinExample extends RecursiveTask<Integer> {
private final int threshold = 5;
private int first;
private int last;
public ForkJoinExample(int first, int last) {
this.first = first;
this.last = last;
}
@Override
protected Integer compute() {
int result = 0;
if (last - first <= threshold) {
// 任务足够小则直接计算
for (int i = first; i <= last; i++) {
result += i;
}
} else {
// 拆分成小任务
int middle = first + (last - first) / 2;
ForkJoinExample leftTask = new ForkJoinExample(first, middle);
ForkJoinExample rightTask = new ForkJoinExample(middle + 1, last);
leftTask.fork();
rightTask.fork();
result = leftTask.join() + rightTask.join();
}
return result;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ForkJoinExample example = new ForkJoinExample(1, 10000);
//ForkJoin 使用 ForkJoinPool 来启动,它是一个特殊的线程池,线程数量取决于 CPU 核数。
//public class ForkJoinPool extends AbstractExecutorService 继承自AbstractExecutorService
ForkJoinPool forkJoinPool = new ForkJoinPool();
Future result = forkJoinPool.submit(example);
System.out.println(result.get());
}
ForkJoinPool实现了工作窃取算法提高CPU的利用率。
每个线程都维护了一个双端队列,用于存储需要执行的任务。
工作窃取算法允许空闲的线程从其它线程的双端队列中窃取一个任务执行。
窃取的任务必须是最晚的任务,避免与队列所属的线程发生竞争。
九、多线程不安全示例
如果多个线程对同一个共享数据进行访问而不同步,就会出现安全问题。
public class ThreadUnsafeExample {
private int cnt = 0;
public void add() {
cnt++;
}
public int get() {
return cnt;
}
}
public static void main(String[] args) throws InterruptedException {
final int threadSize = 1000;
ThreadUnsafeExample example = new ThreadUnsafeExample();
final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
//使用ExecutorService ,cachedThreadPool一个任务创建一个线程
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < threadSize; i++) {
executorService.execute(() -> {
example.add();
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(example.get());
}
//一千个线程同时对cnt进行自增操作,值就可能小于1000
十、Java内存模型 – JMM
Java内存模型试图屏蔽各种硬件和操作系统的内存访问差异,实现Java程序在各种平台下达到一致的内存访问效果。
1、主内存与工作内存
处理器上的寄存器的读写速度比内存快几个数量级,为了解决速度矛盾,在它们之间加入了高速缓存。
加入高速缓存会有缓存一致性的问题。
如果多个缓存共享同一块主内存区域,那么多个缓存的数据可能会 不一致,需要一些协议解决这个问题。
所有的变量都存储在主内存中,每个线程有自己的工作内存,工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。
线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成。
2、内存间交互操作
Java内存模型定义了8个操作完成主内存和工作内存的交互操作。
1、read:把一个变量的值从主内存传输到工作内存中。
2、load:在read之后执行,把read到的值放入工作内存的变量副本中。
3、use:把工作内存中的一个变量的值传递给执行引擎Thread。
4、assign:把一个从执行引擎接收到的值赋给工作内存的变量。
5、store:把工作内存的一个变量的值传送到主内存中。
6、write:在store之后执行,把store得到的值放入主内存的变量中。
7、lock:作用于主内存的变量。
8、unlock
3、内存模型的三大特性
面试题:synchronized关键字和volatile关键字的区别
1、volatile关键字可以看作线程同步的轻量级实现。
但volatile关键字只能用于变量而synchronized关键字可以修饰方法及代码块。
2、volatile关键字可以保证内存可见性,但是不能保证数据的原子性。synchronized可以保证。
3、volatile关键字主要用于解决变量在多个线程之间的可见性,synchronized关键字解决多个线程之间访问资源的同步性。
1、原子性
原子性:不可再分割。
Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,
例如对一个 int 类型的变量执行 assign 赋值操作,这个操作就是原子性的。
但是Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据(long,double)的读写操作划分为两次 32 位的操作来进行,即 load、store、read 和 write 操作可以不具备原子性。
错误认知:int 等原子性的类型在多线程环境中不会出现线程安全问题。
对 int 类型读写操作满足原子性只是说明 load、assign、store 这些单个操作具备原子性。
对九中不安全示例修改:
1、AtomicInteger 能保证多个线程修改的原子性。–基本类型原子操作类
public class AtomicExample {
private AtomicInteger cnt = new AtomicInteger();
public void add() {
cnt.incrementAndGet();//自增
}
public int get() {
return cnt.get();
}
}
public static void main(String[] args) throws InterruptedException {
final int threadSize = 1000;
AtomicExample example = new AtomicExample(); // 只修改这条语句
final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < threadSize; i++) {
executorService.execute(() -> {
example.add();
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(example.get());//1000
}
2、使用synchronized互斥锁保证操作的原子性。
对应内存间交互操作为lock和unlock,在虚拟机实现上对应的字节码指令为monitorenter和monitorexit。
public class AtomicSynchronizedExample {
private int cnt = 0;
public synchronized void add() {
cnt++;
}
public synchronized int get() {
return cnt;
}
}
public static void main(String[] args) throws InterruptedException {
final int threadSize = 1000;
AtomicSynchronizedExample example = new AtomicSynchronizedExample();
final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < threadSize; i++) {
executorService.execute(() -> {
example.add();
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(example.get());
}
2、可见性
可见性是指当一个线程修改了共享变量的值,其它线程能立即得知这个修改。
Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性
三种实现可见性的方式:
1、volatile,直接对内存进行处理,避免副本操作。–不能保证操作的原子性。
2、synchronized,对一个变量执行unlock之前,必须把变量值同步回主内存。
3、final,final修饰的字段在构造器中一旦初始化完成且没有发生this逃逸(其它线程通过this引用访问到初始化了一半的对象),那么其它线程就能看见final字段的值。
3、有序性
有序性:在本线程内观察,所有操作都是有序的。
在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。
在Java内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,会影响多线程并发执行的正确性。
volatile关键字通过添加内存屏障的方式禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。
也可以通过synchronized来保证有序性,保证每个时刻只有一个线程执行同步代码,相当于让线程顺序执行同步代码。
4、先行发生原则 happens-before
JVM 还规定了先行发生原则,让一个操作无需控制就能先于另一个操作完成。
1、单一线程原则
在一个线程内,在程序前面的操作先行发生于后面的操作。
2、管程锁定规则
一个unlock操作先行发生于后面对同一个锁的lock操作。
3、volatile变量规则
对一个volatile变量的写操作先行发生于后面对这个变量的读操作。
4、线程启动规则
Thread对象的start()方法调用先行发生于此线程的每一个动作。
5、线程加入规则
Thread对象的结束先行发生于join()方法的返回。
6、线程中断规则
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
7、对象终结规则
一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
8、传递性
如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。
十一、线程安全
含义:多个线程不管以何种方式访问某个类,且在主调代码中不需要进行同步,都能表现正确的行为。
有不可变、互斥同步、非阻塞同步、无同步方案几种实现方式。
1、不可变
不可变的对象一定是线程安全的。
不可变的类型:
1、final修饰的基本数据类型
2、String
3、枚举类型
4、Number部分子类:Long、Double等数值包装类型,BigInteger、BigDecimal等大数据类型。
原子类AtomicInteger、AtomicLong是可变的。
对于集合类型,可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。
Collections.unmodifiableXXX() 先对原始的集合进行拷贝,需要对集合进行修改的方法都直接抛出异常。
public V put(K key, V value) {
throw new UnsupportedOperationException();
}
2、互斥同步
synchronized和ReentrantLock
互斥同步最主要的问题就是线程阻塞和唤醒带来的性能问题,也称为阻塞同步。
互斥同步属于一种悲观的并发策略,认为只要不去做正确的同步措施,就肯定会出问题。
无论共享数据是否真的出现竞争,都进行加锁、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。
JUC包中的原子类:
1、基本类型:使用原子的方式更新基本类型。AtomicInteger、AtomicLong、AtomicBoolean
2、数组类型:使用原子的方式更新数组里某元素。AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
3、引用类型:AtomicReference、AtomicStampedReference、AtomicMarkableReference
4、对象的属性修改类型:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
3、非阻塞同步
基于冲突检测的乐观并发策略:先进行操作,如果没有其它线程竞争共享数据,那操作就成功了,否则采取补偿措施(不断重试,直到成功为止)。
乐观的并发策略的许多实现都不需要将线程阻塞,称为非阻塞同步。
1、CAS
乐观锁需要 操作和冲突检测两个步骤具备原子性。
硬件支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)。
CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。
2、AtomicInteger
J.U.C 包里面的整数原子类 AtomicInteger 的方法调用了 Unsafe 类的 CAS 操作。
//以下代码使用了 AtomicInteger 执行了自增的操作。
private AtomicInteger cnt = new AtomicInteger();
public void add() {
cnt.incrementAndGet();
}
//以下代码是 incrementAndGet() 的源码,它调用了 Unsafe 的 getAndAddInt() 。
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
/*
以下代码是 getAndAddInt() 源码,var1 指示对象内存地址,var2 指示该字段相对对象内存地址的偏移,var4 指示操作需要加的数值,这里为 1。通过 getIntVolatile(var1, var2) 得到旧的预期值,通过调用 compareAndSwapInt() 来进行 CAS 比较,如果该字段内存地址中的值等于 var5,那么就更新内存地址为 var1+var2 的变量为 var5+var4。
*/
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
//可以看到 getAndAddInt() 在一个循环中进行,发生冲突的做法是不断的进行重试。
return var5;
}
3、ABA
如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。
带有标记的原子引用类 AtomicStampedReference 可以通过控制变量值的版本来保证 CAS 的正确性。
大部分情况下 ABA 问题不会影响程序并发的正确性,
如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。
4、无同步方案
如果一个方法本来就不涉及共享数据,自然就无需任何同步措施去保证正确性。
1、栈封闭
多个线程访问同一个方法的局部变量时,不会出现线程安全问题。
因为局部变量存储在虚拟机栈中,属于线程私有的。
public class StackClosedExample {
public void add100() {
int cnt = 0;
for (int i = 0; i < 100; i++) {
cnt++;
}
System.out.println(cnt);
}
}
public static void main(String[] args) {
StackClosedExample example = new StackClosedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> example.add100());
executorService.execute(() -> example.add100());
executorService.shutdown();
}
//打印结果:100 100
2、可重入代码(Reentrant Code)
也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。
可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。
3、线程本地存储
如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。
大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完。
经典 Web 交互模型中的**“一个请求对应一个服务器线程”(Thread-per-Request)**的处理方式,这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。
java.lang.ThreadLocal 类来实现线程本地存储功能。
//thread1 中设置 threadLocal 为 1,而 thread2 设置 threadLocal 为 2。
//过了一段时间之后,thread1 读取 threadLocal 依然是 1,不受 thread2 的影响。
public class ThreadLocalExample {
public static void main(String[] args) {
ThreadLocal threadLocal = new ThreadLocal();
Thread thread1 = new Thread(() -> {
threadLocal.set(1);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadLocal.get());
threadLocal.remove();
});
Thread thread2 = new Thread(() -> {
threadLocal.set(2);
threadLocal.remove();
});
thread1.start();
thread2.start();
}
}
ThreadLocal
ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只有在指定的线程中可以获取到存储的数据,对于其他线程来说则无法取到数据。
如果你创建了⼀个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是 ThreadLocal 变量名的由来。他们可以使⽤get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从⽽避免了线程安全问题。
//两个线程共享一个空间存储变量,避免线程竞争。
public class ThreadLocalExample1 {
public static void main(String[] args) {
ThreadLocal threadLocal1 = new ThreadLocal();
ThreadLocal threadLocal2 = new ThreadLocal();
Thread thread1 = new Thread(() -> {
threadLocal1.set(1);
threadLocal2.set(1);
});
Thread thread2 = new Thread(() -> {
threadLocal1.set(2);
threadLocal2.set(2);
});
thread1.start();
thread2.start();
}
}
底层结构图:
//每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 对象。
ThreadLocal.ThreadLocalMap threadLocals = null;
//set和get方法所操作的都是当前线程的ThreadLocalMap对象。
//调用一个ThreadLocal的set方法,先得到当前线程的ThreadLocalMap对象,
//然后将 ThreadLocal->value 键值对插入到该 Map 中。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
//get方法也是,先得到当前线程的ThreadLocalMap对象
//如果map不为null,那就使用当前线程作为ThreadLocalMap的Entry的键,然后值就作为相应的的值,如果没有那就设置一个初始值。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。
在不同线程中,访问同一个ThreadLocal的set和get方法,它们对ThreadLocal的读、写操作仅限于各自线程的内部,从而使ThreadLocal可以在多个线程中互不干扰地存储和修改数据。
ThreadLocal原理:
从源码上来看,Thread类中有一个threadLocals变量和一个inheritableThreadLocals变量,都是ThreadLocalMap类型的变量。默认这两个变量都是null, 只有当前线程调用ThreadLocal类的set或get方法时才创建它们,底层实际上是调用的ThreadLocalMap类对应的set和get方法。
最终的变量是放在了当前线程的ThreadLocalMap 中,并不是存在 ThreadLocal 上, ThreadLocal 可以理解为只是 ThreadLocalMap 的封装,传递了变量值。
ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过 getMap(Thread t) 可以访问到该线程的 ThreadLocalMap 对象。
ThreadLocal 内部维护的是⼀个类似 Map 的 ThreadLocalMap 数据结构, key 为当前对象的 ThreadLocal 对象,值为 Object 对象。
当调用ThreadLocal的set/get进行赋值/取值操作时,首先获取当前线程的ThreadLocalMap实例,然后就像操作一个普通的map一样,进行put和get
面试题:什么是ThreadLocal内存泄漏问题?
内存泄漏:
是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
ThreadLocalMap中使用的 key 为ThreadLocal的弱引用,而 value 是强引用。
因此如果ThreadLocal没有被ThreadLocalMap以外的对象强引用,则在下一次GC的时候,key(也就是ThreadLocal实例)就会被回收,而value不会被清理掉。
那么就会ThreadLocalMap中会出现key为null的Entry键值对。
因此在没有额外操作的情况下,此处的value便不会被外部访问到,value也永远无法被GC回收。
而且只要Thread实例一直存在,Thread实例就强引用着ThreadLocalMap,因此ThreadLocalMap就不会被回收,就一直占用着内存。
解决:
调⽤ set() 、 get() 、 remove() ⽅法的时候,会清理掉 key 为 null的记录。
使⽤完 ThreadLocal ⽅法后 最好⼿动调⽤ remove() 方法。
为什么要用弱引用?
弱引用:如果一个对象仅被一个弱引用指向,那么当下一次GC到来时,这个对象一定会被垃圾回收器回收掉。
假如每个key都强引用指向ThreadLocal的对象,那么这个ThreadLocal对象就会因为和Entry对象存在强引用关联而无法被GC回收,造成内存泄漏,除非线程结束后,线程被回收了,map也跟着回收。
synchronized与ThreadLocal:
Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
而ThreadLocal则从另一个角度来解决多线程的并发访问。
ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。
十二、锁优化
面试题:对synchronized关键字的了解?
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被修饰的方法或代码块在任意时刻只能有一个线程执行。
Java早期版本中,synchronized属于重量级锁,效率低。
–因为监视锁(monitor)依赖底层的操作系统的Mutex Lock实现,挂起或唤醒线程都需要操作系统协作,成本高
JDK1.6之后,从JVM层面对synchronized锁进行了大量优化:引入自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术减少开销。
JVM对synchronized的优化
synchronized锁升级:偏向锁 → 轻量级锁 → 重量级锁
前面提到,synchronized关键字就像是汽车的自动档,现在详细讲这个过程。
一脚油门踩下去,synchronized会从无锁升级为偏向锁,再升级为轻量级锁,最后升级为重量级锁,就像自动换挡一样。
那么自旋锁在哪里呢?这里的轻量级锁就是一种自旋锁。
初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。
执行完同步代码块后,线程并不会主动释放偏向锁。
当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。
由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。
这里要明确一下什么是锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。
只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。
在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。
获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性发生的。
这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。
长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。
如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。
这是一种折中的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。
显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。
如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。
当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。
在JDK1.6之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。
一个锁只能按照 偏向锁、轻量级锁、重量级锁的顺序逐渐升级(也有叫锁膨胀的),不允许降级。
1、自旋锁
互斥同步进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。
自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。
自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。
在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。
2、锁消除
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。
锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除。
//对于一些看起来没有加锁的代码,其实隐式的加了很多锁。例如下面的字符串拼接代码就隐式加了锁:
public static String concatString(String s1, String s2, String s3) {
return s1 + s2 + s3;
}
//String 是一个不可变的类,编译器会对 String 的拼接自动优化。
//在 JDK 1.5 之前,会转化为 StringBuffer 对象的连续 append() 操作:
public static String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
//每个 append() 方法中都有一个同步块。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说,sb 的所有引用永远不会逃逸到 concatString() 方法之外,其他线程无法访问到它,因此可以进行消除。
3、锁粗化
如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。
concatString示例代码中连续的 append() 方法就属于这类情况。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。
4、轻量级锁
JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。
Mark Word:–HotSpot虚拟机对象头的内存布局
一个线程的虚拟机栈中,有一部分称为Lock Record的区域,是在轻量级锁运行过程中创建的,用于存放锁对象的Mark Word。
一个锁对象,包含Mark Word和其它对象信息。
轻量级锁使用CAS操作避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。
当尝试获取一个锁对象时,如果锁对象标记为 0 01,说明锁对象的锁未锁定(unlocked)状态。此时虚拟机在当前线程的虚拟机栈中创建 Lock Record,然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。
如果 CAS 操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。
5、偏向锁
偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要。
当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。
当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。
好的编程习惯:
1、给线程起个有意义的名字,这样可以方便找 Bug。
2、缩小同步范围,从而减少锁争用。例如对于 synchronized,应该尽量使用同步块而不是同步方法。
3、多用同步工具,少用 wait() 和 notify()。首先,CountDownLatch, CyclicBarrier, Semaphore 和 Exchanger 这些同步类简化了编码操作,而用 wait() 和 notify() 很难实现复杂控制流;其次,这些同步类是由最好的企业编写和维护,在后续的 JDK 中还会不断优化和完善。
4、使用 BlockingQueue 实现生产者消费者问题。
5、多用并发集合,少用同步集合,例如应该使用 ConcurrentHashMap 而不是 Hashtable。
6、使用本地变量和不可变类来保证线程安全。
7、使用线程池而不是直接创建线程,这是因为创建线程代价很高,线程池可以有效地利用有限的线程来启动任务。