Java多线程基础知识总结

本文详细讲解了Java多线程的基础知识,包括进程与线程的区别、线程生命周期、创建线程的不同方法(继承Thread、实现Runnable、Callable接口和线程池)、核心同步机制(wait/sleep/notify/notifyAll/join/yield/interrupt)、锁的原理(synchronized与lock)、死锁、线程不安全原因、以及CyclicBarrier和CountDownLatch的区别。此外,还涵盖了面试中常见的多线程问题和解答。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

此文章仅为个人以前通过网上知识点进行整合总结,存在部分个人修改,侵删

1.基础知识

1.1进程(process)和线程(thread)的区别
  1. 进程是资源分配最小单位,线程是任务调度和执行的最小单位;
  2. 进程有自己独立的地址空间,每启动一个进程,系统都会为其分配地址空间,建立数据表来维护代码段、堆栈段和数据段,线程没有独立的地址空间,它使用相同的地址空间共享数据;
  3. 在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)
  4. CPU切换一个线程比切换进程花费小;创建一个线程比进程开销小
  5. 线程的占用资源要比进程少很多
  6. 线程之间通信更方便,同一个进程下,线程共享全局变量,静态变量等数据,进程之间的通信需要以通信的方式(IPC)进行;(但多线程程序处理好同步与互斥是个难点)
  7. 多进程程序更安全,生命力更强,一个进程死掉不会对另一个进程造成影响(源于有独立的地址空间),多线程程序更不易维护,一个线程死掉,整个进程就死掉了(因为共享地址空间);
  8. 进程对资源保护要求高,开销大,效率相对较低,线程资源保护要求不高,但开销小,效率高,可频繁切换;
1.2 一个线程的生命周期
  • 新建状态:

    使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

  • 就绪状态:

    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

  • 运行状态:

    如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

  • 阻塞状态:

    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
  • 死亡状态:

    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

1.3 线程创建4种方法
1.3.1: Thread继承

Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法。

public class Main extends Thread{
    public static void main(String[] args) {
        Main myThead=new Main();
        myThead.start();
    }
    public void run(){
        System.out.println("start thread");
    }
}

1.3.2: Runnable接口

如果自己的类已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现一个Runnable 接口。

public class Main implements Runnable{
    public static void main(String[] args) {
        Main myThead=new Main();
        Thread thread=new Thread(myThead);
        thread.start();
    }
    public void run(){
        System.out.println("start thread");
    }
}
1.3.3: Callable接口

有返回值的任务必须实现 Callable 接口,类似的,无返回值的任务必须 Runnable 接口。执行Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务 返回的 Object 了。

Callable所依赖的FutureTask类保存了call方法的返回值,可以用get方法获取。

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
 
public class Main implements Callable<Integer> {
    public static void main(String[] args) throws Exception {
        Main myThead=new Main();
        FutureTask futureTask = new FutureTask(myThead);
        Thread thread = new Thread(futureTask);
        thread.start();
        System.out.println(futureTask.get());
    }
    public Integer call(){
        return 1;
    }
}
1.3.4: 线程池

线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销 毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。

基本的线程池有:

(1)newFixedThreadPool(2)newCachedThreadPool (3)newScheduledThreadPool(4)newSingleThreadExecutor

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class Main {
    public static void main(String[] args) throws Exception {
        ExecutorService pool=Executors.newFixedThreadPool(10);
 
        for(int i=0;i<5;i++)
        {
            Runnable r=new Runnable() {
                @Override
                public void run() {
                    Thread thread=new Thread();
                    System.out.println(thread.getName());
                }
            };
            pool.execute(r);
        }
        pool.shutdown();
    }
}

https://blog.csdn.net/qing040513/article/details/111667627

线程池提交执行任务中execute()方法和submit()的区别?

接收的参数不同
根据前面列出来的方法签名,可以知道:
execute()方法只能接收实现Runnable接口类型的任务
submit()方法则既可以接收Runnable类型的任务,也可以接收Callable类型的任务。
Runnable和Callable的区别是 前者没有返回值,后者可以有返回值,于是这就又引出了下一个区别

