文章目录
- 写在前面
- 多线程回顾
- Thread和Runnable
- 面试官:为什么我们在项目中推荐使用使用Runnable方法而不用继承Thread?
- 面试官:Callable为什么可以带返回值,你知道底层原理?
- 面试题:线程了解?给我讲讲线程的几种状态?
- 面试题:你知道线程等待和阻塞的区别?
- 面试官:给我讲讲线程的生命周期?
- 面试官:如果我在代码种连续调用两次thread.start()会发生什么你知道?
- synchronized
- 声:居然我们学习到锁,给我讲讲锁的本质是什么?
- 声:synchronized使用的几种方式?
- 面试官:给我讲讲synchronized 实例锁(Synchronized)和类锁(Static Synchronized)有什么区别?
- 面试官:给我讲讲锁是如何实现的?
- monitor enter
- monitor exit
- 面试题:synchronized抛出异常是如何保证能正常释放锁?
- 面试题:进入synchronized获取对象锁后,调用Thread.sleep()方法会释放锁资源?
- wait和notify
- 笔试题:如何用wait和notify实现生产者消费者模式?
- 面试题:为什么wait()必须和synchronized一起使用?
- 面试题:为什么Java要把wait()和notify()放在如此Object类里面,而不是像sleep放在Thread中呢?
- 面试题:wait()的时候对象锁会释放锁?
- 面试题:wait()和sleep()区别?
- interruptedException和interrupt()方法
- 声:什么情况下抛出InterruptedException
- 面试官:给我说说轻量级阻塞和重量级阻塞
- 声:你了解线程中断后线程复位和被动复位?
- 声:如何优雅的关闭线程?
- 并发核心概念
- 并发与并行
- 同步
- 1.控制同步
- 面试题:如何控制多个线程执行顺序:给你三个线程如何顺序打印数字?
- 2.数据访问同步
- 不可变对象
- 面试官:String为什么设计成不可变对象?
- 原子操作和原子变量
- 并发问题
- 面试官:多线程场景会出现哪些并发问题?你项目中是如何解决的?
- 数据竞争
- 死锁
- 面试题:什么叫死锁?死锁必须满足哪些条件?如何定位死锁问题?有哪些解决死锁策略?
- 活锁
- 资源不足
- JMM(java memory model)内存模型
- 面试官:你知道JMM内存模型、java内存模型、jvm内存模型区别是什么?
- 声:什么是JMM?
- 我:听着还是好复杂呀,那什么是内存可见性?
- 我:什么是原子操作,我们应该注意什么呢?
- 我:什么是指令重排序
- 面试官:给我讲讲jvm内存模型以及jdk1.7和1.8版本有何区别?
- happen-before
- as if serial(串行)语义
- 面试官:什么是happen-before?
- voliate关键字
- final关键字
写在前面
这是第一次尝试用模拟对话体方式来叙述知识点,这样换种方式来做笔记使我印象更深刻,也希望使读者更容易理解(😊)!
多线程回顾
Thread和Runnable
我:这个使用还不简单,我分分钟就可以创建执行线程给你看
1.继承Thread
package net.dreamzuora;public class ThreadDemo extends Thread{@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "threadImpl run");}}
2.实现Runnable
package net.dreamzuora;public class RunnableDemo implements Runnable {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "runnableImpl run");}
}
3.带返回值的Callable
package net.dreamzuora;import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;public class CallableDemo implements Callable {@Overridepublic String call() throws Exception {TimeUnit.SECONDS.sleep(3);String p = Thread.currentThread().getName() + "callableImpl run";return p;}
}
测试方法:
@Testpublic void threadAndRunnable() throws ExecutionException, InterruptedException {new Thread(new RunnableDemo()).start();new Thread(new ThreadDemo()).start();//包装返回对象FutureTask<String> futureTask = new FutureTask<String>(new CallableDemo());new Thread(futureTask).start();//同步阻塞String p = futureTask.get();System.out.println(p);}
面试官:为什么我们在项目中推荐使用使用Runnable方法而不用继承Thread?
1.Runnable 与 Thread 类的解耦
2.提高性能,不用继承Thread每来个任务就必须创建一个线程,可以交给线程池提交任务,而线程池有线程复用机制,可以一个线程执行多个任务
3.从java语言特性分析:java单继承多实现,因此使用Runnable方式拓展性更强,就比如利用FutureTask包装Callable实现带有返回值的任务。
面试官:你居然谈到线程复用,那你可以给我讲讲线程复用原理?
面试官:Callable为什么可以带返回值,你知道底层原理?
首先Callable接口只是定义了一个带有返回值的方法,利用FutureTask包装Callable返回对象,根据类继承关系会发现FutureTask还是继承了Runnable接口,只是对其进行功能拓展。
jdk1.8的Future特性简介及使用场景
声:那你知道线程有哪些特征?
- main方法启动的主线程为第一个线程
- 线程优先级,每个线程都有其优先级,默认为5,介于[1,10]之间
- 线程共享公共资源,多线程会有并发问题,同步解决资源竞争
- 守护和非守护线程(用户线程)
我:你说的那些东西,我工作当中根本用不着有啥用?
声:你忘记了你因为基础薄弱被大厂虐的很惨?难道你不想进阿里了?不想升职加薪了?
我:真香,我要好好学习,那么什么是守护线程?main()主线程是守护线程?
声:
main不是守护线程,首先我们来理解守护线程的概念,守护线程是程序运行后它默默的在背后提供服务支持,最典型的像GC的内存回收保障程序正常运行,守护线程终止是被动的,只要非守护线程都退出之后守护线程就会被终止,非守护线程结束后意味着程序终止了。
我:我明白了,非守护线程(用户线程)就是我们一般手动创建的线程,而守护线程一般都是java底层默认提供的线程例如垃圾回收器、缓存管理器等用来执行辅助任务的,那么我们怎么创建守护线程呢?
声:你理解很到位,创建守护线程很简单,Thread类给我们提供类 thread.setDaemon(true) 方法,但是你一般不会用它,如果我们使用守护线程进行IO、业务逻辑操作,而它随着用户线程结束而结束,因此你无法保证服务正常执行。
我:我现在明白线程等基本特征,那线程又有哪些状态呢?
声:切记,线程有6种状态,这个面试时候经常会遇到,总共以下几种:
-
New
new Thread() -> 新建状态
-
Runnable
new Thread().start() -> 执行start()方法以后Runnable状态 其实Runnable分为两种状态,取决于获取CPU分配时间片之前和之后两种状态1.就绪状态:等待操作系统分配CPU时间片段2.运行状态:获取CPU分配时间片之后线程运行
-
Blocked
高并发多线程场景下,执行synchronied代码块,线程处于被阻塞状态
-
Waiting
1.不带时间参数的Object.wait() 2.不带时间参数的Thread.join()不带时间参数 3.LockSupport.park()方法
声:你知道LockSupport.park()方法?用在什么场景
我:不知道…
声:刚才强调过,Blocked 仅仅针对 synchronized monitor 锁,可是在 Java 中还有很多其他的锁,比如 ReentrantLock,如果线程在获取这种锁时没有抢到该锁就会进入 Waiting 状态,因为本质上它执行了 LockSupport.park() 方法,所以会进入 Waiting 状态。同样,Object.wait() 和 Thread.join() 也会让线程进入 Waiting 状态。park函数作用
Java的LockSupport.park()实现分析
面试题:线程了解?给我讲讲线程的几种状态?
面试题:你知道线程等待和阻塞的区别?
Blocked 与 Waiting 的区别是 Blocked 在等待其他线程释放 monitor 锁,而 Waiting 则是在等待某个条件,比如 join 的线程执行完毕,或者是 notify()/notifyAll() 。
-
Timed_waiting
1.设置了时间参数的 Thread.sleep(long millis) 方法; 2.设置了时间参数的 Object.wait(long timeout) 方法; 3.设置了时间参数的 Thread.join(long millis) 方法; 4.设置了时间参数的 LockSupport.parkNanos(long nanos) 方法和 LockSupport.parkUntil(long deadline) 方法。
-
Terminated
再来看看最后一种状态,Terminated 终止状态,要想进入这个状态有两种可能。
1.run() 方法执行完毕,线程正常退出。
2.出现一个没有捕获的异常,终止了 run() 方法,最终导致意外终止。
Thread类存储线程状态源码:
Thread.State 类:
public enum State {/*** Thread state for a thread which has not yet started.*/NEW,/*** Thread state for a runnable thread. A thread in the runnable* state is executing in the Java virtual machine but it may* be waiting for other resources from the operating system* such as processor.*/RUNNABLE,/*** Thread state for a thread blocked waiting for a monitor lock.* A thread in the blocked state is waiting for a monitor lock* to enter a synchronized block/method or* reenter a synchronized block/method after calling* {@link Object#wait() Object.wait}.*/BLOCKED,/*** Thread state for a waiting thread.* A thread is in the waiting state due to calling one of the* following methods:* <ul>* <li>{@link Object#wait() Object.wait} with no timeout</li>* <li>{@link #join() Thread.join} with no timeout</li>* <li>{@link LockSupport#park() LockSupport.park}</li>* </ul>** <p>A thread in the waiting state is waiting for another thread to* perform a particular action.** For example, a thread that has called <tt>Object.wait()</tt>* on an object is waiting for another thread to call* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on* that object. A thread that has called <tt>Thread.join()</tt>* is waiting for a specified thread to terminate.*/WAITING,/*** Thread state for a waiting thread with a specified waiting time.* A thread is in the timed waiting state due to calling one of* the following methods with a specified positive waiting time:* <ul>* <li>{@link #sleep Thread.sleep}</li>* <li>{@link Object#wait(long) Object.wait} with timeout</li>* <li>{@link #join(long) Thread.join} with timeout</li>* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>* </ul>*/TIMED_WAITING,/*** Thread state for a terminated thread.* The thread has completed execution.*/TERMINATED;}
最后我们再看线程转换的两个注意点。
1.线程的状态是需要按照箭头方向来走的,比如线程从 New 状态是不可以直接进入 Blocked 状态的,它需要先经历 Runnable 状态。
2.线程生命周期不可逆:一旦进入 Runnable 状态就不能回到 New 状态;一旦被终止就不可能再有任何状态的变化。所以一个线程只能有一次 New 和 Terminated 状态,只有处于中间状态才可以相互转换。
面试官:给我讲讲线程的生命周期?
我:可以参考我之前写过的博客
面试官:如果我在代码种连续调用两次thread.start()会发生什么你知道?
synchronized
声:居然我们学习到锁,给我讲讲锁的本质是什么?
我:在多线程环境下访问共享资源,这个资源可能是变量、对象、文件等,当我们给资源加锁以后,能够保证一段时间只有一个线程去访问,这样就避免数据竞争问题。
声:synchronized使用的几种方式?
我:这个简单,不就下面三种吗?
- 给对象加锁:类加锁、实例加锁
- 同步代码块
- 同步方法
面试官:给我讲讲synchronized 实例锁(Synchronized)和类锁(Static Synchronized)有什么区别?
如果synchronized作用在static方法中就是类锁,类锁是对该类下所有的对象实例都生效,而对象锁只是针对实例加锁
声:那你知道synchronized对对象加锁的原理?
首先我们可以把synchronized锁理解成一个“对象”,居然是“对象”里面肯定有成员变量,其中“对象”有三个成员变量:
1.state:锁是否被占用标识符。
2.threadID:占用锁的线程id。
3.threadIDList:保存正在等待、被阻塞获取锁的线程id,当前占用锁的线程被释放以后,就从threadIdList中唤醒一个线程拿到锁,循环往复。
synchronized(this)或者synchronized(obj)可以理解为就是将锁“对象”
synchronized和加锁的对象this/obj合二为一,怎么理解这句话呢?要访问的共享资源是obj那么这把锁就加载obj对象上,将锁“对象”作为obj对象的成员变量,这样意味着这个obj对象即是共享资源,同事具备锁的“对象”
面试官:给我讲讲锁是如何实现的?
在对象头里有一块数据叫Mark Word,其中包括两个重要的字段:锁标志位、占用锁的threadId。不同版本的JVM,对象头的数据结构不一样。
我:你前面讲的大白话我是听懂了,但是我想知道同步代码块和同步方法是如何避免并发问题的?
声:好,我先给你讲讲同步代码块实现原理
java代码:
public class SynTest {public void synBlock() {synchronized (this) {System.out.println("dreamzuora");}}
}
让我们看看汇编:javap -verbose SynTest.class
public void synBlock();descriptor: ()Vflags: ACC_PUBLICCode:stack=2, locals=3, args_size=10: aload_01: dup2: astore_13: monitorenter4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;7: ldc #3 // String dreamzuora9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V12: aload_113: monitorexit14: goto 2217: astore_218: aload_119: monitorexit20: aload_221: athrow22: return
说到synchronized我们脑子立马就想到monitor计数器、monitor enter、monitor exit,现在我结合上图来回答怎么这三个直接是怎么结合使用的。
[画图能力不行,这个图可能有些出入以下面文字为准]
monitor enter
执行 monitorenter 的线程尝试获得 monitor 的所有权,会发生以下这三种情况之一:
a. 如果该 monitor 的计数为 0,则线程获得该 monitor 并将其计数设置为 1。然后,该线程就是这个 monitor 的所有者。
b. 如果线程已经拥有了这个 monitor ,则它将重新进入,并且累加计数。
c. 如果其他线程已经拥有了这个 monitor,那个这个线程就会被阻塞,直到这个 monitor 的计数变成为 0,代表这个 monitor 已经被释放了,于是当前这个线程就会再次尝试获取这个 monitor。
monitor exit
monitorexit 的作用是将 monitor 的计数器减 1,直到减为 0 为止。代表这个 monitor 已经被释放了,已经没有任何线程拥有它了,也就代表着解锁,所以,其他正在等待这个 monitor 的线程,此时便可以再次尝试获取这个 monitor 的所有权
面试题:synchronized抛出异常是如何保证能正常释放锁?
细心的同学能够看到,反汇编后出现两处monitorexit
13: monitorexit14: goto 2217: astore_218: aload_119: monitorexit
JVM 要保证每个 monitorenter 必须有与之对应的 monitorexit,monitorenter 指令被插入到同步代码块的开始位置,而 monitorexit 需要插入到方法正常结束处和异常处两个地方,这样就可以保证抛异常的情况下也能释放锁
我:那同步方法和同步代码实现原理是一样的?
同步方法:
public synchronized void synMethod() {
}
同步方法汇编指令:
public synchronized void synMethod();descriptor: ()Vflags: ACC_PUBLIC, ACC_SYNCHRONIZEDCode:stack=0, locals=1, args_size=10: returnLineNumberTable:line 16: 0
声:同步代码块和同步方法实现不一样,同步方法并不是依靠 monitorenter 和 monitorexit 指令实现的,在同步方法中有一个ACC_SYNCHRONIZED标记这个方法是同步方法,当线程进入该方法会先判断该方法是否有这个标记,如果有则需要先获得 monitor 锁,然后才能开始执行方法,方法执行之后再释放 monitor 锁。其他方面, synchronized 方法和刚才的 synchronized 代码块是很类似的,例如这时如果其他线程来请求执行方法,也会因为无法获得 monitor 锁而被阻塞。
推荐一个大牛文章将synchronized底裤都扒了
面试题:进入synchronized获取对象锁后,调用Thread.sleep()方法会释放锁资源?
我:调用Thread.sleep()后线程会出让时间片段进入WAITING状态,不会释放锁资源,代码演示如下
package net.dreamzuora.thread;import org.junit.jupiter.api.Test;import java.util.concurrent.CountDownLatch;public class SynSleepDemo {Object obj = new Object();void a() {System.out.println("a:enter");synchronized (obj) {try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("a:exit");}}void b() {System.out.println("b:enter");synchronized (obj) {System.out.println("B");}System.out.println("b:exit");}void c() {synchronized (obj) {System.out.println("C");}}@Testpublic void singleThread() throws InterruptedException {this.a();this.b();this.c();}@Testpublic void multiThread() throws InterruptedException {CountDownLatch countDownLatch = new CountDownLatch(1);new Thread(() -> a()).start();new Thread(() -> b()).start();new Thread(() -> c()).start();countDownLatch.await();}}
控制台输出:
结论:可以看出三个线程同时只能有一个线程获取对象锁,而其中一个线程获取锁以后,并且执行Thread.sleep()方法的时候,其他线程仍然是BLOCKED状态,等到线程1休眠3秒以后其他线程才开始执行。
wait和notify
声:你知道wait()方法作用?
我:在wait()调用之前必须先获得对象锁,并且必须与synchronized一起使用,wait()使当前线程处于waiting状态,并且主动释放对象锁。
声:你知道notify()和notifyAll()作用?
我:notify()或者notifyAll()也必须和synchronized一起使用,notify()用来唤醒调用wait()后处于waiting状态的线程,当有多个线程时随机唤醒一个线程对其发出通知,但是并不会立马被唤醒,需要等待正在执行notify()的线程释放锁以后才可以。notifyAll()方法用来通知所有waiting()的线程。
笔试题:如何用wait和notify实现生产者消费者模式?
package net.dreamzuora.thread;import java.util.LinkedList;
import java.util.Queue;
import java.util.UUID;public class CustomMQ {public static void main(String[] args) {Queue<String> queue = new LinkedList<>();Producer producer = new Producer(queue);Consumer consumer = new Consumer(queue);new Thread(producer, "producer-thread-1").start();new Thread(producer, "producer-thread-2").start();new Thread(consumer, "consumer-thread-1").start();new Thread(consumer, "consumer-thread-2").start();}}
class Producer implements Runnable{private Queue<String> queue;public Producer(Queue<String> queue) {this.queue = queue;}@Overridepublic void run() {while (true){synchronized (queue){String uuid = UUID.randomUUID().toString();System.out.println("thread: " + Thread.currentThread().getName() + " producer: " + uuid);queue.add(uuid);queue.notify();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}
}class Consumer implements Runnable{private Queue<String> queue;public Consumer(Queue<String> queue) {this.queue = queue;}@Overridepublic void run() {while (true){synchronized (queue){if (queue.isEmpty()){try {System.out.println(Thread.currentThread().getName() + "wait...");queue.wait();} catch (InterruptedException e) {e.printStackTrace();}}String uuid = queue.poll();System.out.println("thread: " + Thread.currentThread().getName() + "consumer:" + uuid);}}}
}
代码案例和详细解释可以看看这个大牛的博客
面试题:为什么wait()必须和synchronized一起使用?
面试题:为什么Java要把wait()和notify()放在如此Object类里面,而不是像sleep放在Thread中呢?
面试题:wait()的时候对象锁会释放锁?
我:调用wait()线程不会出让CPU时间片段,但是会释放锁,我可以做个案例,如下
package net.dreamzuora.thread;import org.junit.jupiter.api.Test;import java.util.Date;
import java.util.concurrent.CountDownLatch;public class WaitDemo2 {Object obj = new Object();void a() {synchronized (obj) {try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("1");}void b() {synchronized (obj) {System.out.println("2");}}void c() {synchronized (obj) {System.out.println("3");}}@Testpublic void method() throws InterruptedException {this.a();this.b();this.c();}@Testpublic void thread() throws InterruptedException {CountDownLatch countDownLatch = new CountDownLatch(1);System.out.println(new Date());new Thread(() -> a()).start();new Thread(() -> b()).start();new Thread(() -> c()).start();Thread.sleep(5000);System.out.println(new Date());synchronized (obj){obj.notifyAll();}countDownLatch.await();}
}
控制台:
结论:最先启动的线程占用锁调用wait()方法都进入等待状态,其他线程仍然在执行,而等待的线程到了五秒被notifyAll()唤醒才执行,由此可以得出结论wait()方法时线程会释放锁。
面试题:wait()和sleep()区别?
wait()不会出让CPU时间片段,会释放锁;sleep()出让CPU时间片段,不释放锁。
interruptedException和interrupt()方法
当其他线程通过调用当前线程的 thread.interrupt() 方法,表示向当 前线程打个招呼,告诉他可以中断线程的执行了,至于什么时候中断,取决于当前线程自己。 线程通过检查是否被中断来进行相应,可以通过 Thread.currentThread().isInterrupted()来判断是否被中断。
interrupt() 方法并不像在 for 循环语句中使用 break 语句那样干脆,马上就停止循环。调用 interrupt() 方法仅仅是在当前线程中打一个停止的标记,并不是真的停止线程。
声:什么情况下抛出InterruptedException
我:obj.wait()、Thread.sleep()、Thread.join()等方法申请了中断异常才会抛出异常
面试官:给我说说轻量级阻塞和重量级阻塞
我:这个概念我都没听过~
声:你基础还是不行,遇到面试这种基础题你都不会直接被pass掉了,我来给你讲讲。
声:前面我们讲过线程的几种状态还记得?(
我:脑海回想…NEW、RUNNABLE、BLOCKED、WAITING、Time_Waiting、SLEEP、time_sleep
声:记忆不错,首先我们要知道轻量级阻塞是处于waiting、time_waiting状态,而重量级阻塞是blocked状态的,然后我们会发现只有线程进入synchronized尝试获取类锁或者对象锁,多线程竞争情况线程进入Blocked状态,而synchronized不会声明interruptedException,因此此时即使调用thread.interrupt()正在blocked线程是无法感知中断信息,由此可以确定synchronized修饰的代码会使线程重量级阻塞,相反线程中使用wait()、sleep()等方法会声明interruptedException,因此会响应中断,其属于轻量级阻塞。
我:那居然synchronized是重量级阻塞不会响应中断,那我在synchronized中调用thread.sleep()方法是不是就无法接收到中断信号?
声:你问这个问题说明你还没明白阻塞和等待线程状态,我们前面说过synchronized使线程BLOCKED是多线程竞争类锁或者对象锁时候,那些等待获取锁的线程进入阻塞状态,而一旦获取锁以后就不再是阻塞状态了,那么接下来执行的thread.sleep()也是这样能响应中断的呀!我给你举个例子
package net.dreamzuora.thread;public class ThreadInterruptedDemo {public static void main(String[] args) {Thread thread = new Thread(new SynSleep());thread.start();try {Thread.sleep(3 *1000);} catch (InterruptedException e) {e.printStackTrace();}thread.interrupt();}
}
class SynSleep implements Runnable{@Overridepublic void run() {synchronized (this){System.out.println("sleep");try {Thread.sleep(1000000);} catch (InterruptedException e) {e.printStackTrace();}}}
}
console:
声:你了解线程中断后线程复位和被动复位?
1.Thread.interrupted()手动复位将中断状态true->false 和Thread.currentThread().isInterrupted()方法一起使用。
2.进入InterruptedException异常前中断状态true->false
java线程的中断的方式原理分析
声:如何优雅的关闭线程?
Thread.currentThread().stop():官方不推荐使用,强制杀死线程
Thread.currentThread().destroy():官方不推荐使用,搞不懂官方为什么会有这个方法
回到这节标题,如何优雅的关闭线程?
停止线程方法有三种
- 设置标志位 while(flag) flag为false结束run方法,退出线程,这种方法注意线程安全问题。
- 调用线程stop()方法强制杀死线程,严重影响业务逻辑。
- 利用Thread.interrupted()方法中断线程,原理和1类似给线程设置中断标志符,具体停止线程逻辑交给业务处理,并且不会像flag一样有线程安全问题。
总结:利用interrupted()方法关闭线程
并发核心概念
声:这一章节可能比较枯燥,都是一些概念
并发与并行
并发:在同一时间段同时运行多个任务,单核CPU中运行多任务通过操作系统调度很快从一个任务运行切换到另一个任务
并行:在同一时刻同事运行多个任务,多核CPU同时运行多任务
同步
声:说到同步,我们可能会想到同步代码块,如果你知道同步代码块的作用,那么就好理解同步概念了,同步代码块是为了防止多线程并发访问共享资源而导致线程安全问题,那么同步就是用来协调两个或者多个任务能够按照我们想要的顺序去执行,获得我们预期想要的结果,那么这就是同步的概念。
声:你能想到日常开发当中都会用过同步机制?
我:synchronized
声:恩,说的不错,其实还有Semaphore等,现在让我们学习两种同步方法:
1.控制同步
当前任务结束输出作为下个任务输入
例如:
面试题:如何控制多个线程执行顺序:给你三个线程如何顺序打印数字?
顺序打印代码参考我之前总结的博客
package net.dreamzuora.thread;import org.junit.jupiter.api.Test;import java.util.concurrent.CountDownLatch;public class MultiThreadIncrement {int num = 1;Object obj = new Object();class Worker1 implements Runnable {Object object;public Worker1(Object object) {this.object = object;}@Overridepublic void run() {synchronized (obj) {while (num != 1) {try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + "A");num = 2;obj.notify();}}}class Worker2 implements Runnable {Object obj;public Worker2(Object obj) {this.obj = obj;}@Overridepublic void run() {synchronized (obj) {while (num != 2) {try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + "B");num = 3;obj.notifyAll();}}}class Worker3 implements Runnable {Object obj;public Worker3(Object obj) {this.obj = obj;}@Overridepublic void run() {synchronized (obj) {while (num != 3) {try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + "C");}}}@Testpublic void main() throws InterruptedException {CountDownLatch countDownLatch = new CountDownLatch(1);MultiThreadIncrement multiThreadIncrement = new MultiThreadIncrement();new Thread(multiThreadIncrement.new Worker3(obj), "thread-3").start();new Thread(multiThreadIncrement.new Worker1(obj), "thread-1").start();new Thread(multiThreadIncrement.new Worker2(obj), "thread-2").start();countDownLatch.await();}}
控制台:
并发中有不同的同步机制,比较流行的有以下几种
- 信号量(Semaphore)
- 监视器:一种在共享资源上实现互斥的机制。它有一个互斥、一个条件变量、两种操作(等待
条件和通报条件)。一旦你通报了该条件,在等待它的任务中只有一个会继续执行。
package net.dreamzuora.thread;import org.junit.jupiter.api.Test;import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;public class JUCDemo {int num = 0;AtomicInteger atomicInteger = new AtomicInteger(0);/*** CAS不是通过线程同步解决线程安全性问题*/@Testpublic void casTest(){Object obj = new Object();ExecutorService executorService = Executors.newFixedThreadPool(1000);for (int i = 0; i < 1000; i++) {executorService.submit(() -> {synchronized (obj){atomicInteger.incrementAndGet();}});}System.out.println(atomicInteger);}/*** 同步代码块*/@Testpublic void synTest(){Object obj = new Object();ExecutorService executorService = Executors.newFixedThreadPool(1000);for (int i = 0; i < 1000; i++) {executorService.submit(() -> {synchronized (obj){num++;}});}System.out.println(num);}/*** 信号量达到线程同步* @throws Exception*/@Testpublic void semaphoreTest() throws Exception {CountDownLatch countDownLatch = new CountDownLatch(1);final Semaphore semaphore = new Semaphore(1);ExecutorService executorService = Executors.newFixedThreadPool(1000);for (int i = 0; i < 1000; i++) {executorService.submit(() -> {try {semaphore.acquire();num++;} catch (InterruptedException e) {e.printStackTrace();} finally {semaphore.release();}});}System.out.println(num);countDownLatch.await();}}
2.数据访问同步
当两个或更多任务访问共享变量时,再任意时间里,只有一个任务可以访问该变量。
不可变对象
面试官:String为什么设计成不可变对象?
我:节省内存开销、避免线程安全问题
原子操作和原子变量
声:相信我们项目中用过很多原子操作或者原子变量吧?
我:的确用过很多,原子操作:像操作数据库的时候我们会利用@Transaction事务要不成功要么失败,像redis jedis.set(lockKey, requestId, “NX”, “EX”, expireTime)实现原子操作;原子变量:像利用voliate修饰的bool变量。
声:你回答的不错,让我们明确一下他们的居然定义
原子操作是一种发生在瞬间的操作。在并发应用程序中,可以通过一
个临界段来实现原子操作,以便对整个操作采用同步机制。
原子变量是一种通过原子操作来设置和获取其值的变量。可以使用某种同步机制来实现一个原子变
量,或者也可以使用CAS以无锁方式来实现一个原子变量,而这种方式并不需要任何同步机制。
并发问题
面试官:多线程场景会出现哪些并发问题?你项目中是如何解决的?
数据竞争
我:最常见的,多线程访问共享资源并对其进行增删改操作导致数据混乱问题,需要利用同步机制,或者CAS解决。
死锁
我:多线程情况下每个线程都占用对方要使用的锁却没有释放,导致死锁,死锁问题有典型的哲学家就餐问题
面试题:什么叫死锁?死锁必须满足哪些条件?如何定位死锁问题?有哪些解决死锁策略?
我的博客总结
活锁
马路中间有条小桥,只能容纳一辆车经过,桥两头开来两辆车A和B,A比较礼貌,示意B先过,B也比较礼貌,示意A先过,结果两人一直谦让谁也过不去。(这个形象的比喻摘自:死锁、活锁、饥饿)
资源不足
多线程访问共享资源时候需要先获取锁,如果有个线程持续占有锁而其他线程会一直白白等待。解决方案:加入计时等待机制,减少CPU开销
JMM(java memory model)内存模型
我:之前在面试中面试官就会问给我讲讲jvm内存模型,我巴拉巴拉答了一大堆,面试官说你这答的是jmm内存模型啊
面试官:你知道JMM内存模型、java内存模型、jvm内存模型区别是什么?
我:JMM内存模型就是java内存模型,而jvm内存模型就程序计数器、堆、虚拟机栈、本地方法栈,那什么是JMM内存模型呢?
声:我相信大多数人都不知道他们区别,那我来给你讲讲
声:什么是JMM?
JMM是和多线程相关的一组规范,需要各个JVM的实现遵守JMM规范,实现Java代码在不同JVM运行都能得到相同结果,从 Java 代码到 CPU 指令的这个转化过程要遵守哪些和并发相关的原则和规范,这就是 JMM 的重点内容
我:你说的概念都是什么鬼,听不懂,能不能具体点,这种抽象概念谁记得住啊,老子面试怎么打?
声:你这急性子,我还没讲完呢,得嘞,我来讲讲具体点的
JMM与处理器、缓存、并发、编译器有关,我想我们应该都听过CPU多级缓存、处理器优化、指令重排序等导致结果不一致问题,JMM很好的解决了这些问题。
我想我们都使用过synchronized、volatile、Lock等同步工具和关键字,而它们的实现都遵循了JMM规范。
面试官问道JMM我们应该立马想到JMM最最重要的3个内容:原子性、内存可见性、指令重排序
我:听着还是好复杂呀,那什么是内存可见性?
我:什么是原子操作,我们应该注意什么呢?
我:什么是指令重排序
面试官:给我讲讲jvm内存模型以及jdk1.7和1.8版本有何区别?
我:jmm内存划分:程序计数器、堆、方法区、虚拟机栈、本地方法栈,jdk1.8中将废除了永久代,引入了元空间
关于java内存模型讲解网上一大把,推荐一篇大牛博客总结的很好:Java内存管理-JVM内存模型以及JDK7和JDK8内存模型对比总结
jvm的汇总之后会在jvm常见面试题博客中进行详细梳理
happen-before
声: happen-before我想很多人并不熟悉,这节的内容概念性比较多可能有点枯燥,耐心看哟!
as if serial(串行)语义
单线程重排序
对于单线程程序CPU和编译器都可以对其重排序并不会导致结果不一致问题,这就是as if serial语义
多线程重排序
多线程场景中数据依赖复杂,编译器和CP无法理解之间关系做出合理优化,编译器和CPU只能保证单线程的as if serial
面试官:什么是happen-before?
如果A happen-before B 也就是A的执行结果对B可见,但是并不是意味着A一定要在B之前执行,在多线程中AB执行顺序不确定,又由于指令重排序,因此很容易会出现并发问题,为了解决这种问题java定义了许多内存可见性的约束:
- 单线程每个操作happen-before后续操作(也就是as-if-serial语义保障)
- synchronized解锁happen-before后续线程加锁
- 对volatile写入happen-before对这个变量的读取
- 对final域的写happen-before后续对final域的读