提升你的技能:Java并发编程的关键误区与应对策略,你掌握了吗?

举报
忆愿 发表于 2025/02/13 12:37:55 2025/02/13
【摘要】 文章目录1 描述线程的不同状态以及何时发生状态转换 ?2 原子操作有哪些 ?3 如果两个线程同时在不同的对象实例上调用 synchronized 方法,这些线程中的一个是否会阻塞?如果该方法是静态的,该怎么办?4 描述死锁,存活锁和饥饿的条件。描述这些情况的可能原因 ?1 描述线程的不同状态以及何时发生状态转换 ?线程的生命周期比作一个人的工作日:新建状态:就像一个刚入职的新人,还没开始工作...

1 描述线程的不同状态以及何时发生状态转换 ?

线程的生命周期比作一个人的工作日:

  1. 新建状态:就像一个刚入职的新人,还没开始工作,只是准备好了。
  2. 就绪状态:像是新人准备好开始工作,但还在等待上司分配任务。
  3. 运行状态:新人开始执行工作任务,忙碌地处理各种事情。
  4. 阻塞状态:新人遇到需要等待的事情,比如等待部门的批准或者设备修理,因此暂时无法工作。
  5. 终止状态:新人完成了所有工作,离开了办公室,不再继续工作。

线程在执行过程中会经历几个不同的状态。理解这些状态和转换过程有助于掌握多线程编程。

“新建”状态

线程在开始时是“新建”状态,意味着它已经创建但还未开始执行,使用 new Thread() 创建一个线程对象,此时线程处于新建状态。

调用 start() 方法后,线程进入“就绪”状态。这时,线程已准备好执行,但系统还没有将它调度到 CPU 上。

“就绪”状态

在“就绪”状态的线程等待 CPU 的调度。如果系统决定执行这个线程,它会被转移到“运行”状态。

线程在运行状态下执行其任务。 thread.start() 调用后,线程开始执行 run() 方法中的代码,这时候线程处于运行状态。

“阻塞”状态

线程在运行中可能会因为等待资源或条件的变化而进入“阻塞”状态。这通常发生在 wait()、sleep() 或 join() 方法被调用时。

wait() 使线程在某个条件满足之前等待,sleep() 让线程暂停执行一段时间,而 join() 使当前线程等待另一个线程完成。这些操作会导致线程暂时挂起,直到条件满足或时间到达后才会恢复执行。

“等待”状态

线程也可能因为获取锁失败或被其他线程打断而进入“等待”状态。调用 synchronized 块中的代码会使线程争夺对象的锁,如果锁被其他线程占用,当前线程会进入等待状态,直到锁可用为止。

“终止”状态

线程执行完毕后,进入“终止”状态。此时,线程的 run() 方法已经完成执行,线程将不再运行,线程运行完 run() 方法后会自动进入终止状态,无法再次被启动。

状态转换图

在这里插入图片描述

状态转换代码Demo:

public class ThreadStateDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            try {
                // 线程在这里进入阻塞状态
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            // 线程继续执行并最终进入终止状态
            System.out.println("Thread is finished");
        });

        // 线程刚创建时处于新建状态
        System.out.println("State after creation: " + thread.getState());

        // 启动线程,将线程转移到就绪状态
        thread.start();

        // 线程启动后进入就绪状态,等待 CPU 调度
        System.out.println("State after start: " + thread.getState());

        try {
            // 等待线程完成,这样主线程能准确检测线程状态
            thread.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        // 线程执行完毕后进入终止状态
        System.out.println("State after completion: " + thread.getState());
    }
}

2 原子操作有哪些 ?

原子操作指的是不可中断的操作,这些操作在单个步骤中完成,对外界来说看起来是瞬间完成的,不会看到中间的任何状态。

使用原子操作类可以减少使用synchronized的需要,从而在某些情况下提高程序的性能。就像瞬间完成的魔法动作,要么完全发生,要么完全不发生。