返回值不同
execute()的返回值是void,线程提交后不能得到线程的返回值。
submit()的返回值是Future,通过Future的get()方法可以获取到线程执行的返回值,get()方法是同步的,执行get()方法时,如果线程还没执行完,会同步等待,直到线程执行完成。

虽然submit()方法可以提交Runnable类型的参数,但执行Future方法的get()时,线程执行完会返回null,不会有实际的返回值,这是因为Runable本来就没有返回值

2. wait,sleep,notify,notifyall,join,yield,interrupt

在这里插入图片描述

  1. 新建状态(New):新创建了一个线程对象。

  2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的 start()方法。 该状态的线程位于可运行线程池中,变得可运行,等待获取 CPU 的使用权。

  3. 运行状态(Running):就绪状态的线程获取了 CPU,执行程序代码。

  4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃 CPU 使用权,暂时停止 运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

    (一)、等待阻塞:运行的线程执行 wait()方法,JVM 会把该线程放入等待池中。(wait会释放持有的锁)

    (二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用, 则 JVM 会把该线程放入锁池中。

    (三)、其他阻塞:运行的线程执行 sleep()或 join()方法,或者发出了 I/O 请求时,JVM会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处 理完毕时,线程重新转入就绪状态。(注意,sleep 是不会释放持有的锁)

  5. 死亡状态(Dead):线程执行完了或者因异常退出了 run()方法,该线程结束生命周期。

2.1 sleep 与 wait 区别
  • sleep 是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用 sleep不会释放对象锁。 sleep()使当前线程进入阻塞状态,在指定时间内不会执行。
  • wait 是 Object 类的方法,对此对象调用 wait 方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或 notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。

区别比较:

1、这两个方法来自不同的类分别是 Thread 和 Object

2、最主要是 sleep 方法没有释放锁,而 wait 方法释放了锁,使得其他线程可以使用同步控制块或者方法。

3、wait,notify 和 notifyAll 只能在同步控制方法或者同步控制块里面使用,而 sleep 可以在任何地方使用(使用范围)

4、sleep 必须捕获异常,而 wait,notify 和 notifyAll 不需要捕获异常

(1) sleep方法属于Thread 类中方法,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可运行状态,不会马上进入运行状态,因为线程调度机制恢复线程 的运行也需要时间,一个线程对象调用了sleep方法之后,并不会释放他所持有的所有对象锁,所以也就不会影响其他进程对象的运行。但在 sleep 的过程中有可能被其他对象调用它的interrupt(),产生InterruptedException异常,如果你的程序不捕获这个异常,线程就会异常终止,进入TERMINATED状态,如果你的程序捕获了这个异常,那么程序就会继续执行catch语句块(可能还有 finally 语句块)以及以后的代码。

注意 sleep()方法是一个静态方法,也就是说他只对当前对象有效,通过 t.sleep()让 t对象进入 sleep,这样的做法是错误的,它只会是使当前线程被 sleep 而不是 t 线程

(2) wait 属于 Object 的成员方法,一旦一个对象调用了 wait 方法,必须要采用 notify()和 notifyAll()方法唤醒该进程;如果线程拥有某个或某些对象的同步锁,那么在调用了 wait()后,这个线程就会释放它持有的所有同步资源,而不限于这个被调用了 wait()方法的对象。wait()方法也同样会在 wait 的过程中有可能被其他对象调用 interrupt()方法而产生

2.2 notify,notifyall,join,yield

**yield()**方法是停止当前线程,让同等优先权的线程或更高优先级的线程有执行的机会。 如果没有的话,那么 yield()方法将不会起作用,并且由可执行状态后马上又被执行。

join() 方法是用于在某一个线程的执行过程中调用另一个线程执行,等到被调用的线程执行结束后,再继续执行当前线程。如:t.join();//主要用于等待 t 线程运行结束,若无此句,main 则会执行完毕,导致结果不可预测。

notify() 方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程 等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。

notifyAll() 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系 统的实现

2.3 interrupt

Thread.interrupt()方法: 作用是中断线程。将会设置该线程的中断状态位,即设置为true,中断的结果线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序本身。线程会不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为true)。它并不像stop方法那样会中断一个正在运行的线程

