进程:进程的本质是一个正在执行的程序,程序运行时系统会创建一个进程,并且给每个进程分配独立的内存地址空间保证每个进程地址不会相互干扰。同时,在 CPU 对进程做时间片的切换时,保证进程切换过程中仍然要从进程切换之前运行的位置出开始执行。所以进程通常还会包括程序计数器、堆栈指针。
线程:有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派 CPU 的基本单位指运行中的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。
创建的方式有三种:
import java.util.concurrent.CountDownLatch;
public class ThreadDemo1 extends Thread {
CountDownLatch countDownLatch;
public ThreadDemo1(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + ":my thread ");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}
public static void main(String[] args) {
// 第一种:使用extends Thread方式
CountDownLatch countDownLatch1 = new CountDownLatch(2);
for (int i = 0; i < 2; i++) {
ThreadDemo1 myThread1 = new ThreadDemo1(countDownLatch1);
myThread1.start();
}
try {
countDownLatch1.await();
System.out.println("thread complete...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
import java.util.concurrent.CountDownLatch;
public class ThreadDemo2 implements Runnable{
CountDownLatch countDownLatch;
public ThreadDemo2(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + ":my runnable ");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}
public static void main(String[] args) {
// 第二种:使用implements Runnable方式
CountDownLatch countDownLatch2 = new CountDownLatch(2);
ThreadDemo2 myRunnable = new ThreadDemo2(countDownLatch2);
for (int i = 0; i < 2; i++) {
new Thread(myRunnable).start();
}
try {
countDownLatch2.await();
System.out.println("runnable complete...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class ThreadDemo3 implements Callable<Integer> {
public static void main(String[] args) {
ThreadDemo3 threadDemo03 = new ThreadDemo3();
//1、用futureTask接收结果
FutureTask<Integer> futureTask = new FutureTask<>(threadDemo03);
new Thread(futureTask).start();
//2、接收线程运算后的结果
try {
//futureTask.get();这个是堵塞性的等待
Integer sum = futureTask.get();
System.out.println("sum="+sum);
System.out.println("-------------------");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <101 ; i++) {
sum+=i;
}
return sum;
}
}
启动线程是start方法,非run方法
使用退出标志退出线程
一般 run()方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。它们需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环
例如:最直接的方法就是设一个boolean 类型的标志,并通过设置这个标志为 true或 false 来控制 while循环是否退出。定义了一个退出标志 exit,当 exit 为 true 时,while 循环退出,exit 的默认值为 false.在定义 exit时,使用了一个 Java 关键字 volatile,这个关键字的目的是使 exit 同步,也就是说在同一时刻只能由一个线程来修改 exit 的值。
class FlagThread extends Thread { // 自定义中断标识符 public volatile boolean isInterrupt = false; @Override public void run() { // 如果为 true -> 中断执行 while (!isInterrupt) { System.out.println("业务处理1...."); // 业务逻辑处理 Thread.sleep(10000); System.out.println("业务处理2...."); } } }
Interrupt方法结束线
线程处于阻塞状态:如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时,会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的, 一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正常结束 run 方法。
public static void main(String[] args) throws InterruptedException { // 创建可中断的线程实例 Thread thread = new Thread(() -> { while (!Thread.currentThread().isInterrupted()) { System.out.println("thread 执行步骤1:线程即将进入休眠状态"); try { // 休眠 1s Thread.sleep(1000); } catch (InterruptedException e) { System.out.println("thread 线程接收到中断指令,执行中断操作"); // 中断当前线程的任务执行 break; } System.out.println("thread 执行步骤2:线程执行了任务"); } }); thread.start(); // 启动线程 // 休眠 100ms,等待 thread 线程运行起来 Thread.sleep(100); System.out.println("主线程:试图终止线程 thread"); // 修改中断标识符,中断线程 thread.interrupt(); // 首先会改变当前线程的阻塞状态 true 。同时如何线程调用 sleep wait 这些阻塞的方法。那么会抛出 InterruptedException 异常 }
线程未处于阻塞状态:使用 isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置 true,和使用自定义的标志来控制循环是一样的道理。
stop 方法终止线程
程序中可以直接使用 thread.stop()来强行终止线程,但是 stop 方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用 stop 方法来终止线程。
提交一个任务到线程池中,线程池的处理流程如下:
线程池的好处:
线程池 | 说明 |
---|---|
newCachedThreadPool | 创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行 很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造 的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并 从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资 源。 |
newFixedThreadPool | 创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大 多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务, 则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何 线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之 前,池中的线程将一直存在。 |
newScheduledThreadPool | 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。 |
newSingleThreadExecutor | Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程 池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去! |
系统提供的线程池的实现都有各自的优缺点。我们在实际的使用中更多的是自定义线程池的实现
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
各个参数的含义:
corePoolSize:线程池核心线程数量
maximumPoolSize:线程池最大线程数量
keepAliverTime:当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间
unit:存活时间的单位
workQueue:存放任务的队列
handler:超出线程范围和队列容量的任务的处理程序
使用的是阻塞队列:
拒绝策略分类:
提交一个任务到线程池中,线程池的处理流程如下:
要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析。
任务的性质:CPU密集型任务、IO密集型任务和混合型任务
任务的优先级:高中低
任务的执行时间:长中短
任务 的依赖性;是否依赖其他系统资源
CPU密集型任务应配置尽可能小的线程,如配置NCPU+1个线程的线程池,
IO密集型任务线程并不是一直在执行任务 ,则应配置尽可能多的线程,如2*NCPU 。
可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理,它可以让优先级高的任务先执行。
同步、重量级锁,synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入临界区,同时还可以保证共享变量的内存可见性(单台JVM内)
synchronized锁对象:
synchronized的锁优化
AbstractQueuedSynchronizer,同步器,实现JUC核心基础组件,解决了子类实现同步器时涉及到的大量细节性问题,例如:同步状态、FIFO同步队列等,采用模板方法模式,AQS实现了大量的通用方法,子类通过继承方式实现其抽象方法来管理同步状态
同步锁获取与释放:
package com.boge.flow.aqs;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class AQSDemo {
// 共享的资源
private static int count = 0;
private static Lock lock = new ReentrantLock();
/**
* 造成数据安全问题的原因
* 1.原子性:执行的最小单元 要么都执行。要么都不执行
* 2.可见性:
* 3.有序性:
*/
// 定义一个方法去操作资源
public static void incr(){
try {
Thread.sleep(10);
lock.lock(); // 加锁
// ...
lock.lock();
count ++; // 原子 可见性
}catch (Exception e){
}finally {
lock.unlock();
lock.unlock(); // 释放锁
}
}
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(100);
// 多个线程去操作共享资源
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
incr();
latch.countDown();
}
}).start();
}
latch.await(); // 阻塞
// 获取 共享的数据
System.out.println("count = " + count);
}
}
一种解决多线程环境下成员变量的问题的方案,但是与线程同步无关,其思路就是为每个线程创建一个单独的变量副本。从而每个线程都可以独立的改变自己所拥有的变量副本,而不会影响其他线程对应的变量副本 , ThreadLocal不是用于解决共享变量的问题,也不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制
ThreadLocal可以理解为线程本地变量,他会在每个线程都创建一个副本,那么在线程之间访问内部副本变量就行了,做到了线程之间互相隔离,相比于synchronized的做法是用空间来换时间。
ThreadLocal有一个静态内部类ThreadLocalMap,ThreadLocalMap又包含了一个Entry数组,Entry本身是一个弱引用,他的key是指向ThreadLocal的弱引用,Entry具备了保存key value键值对的能力。
弱引用的目的是为了防止内存泄露,如果是强引用那么ThreadLocal对象除非线程结束否则始终无法被回收,弱引用则会在下一次GC的时候被回收。
但是这样还是会存在内存泄露的问题,假如key和ThreadLocal对象被回收之后,entry中就存在key为null,但是value有值的entry对象,但是永远没办法被访问到,同样除非线程结束运行。
但是只要ThreadLocal使用恰当,在使用完之后调用remove方法删除Entry对象,实际上是不会出现这个问题的。