文章目录
- 基础知识
- 并发编程的优缺点
- 线程和进程的区别
- 创建线程
- 线程的状态和基本操作
- 线程的生命周期及五种基本状态
- Java中用到的线程调度算法是什么?
- 终止线程运行的情况
- sleep() 和 wait() 方法有什么区别?
- 为什么线程通信的方法wait(),notify() 和 notifyAll() 被定义在 Object 类中?
- 为什么 wait(),notify()和 notifyAll() 必须在同步方法或者同步块中被调用?
- 为什么Thread 类的 sleep() 和 yield() 方法是静态的?
- Java中 interrupted 和 isInterrupted 方法的区别
- Java中如何唤醒一个阻塞的线程
- Java如何实现多线程之间的通讯和协作?
- 同步方法和同步块哪个是更好的选择?
- 什么是线程同步和线程互斥?
- 什么叫线程安全?Servlet 是线程安全吗?
- 谈谈线程的优先级
- 线程类的构造方法、静态块是被哪个线程调用的?
- 多线程安全问题,以及引起多线程不安全的原因
- 并发关键字
- 线程死锁
- 并发容器
- 阻塞队列
- 线程池
基础知识
并发编程的优缺点
为什么要使用并发编程(并发编程的优点)
- 充分利用多核 CPU 的计算能力:通过并发编程的形式可以将多核 CPU 的计算能力发挥到机制,性能得到提升
- 方便进行业务拆分,提升系统并发能力和性能
并发编程的缺点
并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度,但是并发编程并不总是能够提高程序运行速度,而且并发编程还会引起很多问题,如:内存泄漏、上下文切换、线程安全、死锁等问题。
并发编程的三要素是什么?
并发编程的三要素(线程的安全性问题体现在):
- 原子性:一个操作或多个操作要么全部成功执行,要么全部执行失败
- 可见性:一个线程对共享变量的修改,另一个线程能够及时看到
- 有序性:程序执行的顺序按照代码的先后顺序执行
出现线程安全问题的原因:
- 线程切换带来的原子性问题
- 缓存导致的可见性问题
- 编译优化带来的有序性问题
并行和并发有什么区别?
- 并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”;
- 并发:多个任务在同一个 CPU 核上,按细分的时间片交替执行,从逻辑上看那些任务是同时执行;
- 串行:有 n 个任务,由一个线程按照顺序执行。由于任务、方法都在一个线程所以不存在线程不安全情况。
并行 = 两台咖啡机 + 两行队列
并发 = 一台咖啡机 + 两行队列
串行 = 一台咖啡机 + 一行队列
线程和进程的区别
什么是线程?什么是进程?
进程
进程是系统分配资源的最小单位。简单来说,一个在内存中运行的应用程序就是一个进行。每个进程都有自己独立的一块内存,一个进程可以由多个线程。例如,电脑中正常运行的爱奇艺视频就是一个进程
线程
线程是系统调度的最小单位。简单来说,在爱奇艺视频中,我正在同时下载两部电影,每个下载的路径都是自己独立的一个线程。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。
进程和线程的区别
根本区别:进程是系统分配资源的最小单位,进程是系统进行任务调度的最小单位;
资源开销:每个进程都有自己独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看作是轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己的运行栈和程序计数器,线程之间切换的开销小;
包含关系:如果一个进程有多个线程,则执行过程是多线共同完成的;线程是进程的一部分,所以线程也被称为是轻量级的进程
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址和空间是相互独立的
影响关系:一个线程崩溃后,不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所有多进程比多线程健壮
执行过程:每个独立的进程都有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行空指,两者均可并发执行
什么是多线程,多线程的优劣?
多线程:多线程是指程序中包含多个执行流,即在一个程序中可以同时运行不同的线程来执行不同的任务。
多线程的好处:可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可以调用其他线程,执行其他的线程上的任务,这样就大大提高了程序的执行效率。也就是说允许单个程序创建多个并发执行的线程来完成各自的任务。
多线程的劣势:
- 线程也是程序,所以线程需要占用内存,线程越多占用内存越多;
- 多线程需要协调和管理,所以需要CPU 时间跟踪线程;
- 线程之间对共享资源的访问会相互影响,必须解决资源共享的问题
什么是上下文切换
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 在任意时刻都只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完,就会重新处于就绪的状态,把CPU 让给其他线程使用,这个过程就属于一次上下文切换。
概括来说:当前任务在执行完 CPU 时间片切换到另一个任务之前,会先保存自己的状态,以便下次再次切换回这个任务时,可以继续从该状态执行,保存好之后,就可以加载下一个任务。从第一个任务的保存再到下一个任务的加载的过程就是一次上下文切换。
注意:什么是上下文?
- 一个线程在执行的时候,CPU 的寄存器上的所有值、状态以及堆栈上的内容
- 切换时要保存当前线程的所有状态,即保存当前线程的上下文,以便再次执行该线程时,能够恢复切换时的状态,继续执行
守护线程和用户线程的区别
- 用户线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程。
- 守护线程:运行在后台,为其他前台线程服务。也就是说守护线程是JVM中非守护线程的“佣人”。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作。
main 函数所在的线程就是一个用户线程,main 函数启动的同时还启动了好多守护线程,比如垃圾回收线程。
比较明显的区别:用户线程结束,JVM 退出,这时不管有没有守护线程运行都会退出,即守护线程不会影响 JVM 的退出。
注意:
- setDaemon(true) 必须在 start() 方法前执行;
- 在守护线程中产生的线程也是守护线程;
- 不是所有的任务都可以分配给守护线程来执行,比如读写操作或计算逻辑
- 守护线程不能依靠finally 块的内容来确保执行关闭或清理资源的逻辑。
创建线程
创建线程的几种方法
- 继承 Thread 类;
- 实现 Runnable 接口;
- 使用 Executors 工具类创建线程池
继承 Thread 类
- 自定义 Thread 子类,重写 run 方法,实现相关逻辑
- 创建自定义的线程子类对象
- 调用子类实例的 start 方法来启动线程
public class Main{
public static void main(String[] args){
MyThread thread = new MyThread();
thread.start();
System.out.println(Thread.currentThread().getName() + " main() 方法正在运行");
}
}
class MyThread extends Thread{
@Override
public void run(){
System.out.println(Thread.currentThread().getName() + " run() 方法正在运行");
}
}
实现 Runnable 接口
- 定义 Runnable 接口实现类 MyRunnable,并重写 run() 方法;
- 创建 MyRunnable 实例,以该实例作为 target 创建 Thread 对象,该对象才是真正的线程对象
- 调用线程对象的 start() 方法
public class Main{
public static void main(String[] args){
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
System.out.println(Thread.currentThread().getName() + " main() 方法正在运行");
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " run() 方法正在运行2");
}
}
使用 Executors 工具类创建线程池
这个方法在后边的线程池具体说
线程的 run() 和 start() 方法有什么区别?
每个线程所要执行的任务都是该线程对象所对应的 run() 方法来完成操作,run() 方法称为线程体。线程的启动是通过调用对象的 start() 方法。
start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 方法可以重复调用,start() 方法只能调用一次。
/**
start() 方法启动一个线程实现了多线程运行,调用完 start() 方法,该线程处于就绪状态,等待抢占 CPU 执行run() 方法。抢占到cpu,该线程就处于运行状态,此时用 run() 方法来完成其运行状态。run() 方法运行结束,此线程终止。cpu 再被其他线程抢占。
run() 方法是在本线程里的,只是相当于 main 线程下的一个普通函数,而不是多线程的。*/
为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
当 new 一个线程对象,该线程就会进入一个新建状态。调用 start() 方法,会启动一个线程并使该线程进入就绪状态,当分配到时间片抢占到CPU就可以开始工作了。在 start() 函数内部会执行线程的相应准备工作,然后自动调用 run() 方法的内容,这是真正的多线程工作。
如果直接调用线程对象的 run() 方法,会把 run() 方法当作一个 main 线程下的普通方法去执行,并不会在某个多线程中执行它,所以这不是多线程工作。
总结:调用 start() 方法可启动线程并使线程进入就绪状态,而 run() 方法只是 thread 的一个普通方法调用,还是在主线程里面执行。
线程的状态和基本操作
线程的生命周期及五种基本状态
- 初始化(new):新创建了一个线程对象;
- 就绪(ready):新建的线程对象调用 start() 方法,该线程处于就绪状态,等待被系统调度选中,获取 cpu 的使用权;
- 运行(running):就绪状态的线程获得了 cpu 时间片,执行线程。一旦当前线程的时间片用完,或当前线程调用 yield() 方法,当前线程重新处于就绪状态,等待下次调度。注:线程的就绪状态时到运行状态的唯一入口,即就是说,线程要想进入运行状态,首先必须处于就绪状态中;
- 阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 cpu 的使用权,停止执行,此时线程进入阻塞状态,直到其进入就绪状态,才有机会被 cpu 调用重新就如运行状态。阻塞情况又分为三种:
- 等待(waiting):运行状态中的线程执行 wait() 方法,JVM 会把该线程放入等待队列中,使当前线程进入到等待阻塞状态,在当前线程被其他线程唤醒后,又会回到就绪状态,等待调用;
- 阻塞(blocked):线程在获取 synchronized 同步锁失败,JVM 会把该线程放入到锁池中,线程会进入同步阻塞状态,在当前线程获取到锁后,当前线程又会重新回到就绪状态,等待调用;
- 超时等待(timed_waiting):通过调用线程的 sleep() 方法或 join() 方法或发出 I/O 请求时,线程会进入到超时等待状态,当 sleep() 状态超时、join() 状态超时、join() 等待线程终止或者超时、或者 I/O 处理完毕时,线程会重新转入就绪状态,等待调用;
- 终止(terminal):线程 run()、main() 方法执行结束,则该线程结束生命周期。
Java中用到的线程调度算法是什么?
计算机通常只有一个 CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令。所谓多线程并发运行,其实从宏观上看,是各个线程轮流获得 cpu 的使用权,分别执行各自的任务。在线程池中,会有多个处于就绪状态的线程在等待CPU,Java虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配 CPU 的使用权。
分别由两种调度模式:分时调度和抢占式调度
分时调度是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用 cpu 的时间片。
Java虚拟机采用的是抢占式调度,是指让就绪状态下的优先级高的线程优先占用 cpu,如果可运行池下的线程优先级相同,则随机选取一个线程,使其占用 cpu。处于就绪状态下的线程会一直等待调用,直至它不得不放弃cpu。
终止线程运行的情况
- 当前正常运行的线程调用 yield() 方法,让出对 cpu 的占用
- 线程体中调用 sleep() 方法,使线程进入睡眠状态;
- 线程由于 IO 操作受到阻塞;
- 出现优先级更高的线程;
- 该线程的时间片用完。
sleep() 和 wait() 方法有什么区别?
两者都可以暂停线程的执行
- 所属类不同:sleep() 是 Thread 线程类的静态方法,wait() 是 Object 类的方法;
- 是否释放锁:slee() 方法无视锁的存在,即不会请求锁也不会释放锁,wait() 方法使用前需要请求锁,wait() 方法执行时会释放锁,等到被唤醒重新请求锁;
- 用途不同:wait() 通常被用于线程之间的通信,sleep() 通常被用于暂停执行;
- 用法不同:wait() 方法被调用后,线程不会自己苏醒,需要别的线程调用同一对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行后,到了一定时间,线程会自动苏醒。
为什么线程通信的方法wait(),notify() 和 notifyAll() 被定义在 Object 类中?
在Java中任何对象都可以作为锁,并且wait() ,notify() 等方法的作用是等待线程对象的锁或者唤醒线程对象的锁,在Java的线程中并没有可供任意对象使用的锁,所以任意对象调用的方法一定定义在 Object 类中。
这里又有一个问题,既然是线程放弃锁,那么应该也可以把 wait() 定义在 Thread 类中吧?如果把 wait() 方法定义在 Thread 中,一个线程可以持有很多把锁,在调用 wait() 方法时要放弃一个锁,此时就不知道要放弃哪个锁了?
为什么 wait(),notify()和 notifyAll() 必须在同步方法或者同步块中被调用?
当一个线程需要调用对象的 wait() 方法时,这个线程必须持有该对象的锁,接着该线程执行 wait() 方法时就会释放该对象的锁,并进入等待状态直到被其他线程调用这个对象上的释放锁的方法。同样的,当一个线程需要调用某个对象的 notify() 方法时,它要先获得这个对象的锁,在调用 notify() 方法时才能释放这个对象的锁,以便其他在等待的线程可以得到这个对象的锁。
由于所有的这些方法都需要线程持有对象的锁,所以他们只能在同步方法或同步代码块中被调用。
为什么Thread 类的 sleep() 和 yield() 方法是静态的?
Thread 类的 yield() 方法和 sleep() 方法是作用在当前正在执行的线程上。所以当其他处于等待状态的线程调用这些方法是没有意义的,cpu 每次只能运行一个线程,系统知道要将这些方法作用到哪个线程上,所以可以直接将这些方法设为静态方法。
Java中 interrupted 和 isInterrupted 方法的区别
interrupt():用于中断线程,调用该方法的线程的状态会被设置为“中断”状态。
注意:线程终端仅仅是置线程的中断状态位,并不会停止线程。需要用户自己去件时线程的状态并做处理。支持线程中段的方法就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常(interruptedException)。
interrupted():是静态方法,查看当前中断信号是true还是false,并且清楚中断信号。如果当前线程被中断了,第一次调用 interrupted 则返回 true,第二次和之后就返回 false 了。
isInterrupted():不是静态方法,只是查看当前线程的中断信号是true还是 false。
Java中如何唤醒一个阻塞的线程
首先,wait() 、notify() 方法是针对对象的,调用任意对象的 wait() 方法都将导致线程阻塞,阻塞的同时也将释放当前对象的锁,相应的,调用任意对象的 notify() 方法则将随机接触该对象阻塞的一个线程,但他需要重新获取对象的锁,知道获取成功才能往下执行。
注意:这方法都要在同步方法或者同步代码块中被调用,并且要保证同步方法或同步代码块锁的对象与 wait()、notify() 方法调用的是同一个。
Java如何实现多线程之间的通讯和协作?
可以通过中断 和 共享变量 的方式实现线程间的通讯和协作。
比如说最经典的生产者-消费者模型:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的同时,生产者需要释放对临界资源(队列)的占用权。因为生产者如果不释放对队列的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去。因此,一般情况下,当队列满时,会让生产者处于等待状态,且交出对临界资源的占用权。然后消费者消费商品,然后消费者会停止生产者队列有空间了。同样的,当队列为空时,消费者也需要等待且释放为队列的占用权,等待生产者停止队列中有商品了。这种互相通信的过程就是线程间的协作。
同步方法和同步块哪个是更好的选择?
同步块更符合开放调用的原则,只在需要锁住的代码块中所住相应的对象,这样从侧面开说也可以避免死锁。
什么是线程同步和线程互斥?
当一个线程对共享的数据进行操作时,应使之称为一个“原子操作”,即在没有完成相关操作之前,不允许其他线程打断它,否则就会破坏数据的完整性,必然会得到错误的处理,这就是线程的同步。
在多线程应用中,需要考虑不同线程之间的数据同步和防止死锁。当多个线程之间同时等待对方释放资源的时候就会形成死锁。为了防止死锁的发生,需要同步来实现线程安全。
线程互斥是指对于共享的线程资源,在各个线程访问它时的排他性。当有若干个线程都需要使用某一共享资源时,任何时刻下最多只允许一个线程去使用,其他线程若要使用该资源必须等待,直到占用资源的线程释放该资源。
什么叫线程安全?Servlet 是线程安全吗?
线程安全是指某个方法在多线程环境下被调用时,能够正确的处理多个线程之间的共享变量,使程序能够正确完成。
servlet 不是线程安全的,servlet 是单例模式(单实例多线程调用),当多个线程同时访问同一个方法时,不同保证共享变量的线程是安全性的。
谈谈线程的优先级
每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是与操作系统相关的。我们可以定义线程的优先级,但并不能保证优先级高的一定会优于优先级低的执行。线程优先级是一个int 类型的变量,从1~10,10代表最高优先级。
Java 的线程调度会委托给操作系统去处理,所以与具体的操作系统的优先级有关。因此优先级高低并不全能保证线程调度的先后。
线程类的构造方法、静态块是被哪个线程调用的?
线程类的构造方法、静态方法是被 new 这个线程对象所在的线程调用的,而 run 方法才是被new 出来的线程对象自身锁调用。
例如:在 main 函数中 new 了 Thread1, 在 Thread1 中 new 了 Thread2,那么
(1)Thread1 的构造方法、静态块是 main 线程调用的,Thread1 的 run() 方法是 Thread1 自身调用的;
(2)Thread2 的构造方法、静态块是 Thread1 线程调用的,Thread2 的 run() 方法是 Thread2 自身调用的。
多线程安全问题,以及引起多线程不安全的原因
线程不安全指的是当多个线程对同一全局变量或静态属性,做读写操作时,可能会发生数据冲突问题,也就是线程安全问题。
举例:买火车票的问题。有两个自动售票机售最后一张票,在第一个乘客点击准备购买车票但是还未付款成功,与此同时另一个人看到还有一张票,也点击进行准备购买车票,这时就会造成不安全的现象,即最后这一张票卖了两次。
引起多线程不安全的原因:
- 原子性:一个操作或多个操作要么全部成功执行,要么全部执行失败;
- 可见性:一个线程对共享变量的修改,其他线程能够及时看到;
- 重排序:在执行程序时,为了提高性能,处理器和编译器通常会对指令进行重排序。
并发关键字
synchronized
synchronized 的作用
在Java中,synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时调用。synchronized 关键字可以修饰变量、代码块、方法、类。
在Java中 synchronized 属于重量级锁,效率较为低下。
synchronized 的用途
- 修饰实例方法:对当前对象实例进行加锁,进入同步代码前要先获得当前实例的锁。
- 修饰静态方法:作用于当前类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B调用这个实例对象所属类的静态 synchronized 方法是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象的锁。
- 修饰代码块:指定加锁对象。需要在 synchronized() 中指定加锁的对象,进入同步代码块前需要获得当前对象的锁。
总结:synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是给 Class 类上锁。synchronized 关键字加到实例方法上是给实例对象上锁。
双重校验锁实现单例模型
public class Singleton{
private volatile static Singleton uniqueInstance;
private Singleton(){
}
public static Singleton getUniqueInstance(){
// 先判断对象是否已经实现,没有实例化过才进入加锁代码
if(uniqueInstance == null){
// 类对象加锁
synchronized(Singleton.class){
// 再次判断是否经历过实例化
if(uniqueInstance == null){
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
使用 volatile 关键字可以解决可见性、部分的重排序问题,保证在多线程下也能正常运行。uniqueInstance = new Singleton();这段代码其实是分为三部执行:
- 为uniqueInstance 分配内存空间;
- 初始化 uniqueInstance;
- 将 uniqueInstance 指向分配的内存地址。
但是由于 JVM 具有指令重排序的特征,执行顺序可能变成1 -> 3 -> 2。指令重排序并不会在单线程环境下产生问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。比如,线程T1执行了 1 和 3,此时T2调用 getUniqueInstance() 方法后发现 uniqueInstance 不为空,因此直接返回 uniqueInstance,但此时该变量还未被初始化。
当一个线程进入一个对象的 synchronized 方法1 后,其他线程是否可进入此对象的 synchronized 方法B?
不能。其他线程只能访问该对象的非同步方法。因为非静态方法上的 synchronized 锁加到了该对象上,如果已经进入 A 方法说明对象锁已经被加上,那么 synchronized 方法B 就只能在等锁池等待锁的对象。
volatile
volatile 关键字的作用
volatile 是Java虚拟机提供的轻量级的同步机制
1)保证可见性
- JMM模型的线程工作:
各个线程对主内存中共享变量 X 的操作都是各线程各自拷贝一份到自己的工作内存中再操作回主内存中。- 存在的问题:
如果线程 A 修改了共享变量 X 的值还未写回主内存,这时另外一个线程 B 也对 X 进行操作,但此时线程 A 工作内存中的各项变量对线程 B 来说是不可见的,所以线程 B 取得的 X 的值是原内存中的 X 的值。这种工作内存与主内存延迟的现象就会造成可见性的问题。- 解决:volatile
当共享变量被 volatile 修饰时,他会保证修改的值会立刻被更新到主存,当有其他线程读写共享变量时,就回去内存中读取新值。
2)不保证原子性
- 原子性:
不可分割、完整性,即某个线程正在做某个具体业务时,中间不可以被分割,需要整体完整性,要么全部成功执行,要么全部执行失败- 解决方法:
- 加 synchronized
- 使用 JUC 下的 AtomicInteger
public class Volatile{
public static void main(String[] args){
atomicByVolatile();
}
public static void atomicByVolatile(){
Test test = new Test();
for(int i = 0; i < 20; i++){
new Thread(() -> {
for(int j = 1; j <= 1000; j++){
test.addSelf();
test.atomicAddSelf();
}
}, "Thread " + i).start();
}
while(Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t finally num value is "+test.n);
System.out.println(Thread.currentThread().getName()+"\t finally atomicnum value is "+test.atomicInteger);
}
}
class Test{
volatile int n = 0;
public void addSelf(){
n++;
}
AtomicInteger atomicInteger = new AtomicInteger();
public void atomicAddSelf(){
atomicInteger.getAndIncrement();
}
}
//打印结果:
/**
main finally num value is 18864 **不保证原子性**
main finally atomicnum value is 20000 **保证原子性**
*/
3)禁止重排序
- 指令重排序
多线程环境下线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。- 内存屏障作用
- 保证特定操作的执行顺序
- 保证某些变量的内存可见性
Java中能创建 volatile 数组吗?
可以。Java可以用 volatile 创建数组,不过创建的只是一个数组的引用,而不是数组。意思就是,如果改变的是引用指向,将会受到 volatile 的保护,但是如果多线程同时改变数组的元素,volatile 就不能起到之前的保护作用了。
synchronized 和 volatile的区别是什么?
synchronized 表示只有一个线程可以获取作用对象的锁,执行代码。
volatile 表示变量在 CPU 的寄存器下是不确定的,必须从主存中读取。保证多线程环境下变量的可见性,禁止指令重排序
区别:
- volatile 是变量修饰符,而synchronized 可以修饰变量、方法、类;
- volatile 仅能实现变量的修改可见性,并不能保证原子性,而 synchronized 可以保证变量的修改可见性和原子性;
- volatile 不会造成线程阻塞,synchronized 可能会造成线程的阻塞;
- volatile 标记的变量不会被编译器优化,synchronized 标记的变量可以被编译器优化。
- volatile 关键字是线程同步的轻量级实现,所以volatile 性能肯定比synchronized 的性能要好。但是volatile 关键字只能作用于变量而synchronized 关键字可以修饰方法以及代码块。
线程死锁
什么是线程死锁
死锁是指两个或两个以上的线程(进程)在执行中,由于竞争资源或彼此通信而造成的一种堵塞的现象,若无外力作用,他们都无法推进下去。此时成系统处于死锁状态。
如:线程A持有锁2,并尝试去获取锁1 的同时,线程B持有锁1,并尝试去获取锁2的情况下,就会发生 AB 两个线程由于相互持有对方需要的锁,而发生阻塞现象,我们称之为死锁。
形成死锁的四个必要条件是什么?怎么防止死锁?
- 互斥:线程对于所分配到的资源具有排他性,即在任意时刻,一个资源只能被一个线程所占用,直到该线程被释放;
- 请求与保持:一个线程因请求资源被占用而发生阻塞时,对已获得的资源也要保持不放;
- 不剥夺:线程已获得的资源在未使用完之前不能被其他线程强行剥夺
- 循环等待:当发生死锁时,所等待的线程必然会形成一个环路,造成永久阻塞。
防止死锁的方法:
- 尽量降低锁的使用粒度;
- 尽量不要几个功能用同一把锁;
- 尽量减少同步的代码块
死锁与活锁的区别?
死锁:是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力,它们将无法推进下去。
活锁:任务或执行者没有被阻塞,只有由于某些条件没有满足,导致线程一直重复尝试、失败、尝试、失败。
区别:处于活锁的实体是在不断地改变状态,而处于死锁地实体则表现为等待;且活锁有可能自行解开,死锁则不能。
并发容器
SynchronizedMap 和 ConcurrentHashMap 的区别
SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线程来访问 map。
ConcurrentHashMap 使用分段锁来保证在多线程下的性能。一次锁住一份桶,ConcurrentHashMap 默认将 hash 表分为 16 个桶,这样选择能同时有 16 个写线程执行,并发性能的提升是显而易见的。
阻塞队列
阻塞队列拆分为“阻塞” 和“队列”,所谓阻塞,即在多线程领域,某些情况下会挂起线程(也就是线程阻塞),一旦条件满足,被挂起的线程优先被启动唤醒。
其中,Thread 1是往阻塞队列中添加元素,Thread 2是从阻塞队列移除元素
- 当阻塞队列是空时,从队列中获取元素的队列会被阻塞(Thread 2被阻塞)
- 当阻塞队列是满时,向队列中添加元素的队列会被阻塞。(Thread 1被阻塞)
分类
- ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按照先进先出的规则排序。
- LinkedBlockingQueue:是一个基于链表结构的有界阻塞队列(大小默认为Integer.MAX_VALUE),此队列按照先进先出对元素进行排序,吞吐量 通常要高于 ArrayBlockingQueue
- SynchronizedQueue:是一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常高于LinkedBlockingQueue。
- PriorityBlockingQueue:支持优先级排列的延迟无界阻塞队列
- DelayQueue:使用优先级队列实现的延迟无界阻塞队列
- LinkedTransferQueue:由链表结构组成的无界阻塞队列。
用途
- 生产者消费者模型
- 线程池
- 消息中间件