interrupt()方法只是改变中断状态,不会中断一个正在运行的线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出InterruptedException的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。这一方法实际完成的是,给受阻塞的线程发出一个中断信号,这样受阻线程检查到中断标识,就得以退出阻塞的状态。

更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,此时调用该线程的interrupt()方法,那么该线程将抛出一个 InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态。如果线程没有被阻塞,这时调用 interrupt()将不起作用,直到执行到wait(),sleep(),join()时,才马上会抛出 InterruptedException。

this.interrupted():测试当前线程是否已经中断(静态方法)。如果连续调用该方法,则第二次调用将返回false。在api文档中说明interrupted()方法具有清除状态的功能。执行后具有将状态标识清除为false的功能。

**this.isInterrupted()😗*测试线程是否已经中断,但是不能清除状态标识。

3. 锁

https://blog.csdn.net/qq_38905818/article/details/100701447?spm=1001.2014.3001.5501

锁是在不同线程竞争资源的情况下来分配不同线程执行方式的同步控制工具,只有线程获取到锁之后才能访问同步代码,否则等待其他线程使用结束后释放锁

synchronized

通常和wait,notify,notifyAll一块使用。
wait():释放占有的对象锁,释放CPU,进入等待队列只能通过notify/all继续该线程。
sleep():则是释放CPU,但是不释放占有的对象锁,可以在sleep结束后自动继续该线程。
notify():唤醒等待队列中的一个线程,使其获得锁进行访问。
notifyAll():唤醒等待队列中等待该对象锁的全部线程,让其竞争去获得锁。

lock

拥有synchronize相同的语义,但是添加一些其他特性,如中断锁等候和定时锁等候,所以可以使用lock代替synchronize,但必须手动加锁释放锁

3.1 synchronized和lock的区别

**性能:**资源竞争激烈的情况下,lock性能会比synchronized好;如果竞争资源不激烈,两者的性能是差不多的
**用法:**synchronized可以用在代码块上,方法上。lock通过代码实现,有更精确的线程语义,但需要手动释放,还提供了多样化的同步,比如公平锁、有时间限制的同步、可以被中断的同步
**原理:**synchronized在JVM级别实现,会在生成的字节码中加上monitorenter和monitorexit,任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。monitor是JVM的一个同步工具,synchronized还通过内存指令屏障来保证共享变量的可见性。lock使用AQS在代码级别实现,通过Unsafe.park调用操作系统内核进行阻塞
**功能:**比如ReentrantLock功能更强大

ReentantLock 继承接口 Lock 并实现了接口中定义的方法,他是一种可重入锁,除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。

ReentrantLock可以指定是公平锁还是非公平锁,而synchronized只能是非公平锁,所谓的公平锁就是先等待的线程先获得锁
ReentrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程
ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制

在这里插入图片描述

3.2 锁的种类

公平锁/非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁
ReentrantLock通过构造函数指定该锁是否是公平锁,默认是非公平锁。Synchronized是一种非公平锁

可重入锁
指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁
ReentrantLock, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock重新进入锁
Synchronized,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁

独享锁/共享锁
独享锁是指该锁一次只能被一个线程所持有,共享锁是指该锁可被多个线程所持有
ReentrantLock是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。Synchronized是独享锁

乐观锁/悲观锁
悲观锁在Java中的使用,就是各种锁
乐观锁在Java中的使用,是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新

在修改数据之前先锁定,再修改的方式被称之为悲观并发控制(Pessimistic Concurrency Control,缩写“PCC”,又名“悲观锁”)。之所以叫悲观锁,是因为这是一种对数据的修改持有悲观态度的并发控制方式。总是假设最坏的情况,每次读取数据的时候都默认其他线程会更改数据,因此需要进行加锁操作,当其他线程想要访问数据时,都需要阻塞挂起。

乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。

