Java多线程-知识点总结

本文详细介绍了Java多线程的相关概念,包括Java内存模型、线程生命周期、线程安全、同步机制如synchronized和Lock接口,以及并发工具如Semaphore、CyclicBarrier和线程池等。通过理解这些概念,开发者可以更好地编写高性能、线程安全的Java程序。
摘要由CSDN通过智能技术生成

什么是Java内存模型?

Java 内存模型是通过各种操作来定义,包括对变量的读/写操作,监视器的加锁和释放操作,以及线程启动和合并操作。JMM为程序中所有的操作定义了一个偏序关系,称之为Happens-Before。要想保证操作B的线程能看到操作A的结果(无论A和B是否在同一个线程中执行),那么在A和B之间必须满足Happens-Before关系。如果两个操作之间缺乏Happens-Before关系,那么JVM可以对它们任意地排序。

Java内存模型说明了某个线程的内存操作在哪些情况下对于其他线程是可见的

在正确同步的程序中不存在数据竞争,并会表现出串行一致性,这意味着程序中的所有操作都会按照一种固定和全局的顺序执行。

Happens-Before规则包括:

程序顺序规则:如果程序中操作A在操作B之前,那么线程中操作A将在B操作之前执行。

监视器规则:在监视器锁上的解锁操作,必须在同一个监视器锁上的加锁操作之前执行。

volatile 变量规则:对 volatile 变量的写入操作必须在对该变量的读操作之前执行。原子变量与 volatile 变量在读操作和写操作上有着相同的内存语义。

线程启动规则:在线程上对 Thread.start() 的调用必须在该线程中执行任何操作之前执行。

线程结束规则:线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行。

中断规则规则:当一个线程在另一个线程上调用 interrupt 时,必须在被中断线程检测到 interrupt调用之前执行。

终结器规则:对象的构造函数必须在启动该对象的终结器之前执行完成。

传递性规则:如果操作A在操作B之前执行,并且操作B在操作C之前执行,那么操作A必须在操作C之前执行。

进程和线程有什么不同?

一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用。

线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而线程可以共享进程中的内存空间。每个线程都拥有单独的栈内存用来存储本地数据。

多线程编程的好处是什么?

在多线程程序中,多个线程被并发的执行以提高程序的效率,CPU不会因为某个线程需要等待资源而进入空闲状态。多个线程共享堆内存,因此创建多个线程去执行一些任务会比创建多个进程更好。举个例子,Servlets比CGI更好,是因为Servlets支持多线程而CGI不支持。

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

原子操作是指一个不受其它操作影响的操作任务单元。原子操作是在多线程环境下避免数据不一致必须的手段。

int++并不是一个原子操作,需要经历“读取-赋值-写入”,所以当一个线程读取它的值并加1时,另外一个线程有可能会读到之前的值,这就会引发错误。

什么是CAS?ABA?

CAS(Compare and Swap),从内存领域来说这是乐观锁,因为它在对共享变量更新之前会先比较当前值是否与更新前的值一致,如果是,则更新,如果不是,则无限循环执行(称为自旋),直到当前值与更新前的值一致为止,才执行更新。
简单的来说,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则返回V。

在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现。

ABA:在某些算法中如果V中值先由A变为B,再由B变为A,那么仍然认为发生了变化,并需要重新执行算法中的某些步骤。

为了解决ABA问题,一个解决方案是:不只是更新内存V中的值,而是更新两个值,包含一个引用和一个版本号。即使值A->B->A,版本号也将不同,无需重复执行。

什么是死锁(Deadlock)?如何分析和避免死锁?

死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

死锁的发生必须满足以下四个条件:

    • 互斥条件:一个资源每次只能被一个进程使用。
    • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
    • 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
    • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁。

分析死锁,需要查看Java应用程序的线程栈。需要找出那些状态为BLOCKED的线程和它们等待的资源。每个资源都有一个唯一的id,用这个id可以找出哪些线程已经拥有了它的对象锁。

避免嵌套锁,只在需要的地方使用锁和避免无限期等待是避免死锁的通常办法。如何分析死锁

