Java多线程1(补档)


什么是线程和进程?从Java方面讲一下

进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。

在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。

线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。多个线程共享进程的方法区资源,但每个线程有自己的程序计数器虚拟机栈本地方法栈

基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。

创建线程的方式有哪些

  1. 继承 Thread 类创建线程;
  2. 实现 Runnable 接口创建线程;
  3. 通过 Callable 接口并利用 Future 创建线程;
  4. 通过线程池创建线程。

Runnable 和 Callable 有什么区别

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

说说线程的生命周期和状态

Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:

  • NEW: 初始状态,线程被创建出来但没有被调用 start()
  • RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。
  • BLOCKED:阻塞状态,需要等待锁释放。
  • WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
  • TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
  • TERMINATED:终止状态,表示该线程已经运行完毕。

什么是线程上下文切换

线程在执行过程中会有自己的运行条件和状态(也称上下文),比如线程私有的程序计数器,栈信息等。

线程切换时需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换

什么是线程死锁?写一个死锁

线程死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

public class DeadLockDemo {
    private static Object resource1 = new Object();//资源 1
    private static Object resource2 = new Object();//资源 2

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 1").start();

        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "线程 2").start();
    }
}

Output

Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1

实现线程同步的方法有哪些

  1. 使用 Synchronized 关键字;
  2. wait 和 notify 等待和通知;
  3. 使用特殊域变量 volatile 实现线程同步;
  4. 使用ReentrantLock实现线程同步;
  5. 使用阻塞队列实现线程同步;
  6. 使用信号量 Semaphore。
  7. 原子变量

线程之间通信的方法有哪些

  1. 线程之间采用synchronized:可以利用**wait()、notify()、notifyAll()**来实现线程通信。
  2. 线程之间采用Lock:可以利用**await()、signal()、signalAll()**来实现线程通信。
  3. BlockingQueue:程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。

sleep() 方法和 wait() 方法对比

共同点:两者都可以暂停线程的执行。

区别

  • 锁的释放sleep() 方法没有释放锁,而 wait() 方法释放了锁 。
  • 使用场景:sleep() 方法可以在任何地方使用,而 wait() 方法则只能在同步方法或同步块中使用;
  • 功能wait() 通常被用于线程间交互/通信,sleep()通常被用于暂停执行。
  • 自动苏醒wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。sleep()方法执行完成后,线程会自动苏醒,或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。
  • 所属类sleep()Thread 类的静态本地方法,wait() 则是 Object 类的本地方法。

如何优雅的停止一个线程

interrupt 停止线程

其核心就是通过调用线程的 isInterrupt() 方法进而判断信号,当线程检测到为 true 时则说明接收到终止信号,此时我们需要做相应的处理来退出线程的运行,比如break循环。

如果 sleep、wait等可以让线程进入阻塞的方法使线程休眠了,而处于休眠中的线程被中断,那么线程是可以感受到中断信号的,并且会抛出一个 InterruptedException异常,同时清除中断信号,将中断标记位设置成 false。

用 volatile 标记位的停止方法

关于 volatile 作为标记位的核心就是他的可见性特性,可以while(mark)作为线程中循环的条件

线程并发会带来哪些问题

  1. 产生不同结果:当多个线程同时访问共享资源,并尝试同时修改该资源时,由于执行顺序的不确定性,可能导致结果依赖于线程执行的具体顺序,从而产生意想不到的结果。
  2. 死锁:当线程之间存在循环依赖的资源请求关系时,可能导致死锁,使得所有线程都无法继续执行。
  3. 活锁:活锁类似于死锁,但不同之处在于线程不是阻塞,而是一直响应其他线程,导致它们无法继续工作。比如两线程一直同时申请一个资源,一直失败
  4. 资源竞争:如果资源分配和释放策略不当,可能会导致某些线程长时间无法获得资源。线程饥饿
  5. 数据不一致:当多个线程同时访问和修改共享数据时,由于缺乏适当的同步机制,可能导致数据的不一致性。例如,一个线程正在修改数据时,另一个线程可能读取到中间状态的数据。
  6. 上下文切换开销:由于线程切换的开销较大,多线程并发执行可能会导致频繁的上下文切换,消耗额外的CPU时间和系统资源。

可以直接调用 Thread 类的 run 方法吗?

可以

但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

JUC了解吗

JUC是java.util.concurrent的缩写,是Java提供的并发包,包含了我们在并发编程时用到的一些工具

  1. 原子更新:Java从JDK1.5开始提供了java.util.concurrent.atomic包,方便程序员在多线程环境下,无锁的进行原子操作。

  2. 锁和条件变量:java.util.concurrent.locks包下包含了同步器的框架 AbstractQueuedSynchronizer,基于AQS构建的Lock以及与Lock配合可以实现等待/通知模式的Condition。

  3. 线程池:涉及到的类比如:Executor、Executors、ThreadPoolExector、 AbstractExecutorService、Future、Callable、ScheduledThreadPoolExecutor等等。

  4. 阻塞队列:涉及到的类比如:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、LinkedBlockingDeque等等。

  5. 并发容器:涉及到的类比如:ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue、BlockingQueue等等。

  6. 同步器:剩下的是一些在并发编程中时常会用到的工具类,主要用来协助线程同步。比如:CountDownLatch、CyclicBarrier、Exchanger、Semaphore、FutureTask等等。

并发编程三个重要特性是什么

原子性:一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。

可见性:当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。

有序性:由于指令重排序问题,代码的执行顺序未必就是编写代码时候的顺序。


文章作者: Aiaa
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Aiaa !
  目录