偏向锁/轻量级锁/重量级锁
针对Synchronized的锁状态:
偏向锁是为了减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。指一段同步代码一直被一个线程所访问,在无竞争情况下把整个同步都消除掉
轻量级锁是为了减少无实际竞争情况下,使用重量级锁产生的性能消耗。指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过CAS自旋的形式尝试获取锁,不会阻塞
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁

自旋锁/自适应自旋锁
指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU,默认自旋次数为10
自适应自旋锁的自旋次数不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定,是虚拟机对锁状况的一个预测

https://blog.csdn.net/qq_37113604/article/details/81582784

3.3 CAS(Compare and Swap )原理

乐观锁:

乐观锁( Optimistic Locking )在上文已经说过了,其实就是一种思想。相对悲观锁而言,乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。

上面提到的乐观锁的概念中其实已经阐述了它的具体实现细节:主要就是两个步骤:冲突检测和数据更新。其实现方式有一种比较典型的就是 Compare and Swap ( CAS )。

乐观锁的一种典型实现机制(CAS,compare and swap):

一个 CAS 涉及到以下操作: 我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。

比较 A 与 V 是否相等。(比较) 如果比较相等,将 B 写入 V。(交换) 返回操作是否成功。 当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。可见 CAS 其实是一个乐观锁。

在这里插入图片描述

乐观锁主要就是两个步骤:冲突检测和数据更新。当多个线程尝试使用CAS同时更新同一个变量时,只有一个线程可以更新变量的值,其他的线程都会失败,失败的线程并不会挂起,而是告知这次竞争中失败了,并可以再次尝试。

CAS操作包括三个操作数:需要读写的内存位置(V)、预期原值(A)、新值(B)。

if(如果内存位置与预期原值的A相匹配)
{
	那么将内存位置的值更新为新值B}
else  //如果内存位置与预期原值的值不匹配:
{
	那么处理器不会做任何操作。
}A;比较VA;一致:写B

无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS其实就是一个:我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。这其实和乐观锁的冲突检测+数据更新的原理是一样的。

乐观锁是一种思想,CAS只是这种思想的一种实现方式。

乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

3.3.1 ABA问题
public class ABADemo1 {
 
    public static AtomicReference<Integer> atomicReference=new AtomicReference<>(100);
 
    public static void main(String[] args) {
        System.out.println("------------ABA问题的产生----------------");
        new Thread(()->{
            //100改101再改回100
            atomicReference.compareAndSet(100,101);
            atomicReference.compareAndSet(101,100);
        },"t1").start();
 
        new Thread(()->{
            //暂停1s,保证上面的t1完成一次ABA操作
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) { 
                e.printStackTrace();
            }
            //只看结果值,不看过程
            System.out.println(atomicReference.compareAndSet(100,2019)+"---"+atomicReference.get());;
        },"t2").start();
    }
}

比如说一个线程1从内存位置V中取出A,这时候另一个线程2也从内存中取出A,并且线程2进行了一些操作将值变成了B,然后线程2又将V位置的数据 变成了A,这时候线程1进行CAS操作发现内存中仍然是A,然后线程1操作成功。只关注开始和结尾,不关心中间过程。

JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。

5. AQS(AbstractQueuedSynchronized)原理

https://www.cnblogs.com/mghio/p/13096392.html

AQS 基础数据结构
同步队列

队列同步器 AQS(下文简称为同步器)主要是依赖于内部的一个 FIFO(first-in-first-out)双向队列来对同步状态进行管理的,当线程获取同步状态失败时,同步器会将当前线程和当前等待状态等信息封装成一个内部定义的节点 Node,然后将其加入队列,同时阻塞当前线程;当同步状态释放时,会将同步队列中首节点唤醒,让其再次尝试去获取同步状态。同步队列的基本结构如下:

在这里插入图片描述

6.死锁

6.1关于死锁的基本概念

https://blog.csdn.net/guaiguaihenguai/article/details/80303835

死锁概念及产生原理

概念: 多个并发进程因争夺系统资源而产生相互等待的现象

