进程和线程的区别

  • 进程:一个内存中运行的应用程序,最常见的即为 .exe 为后缀的文件。
  • 线程:进程中的执行单元。一个进程至少有一个线程,多个线程可以共享数据。
    它们的主要区别如下:
    根本区别 :进程是操作系统资源分配的基本单位,线程是CPU任务调度和执行的基本单位。
    资源开销 :每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,线程之间切换的开销小。
    包含关系 :如果一个进程有多个线程,则执行过程是多线程同步执行,执行的先后顺序取决于CPU的调度,线程也被称为轻量级进程
    内存分配 :进程之间的地址空间和资源是相互独立的,而同一进程的线程共享本进程的地址空间和资源,且每个线程有自己的程序计数器虚拟机栈本地方法栈
    影响关系 :一个进程崩溃不会对其他进程产生影响,但一个线程崩溃则会影响整个进程。
    执行过程 :每个独立的进程有程序运行的入口、顺序执行序列和程序出口,而线程不能独立执行。

线程的属性

  1. 线程编号(Id)
  • 类型:long
  • 作用:用于标识不同的线程。不同的线程有不同的编号。
  • 注意事项:线程编号只在当前的运行中是唯一的,下一次运行,该线程的编号可能会发生变化。因此不适合作为唯一标识符。
  • 查看方式:myThread().getId().
  1. 线程名称(Name)
  • 类型:String
  • 作用:用于区别不同的线程。默认值与线程的编号有关,默认格式为:Thread-线程号。main函数的线程名叫main。可以使用Thread类中的构造方法自己设置名称。
  • 注意事项:设置简明的线程名可以方便我们调试代码和定位。
  • 查看方式:myThread().getName().
  1. 线程类别(Daemon)
  • 类型:Boolean
  • 作用:默认为false(用户线程),可手动设置为true(守护线程).
  • 注意事项:须在线程启动前设置,false:主线程结束后用户线程继续运行,JVM存活,反之全部消亡.
  • 设置方式:t1.setDaemon().
  1. 优先级(Priority)
  • 类型:int
    作用:给线程调度器的提示
    注意事项:优先级默认为5,设置优先级可能会导致出现线程饥饿问题.
  • 设置方式:t1.setPriority().

线程的创建方式

  1. 继承Thread类:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class Test {
    public static void main(String[] args) {
    myThread thread = new myThread();
    thread.start();
    }
    }

    class myThread extends Thread {
    @Override
    public void run() {
    System.out.println("Create a thread");
    }
    }
  2. 实现Runable接口:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class Test {
    public static void main(String[] args) {
    myThread thread = new myThread();
    Thread t1 = new Thread(thread);
    //通过Thread类的对象调用start():① 启动线程 ②调用当前线程的run()-->调用了Runnable类型的target的run()
    t1.start();
    }
    }

    class myThread implements Runnable {
    @Override
    public void run() {
    System.out.println("Create a thread");
    }
    }
  3. 匿名内部类:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Test {
    public static void main(String[] args) {
    Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
    System.out.println("create a thead");
    }
    });
    t1.start();
    }
    }
    //也可以使用lambda表达式创建
    通过阅读源码可知,执行myThread.start()时,其实是调用了JAVA的本地方法:private native void start0();,此时才真正实现了多线程运行,而直接调用run()方法,就相当于执行主线程下的一个普通方法而已。
  • 假设两个线程使用while(true)循环执行同一任务,调用start()方法会随机交替执行,而run()方法只有此代码中的循环被执行。

线程的生命周期和状态

  • 在操作系统中线程共有五个生命周期:创建,就绪,运行,阻塞,终止.而JAVA对其进一步细分为六个状态:
  • NEW: 初始状态,即Thread t1 = new Thread()
  • RUNNABLE: 运行状态,线程被调用了start()等待运行的状态。
  • WAITING:等待状态,倘若其他线程的某些动作尚未完成,该线程就会一直等待。
  • TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
  • TERMINATED:终止状态,表示该线程已经运行完毕。
    Java线程状态变迁图

死锁(待补充)

什么是死锁

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

死锁产生的四个必要条件

  1. 互斥条件:该资源任意一个时刻只由一个线程占用。
  2. 不可抢占: 资源只能由占有者主动释放,请求者不可强制夺取资源。
  3. 请求和保持: 一个线程在请求其他资源时对自已获得的资源保持不放。
  4. 循环等待: P1占有P2的资源,P2占有P3的资源,P3占有P1的资源,形成类似环形链表的等待回路。

悲观锁和乐观锁

悲观锁

悲观锁总是假设最坏的情况,认为共享资源每次被访问时都会出现问题,因此在每次获取资源时都会上锁,此刻其他线程想再拿到这个资源就会阻塞直到锁被上一个资源持有者释放。
synchronizedReentrantLock就是悲观锁思想实现的。
悲观锁通常多用于写多比较多的情况下,避免频繁失败和重试影响性能。

乐观锁

乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源是否被其它线程修改了。
乐观锁通常多于写比较少的情况下,避免频繁加锁影响性能,大大提升了系统的吞吐量。

synchronized

使用方法

  1. 修饰实例方法(锁当前对象)
    1
    2
    3
    synchronized void method() {
    //code here...
    }
  2. 修饰静态方法(锁当前类)
    1
    2
    3
    synchronized static void method() {
    //code here...
    }
    如果线程A调用了静态synchronized方法,而线程B调用了非静态synchronized方法,两者并不互斥,静态synchronized方法锁的是类对象,而非静态synchronized方法锁的是当前对象实例。
  3. 修饰代码块(锁指定对象/类)
    1
    2
    3
    synchronized (this) {
    //code here...
    }
    此外,构造方法不能使用synchronized修饰,因为即使多个线程同时构造同一个类的对象,它们new出来的对象是互不相干的,这意味着不会发生资源共享,不用考虑线程安全问题。

线程池

什么是线程池

线程池就是管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务

使用线程池的优点

  1. 降低资源消耗。不会频繁的创建和销毁线程造成不必要的系统开销。
  2. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的可管理性。线程池可以统一管理,分配和调优池中的线程。

什么时候使用线程池

如果创建线程池

方式一:使用内置线程池
线程池创建方式

  • 如果发现线程池中有空闲线程,则直接执行该任务;
  • 如果没有空闲线程,且当前运行的线程数少于corePoolSize,则创建新的线程执行该任务;
  • 如果没有空闲线程,且当前的线程数等于corePoolSize,同时阻塞队列未满,则将任务入队列,而不添加新的线程;
  • 如果没有空闲线程,且阻塞队列已满,同时池中的线程数小于maximumPoolSize,则创建新的线程执行任务;
  • 如果没有空闲线程,且阻塞队列已满,同时池中的线程数等于maximumPoolSize,则根据构造函数中的handler指定的策略来拒绝新的任务。

方式二:自定义线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(args...);

线程池种的常见参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
int maximumPoolSize,//线程池的最大线程数
long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}

执行流程

执行流程

饱和策略

ThreadPoolExecutor.AbortPolicy: 抛出RejectedExecutionException直接拒绝新任务。
ThreadPoolExecutor.CallerRunsPolicy: 返回给调用线程执行被拒绝的任务,如果执行程序已关闭,丢弃该任务。
ThreadPoolExecutor.DiscardPolicy: 抛弃最新的任务。
ThreadPoolExecutor.DiscardOldestPolicy: 抛弃最早未处理的任务。