java如何开启线程?

在 Java 中,开启线程主要有以下两种方式:

  • 继承 Thread 类
    • 定义一个类继承自 Thread 类,并重写 run 方法。run 方法里存放的是该线程要执行的任务代码。例如:

收起

java

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程执行的代码");
    }
}

  • 在主线程或者其他地方创建该类的实例,然后调用 start 方法启动线程:

收起

java

public class Main {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

  • 实现 Runnable 接口
    • 创建一个类实现 Runnable 接口,实现接口中的 run 方法,放入线程要执行的任务逻辑:

收起

java

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("通过实现Runnable接口执行的线程代码");
    }
}

  • 在主线程里,先创建这个实现类的实例,再将它作为参数传入 Thread 类的构造器创建一个 Thread 对象,最后调用 Thread 对象的 start 方法开启线程:

收起

java

public class Main {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}

两种方式各有优劣,实现 Runnable 接口避免了单继承局限,更适合 Java 的类继承体系设计,使用场景更为广泛。

怎么保证线程安全?

1 方法及同步。synchronized关键字

2 代码块级别同步 ReentrantLock

3 使用原子类(Atomic Classes)

4 使用线程安全的集合类

  • 有时候,我们希望每个线程都有自己独立的变量副本,而不是共享一个变量。这时可以使用Thread - Local

Volatile和Synchronized有什么区别?

  • Volatile:主要用于确保变量的可见性。它保证了一个线程对变量的修改能及时被其他线程看到。当一个变量被volatile修饰后,每次访问这个变量时,线程都会从主存中读取最新的值,而不是使用线程本地缓存中的值。这在多线程环境下,对于一些状态标记变量等简单场景非常有用。
  • Synchronized:它的功能更加强大,主要用于保证在同一时刻,只有一个线程能够访问被它修饰的方法或者代码块。这不仅保证了变量的可见性,还保证了操作的原子性,即一组操作要么全部执行成功,要么全部不执行,避免了多线程操作共享资源时可能出现的竞态条件。

Volatile能不能保证线程安全?

不能 volatile关键字主要用于确保变量的可见性。在多线程环境中,当一个线程修改了被volatile修饰的变量的值时,这个新值会立即被刷新到主存中,并且其他线程访问这个变量时会强制从主存中重新读取,而不是使用线程自己缓存中的旧值。

DCL(Double check lock)单例为什么要加Volatile?

  • 在 DCL 单例模式中,如果没有volatile关键字,可能会出现指令重排问题。在 Java 中,为了提高程序的执行效率,编译器和处理器可能会对指令进行重排序。
  • 当在单例变量前添加volatile关键字后,它会禁止指令重排。volatile关键字通过内存屏障(Memory Barrier)来保证有序性。

JAVA线程锁机制是怎样的?

Java 的线程锁机制用于协调多个线程对共享资源的访问,确保在同一时刻,只有一个线程能操作特定资源,避免数据竞争与不一致问题,主要包含以下几部分:

  • 内置锁(synchronized 关键字)
    • 对象锁:使用 synchronized 修饰实例方法时,锁关联到当前对象实例。例如:

收起

java

public class MyClass {
    public synchronized void method() {
        // 访问共享资源
    }
}

当一个线程进入 method 方法,会获取该对象的锁 ,其他线程再调用此方法就得等待锁释放。

  • 类锁:用 synchronized 修饰静态方法,锁关联的是类对象。像下面这样:

收起

java

public class MyClass {
    public static synchronized void staticMethod() {
        // 访问共享资源
    }
}

所有该类实例共享这把锁,不管创建多少个对象,类锁同一时间只允许一个线程访问静态方法。

  • 显式锁(Lock 接口)
    • Java 提供了 java.util.concurrent.locks 包,其中 Lock 接口是显式锁的基础。常用的实现类是 ReentrantLock,它支持重入,意味着持有锁的线程可以多次获取同一把锁而不造成死锁。示例如下:

收起

java

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyLockExample {
    private static final Lock lock = new ReentrantLock();
    public static void main(String[] args) {
        lock.lock();
        try {
            // 操作共享资源
        } finally {
            lock.unlock();
        }
    }
}

解锁操作要放在 finally 块,保证锁一定被释放,避免死锁。

  • 读写锁(ReadWriteLock)
    • 用于区分读操作和写操作的锁。读锁允许多个线程同时持有,因为读操作不修改共享资源;写锁则是排他的,同一时间只能有一个线程持有写锁。ReentrantReadWriteLock 是其常用实现:

收起

java

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {
    private static final ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        rwLock.readLock().lock();
        try {
            // 多个线程可并发读
        } finally {
            rwLock.readLock().unlock();
        }

        rwLock.writeLock().lock();
        try {
            // 独占写操作
        } finally {
            rwLock.writeLock().unlock();
        }
    }
}

偏向锁、轻量级锁、重量级锁有什么区别?锁机制是如何升级的?

在 Java 的线程锁机制里,偏向锁、轻量级锁、重量级锁存在诸多区别,且锁会依据线程竞争状况逐步升级:

区别

  • 偏向锁
    • 获取开销:偏向锁的获取几乎没有开销,因为它假设只有一个线程会频繁访问同步块。当线程首次进入同步块,虚拟机会把锁对象头的部分标记设置成偏向该线程,后续此线程再进入,无需额外同步操作。
    • 适用场景:非常适合单线程访问同步块占多数的场景,像一些初始化操作代码块,只有单线程会多次进出。
    • 竞争处理:一旦有第二个线程尝试获取偏向锁,就会触发偏向锁的撤销,进入轻量级锁竞争流程。
  • 轻量级锁
    • 获取开销:稍大于偏向锁,基于 CAS(Compare and Swap)操作尝试将锁对象头的指针指向线程栈帧中的锁记录。成功则获取锁,失败意味着有竞争。
    • 适用场景:适用于线程交替执行同步块的场景,竞争不激烈。比如两个线程轮流访问某个共享资源,很少出现同时争抢的情况。
    • 竞争处理:若多个线程频繁竞争轻量级锁,CAS 操作不断失败,会膨胀为重量级锁。
  • 重量级锁
    • 获取开销:开销最大,依赖操作系统的互斥锁(mutex)来实现,涉及线程的阻塞与唤醒,需要从用户态切换到内核态,消耗大量系统资源。
    • 适用场景:用于高竞争环境,多线程频繁争用同一把锁,确保同步的严格性。
    • 竞争处理:等待锁的线程会被阻塞挂起,直到持有锁的线程释放锁,然后由操作系统唤醒等待队列中的线程去竞争。

锁升级流程

  • 起初,对象没有被锁定时,处于无锁状态。当一个线程访问同步块,虚拟机会先尝试给对象加上偏向锁,将对象头标记为偏向该线程。
  • 随着第二个线程也要访问该同步块,偏向锁无法满足需求,偏向锁被撤销,升级为轻量级锁,多个线程通过 CAS 操作竞争锁。
  • 要是竞争进一步加剧,CAS 频繁失败,轻量级锁会膨胀为重量级锁 ,后续线程争用就得依赖操作系统调度,进入阻塞、唤醒流程。整个升级过程是 Java 虚拟机自动完成的,目的是依据不同竞争程度,适配最经济、高效的锁机制。

谈谈你对AQS的理解。

AQS 是 Java 并发包(java.util.concurrent)中的一个基础框架,是构建锁和同步器的基础。它提供了一个基于 FIFO(先进先出)队列的同步框架,用于实现各种锁和同步器,如

ReentrantLock、

CountDownLatch、

Semaphore等都是基于 AQS 实现的

核心数据结构

同步队列

(state)变量

AQS 采用了模板方法模式,它定义了一系列抽象方法和模板方法

AQS如何实现可重入锁?

  • AQS(AbstractQueuedSynchronizer)通过一个int类型的变量state来记录锁的状态。对于可重入锁而言,这个state变量具有重要的作用。当state为 0 时,表示锁没有被占用;当state大于 0 时,表示锁已经被占用,并且state的值表示锁被重入的次数。

有A,B,C三个线程,如何保证三个线程同时执行?

使用 CountDownLatch 实现

CountDownLatch 原理:CountDownLatch 是一个同步辅助类,它允许一个或多个线程等待其他线程完成操作。它内部维护一个计数器,通过

countDown方法来递减计数器的值,

await方法会使当前线程等待,直到计数器的值为 0

  • 在每个线程(A、B、C)的任务执行完毕后,调用latch.countDown()来递减计数器的值。
  • 在主线程中,调用latch.await(),这会使主线程阻塞,直到计数器的值为 0,也就是三个线程都执行完毕

使用 CyclicBarrier 实现

  • CyclicBarrier 原理:CyclicBarrier 是一个同步辅助类,它允许一组线程互相等待,直到所有线程都到达一个公共的屏障点(barrier point)。当最后一个线程到达屏障点后,所有等待的线程会被释放,然后可以继续执行后续的任务。

如何在并发情况下保证三个线程依次执行?

方式一:使用 ReentrantLock 和 Condition 实现线程顺序执行

  • 原理介绍:Semaphore是一个计数信号量,它可以控制同时访问某个资源的线程数量。通过合理地初始化信号量的值和使用acquire(获取信号量)与release(释放信号量)方法,可以实现线程的顺序执行。

方式二:使用 Semaphore 实现线程顺序执行

  • 原理介绍:Semaphore是一个计数信号量,它可以控制同时访问某个资源的线程数量。通过合理地初始化信号量的值和使用acquire(获取信号量)与release(释放信号量)方法,可以实现线程的顺序执行。

-如何保证三个线程有序交错进行?

使用 Semaphore 实现三个线程有序交错进行

  • Semaphore 原理:Semaphore 是一个计数信号量,用于控制同时访问某个特定资源的线程数量。它维护了一个许可证的计数,线程通过acquire方法获取许可证,通过release方法释放许可证。当许可证计数为 0 时,获取许可证的线程会被阻塞。

使用 BlockingQueue 实现三个线程有序交错进行

  • BlockingQueue 原理:BlockingQueue 是一个阻塞队列,它在队列为空时获取元素的操作会被阻塞,在队列已满时添加元素的操作也会被阻塞。可以利用这个特性来控制线程的执行顺序。

如何对一个字符串快速进行排序?

  • 在面对字符串排序问题时,首先要考虑数据规模、稳定性要求和时间复杂度等因素。如果数据规模较小,简单的排序算法如冒泡排序、插入排序可能就足够了,它们实现简单,易于理解。但如果数据规模较大,更高效的排序算法如快速排序、归并排序或者 Java 自带的Arrays.sort()(其底层实现基于双轴快速排序和归并排序的混合排序)会是更好的选择。

冒泡排序

  • 原理:将数组分成两半,分别对这两半进行排序,然后将排序好的两部分合并起来。

插入排序:

  • 原理:将待排序的元素插入到已经有序的部分合适位置。对于字符串数组,每次将一个字符串插入到前面已经排序好的字符串序列中合适的位置。

快速排序:

  • 原理:选择一个基准元素,将数组分为两部分,小于基准的放在左边,大于基准的放在右边,然后对这两部分递归地进行排序。

归并排序:

  • 原理:将数组分成两半,分别对这两半进行排序,然后将排序好的两部分合并起来。

昨天是历史。明天是谜团。只有今天是天赐的礼物。