原理: 当一组进程中的每个进程都在等待某个事件发生,而只有这组进程中的其他进程才能触发该事件,这就称这组进程发生了死锁。

本质原因:

​ 1)、系统资源有限。

​ 2)、进程推进顺序不合理。

死锁产生的4个必要条件(进程和资源)

  1. 互斥:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
  2. 占有且等待: 一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
  3. 不可抢占:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
  4. 循环等待:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。

当以上四个条件均满足,必然会造成死锁,发生死锁的进程无法进行下去,它们所持有的资源也无法释放。这样会导致CPU的吞吐量下降。所以死锁情况是会浪费系统资源和影响计算机的使用性能的。那么,解决死锁问题就是相当有必要的了。

避免死锁的方法

1、死锁预防 ----- 确保系统永远不会进入死锁状态

产生死锁需要四个条件,那么,只要这四个条件中至少有一个条件得不到满足,就不可能发生死锁了。由于互斥条件是非共享资源所必须的,不仅不能改变,还应加以保证,所以,主要是破坏产生死锁的其他三个条件。

a、破坏“占有且等待”条件

方法1:所有的进程在开始运行之前,必须一次性地申请其在整个运行过程中所需要的全部资源。

优点:简单易实施且安全。

缺点:因为某项资源不满足,进程无法启动,而其他已经满足了的资源也不会得到利用,严重降低了资源的利用率,造成资源浪费。使进程经常发生饥饿现象。

方法2:该方法是对第一种方法的改进,允许进程只获得运行初期需要的资源,便开始运行,在运行过程中逐步释放掉分配到的已经使用完毕的资源,然后再去请求新的资源。这样的话,资源的利用率会得到提高,也会减少进程的饥饿问题。

b、破坏“不可抢占”条件

当一个已经持有了一些资源的进程在提出新的资源请求没有得到满足时,它必须释放已经保持的所有资源,待以后需要使用的时候再重新申请。这就意味着进程已占有的资源会被短暂地释放或者说是被抢占了。

该种方法实现起来比较复杂,且代价也比较大。释放已经保持的资源很有可能会导致进程之前的工作实效等,反复的申请和释放资源会导致进程的执行被无限的推迟,这不仅会延长进程的周转周期,还会影响系统的吞吐量。

c、破坏“循环等待”条件

可以通过定义资源类型的线性顺序来预防,可将每个资源编号,当一个进程占有编号为i的资源时,那么它下一次申请资源只能申请编号大于i的资源。如图所示:

这样虽然避免了循环等待,但是这种方法是比较低效的,资源的执行速度回变慢,并且可能在没有必要的情况下拒绝资源的访问,比如说,进程c想要申请资源1,如果资源1并没有被其他进程占有,此时将它分配个进程c是没有问题的,但是为了避免产生循环等待,该申请会被拒绝,这样就降低了资源的利用率

2、避免死锁 ----- 在使用前进行判断,只允许不会产生死锁的进程申请资源

的死锁避免是利用额外的检验信息,在分配资源时判断是否会出现死锁,只在不会出现死锁的情况下才分配资源。

两种避免办法:

1、如果一个进程的请求会导致死锁,则不启动该进程

2、如果一个进程的增加资源请求会导致死锁 ,则拒绝该申请。

避免死锁的具体实现通常利用银行家算法

银行家算法

7.线程不安全原因

一、什么是线程不安全和线程安全

定义:线程不安全:多线程并发执行某个代码时,产生了逻辑上的错误,结果和预期值不相同

线程安全是指多线程执行时没有产生逻辑错误,结果和预期值相同

二、线程不安全产生的原因

线程是抢占执行的。
有的操作不是原子的。当 cpu 执行一个线程过程时,调度器可能调走CPU,去执行另一个线程,此线程的操作可能还没有结束;(通过锁来解决)
多个线程尝试修改同一个变量(一对一修改、多对一读取、多对不同变量修改,是安全的)
内存可变性
指令重排序:java的编译器在编译代码时,会针对指令进行优化,调整指令的先后顺序,保证原有逻辑不变的情况下,来提高程序的运行效率。

三、怎样解决线程不安全