原子操作的类型:

  1. 原子变量类(java.util.concurrent.atomic包):
    • AtomicInteger、AtomicLong、AtomicBoolean:提供对整型、长整型和布尔型变量的原子操作。
    • AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray:提供对整型数组、长整型数组和其他引用类型数组的原子操作。
    • AtomicReference、AtomicStampedReference、AtomicMarkableReference:提供对引用类型变量的原子操作,后者还提供了版本号或者标记以解决ABA问题。
    • AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater:基于反射的实用工具,可以对指定类的指定volatile字段进行原子更新。
  2. 原子操作方法:
    • compareAndSet(expectedValue, newValue):如果当前值等于预期值,则以原子方式将同步状态设置为给定的更新值。
    • weakCompareAndSet:与compareAndSet类似,但是对处理器的内存模型来说是一个弱操作。
    • getAndSet(newValue):以原子方式设置为给定值,并返回旧值。
    • getAndIncrement、getAndDecrement、getAndAdd:以原子方式增加或减少,并返回旧值。
    • incrementAndGet、decrementAndGet、addAndGet:以原子方式增加或减少,并返回新值。
  3. 锁和同步块:
    • synchronized关键字:在同步块内,对共享资源的操作可以看作是原子的。
    • ReentrantLock:提供了lock()和unlock()方法,确保在锁定的代码块中的操作是原子的。
  4. 线程局部变量(ThreadLocal):
    • ThreadLocal类:虽然不是原子类,但它为每个使用该变量的线程提供了一个独立的变量副本,从而避免了同步的需要。

3 如果两个线程同时在不同的对象实例上调用 synchronized 方法,这些线程中的一个是否会阻塞?如果该方法是静态的,该怎么办?

如果两个线程在不同的对象实例上调用synchronized方法,这些线程不会互相阻塞。

因为synchronized方法锁定的是调用该方法的对象实例,每个对象实例都有自己的监视器锁(monitor lock),所以不同实例上的同步方法就像各自独立的房间,各自有各自的锁,线程们在自己的房间里活动,互不干扰。

就好比两个工人各自进入不同的仓库工作,他们各自有各自仓库的钥匙,因此可以在自己的仓库里自由工作,不会因为对方而阻塞。

如果该方法是静态的,情况就不同了。

静态的synchronized方法锁定的是类的Class对象,而不是类的任何实例。这意味着,即使有两个不同的实例,只要它们调用的是同一个类的同一个静态同步方法,这些线程也会互相阻塞,因为它们需要争用同一把锁——类的Class对象的锁。

静态同步方法就像是公司大楼的唯一会议室,所有工人如果想进入这个会议室讨论就必须排队,一次只能有一个工人(线程)使用会议室(锁),其他的工人必须等待会议室空闲。

方法示例Demo:

class Counter {
    public void increment() {
        synchronized (this) {
            // 一些操作
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Counter counter1 = new Counter();
        Counter counter2 = new Counter();

        // thread1 和 thread2 分别在不同的 Counter 实例上调用 increment方法
        Thread thread1 = new Thread(() -> {
            counter1.increment();
        });

        Thread thread2 = new Thread(() -> {
            counter2.increment();
        });

        thread1.start();
        thread2.start();
    }
}

静态同步方法示例Demo:

class Counter {
    public static synchronized void increment() {
        // 一些操作
    }
}

public class Main {
    public static void main(String[] args) {
        Thread thread1 = new Thread(Counter::increment);
        Thread thread2 = new Thread(Counter::increment);

        thread1.start();
        thread2.start();
    }
}

4 描述死锁,存活锁和饥饿的条件。描述这些情况的可能原因 ?

死锁:

想象两个人在狭窄的走廊相遇,一个人拿着左边的钥匙,另一个人拿着右边的钥匙,他们都想通过对方,但都不愿意放下手中的钥匙。

条件:资源像独木桥,一次只能一个人通过。每个人手上都有别人需要的“钥匙”,没有人愿意主动让路,两个人形成了一个圈,互相等待对方让路。

可能原因:

  • 资源分配不当,就像厨房里只有一个锅,大家都要炒菜。
  • 线程获取资源的顺序不一致,就像两个人同时往相反方向走,总在走廊中间相遇。
  • 没有释放资源的机制,就像有人借了东西不还。

存活锁:

想象两只蚂蚁在一条直线上相遇,它们总是试图绕过对方,但每次都选择了相同的方向,结果就是它们不停地原地绕圈。

条件:线程像两只蚂蚁,一直在尝试前进,尽管它们在动,但始终没有真正的前进。

可能原因:线程总是重复相同的操作,就像两只蚂蚁总是向同一个方向躲避。

饥饿:

想象一个餐厅里,服务员总是先服务坐在窗边的客人,而角落里的客人即使举手示意,也总是被忽略。

条件:线程像角落里的客人,有能力(可运行状态),但总是得不到服务(CPU时间)。

可能原因:

  • 调度器不公平,就像服务员总是忽略某些客人。
  • 某些线程占用资源太长时间,就像某些客人霸占着餐桌不走。
  • 线程在等待队列中的位置太靠后,就像坐在餐厅最里面的客人,服务员总是最后才注意到他们。

1024 程序员节快乐

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。