Java并发基础笔记

223 阅读8分钟

线程和进程

什么是进程

进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
在Java中,启动main函数时,其实是启动了一个JVM进程,而main函数所在的线程就是这个进程中的一个线程(主线程)。

什么是线程

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

线程与进程的关系,区别和优缺点

线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而同一进程中的线程可能会互相影响。
线程执行开销小,但不利于资源的管理和保护,进程正相反。

程序计数器为什么是私有的

程序计数器的主要作用:

  1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能知道该线程上次运行到哪里了。

PS:如果执行的是native方法,程序计数器记录的是undefined地址,只有执行Java代码时,程序计数器记录的才是下一条指令的地址。
总结:程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。

虚拟机栈和本地方法为什么是私有的

虚拟机栈:每个Java方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应一个栈帧在Java虚拟机中入栈和出栈的过程。
本地方法栈:和虚拟机栈作用相似,区别:虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈为虚拟机使用到的Native方法服务。在HotSpot虚拟机中和Java虚拟机栈合二为一。
总结:虚拟机栈和本地方法栈私有是为了保证线程中的局部变量不被其它的线程访问。

堆和方法区

堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要存放新创建的对象(所有对象在这分配内存),方法区主要存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

并发与并行的区别

并发:同一时间段,多个任务都在执行(单位时间内不一定同时执行)。
并行:单位时间内,多个任务同时执行。

使用多线程的原因

  • 从总体上来看:
    线程可以比作轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本小于进程。另外,多核CPU时代意味着多个线程可以同时运行,减少了线程上下文切换的开销。
    现在的系统要求百万计甚至千万级的并发量,多线程并发编程是开发高并发系统的基础。
  • 从计算机底层来看:
    在单核时代,多线程主要为了提高CPU和IO设备的综合利用率。
    在多核时代,多线程主要为了提高CPU利用率。(只用一个线程,CPU只会一个核心被利用,多线程可以让多个CPU核心被利用)

使用多线程可能带来的问题

并发编程并不能总是提高程序的执行效率和运行速度,而且会带来:内存泄露、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。

线程的生命周期和状态

Java线程在运行的生命周期中的指定时刻只可能处于以下6种不同状态:NEW(新建),RUNNABLE(运行中),BLOCKED(阻塞),WAITING(等待),TIME_WAITING(超时等待),TERMINATED(终止)。

操作系统隐藏Java虚拟机(JVM)中RUNNABLE和RUNNING状态,它只能看到RUNNABLE状态,所以Java系统一般将这两个状态统称为RUNNABLE(运行中)状态。

线程创建后处于NEW(新建)状态,调用start()方法后开始运行,线程处于READY(可运行)状态。可运行状态的线程获得CPU时间片后就处于RUNNING(运行)状态。
当线程执行wait()方法之后,线程进入WAITING(等待)状态。进入等待状态的线程需要依靠其它线程的通知才能返回运行状态,而TIME_WAITING(超时等待)相等于在等待状态的基础上增加了超时限制,比如通过sleep(long millis)方法可以将Java线程置于TIME_WATING状态。
当超时时间达到后,Java线程将会返回到RUNNABLE状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到BLOCKED(阻塞)状态。线程在执行Runnable的run() 方法之后,将会进入到TERMINATED(终止)状态。

上下文切换

多线程编程中一般线程的个数都大于CPU核心的个数,一个CPU核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU采取为每个线程分配时间片并轮播的形式。当一个线程的时间片用完,就会重新处于就绪状态让给其它线程使用,这个过程就属于一次上下文切换。
当前任务在执行完CPU时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换

线程死锁

什么是线程死锁

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

  1. 互斥条件:该资源任意一个时刻只由一个线程占用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源在未使用完之前不能被其它线程强行剥夺,只有自己使用完毕之后才能释放资源。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

避免线程死锁

  1. 破坏互斥条件:该条件无法破坏,因为用锁的目的就是让其互斥(临界资源需要互斥访问)。
  2. 破坏请求与保持条件:一次性申请所有的资源。
  3. 破坏不剥夺条件:占用部分资源的线程进一步申请其它资源时,如果申请不到,可以主动释放它占有的资源。
  4. 破坏循环等待条件:靠按序申请来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

sleep()方法和wait()方法的区别和共同点

  • 主要区别:sleep()没有释放锁,而wait()释放了锁。
  • 都可以暂停线程的执行。
  • wait()通常被用于线程间交互/通信,sleep()通常被用于暂停执行。
  • wait()被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()sleep()执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。

调用start()方法时会执行run()方法,为什么不直接调用run()方法

经典Java多线程面试题。
new 一个 Thread,线程进入新建状态;调用start(),启动一个线程并使线程进入就绪状态,当分配到时间片后就可以开始运行。start()会执行线程的相应准备工作,然后自动执行run()的内容,这是真正的多线程工作。若直接执行run(),会把run()当成一个main线程下的普通方法去执行,并不会在某个线程中执行它,所以这不是多线程工作。
总结:调用start()方法可启动线程并使线程进入就绪状态,而run()只是thread的一个普通方法调用,还是在主线程里执行。

模拟卖票系统

/**
 * 实现Runnable
 * @author Shenyf
 * @date 2019/11/14 17:44
 */
public class Ticket implements Runnable{
    //设置总票数
    int ticket = 100;

    @Override
    public void run() {
        //模拟售票
        while (true) {
            //加入同步锁
            synchronized (this) {
                if (ticket > 0) {
                    //加入线程定时休眠Thread.sleep(),让线程安全问题效果明显些
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    /** Thread.currentThread()是线程获取当前线程对象的方法 getName()获取调用者的线程名**/
                    System.out.println(Thread.currentThread().getName() + "正在售票:" + ticket--);
                }
            }
        }
    }

}

/**
 * 开启多线程的代码
 * @author Shenyf
 * @date 2019/11/14 19:53
 */
public class ThreadDemo01 {

    public static void main(String[] args) {
        //创建Ticket的Runnable对象
        Ticket ticket = new Ticket();
        //创建线程3个对象模拟三个售票窗口,并把Runnable对象加入Thread和给Thread命名
        new Thread(ticket, "窗口1").start();
        new Thread(ticket, "窗口2").start();
        new Thread(ticket, "窗口3").start();
    }

}