加锁
锁的特点:互斥的,同一时刻只有一个线程可以 获取到锁,其他线程如果尝试获取锁,就会发生阻塞等待,等到刚那个线程 释放锁 ,此时剩下的线程再重新竞争锁

基本操作:加锁,解锁(释放锁)

使用锁 —— 关键字 synchronized

  • 加到普通方法前:表示锁 this;
  • 加到静态方法前:表示锁当前类的类对象(类对象就是 JVM运行时,将 .class 文件加载到内存中获取到的(类加载));
  • 对象名 . getClass() --》 获取类对象加到某个代码之前,显示指定给某个对象加锁;如果两个对象有两把锁,各自锁各自的,就不会涉及冲突 / 互斥,但是,一旦锁里边的内容出现问题,就可能出现死锁,其他程序就只能一直等待,此时就得重启

8. CyclicBarrier和CountDownLatch区别

CountDownLatchCyclicBarrier
减计数方式加计数方式
计算为0时释放所有等待的线程计数达到指定值时释放所有等待线程
计数为0时,无法重置计数达到指定值时,计数置为0重新开始
调用countDown()方法计数减一,调用await()方法只进行阻塞,对计数没任何影响调用await()方法计数加1,若加1后的值不等于构造方法的值,则线程阻塞
不可重复利用可重复利用

https://blog.csdn.net/tolcf/article/details/50925145

n. Java多线程面试题

这篇文章主要是对多线程的问题进行总结的,因此罗列了40个多线程的问题。

这些多线程的问题,有些来源于各大网站、有些来源于自己的思考。可能有些问题网上有、可能有些问题对应的答案也有、也可能有些各位网友也都看过,但是本文写作的重心就是所有的问题都会按照自己的理解回答一遍,不会去看网上的答案,因此可能有些问题讲的不对,能指正的希望大家不吝指教。

1、多线程有什么用?

一个可能在很多人看来很扯淡的一个问题:我会用多线程就好了,还管它有什么用?在我看来,这个回答更扯淡。所谓"知其然知其所以然",“会用"只是"知其然”,“为什么用"才是"知其所以然”,只有达到"知其然知其所以然"的程度才可以说是把一个知识点运用自如。OK,下面说说我对这个问题的看法:

1)发挥多核CPU的优势

随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4核、8核甚至16核的也都不少见,如果是单线程的程序,那么在双核CPU上就浪费了50%,在4核CPU上就浪费了75%。单核CPU上所谓的"多线程"那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程"同时"运行罢了。多核CPU上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的。

2)防止阻塞

从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核CPU我们还是要应用多线程,就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。

3)便于建模

这是另外一个没有这么明显的优点了。假设有一个大的任务A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务A分解成几个小任务,任务B、任务C、任务D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。

2、创建线程的方式

比较常见的一个问题了,一般就是两种:

1)继承Thread类

2)实现Runnable接口

至于哪个好,不用说肯定是后者好,因为实现接口的方式比继承类的方式更灵活,也能减少程序之间的耦合度,面向接口编程也是设计模式6大原则的核心。

3、start()方法和run()方法的区别

只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。如果只是调用run()方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其run()方法里面的代码。

4、Runnable接口和Callable接口的区别

有点深的问题了,也看出一个Java程序员学习知识的广度。

Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。

这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。而Callable+Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,真的是非常有用。

5、CyclicBarrier和CountDownLatch的区别

两个看上去有点像的类,都在java.util.concurrent下,都可以用来表示代码运行到某个点上,二者的区别在于:

1)CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行。

2)CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务。

  1. CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了。

6、volatile关键字的作用

一个非常重要的问题,是每个学习、应用多线程的Java程序员都必须掌握的。理解volatile关键字的作用的前提是要理解Java内存模型,这里就不讲Java内存模型了,可以参见第31点,volatile关键字的作用主要有两个:

1)多线程主要围绕可见性和原子性两个特性而展开,使用volatile关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到volatile变量,一定是最新的数据。

