多线程,高并发面试题
- 多线程有什么用
- 发挥多核CPU 的优势
- 多核 CPU 上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU 的优势来,达到充分利用CPU 的目的。
- 防止阻塞
- .多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。
- 线程和进程的区别是什么
- 它们是不同的操作系统资源管理方式
- 进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响
- 而线程只是一个进程中的不同执行路径
- 线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程
- Java 实现线程有哪几种方式
- 继承 Thread 类实现多线程
- 实现 Runnable 接口方式实现多线程
- 使用 ExecutorService、Callable、Future 实现有返回结果的多线程
- 启动线程方法 start()和 run()有什么区别
- 只有调用了 start()方法,才会表现出多线程的特性,不同线程的 run()方法里面的代码交替执行
- 如果只是调用 run()方法,那么代码还是同步执行的,必须等待一个线程的 run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其 run()方法里面的代码。
- .一个线程的生命周期有哪几种状态?它们之间如何流转的
- NEW:毫无疑问表示的是刚创建的线程,还没有开始启动
- RUNNABLE: 表示线程已经触发 start()方式调用,线程正式启动,线程处于运行中状态。
- BLOCKED:表示线程阻塞,等待获取锁,如碰到 synchronized、lock 等关键字等占用临界区的情况,一旦获取到锁就进行 RUNNABLE 状态继续运行
- WAITING:表示线程处于无限制等待状态,等待一个特殊的事件来重新唤醒,如通过wait()方法进行等待的线程等待一个 notify()或者 notifyAll()方法,通过 join()方法进行等待的线程等待目标线程运行结束而唤醒,一旦通过相关事件唤醒线程,线程就进入了 RUNNABLE 状态继续运行
- TIMED_WAITING:表示线程进入了一个有时限的等待,如 sleep(3000),等待 3 秒后线程重新进行 RUNNABLE 状态继续运行
- TERMINATED:表示线程执行完毕后,进行终止状态。需要注意的是,一旦线程通过 start 方法启动后就再也不能回到初始 NEW 状态,线程终止后也不能再回到RUNNABLE 状态
- 线程中的 wait()和 sleep()方法有什么区别
- sleep 方法和 wait 方法都可以用来放弃 CPU 一定的时间,不同点在于如果线程持有某个对象的监视器,sleep 方法不会放弃这个对象的监视器,wait方法会放弃这个对象的监视器
- 多线程同步有哪几种方法
- Synchronized 关键字,Lock 锁实现,分布式锁等
- 多线程之间如何进行通信
- wait/notify/notifyAll
- violatile 关键字的作用
- 多线程主要围绕可见性和原子性两个特性而展开,使用 volatile 关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到 volatile 变量,一定是最新的数据
- 为了获取更好的性能 JVM 可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用 volatile 则会对禁止语义重排序,当然这也一定程度上降低了代码执行效率从实践角度而言,volatile 的一个重要 作 用 就 是 和 CAS 结 合 , 保 证 了 原 子 性
- 新建 T1、T2、T3 三个线程,如何保证它们按顺序执行
1. 用 join 方法。 - 怎么控制同一时间只有 3 个线程运行
1. 用 Semaphore。 - 为什么要使用线程池
1. 不用线程池的话,每个线程都要通过 new Thread(xxRunnable).start()的方式来创建并运行一个线程
2. 线程少的话这不会是问题,而真实环境可能会开启多个线程让系统和程序达到最佳效率,当线程数达到一定数量就会耗尽系统的 CPU 和内存资源,也会造成 GC频繁收集和停顿
3. 因为每次创建和销毁一个线程都是要消耗系统资源的,如果为每个任务都创建线程这无疑是一个很大的性能瓶颈
4. 线程池中的线程复用极大节省了系统资源,当线程一段时间不再有任务处理时它也会自动销毁,而不会长驻内存。 - 线程池
1. 什么是线程池
1. 是装有线程的池子,我们可以把要执行的多线程交给线程池来处理,和连接池的概念一样,通过维护一定数量的线程池来达到多个线程的复用。
2. 线程池核心类
1. ThreadPoolExecutor 是线程池核心类,
3. 如何提交线程
1. submit
2. execute
4. submit 和 execute 分别有什么区别呢
1. execute 没有返回值,如果不需要知道线程的结果就使用 execute 方法,性能会好很多。
2. submit 返回一个 Future 对象,如果想知道线程结果就使用 submit 提交,而且它能在主线程中通过 Future 的 get 方法捕获线程中的异常。
5. 如何关闭线程池
1. 不再接受新的任务,之前提交的任务等执行结束再关闭线程池。es.shutdownNow();
2. 不再接受新的任务,试图停止池中的任务再关闭线程池,返回所有未处理的线程list 列表。 - CyclicBarrier 和 CountDownLatch 的区别
1. 都在 java.util.concurrent 下,都可以用来表示代码运行到某个点上
2. CyclicBarrier 的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行
3. CountDownLatch 则不是,某线程运行到某个点上之后,只是给某个数值-1 而已,该线程继续运行
4. CyclicBarrier 只能唤起一个任务,CountDownLatch 可以唤起多个任务
5. CyclicBarrier 可 重 用 , CountDownLatch 不 可 重 用 , 计 数 值 为 0 该CountDownLatch就不可再用了 - 什么是活锁、饥饿、无锁、死锁?
1. 死锁:是多线程中最差的一种情况,多个线程相互占用对方的资源的锁,而又相互等对方释放锁,此时若无外力干预,这些线程则一直处理阻塞的假死状态,形成死锁
2. 活锁:活锁是拿到资源却又相互释放不执行。当多线程中出现了相互谦让,都主动将资源释放给别的线程使用,这样这个资源在多个线程之间跳动而又得不到执行,这就是活锁
3. 饥饿:优先级高的线程能够插队并优先执行,这样如果优先级高的线程一直抢占优先级低线程的资源,导致低优先级线程无法得到执行,这就是饥饿
4. 无锁:即没有对资源进行锁定,即所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。无锁典型的特点就是一个修改操作在一个循环内进行,线程会不断的尝试修改共享资源,如果没有冲突就修改成功并退出否则就会继续下一次循环尝试。所以,如果有多个线程修改同一个值必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功
5. CAS 原理及应用即是无锁的实现 - 什么是原子性、可见性、有序性
1. 原子性:是指一个线程的操作是不能被其他线程打断,同一时间只有一个线程对一个变量进行操作。在多线程情况下,每个线程的执行结果不受其他线程的干扰,比如说多个线程同时对同一个共享成员变量 n++100 次,如果 n 初始值为 0,n 最后的值应该是 100,所以说它们是互不干扰的,这就是传说的中的原子性。但 n++并不是原子性的操作,要使用 AtomicInteger
2. 可见性:是指某个线程修改了某一个共享变量的值,而其他线程是否可以看见该共享变量修改后的值。在单线程中肯定不会有这种问题,单线程读到的肯定都是最新的值,而在多线程编程中就不一定了。每个线程都有自己的工作内存,线程先把共享变量的值从主内存读到工作内存,形成一个副本,当计算完后再把副本的值刷回主内存,从读取到最后刷回主内存这是一个过程,当还没刷回主内存的时候这时候对其他线程是不可见的,所以其他线程从主内存读到的值是修改之前的旧值。像CPU 的缓存优化、硬件优化、指令重排及对 JVM 编译器的优化,都会出现可见性的问题。
3. 有序性:为了优化程序执行和提高 CPU 的处理性能,JVM 和操作系统都会对指令进行重排,也就说前面的代码并不一定都会在后面的代码前面执行,即后面的代码可能会插到前面的代码之前执行,只要不影响当前线程的执行结果。所以,指令重排只会保证当前线程执行结果一致,但指令重排后势必会影响多线程的执行结果 - 什么是守护线程?
1. 与守护线程相对应的就是用户线程,守护线程就是守护用户线程,当用户线程全部执行完结束之后,守护线程才会跟着结束
2. 也就是守护线程必须伴随着用户线程,如果一个应用内只存在一个守护线程,没有用户线程,守护线程自然会退出。 - 一个线程运行时发生异常会怎样
1. 如果异常没有被捕获该线程将会停止执行
2. Thread.UncaughtExceptionHandler 是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口
3. 当一个未捕获异常将造成线程中断的时 候 JVM 会 使 用 Thread.getUncaughtExceptionHandler() 来 查 询 线程 的UncaughtExceptionHandler 并 将 线 程 和 异 常 作 为 参 数 传 递 给 handler 的uncaughtException()方法进行处理。 - 线程 yield()方法有什么用
1. Yield 方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行
2. 它是一个静态方法而且只保证当前线程放弃 CPU 占用而不能保证使其它线程一定能占用 CPU
3. 执行yield()的线程有可能在进入到暂停状态后马上又被执行 - 什么是重入锁
1. 指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的 - Fork/Join 框架是干什么的
1. 大任务自动分散小任务,并发执行,合并小任务结果 - 线程数过多会造成什么异常
1. 线程过多会造成栈溢出,也有可能会造成堆异常 - 什么是 CAS 算法
1. CAS,全称为 Compare and Swap,即比较-替换。
2. 假设有三个操作数:内存值 V、旧的预期值 A、要修改的值 B,当且仅当预期值 A 和内存值 V 相同时,才会将内存值修改为 B 并返回 true,否则什么都不做并返回 false。
3. 当然 CAS 一定要 volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值 A 对某条线程来说,永远是一个不会变的值 A,只要某次 CAS 操作失败,永远都不可能成功
4. java.util.concurrent.atomic 包下面的 Atom****类都有 CAS 算法的应用 - 怎么检测一个线程是否拥有锁
1. java.lang.Thread#holdsLock 方法 - Jdk 中排查多线程问题用什么命令
1. jstack - 线程同步需要注意什么
1. 尽量缩小同步的范围,增加系统吞吐量。
2. 分布式同步锁无意义,要使用分布式锁
3. 防止死锁,注意加锁顺序 - 线程 wait()方法使用有什么前提
1. 要在同步块中使用 - 线程之间如何传递数据
1. 通 过 在 线 程 之 间 共 享 对 象 就 可 以 了 , 然 后 通 过 wait/notify/notifyAll 、await/signal/signalAll 进行唤起和等待,比方说阻塞队列 BlockingQueue 就是为线程之间共享数据而设计的 - 保证"可见性"有哪几种方式
1. synchronized 和 viotatile - 说几个常用的 Lock 接口实现锁
1. ReentrantLock、ReadWriteLock - ThreadLocal 是什么
1. ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
2. 用来解决数据库连接、Session 管理等。 - ReadWriteLock 有什么用
1. ReadWriteLock 是一个读写锁接口
2. ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能 - FutureTask 是什么
1. FutureTask 表示一个异步运算的任务
2. FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成,取消任务等操作 - 怎么唤醒一个阻塞的线程
1. 如果线程是因为调用了 wait()、sleep()或者 join()方法而导致的阻塞,可以中断线程,并且通过抛出 InterruptedException 来唤醒它
2. 如果线程遇到了 IO 阻塞,无能为力,因为 IO是操作系统实现的,Java 代码并没有办法直接接触到操作系统 - 可变对象对多线程有什么帮助?
1. 不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。 - 多线程上下文切换是什么意思
1. 多线程的上下文切换是指 CPU 控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取 CPU 执行权的线程的过程。 - Java 中用到了什么线程调度算法?
1. 抢占式。一个线程用完 CPU 之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行 - Thread.sleep(0)的作用是什么
1. 由于 Java 采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到 CPU控制权的情况,为了让某些优先级比较低的线程也能获取到 CPU 控制权,可以使用 Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡 CPU 控制权的一种操作。 - 什么是乐观锁和悲观锁
1. 乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
2. 悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了 - Hashtable 的 size()方法为什么要做同步
1. 同一时间只能有一条线程执行固定类的同步方法,但是对于类的非同步方法,可以多条线程同时访问
2. 可能线程 A 在执行 Hashtable 的 put方法添加数据,线程 B 则可以正常调用 size()方法读取 Hashtable 中当前元素的个数,那读取到的值可能不是最新的,可能线程 A 添加了完了数据,但是没有对size++,线程 B 就已经读取 size了,那么对于线程 B 来说读取到的 size 一定是不准确的。而给 size()方法加了同步之后,意味着线程 B 调用 size()方法只有在线程 A调用 put 方完毕之后才可以调用,这样就保证了线程安全性 - 什么是自旋锁
1. 自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其他线程改变时才能进入临界区 - Runnable 和 Thread 用哪个好
1. Java 不支持类的多重继承,但允许你实现多个接口。所以如果你要继承其他类,也为了减少类之间的耦合性,Runnable 会更好 - 为什么 wait/notify/notifyAll 这些方法不在 thread 类里面
1. JAVA 提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果 wait()方法定义在 Thread 类中,线程正在等待的是哪个锁就不明显了。简单的说,由于 wait,notify 和 notifyAll 都是锁级别的操作,所以把他们定义在 Object 类中因为锁属于对象。 - 为什么 wait 和 notify 方法要在同步块中调用
1. 主 要 是 因 为 Java API 强 制 要 求 这 样 做 , 如 果 你 不 这 么 做 , 你 的 代 码会 抛 出IllegalMonitorStateException 异常。
2. 还有一个原因是为了避免 wait 和 notify之间产生竞态条件。 - 为什么你应该在循环中检查等待条件
1. 处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出
2. 当一个等待线程醒来时,不能认为它原来的等待状态仍然是有效的,在 notify()方法调用之后和等待线程醒来之前这段时间它可能会改变。这就是在循环中使用 wait()方法效果更好的原因 - Java 中堆和栈有什么不同
1. 每个线程都有自己的栈内存,用于存储本地变量,方法参数和栈调用,一个线程中存储的变量对其它线程是不可见的
2. 堆是所有线程共享的一片公用内存区域。对象都在堆里创建,为了提升效率线程会从堆中弄一个缓存到自己的栈,如果多个线程使用该变量就可能引发问题,这时 volatile 变量就可以发挥作用了,它要求线程从主存中读取变量的值。 - 如何在 Java 中获取线程堆栈
1. 获取线程堆栈时,JVM会把所有线程的状态存到日志文件或者输出到控制台
2. 在 Windows 你可以使用 Ctrl + Break 组合键来获取线程堆栈
3. Linux 下用 kill -3 命令
4. 用 jstack 这个工具来获取,它对线程 id 进行操作,用 jps 这个工具找到 id。 - 什么是阻塞式方法
1. 阻塞式方法是指程序会一直等待该方法完成期间不做其他事情
2. ServerSocket 的accept()方法就是一直等待客户端连接
3. 这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。此外,还有异步和非阻塞式方法在任务完成前就返回。 - 提交任务时线程池队列已满会时发会生什么
1. 当线程数小于最大线程池数 maximumPoolSize 时就会创建新线程来处理
2. 线程数大于等于最大线程池数 maximumPoolSize 时就会执行拒绝策略 - 为什么要使用并发编程
1. 充分利用多核CPU的计算能力;
2. 方便进行业务拆分,提升应用性能 - 并发编程有什么缺点
1. 并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度,
2. 但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、线程安全、死锁等问题。 - 并发编程三个必要因素是什么
1. 原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
2. 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile)
3. 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序) - Java 程序中怎么保证多线程的运行安全
1. 线程切换带来的原子性问题 解决办法:使用多线程之间同步synchronized或使用锁(lock)
2. 缓存导致的可见性问题 解决办法:synchronized、volatile、LOCK,可以解决可见性问题
3. 编译优化带来的有序性问题 解决办法:Happens-Before 规则可以解决有序性问题 - 并行和并发有什么区别
1. 并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。
2. 并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。
3. 串行:有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题 - 什么是多线程
1. 多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务 - 多线程的好处
1. 可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可以运行其它的线程而不是等待,这样就大大提高了程序的效率。也就是说允许单个程序创建多个并行执行的线程来完成各自的任务 - 多线程的劣势:
1. 线程也是程序,所以线程需要占用内存,线程越多占用内存也越多
2. 多线程需要协调和管理,所以需要 CPU 时间跟踪线程
3. 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题 - 什么是线程死锁
1. 死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)
2. 多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 - 形成死锁的四个必要条件是什么
1. 互斥条件:在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,就只能等待,直至占有资源的进程使用完毕释放。
2. 占有且等待条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放
3. 不可抢占条件:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。(比如一个进程集合,A在等B,B在等C,C在等A) - 如何避免线程死锁
1. 避免一个线程同时获得多个锁
2. 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
3. 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制 - runnable和callable有什么区别
- 相同点:
- 都是接口
- 都可以编写多线程程序
- 都采用Thread.start()启动线程
- 主要区别:
- Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
- Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息 注:Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
- 线程的run()和start()有什么区别
1. 每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程
2. start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start()只能调用一次
3. start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程
4. run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。 - 为什么我们调用start()方法时会执行 run()方法,为什么我们不能直接调用run()方法
1. new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到 时间片 后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行run() 方法的内容,这是真正的多线程工作。
2. 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
3. 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。 - 线程的调度策略
1. 线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行
1. 线程体中调用了 yield 方法让出了对 cpu 的占用权利
2. 线程体中调用了 sleep 方法使线程进入睡眠状态
3. 线程由于 IO 操作受到阻塞
4. 另外一个更高优先级线程出现
5. 在支持时间片的系统中,该线程的时间片用完 - 什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing )
1. 线程调度器是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。
2. 时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配 CPU 时间可以基于线程优先级或者线程等待的时间
3. 线程调度并不受到 Java 虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。 - 请说出与线程同步以及线程调度相关的方法
1. wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
2. sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException 异常;
3. notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
4. notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态 - sleep() 和 wait() 有什么区别
1. 两者都可以暂停线程的执行
2. 类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法
3. 是否释放锁:sleep() 不释放锁;wait() 释放锁。
4. 用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行
5. 用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(longtimeout)超时后线程会自动苏醒 - 为什么线程通信的方法 **wait(), notify()**和 **notifyAll()**被定义在 Object 类里?
1. 因为Java所有类的都继承了Object,Java想让任何对象都可以作为锁,并且 wait(),notify()等方法用于等待对象的锁或者唤醒线程,在 Java 的线程中并没有可供任何对象使用的锁,所以任意对象调用方法一定定义在Object类中。 - 为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用
1. 当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 notify()方法。同样的,当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用 - Thread 类中的 yield 方法有什么作用
1. 使当前线程从执行状态(运行状态)变为可执行态(就绪状态)
2. 当前线程到了就绪状态,那么接下来哪个线程会从就绪状态变成执行状态呢?可能是当前线程,也可能是其他线程,看系统的分配了 - 为什么Thread类的sleep()和yield ()方法是静态的
1. Thread 类的 sleep()和 yield()方法将在当前正在执行的线程上运行。所以在其他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法 - 线程的 sleep()方法和 yield()方法有什么区别
1. sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会
2. 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态;
3. sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常
4. sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。 - 如何停止一个正在运行的线程
1. 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
2. 使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。
3. 使用interrupt方法中断线程 - Java 中 interrupted 和 isInterrupted 方法的区别
1. interrupt:用于中断线程。调用该方法的线程的状态为将被置为”中断”状态
2. 线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException 的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常
3. interrupted:是静态方法,查看当前中断信号是true还是false并且清除中断信号。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了。
4. isInterrupted:是可以返回当前中断信号是true还是false,与interrupt最大的差别 - Java 如何实现多线程之间的通讯和协作
1. 可以通过中断 和 共享变量的方式实现线程间的通讯和协作
2. Java中线程通信协作的最常见方式
1. syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll()
2. ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()
3. 线程间直接的数据交换:
1. 通过管道进行线程间通信:字节流、字符流 - 什么是线程同步和线程互斥,有哪几种实现方式
1. 当一个线程对共享的数据进行操作时,应使之成为一个”原子操作“,即在没有完成相关操作之前,不允许其他线程打断它,否则,就会破坏数据的完整性,必然会得到错误的处理结果,这就是线程的同步。
2. 在多线程应用中,考虑不同线程之间的数据同步和防止死锁。当两个或多个线程之间同时等待对方释放资源的时候就会形成线程之间的死锁。为了防止死锁的发生,需要通过同步来实现线程安全。
3. 线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步
4. 线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作
5. 用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量
6. 实现线程同步的方法
1. 同步代码方法:sychronized 关键字修饰的方法
2. 同步代码块:sychronized 关键字修饰的代码块
3. 使用特殊变量域volatile实现线程同步:volatile关键字为域变量的访问提供了一种免锁机制
4. 使用重入锁实现线程同步:reentrantlock类是可冲入、互斥、实现了lock接口的锁他与sychronized方法具有相同的基本行为和语义 - 在监视器(Monitor)内部,是如何做线程同步的?程序应该做哪种级别的同步?
1. 在 java 虚拟机中,监视器和锁在Java虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。每一个监视器都和一个对象引用相关联。线程在获取锁之前不允许执行同步代码
2. 一旦方法或者代码块被 synchronized 修饰,那么这个部分就放入了监视器的监视区域,确保一次只能有一个线程执行该部分的代码,线程在获取锁之前不允许执行该部分的代码
3. 另外 java 还提供了显式监视器( Lock )和隐式监视器( synchronized )两种锁方案 - 如果你提交任务时,线程池队列已满,这时会发生什么
1. 如果使用的是无界队列 LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务
2. 如果使用的是有界队列比如 ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue 满了,会根据maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是 AbortPolicy - 你对线程优先级的理解是什么
1. 每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个 int 变量(从 1-10),1 代表最低优先级,10 代表最高优先级。
2. Java 的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,如非特别需要,一般无需设置线程优先级。
3. 设置优先级可以通过setPriority()方法设置,但是设置了不一定会该变,这个是不准确的 - 线程类的构造方法、静态块是被哪个线程调用的
1. 线程类的构造方法、静态块是被 new这个线程类所在的线程所调用的,而 run 方法里面的代码才是被线程自身所调用的 - Java 线程数过多会造成什么异常?
1. 线程的生命周期开销非常高
2. 消耗过多的 CPU
资源如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争 CPU资源时还将产生其他性能的开销
3. 降低JVM稳定性
在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError 异常。 - 多线程的常用方法
1. sleep**()** :强迫一个线程睡眠N毫秒
2. isAlive**()** :判断一个线程是否存活。
3. join() :等待线程终止。
4. activeCount**()** :程序中活跃的线程数。
5. enumerate**()** :枚举程序中的线程。
6. currentThread() :得到当前线程。
7. isDaemon**()** :一个线程是否为守护线程。
8. setDaemon**()** :设置一个线程为守护线程。
9. setName() :为线程设置一个名称。
10. wait**()** :强迫一个线程等待。
11. notify**()** :通知一个线程继续运行。
12. setPriority() :设置一个线程的优先级。 - 常用的并发工具类有哪些
1. CountDownLatch 类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他3个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。
2. CyclicBarrier (回环栅栏) CyclicBarrier它的作用就是会让所有线程都等待完成后才会继续下一步行动。CyclicBarrier初始时还可带一个Runnable的参数, 此Runnable任务在CyclicBarrier的数目达到后,所有其它线程被唤醒前被执行。
3. Semaphore (信号量) Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量(允许自定义多少线程同时访问)。就这一点而言,单纯的synchronized 关键字是实现不了的 - 常用并发列队的介绍
- 非堵塞队列:
- ArrayDeque,(数组双端队列)
- ArrayDeque (非堵塞队列)是JDK容器中的一个双端队列实现,内部使用数组进行元素存储,不允许存储null值,可以高效的进行元素查找和尾部插入取出,是用作队列、双端队列、栈的绝佳选择,性能比LinkedList还要好
- PriorityQueue, (优先级队列)
1. PriorityQueue (非堵塞队列) 一个基于优先级的无界优先级队列。优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。该队列不允许使用 null 元素也不允许插入不可比较的对象 - ConcurrentLinkedQueue,(基于链表的并发队列)
- ConcurrentLinkedQueue (非堵塞队列): 是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能。ConcurrentLinkedQueue的性能要好于BlockingQueue接口,它是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则。该队列不允许null元素。
- 堵塞队列
- DelayQueue,(基于时间优先级的队列,延期阻塞队列)
- DelayQueue是一个没有边界BlockingQueue实现,加入其中的元素必需实现Delayed接口。当生产者线程调用put之类的方法加入元素时,会触发Delayed接口中的compareTo方法进行排序,也就是说队列中元素的顺序是按到期时间排序的,而非它们进入队列的顺序。排在队列头部的元素是最早到期的,越往后到期时间赿晚。
- ArrayBlockingQueue(基于数组的并发阻塞队列)
- ArrayBlockingQueue是一个有边界的阻塞队列,它的内部实现是一个数组。有边界的意思是它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。ArrayBlockingQueue是以先进先出的方式存储数据
- LinkedBlockingQueue,(基于链表的FIFO阻塞队列)
- LinkedBlockingQueue阻塞队列大小的配置是可选的,如果我们初始化时指定一个大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE的容量 。它的内部实现是一个链表
- LinkedBlockingDeque, (基于链表的FIFO双端阻塞队列)
- LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列,即可以从队列的两端插入和移除元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争
- 相比于其他阻塞队列,LinkedBlockingDeque多了addFirst、addLast、peekFirst、peekLast等方法,以first结尾的方法,表示插入、获取或移除双端队列的第一个元素。以last结尾的方法,表示插入、获取或移除双端队列的最后一个元素
- LinkedBlockingDeque是可选容量的,在初始化时可以设置容量防止其过度膨胀,如果不设置,默认容量大小为Integer.MAX_VALUE。
- PriorityBlockingQueue,(带优先级的无界阻塞队列)
- priorityBlockingQueue是一个无界队列,它没有限制,在内存允许的情况下可以无限添加元素;它又是具有优先级的队列,是通过构造函数传入的对象来判断,传入的对象必须实现comparable接口。
- SynchronousQueue(并发同步阻塞队列)
- SynchronousQueue是一个内部只能包含一个元素的队列。插入元素到队列的线程被阻塞,直到另一个线程从队列中获取了队列中存储的元素。同样,如果线程尝试获取元素并且当前不存在任何元素,则该线程将被阻塞,直到线程将元素插入队列。
- 并发队列的常用方法
1. add() :在不超出队列长度的情况下插入元素,可以立即执行,成功返回true,如果队列满了就抛出异常。
2. offer() :在不超出队列长度的情况下插入元素的时候则可以立即在队列的尾部插入指定元素,成功时返回true,如果此队列已满,则返回false。
3. put() :插入元素的时候,如果队列满了就进行等待,直到队列可用。
4. take():从队列中获取值,如果队列中没有值,线程会一直阻塞,直到队列中有值,并且该方法取得了该值。
5. poll(long timeout,TimeUnit unit):在给定的时间里,从队列中获取值,如果没有取到会抛出异常。
6. remainingCapacity() :获取队列中剩余的空间
7. remove(Object o) :从队列中移除指定的值。
8. contains(Object o): 判断队列中是否拥有该值。
9. drainTo(Collectionc):将队列中值,全部移除,并发设置到给定的集合中。 - 阻塞队列和非阻塞队列区别
1. 当阻塞队列为空的时,从队列中获取元素的操作将会被阻塞
2. 当阻塞队列是满时,往队列里添加元素的操作会被阻塞
3. 试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素
4. 试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来 - 怎么判断并发队列是阻塞队列还是非阻塞队列
1. 在并发队列上JDK提供了Queue接口,一个是以Queue接口下的BlockingQueue接口为代表的阻塞队列,另一个是高性能(无堵塞)队列 - 并发队列和并发集合的区别:
1. 队列遵循“先进先出”的规则,可以想象成排队检票,队列一般用来解决大数据量采集处理和显示的
2. 并发集合就是在多个线程中共享数据的
3. 并发队列多个线程以有次序共享数据的重要组件 - Java 中的同步集合与并发集合有什么区别?
1. 同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。
2. 在 Java1.5 之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。
3. Java5 介绍了并发集合像ConcurrentHashMap,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。 - CopyOnWriteArrayList 的设计思想
1. 读写分离,读和写分开
2. 最终一致性
3. 使用另外开辟空间的思路,来解决并发冲突 - CopyOnWriteArrayList 的缺点
1. 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc 或者 full gc。
2. 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求
3. 由于实际使用中可能没法保证 CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次 add/set 都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障 - CopyOnWriteArrayList 的使用场景
1. 合适读多写少的场景。 - CopyOnWriteArrayList是什么?
1. CopyOnWriteArrayList 是一个并发容器。它是线程安全的,前提条件是非复合场景下操作它是线程安全的。
2. CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException。在CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行 - SynchronizedMap 和 ConcurrentHashMap 有什么区别?
1. SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为 map。
2. ConcurrentHashMap 使用分段锁来保证在多线程下的性能
3. ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将hash 表分为 16 个桶,诸如 get,put,remove 等常用操作只锁当前需要用到的桶
4. 这样,原来只能一个线程进入,现在却能同时有 16 个写线程执行,并发性能的提升是显而易见的。
5. 另外 ConcurrentHashMap 使用了一种不同的迭代方式。在这种迭代方式中,当iterator 被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时 new新的数据从而不影响原有的数据,iterator 完成后再将头指针替换为新的数据 ,这样 iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变 - 什么是并发容器的实现
1. 何为同步容器:可以简单地理解为通过 synchronized 来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如 Vector,Hashtable,以及Collections.synchronizedSet,synchronizedList 等方法返回的容器。可以通过查看 Vector,Hashtable 等这些同步容器的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字 synchronized
2. 并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在ConcurrentHashMap 中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问 map,并且执行读操作的线程和写操作的线程也可以并发的访问map,同时允许一定数量的写操作线程并发地修改 map,所以它可以在并发环境下实现更高的吞吐量 - Java 中 ConcurrentHashMap 的并发度是什么
1. ConcurrentHashMap 把实际 map 划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是 ConcurrentHashMap 类构造函数的一个可选参数,默认值为 16,这样在多线程情况下就能避免争用
2. 在 JDK8 后,它摒弃了 Segment(锁段)的概念,而是启用了一种全新的方式实现,利用 CAS 算法。 - ConcurrentHashMap和HashTable的不同之处?(JDK8之前)
1. HashTable就是实现了HashMap加上了synchronized,而ConcurrentHashMap底层采用分段的数组+链表实现,线程安全
2. ConcurrentHashMap通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍
3. 并且读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。
4. Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占
5. ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
6. 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容 - 说一下Atomic的原理
- Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。
- 原子类的常用类
1. AtomicBoolean
2. AtomicInteger
3. AtomicLong
4. AtomicReference - 什么是原子类
- java.util.concurrent.atomic包:是原子类的小工具包,支持在单个变量上解除锁的线程安全编程原子变量类相当于一种泛化的 volatile 变量,能够支持原子的和有条件的读-改-写操作
- 比如:AtomicInteger 表示一个int类型的值,并提供了 get 和 set 方法,这些 Volatile 类型的int变量在读取和写入上有着相同的内存语义。它还提供了一个原子的 compareAndSet 方法(如果该方法成功执行,那么将实现与读取/写入一个 volatile 变量相同的内存效果),以及原子的添加、递增和递减等方法。AtomicInteger 表面上非常像一个扩展的 Counter 类,但在发生竞争的情况下能提供更高的可伸缩性,因为它直接利用了硬件对并发的支持。
- 简单来说就是原子类来实现CAS无锁模式的算法
- 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 问题
- 循环时间长开销大
- 对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized
- 只能保证一个共享变量的原子操作
- 当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁
- Lock接口和synchronized对比同步它有什么优势
- Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完不同的性质,并且可以支持多个相关类的条件对象
- 它的优势有:
- 可以使锁更公平
- 可以使线程在等待锁的时候响应中断
- 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
- 可以在不同的范围,以不同的顺序获取和释放锁
- 整体上来说 Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操作。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。
- final可变对象,它对写并发应用有什么帮助?
- 不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(Mutable Objects)。
- 不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多不可变类,如String、基本类型的包装类、BigInteger 和 BigDecimal 等。
- 只有满足如下状态,一个对象才是不可变的
- 它的状态不能在创建后再被修改
- 所有域都是 final 类型;并且,它被正确创建(创建期间没有发生 this 引用的逸出)
- 不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率
- synchronized和volatile的区别是什么
- synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程
- volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序
- 区别
- volatile 是变量修饰符;synchronized 可以修饰类、方法、变量
- volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性
- volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
- volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。
- volatile变量和atomic变量有什么不同
- volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用 volatile 修饰 count 变量,那么 count++ 操作就不是原子性的
- 而 AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作
- Java中能创建volatile数组吗
- 能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。意思是,如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了
- volatile关键字的作用
- 对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。 volatile 提供 happensbefore 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主内存中,当有其他线程需要读取时,它会去内存中读取新值
- 从实践角度而言,volatile 的一个重要作用
- 就是和 CAS 结合,保证了原子性
- volatile 常用于多线程环境下的单次操作(单次读或者单次写)。
- synchronized和ReentrantLock区别是什么
- synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量
- synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在Java 6 中对 synchronized 进行了非常多的改进
- 相同点:两者都是可重入锁
- 主要区别如下:
- ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作
- ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁
- ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等
- 二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁,
- synchronized 操作的应该是对象头中 mark word
- Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
- 普通同步方法,锁是当前实例对象
- 静态同步方法,锁是当前类的class对象
- 同步方法块,锁是括号里面的对象
- synchronized和Lock有什么区别
- 首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类;
- synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁
- synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁
- 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到
- synchronized,volatile,CAS比较
- synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞
- volatile 提供多线程共享变量可见性和禁止指令重排序优化
- CAS 是基于冲突检测的乐观锁(非阻塞)
- 当一个线程进入一个对象的 synchronized 方法 A 之后,其它线程是否可进入此对象的 synchronized 方法 B
1. 不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静态方法上的synchronized 修饰符要求执行方法时要获得对象的锁,如果已经进入A 方法说明对象锁已经被取走,那么试图进入 B 方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁 - 线程B怎么知道线程A修改了变量
- volatile 修饰变量
- synchronized 修饰修改变量的方法
- wait/notify
- while 轮询
- 多线程中synchronized锁升级的原理是什么
- synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级
- 锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗
- 偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁/解锁的一些CAS操作(比如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁。 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
- 轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,轻量级锁就会升级为重量级锁;
- 重量级锁是synchronized ,是 Java 虚拟机中最为基础的锁实现。在这种状态下,Java 虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。
- 什么是自旋
1. 很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized 里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized的边界做忙循环,这就是自旋。如果做了多次循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略
2. 忙循环:就是程序员用循环让一个线程等待,不像传统方法wait(), sleep() 或 yield() 它们都放弃了CPU控制,而忙循环不会放弃CPU,它就是在运行一个空循环。这么做的目的是为了保留CPU缓存,在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存。为了避免重建缓存和减少等待重建的时间就可以使用它了。 - synchronized可重入的原理
1. 重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁。 - 说一下synchronized底层实现原理
- Synchronized的语义底层是通过一个monitor(监视器锁)的对象来完成,
- 每个对象有一个监视器锁(monitor)。每个Synchronized修饰过的代码当它的monitor被占用时就会处于锁定状态并且尝试获取monitor的所有权 ,过程
- 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者
- 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1
- 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权
- synchronized是可以通过 反汇编指令 javap命令,查看相应的字节码文件
- synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(Stringa) 因为JVM中,字符串常量池具有缓存功能
- 重排序遵守的规则
- as-if-serial
- 不管怎么排序,结果不能改变
- 不存在数据依赖的可以被编译器和处理器重排序
- 一个操作依赖两个操作,这两个操作如果不存在依赖可以重排序
- 单线程根据此规则不会有问题,但是重排序后多线程会有问题
- as-if-serial规则和happens-before规则的区别
- as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变
- as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的
- as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度
- 重排序实际执行的指令步骤
- 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序。现代处理器采用了指令级并行技术(ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
- 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行
- 这些重排序对于单线程没问题,但是多线程都可能会导致多线程程序出现内存可见性问题。
- 什么是重排序
- 程序执行的顺序按照代码的先后顺序执行。
- 一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,进行重新排序(重排序),它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的
- 重排序对单线程运行是不会有任何问题,但是多线程就不一定了
- Thread 类的 sleep()方法和对象的 wait()方法都可以让线程暂停执行,它们有什么区别?
1. sleep()方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复(线程回到就绪状)。
2. wait()是 Object 类的方法,调用对象的 wait()方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的 notify()方法(或 notifyAll()方法)时才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态 - 线程的 sleep()方法和 yield()方法有什么区别
1. sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;
2. yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
3. 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态;
4. sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常;
5. sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性。 - 当一个线程进入一个对象的 synchronized 方法 A 之后,其它线程是否可进入此对象的 synchronized 方法 B?
1. 不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静态方法上的 synchronized 修饰符要求执行方法时要获得对象的锁,如果已经进入A 方法说明对象锁已经被取走,那么试图进入 B 方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁 - 请说出与线程同步以及线程调度相关的方法
1. wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
2. sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常;
3. notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
4. notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
5. Java 5 通过 Lock 接口提供了显式的锁机制(explicit lock),增强了灵活性以及对线程的协调。Lock 接口中定义了加锁(lock())和解锁(unlock())的方法,同时还提供了 newCondition()方法来产生用于线程之间通信的 Condition 对象;
6. 此外,Java 5 还提供了信号量机制(semaphore),信号量可以用来限制对某个共享资源进行访问的线程的数量。在对资源进行访问之前,线程必须得到信号量的许可(调用 Semaphore 对象的 acquire()方法);在完成对资源的访问后,线程必须向信号量归还许可(调用 Semaphore 对象的 release()方法) - 编写多线程程序有几种实现方式
1. Java 5 以前实现多线程有两种实现方法:一种是继承 Thread 类;另一种是实现Runnable 接口。两种方式都要通过重写 run()方法来定义线程的行为,推荐使用后者,因为 Java 中的继承是单继承,一个类有一个父类,如果继承了 Thread 类就无法再继承其他类了,显然使用 Runnable 接口更为灵活
2. Java 5 以后创建线程还有第三种方式:实现 Callable 接口,该接口中的 call方法可以在线程执行结束时产生一个返回值 - synchronized 关键字的用法
1. synchronized 关键字可以将对象或者方法标记为同步,以实现对对象和方法的互斥访问,可以用 synchronized(对象) { … }定义同步代码块,或者在声明方法时将 synchronized 作为方法的修饰符。 - 说明同步和异步
1. 如果系统中存在临界资源(资源数量少于竞争资源的线程数量的资源),例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就必须进行同步存取(数据库操作中的排他锁就是最好的例子)。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。事实上,所谓的同步就是指阻塞式操作,而异步就是非阻塞式操作 - 启动一个线程是调用 run()还是 start()方法
1. 启动一个线程是调用 start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由 JVM 调度并执行,这并不意味着线程就会立即运行。run()方法是线程启动后要进行回调(callback)的方法 - 什么是线程池(thread pool)
1. 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在 Java 中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是”池化资源”技术产生的原因。
2. 线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。J
3. Java 5+中的 Executor 接口定义一个执行线程的工具。它的子类型即线程池接口是 ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,因此在工具类 Executors 面提供了一些静态工厂方法,生成一些常用的线程池
4. newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行
5. newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
6. newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
7. newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
8. newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求
9. 如果希望在服务器上使用线程池,强烈建议使用 newFixedThreadPool方法来创建线程池,这样能获得更好的性能 - 线程的基本状态以及状态之间的关系
1. 其中 Running 表示运行状态,
2. Runnable 表示就绪状态(万事俱备,只欠CPU),
3. Blocked 表示阻塞状态,阻塞状态又有多种情况,可能是因为调用 wait()方法进入等待池,也可能是执行同步方法或同步代码块进入等锁池,或者是调用了 sleep()方法或 join()方法等待休眠或其他线程结束,或是因为发生了 I/O 中断 - 简述 synchronized 和 java.util.concurrent.locks.Lock的异同?
1. Lock 是 Java 5 以后引入的新的 API,和关键字 synchronized 相比主要相同点:Lock 能完成 synchronized 所实现的所有功能;
2. 主要不同点:Lock 有比synchronized 更精确的线程语义和更好的性能,而且不强制性的要求一定要获得锁。synchronized 会自动释放锁,而 Lock 一定要求程序员手工释放,并且最好在 finally 块中释放(这是释放外部资源的最好的地方) - Java 中能创建 volatile 数组吗
1. 能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。我的意思是,如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了。 - volatile 能使得一个非原子操作变成原子操作吗
1. 一个典型的例子是在类中有一个 long 类型的成员变量。如果你知道该成员变量会被多个线程访问,如计数器、价格等,你最好是将其设置为 volatile。为什么?因为 Java 中读取 long 类型变量不是原子的,需要分成两步,如果一个线程正在修改该 long 变量的值,另一个线程可能只能看到该值的一半(前 32 位)。但是对一个 volatile 型的 long 或 double 变量的读写是原子。 - volatile 修饰符的有过什么实践
1. 一种实践是用 volatile 修饰 long 和 double 变量,使其能按原子类型来读写。double 和 long 都是 64 位宽,因此对这两种类型的读是分为两部分的,第一次读取第一个 32 位,然后再读剩下的 32 位,这个过程不是原子的,但 Java 中volatile 型的 long 或double 变量的读写是原子的。
2. volatile 修复符的另一个作用是提供内存屏障(memory barrier),例如在分布式框架中的应用。简单的说,就是当你写一个 volatile 变量之前,Java 内存模型会插入一个写屏障(writebarrier),读一个 volatile 变量之前,会插入一个读屏障(read barrier)。意思就是说,在你写一个 volatile 域时,能保证任何线程都能看到你写的值,同时在写之前,也能保证任何数值的更新对所有线程是可见的,因为内存屏障会将其他所有写的值更新到缓存 - volatile 类型变量提供什么保证
1. volatile 变量提供顺序和可见性保证,例如,JVM 或者 JIT 为了获得更好的性能会对语句重排序,但是 volatile 类型变量即使在没有同步块的情况下赋值也不会与其他语句重排序。
2. volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。某些情况下,volatile 还能提供原子性,如读 64 位数据类型,像 long 和 double 都不是原子的,但 volatile 类型的 double 和long 就是原子的 - 10 个线程和 2 个线程的同步代码,哪个更容易写
1. 从写代码的角度来说,两者的复杂度是相同的,因为同步代码与线程数量是相互独立的。但是同步策略的选择依赖于线程的数量,因为越多的线程意味着更大的竞争,所以你需要利用同步技术,如锁分离, - 你是如何调用 wait()方法的?使用 if 块还是循环?为什么
1. wait() 方法应该在循环调用,因为当线程获取到 CPU 开始执行的时候,其他条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好
2. 用while循环 - 什么是多线程环境下的伪共享
1. 伪共享是多线程系统(每个处理器有自己的局部缓存)中一个众所周知的性能问题。伪共享发生在不同处理器的上的线程对变量的修改依赖于相同的缓存行
2. 伪共享问题很难被发现,因为线程可能访问完全不同的全局变量,内存中却碰巧在很相近的位置上。如其他诸多的并发问题,避免伪共享的最基本方式是仔细审查代码,根据缓存行来调整你的数据结构 - 什么是线程局部变量
1. 线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java 提供 ThreadLocal 类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。 - Java 中 sleep 方法和 wait 方法的区别?
1. 虽然两者都是用来暂停当前运行的线程,但是 sleep() 实际上只是短暂停顿,因为它不会释放锁,而 wait() 意味着条件等待,这就是为什么该方法要释放锁,因为只有这样,其他等待的线程才能在满足条件时获取到该锁。 - Java 中,编写多线程程序的时候你会遵循哪些最佳实践
1. 给线程命名,这样可以帮助调试。
2. 最小化同步的范围,而不是将整个方法同步,只对关键部分做同步。
3. 如果可以,更偏向于使用 volatile 而不是 synchronized。
4. 使用更高层次的并发工具,而不是使用 wait() 和 notify() 来实现线程间通信,如 BlockingQueue,CountDownLatch 及 Semeaphore。
5. 优先使用并发集合,而不是对集合进行同步。并发集合提供更好的可扩展性 - mysql锁的优化策略
1. 读写分离
2. 分段加锁
3. 减少锁持有的时间
4. 多个线程尽量以相同的顺序去获取资源
5. 不能将锁的粒度过于细化,不然可能会出现线程的加锁和释放次数过多,反而效率不如一次加一把大锁。 - 并发编程三要素
1. 原子性
1. 原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。
2. 可见性
1. 可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。
3. 有序性
1. 有序性,即程序的执行顺序按照代码的先后顺序来执行 - 实现可见性的方法有哪些
1. synchronized 或者 Lock:保证同一个时刻只有一个线程获取锁执行代码,锁释放 之前把最新的值刷新到主内存,实现可见性。 - 多线程的价值?
1. 发挥多核 CPU 的优势
1. 多线程,可以真正发挥出多核 CPU 的优势来,达到充分利用 CPU 的目的,采用多线程的方式去同时完成几件事情而不互相干扰
2. 防止阻塞
1. 从程序运行效率的角度来看,单核 CPU 不但不会发挥出多线程的优势,反而会因为在单核 CPU 上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核 CPU 我们还是要应用多线程,就是为了防止阻塞。试想,如果单核 CPU 使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据塞,也不会影响其它任务的执行。
3. 便于建模
1. 这是另外一个没有这么明显的优点了。假设有一个大的任务 A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务 A 分解成几个小任务,任务 B、任务 C、任务 D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了 - 什么是 AQS
1. AQS 是 AbustactQueuedSynchronizer 的简称,它是一个 Java 提高的底层同步工具类,用一个 int 类型的变量表示同步状态,并提供了一系列的 CAS 操作来管理这个同步状态。
2. AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等皆是基于AQS 的 - AQS 支持两种同步方式
1. 独占式
2. 共享式
3. 这样方便使用者实现不同类型的同步组件,独占式如 ReentrantLock,共享式如Semaphore,CountDownLatch,组合式的如 ReentrantReadWriteLock。总之,AQS 为使用提供了底层支撑,如何组装实现,使用者可以自由发挥 - 线程 B 怎么知道线程 A 修改了变量
1. volatile 修饰变量
2. synchronized 修饰修改变量的方法
3. wait/notify
4. while 轮询 - synchronized、volatile、CAS 比较
1. synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。
2. volatile 提供多线程共享变量可见性和禁止指令重排序优化。
3. CAS 是基于冲突检测的乐观锁(非阻塞) - ThreadLocal 是什么?有什么用?
1. ThreadLocal 是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。简单说 ThreadLocal 就是一种以空间换时间的做法,在每个 Thread 里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。 - 为什么 wait()方法和 notify()/notifyAll()方法要在同步块中被调用
1. 这是 JDK 强制的,wait()方法和 notify()/notifyAll()方法在调用前都必须先获得对象的锁 - 死锁的原因
1. 是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖的闭环。
1. 例如:线程在获得了锁 A 并且没有释放的情况下去申请锁 B,这时,另一个线程已经获得了锁 B,在释放锁 B 之前又要先获得锁 A,因此闭环发生,陷入死锁循环。
2. 默认的锁申请操作是阻塞的。
1. 所以要避免死锁,就要在一遇到多个对象锁交叉的情况,就要仔细审查这几个对象的类中的所有方法,是否存在着导致锁依赖的环路的可能性。总之是尽量避免在一个同步方法中调用其它对象的延时方法和同步方法