什么是竞态条件和临界区?

当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。

导致竞态条件发生的代码区称作临界区。在临界区中使用适当的同步就可以避免竞态条件。

什么是线程优先级?

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

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

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

时间分片是指将可用的CPU时间分配给可用的Runnable线程的过程。分配CPU时间可以基于线程优先级或者线程等待的时间。线程调度并不受到Java虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让程序依赖于线程的优先级)。

什么是上下文切换(context-switching)?

CPU给每个任务都服务一定的时间,然后把当前任务的状态保存下来,在加载下一任务的状态后,继续服务下一任务。任务的状态保存及再加载,这段过程就叫做上下文切换。

上下文切换是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。上下文切换是多任务操作系统和多线程环境的基本特征。

如何确保线程安全?

在Java中可以有很多方法来保证线程安全:同步(synchronized、lock、ReentrantLock等),原子类(atomic concurrent classes),实现并发锁,使用volatile关键字,使用不变类和线程安全类。线程安全教程

有哪些不同的线程生命周期?

新建一个线程时,它的状态是New调用线程的start()方法时,状态被改变为Runnable。线程调度器会为Runnable线程池中的线程分配CPU时间并且将它们的状态改变为Running。其它的线程状态还有Waiting,Blocked 和Dead。读这篇文章可以了解更多关于线程生命周期的知识。

Java语言定义了5种线程状态,在任意一个时间点,一个线程只能有且只有其中的一种状态,这5种状态分别如下。

  • 新建(New):创建后尚未启动的线程处于这种状态。
  • 运行(Runable):Runable包括了操作系统线程状态中的Running和Ready,也就是处于此状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执行时间。
  • 无限期等待(Waiting):处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显式地唤醒。以下方法会让线程陷入无限期的等待状态:
  1. 没有设置Timeout参数的Object.wait()方法。
  2. 没有设置Timeout参数的Thread.join()方法。
  3. LockSupport.park()方法。
  • 限期等待(Timed Waiting):处于这种状态的线程也不会被分配CPU执行时间,不过无须等待被其他线程显式地唤醒,在一定时间之后它们会由系统自动唤醒。以下方法会让线程进入限期等待状态:
  1. Thread.sleep()方法。
  2. 设置了Timeout参数的Object.wait()方法。
  3. 设置了Timeout参数的Thread.join()方法。
  4. LockSupport.parkNanos()方法。
  5. LockSupport.parkUntil()方法。
  • 阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是:“阻塞状态”在等待着获取到一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。
  • 结束(Terminated):已终止线程的线程状态,线程已经结束执行。

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

在Java程序中创建一个线程,它就被称为用户线程。

守护线程是在后台执行并且不会阻止JVM终止的线程。当没有用户线程在运行的时候,JVM关闭程序并且退出。一个守护线程创建的子线程依然是守护线程。

如何创建一个线程?

有两种创建线程的方法:一是实现Runnable接口,然后将它传递给Thread的构造函数,创建一个Thread对象;二是直接继承Thread类。Java中创建线程

用Runnable还是Thread?

Java不支持类的多重继承,但允许实现多个接口。所以如果要继承其它类,实现Runnable接口更佳。

如何创建守护线程?

使用Thread类的setDaemon(true)方法可以将线程设置为守护线程,需要注意的是,需要在调用start()方法前调用这个方法,否则会抛出IllegalThreadStateException异常。

怎么检测一个线程是否拥有锁?

在java.lang.Thread中有一个方法叫holdsLock(),如果当且仅当当前线程拥有某个具体对象的锁,它返回true。

可以直接调用Thread类的run()方法么?

当然可以,但是如果调用Thread的run()方法,它的行为就会和普通的方法一样,只会是在原来的线程中调用,没有新的线程启动。

调用start()方法才会启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。

可以多次调用Thread的start()方法吗?

调用start方法后线程状态不再是新建状态(NEW),而是变成运行态,所以如果对一个线程对象多次调用start方法的话,会产生:IllegalThreadStateException异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值