2)代码底层执行不像我们看到的高级语言----Java程序这么简单,它的执行是Java代码–>字节码–>根据字节码执行对应的C/C++代码–>C/C++代码被编译成汇编语言–>和硬件电路交互,现实中,为了获取更好的性能JVM可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用volatile则会对禁止语义重排序,当然这也一定程度上降低了代码执行效率。

从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性,详细的可以参见java.util.concurrent.atomic包下的类,比如AtomicInteger,更多详情请点击这里进行学习。

7、什么是线程安全

又是一个理论的问题,各式各样的答案有很多,我给出一个个人认为解释地最好的:如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的

这个问题有值得一提的地方,就是线程安全也是有几个级别的:

1)不可变

像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用

2)绝对线程安全

不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说Extraterritoriality、CopyOnWriteArraySet

3)相对线程安全

相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制

4)线程非安全

这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程非安全的类,点击这里了解为什么不安全。

8、Java中如何获取到线程dump文件

死循环、死锁、阻塞、页面打开慢等问题,打线程dump是最好的解决问题的途径。所谓线程dump也就是线程堆栈,获取到线程堆栈有两步:

1)获取到线程的pid,可以通过使用jps命令,在Linux环境下还可以使用ps -ef | grep java

2)打印线程堆栈,可以通过使用jstack pid命令,在Linux环境下还可以使用kill -3 pid

另外提一点,Thread类提供了一个getStackTrace()方法也可以用于获取线程堆栈。这是一个实例方法,因此此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈。

9、一个线程如果出现了运行时异常会怎么样

如果这个异常没有被捕获的话,这个线程就停止执行了。另外重要的一点是:如果这个线程持有某个某个对象的监视器,那么这个对象监视器会被立即释放

10、如何在两个线程之间共享数据

通过在线程之间共享对象就可以了,然后通过wait/notify/notifyAll、await/signal/signalAll进行唤起和等待,比方说阻塞队列BlockingQueue就是为线程之间共享数据而设计的

11、sleep方法和wait方法有什么区别

这个问题常问,sleep方法和wait方法都可以用来放弃CPU一定的时间,不同点在于如果线程持有某个对象的监视器,sleep方法不会放弃这个对象的监视器,wait方法会放弃这个对象的监视器

12、生产者消费者模型的作用是什么

这个问题很理论,但是很重要:

1)通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用

2)解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要收到相互的制约

13、ThreadLocal有什么用

简单说ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了

14、为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用

这是JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁

15、wait()方法和notify()/notifyAll()方法在放弃对象监视器时有什么区别

wait()方法和notify()/notifyAll()方法在放弃对象监视器的时候的区别在于:wait()方法立即释放对象监视器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器

16、为什么要使用线程池

避免频繁地创建和销毁线程,达到线程对象的重用。另外,使用线程池还可以根据项目灵活地控制并发的数目。点击这里学习线程池详解。

17、怎么唤醒一个阻塞的线程

如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。

18、不可变对象对多线程有什么帮助

前面有提到过的一个问题,不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。

19、什么是多线程的上下文切换

多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。

20、线程类的构造方法、静态块是被哪个线程调用的

这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。

如果说上面的说法让你感到困惑,那么我举个例子,假设Thread2中new了Thread1,main函数中new了Thread2,那么:

1)Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的

2)Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的

21、高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?

这是我在并发编程网上看到的一个问题,把这个问题放在最后一个,希望每个人都能看到并且思考一下,因为这个问题非常好、非常实际、非常专业。关于这个问题,个人看法是:

1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换

2)并发不高、任务执行时间长的业务要区分开看:

a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务

b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换

常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。

如果说上面的说法让你感到困惑,那么我举个例子,假设Thread2中new了Thread1,main函数中new了Thread2,那么:

1)Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的

2)Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的

21、高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?

这是我在并发编程网上看到的一个问题,把这个问题放在最后一个,希望每个人都能看到并且思考一下,因为这个问题非常好、非常实际、非常专业。关于这个问题,个人看法是:

1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换

2)并发不高、任务执行时间长的业务要区分开看:

a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务

b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换

c)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考其他有关线程池的文章。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值