文章目录

  • 什么是线程?
  • Java线程
  • 线程安全
    • 数据的共享
    • 线程安全
  • synchronized锁
  • volatile
  • synchronized 与 volatile 比较
  • wait-notify
  • 线程池
    • 为什么使用线程池
    • Java中提供的线程池
      • ThreadPoolExecutor的构造方法
      • 创建线程的策略
      • 拒绝策略
  • 总结

什么是线程?

进程(process)与线程(thread)的关系:

进程和线程之间是一对多的关系,一个线程一定属于一个进程,一个进程下可以允许有多个线程,一个进程内至少有一个线程。

为什么OS要引入线程?
由于进程这一概念天生就是资源隔离的,所有进程之间进行数据通信注定是一个高成本的工作,现实中,一个任务需要多个执行流一起配合完成,是非常常见的,所以需要一种方便数据通信的执行流概念出来,线程就承担了这一职责。

什么是线程?

线程:线程是OS进行调度的基本单位,独立执行流的承载单位。

进程和线程的区别

  • 概念:进程是OS进行资源分配的基本单位,线程是OS进行调度的基本单位。
  • 由于进程把调度单位这一职责让渡给了线程,所以使得单纯进程的创建和销毁适当容易。
  • 由于线程的创建和销毁不涉及资源分配、回收的问题,所以,通常理解,线程的创建和销毁成本要低于进程的成本。

Java线程

Java线程
一、如何在代码中创建线程
1、通过继承Thread类,并且重写run方法。
2、通过实现Runnable接口,并且重写run方法。
二、启动线程
当此时手里有一个Thread对象时,调用其start()方法。
注意:一个已经调用过start()不能再调用start()了,再调用就会有异常发生。
三、怎么理解 t.start() 做了什么?
t.start()把线程的状态从新建变成了就绪,不负责分配CPU,至于先执行子线程中的语句还是先执行主线程中的语句都是有可能的,但大概率是主线程中的打印先执行,因为主线程刚刚执行完t.start()就马上发生线程调度的概率不大。
四、什么时候子线程中的语句会先执行?
1、非常碰巧的在start()之后,打印之前,发生了一次线程调度。
2、主线程的状态:运行 -》就绪,主线程不再持有CPU。
3、调取的时候选中子线程调度,子线程状态从就绪到运行。
五、什么情况下会出现线程调度?
1、CPU空闲:
①当前运行着的CPU执行结束了 ——》 运行 -》结束
②等待外部条件 ——》 运行 -》阻塞
③主动放弃 ——》 运行 -》就绪
2、被调度器主动调度
①高优先级线程抢占
②时间片耗尽
六、多线程程序的随机性
在多线程中,代码是固定的,但是会出现现象是随机的可能性,主要原因就是调度的随机性体现在线程的运行过程中。PS:一个线程内部是没有这些问题的。
七、线程状态
①new:新建
②runnable:就绪+运行
③terminated:结束
④blocked+waiting+timed_waiting:阻塞
1)理论中的状态

2)Java代码中实际看到的状态

八、Thread下的常见方法
①Thread.join()方法:等待线程停止。
②Thread.sleep()方法:让线程休眠多少毫秒,从线程状态来看,调用sleep(?),就是让当前线程从 “运行” -》“阻塞”。
③Thread.currentThread()方法:返回当前线程的引用。
④Thread.yield()方法:让线程让出CPU,线程从 “运行” -》“就绪”,随时可以继续被调度回CPU,yield主要用于执行一些耗时较久的计算任务时,为了防止计算机处于“卡顿”的现象,时不时的让出一些CPU资源,给OS内的其他进程。
Thread.interrupt():中断线程,一个线程A主动让线程B停止,A给B发送一个中止信号,B通过 ①Thread.interrupted()方法检测当前线程是否被中止 ②当B处于休眠状态时,捕获了InterruptedException异常时,代表线程被中止了。
九、使用多线程的场景
1、计算密集性的任务时,为了提升整体速度,可以引入多线程;
2、当一个执行流阻塞时,为了还能处理其他任务,可以引入多线程。

线程安全

数据的共享

JVM的内存区域划分

①堆区、方法区、运行时常量池是整个进程只有一份。
②PC(保存的PC的值)、栈(虚拟机栈、本地方法栈)是每个线程独一份的。
④为什么每个线程都得有自己的PC:每个线程都是独立的执行流,下一条要执行的指令和其他线程无关,所以有自己的PC。
⑤为什么每个线程都得有自己的栈:每个线程都是独立的执行流,有各自调用的方法链,有各自要处理的临时数据,所以栈也是独一份的。

线程中的数据共享:

  • 局部变量,保存在栈中,所以是线程私有
  • 类对象(Class对象)、静态属性,保存在方法区中,所以线程之间是共享的,前提是有访问权限
  • 对象(对象内部的属性),保存在堆区,所以线程之间是共享的,前提是,线程持有该对象的引用

线程安全

什么是线程安全?

线程安全:代码的运行结果应该是100%符合预期。

线程不安全现象出现的原因:
一、开发者角度
①多个线程之间操作同一块数据了(共享数据)——不仅仅是内存数据;
②至少有一个线程在修改这块共享数据。

1)在多线程的代码中,哪些情况下不需要考虑线程安全问题?

①几个线程之间互相没有任何数据共享的情况下,天生就是线程安全的;②几个线程之间即使有共享数据,但都是做读操作,没有写操作时,也是天生线程安全的。
二、系统角度
前置知识:
①java代码中的一条语句,很可能对应多条指令,例如 r++,实质是r=r+1
②线程调度是可能发生在任意时刻的,但是不会切割指令(一条指令只有执行完/完全没有执行两种可能)

系统角度导致线程不安全现象出现的原因有三个,如下所示:
1)原子性
原子性:原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着”同生共死”的感觉。
程序员预期是r++或r–是一个原子性的操作,但实际执行起来,保证不了原子性,所以会出错。

最常见违反原子性的场景:
①read-write 场景:i++;
②check-update 场景:if(a==10){ a=…; }
2)内存可见性
内存可见性:一个线程对数据的操作,很可能其他线程是无法感知的,甚至,某些情况下,会被优化成完全看不到的结果。

多线程操作共享变量实现可见性过程JVM的内存模型如下:

所有的变量都存储在主内存中每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中是该变量的一份拷贝)。
要实现共享变量的可见性,必须保证两点:
①线程修改后的共享变量值能够及时从工作内存刷新到主内存中;
②其他线程能够及时的把共享变量的最新值从主内存更新到自己的工作内存中。

JVM内存模型两条规定:
1、线程对共享变量的所有操作必须在自己的内存中进行,不能直接从主内存中读写
2、不同线程之间无法直接访问其它线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成

3)代码重排序
代码重排序:所谓重排序就是执行的指令和书写指令不一致。
假设我们写了一个 Java 程序,包含一系列的语句,我们会默认期望这些语句的实际运行顺序和写的代码顺序一致。但实际上,编译器、JVM 或者 CPU 都有可能出于优化等目的,对于实际指令执行的顺序进行调整,这就是重排序。

JVM规定了一些重排序的基本原则:happens-before 规则
happens-before原则是JMM中非常重要的原则,它是判断数据是否存在竞争、线程是否安全的主要依据,保证了多线程环境下的可见性。

synchronized锁

synchronized锁的三种用法
1、修饰普通方法:作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁

synchronized void method() {//业务代码}

2、修饰静态方法:给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得当前 class 的锁

synchronized void staic method() {//业务代码}

3、修饰代码块:指定加锁对象,对给定对象/类加锁,synchronized(this/object) 表示进入同步代码前要获得给定对象的锁,synchronized(类.class) 表示进入同步代码前要获得当前 class 的锁

synchronized(this) {//业务代码}

对于synchronized锁来说,只会有一个线程持有锁,其余加锁失败的线程都会被:①进入该锁的阻塞队列(等待队列)②放弃CPU(运行 -》阻塞)。

synchronized锁的互斥
互斥的必要条件:线程都有加锁操作 && 锁的是同一个对象

观察以下代码:

public class SomeClass {synchronized void m1(){}synchronized static void m2(){}void m3(){}void m4(){synchronized (this){}}void m5(){synchronized (SomeClass.class){}}Object o1=new Object();void m6(){synchronized (o1){}}static Object o2=new Object();void m7(){synchronized (o2){}}}//s1=new SomeClass();//s2=new SomeClass();//s3=s1;

线程互斥的情况如下:

synchronized锁的作用
1)、保证了原子性,被synchronized修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得类或对象的锁,直到执行完才能释放,通过正确的加锁操作使得应该原子性的代码之间互斥来实现。

2)、在有限程度上保证了内存可见性,synchronized锁对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到主内存当中,保证资源变量的可见性。

3)、给代码重排序增加一些约束,synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。

volatile

理解volatile:是java虚拟机提供的最轻量级的同步机制,针对volatile修饰的变量给java虚拟机特殊的约定,线程对volatile变量的修改会立刻被其他线程所感知,即不会出现数据脏读的现象,从而保证数据的“可见性”。

volatile 的原子性问题
在多线程环境下,volatile 关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性。也就是说,多线程环境下,使用 volatile 修饰的变量是线程不安全的。
特别地,对任意单个使用 volatile 修饰的变量的读 / 写是具有原子性,但类似于 flag = !flag 这种复合操作不具有原子性。简单地说就是,单纯的赋值操作是原子性的

禁止指令重排序
①在每个 volatile 读操作的后面插入 LoadLoad 屏障和 LoadStore 屏障。
②在每个 volatile 写操作的前后分别插入一个 StoreStore 屏障和一个 StoreLoad 屏障。
也就是说,编译器不会对 volatile 读与 volatile 读后面的任意内存操作重排序;编译器不会对 volatile 写与 volatile 写前面的任意内存操作重排序。

保证内存可见性(最重要的作用)
volatile变量在每次被线程访问时,都强迫从主线程中重读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存中,这样任何时刻,不同的线程总能看到该变量最新的值。

线程写volatile变量的过程:
①.改变线程工作内存中volatile变量副本的值;
②.将改变后的副本的值从工作内存刷新到主内存。

线程读volatile变量的过程:
①.从主内存中读取volatile变量的最新值到线程的工作内存中;
②.从工作内存中读取volatile变量的副本。

synchronized 与 volatile 比较

  1. volatile不需要加锁,比synchronized更轻量级,不会堵塞程序;
  2. 从内存可见角度讲,volatile读相当于加锁,volatile写相当于解锁;
  3. synchronized既能保证可见性,又能保障原子性,而volatile只能保障可见性,不能保证原子性;
  4. synchronized使用更加广泛。

wait-notify

什么是等待/通知机制?

通俗来讲:
等待/通知机制在我们生活中很常见,用一个通俗的例子就是厨师和服务员之间就存在等待/通知机制。

  • 厨师做完一道菜的时间是不确定的,所以菜到服务员手中的时间也是不确定的。
  • 服务员就需要去“等待(wait)”。
  • 厨师把菜做完之后,按一下铃“通知(nofity)”服务员。
  • 服务员听到铃声之后就知道菜做好了,就可以去端菜了。

使用专业术语讲:
等待/通知机制,是指线程A调用了对象O的wait()方法进入等待状态,而线程B调用了对象O的notify()/notifyAll()方法,线程A收到通知后退出等待队列,进入可运行状态,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify()/notifyAll()方法的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

注意:
①要使用wait-notify,必须首先对“对象”进行 synchronized 加锁。
②当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。
③notify 和wait 的顺序不能错,如果A线程先执行notify方法,B线程再执行wait方法,那么B线程是无法被唤醒的。

wait()的中止条件:
①被通知唤醒了
②线程被中止了(异常)
③假唤醒
④超时时间到达

notify 和 notifyAll的区别:
notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。
notifyAll 会唤醒所有等待(对象的)线程,哪一个线程将会第一个处理取决于操作系统的实现。

线程池

为什么使用线程池

1、创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,会很大程度上影响处理效率;
2、线程并发数量过多,抢占系统资源从而导致阻塞,线程能共享系统资源,如果同时执行的线程过多,就有可能导致系统资源不足而产生阻塞的情况;
3、对线程进行一些简单的管理。

Java中提供的线程池

在Java中,线程池的概念是Executor这个接口,具体实现为ThreadPoolExecutor类。

ThreadPoolExecutor的构造方法

public MyThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)

int corePoolSize:该线程池中核心线程数最大值(也可以形象的称为正式员工的名额上限)

核心线程
线程池新建线程的时候,如果当前线程总数小于corePoolSize,则新建的是核心线程,如果超过corePoolSize,则新建的是非核心线程。
核心线程默认情况下是一直存活在线程池中,即使什么也不做。但是如果指定ThreadPoolExecutor的allowCoreThreadTimeOut这个属性为true,那么核心线程如果不干活的话,超时时间到了,就会被销毁掉。

int maximumPoolSize:该线程池中线程总数最大值(正式工加临时工的总数)

long keepAliveTime:该线程池中非核心线程闲置超时时长,如果一个非核心线程(临时员工)闲置的时间超过了这个参数设定的时长,就会被销毁。

TimeUnit unit

keepAliveTime的单位,TimeUnit是一个枚举类型,其包括:
NANOSECONDS : 1微毫秒 = 1微秒 / 1000
MICROSECONDS : 1微秒 = 1毫秒 / 1000
MILLISECONDS : 1毫秒 = 1秒 /1000
SECONDS : 秒
MINUTES : 分
HOURS : 小时
DAYS : 天

BlockingQueue workQueue:任务队列

该线程池中的任务队列:维护着等待执行的Runnable对象
当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务。

ThreadFactory threadFactory:线程工厂

创建线程的方式,这是一个接口,new它的时候需要实现他的Thread newThread(Runnable r)方法

ThreadFactory源码:

public interface ThreadFactory {Thread newThread(Runnable var1);}

RejectedExecutionHandler handler:拒绝策略

创建线程的策略

怎么向线程池提交一个要执行的任务呢?
通过ThreadPoolExecutor.execute(Runnable task)方法即可向线程池内添加一个任务。

ThreadPoolExecutor的策略:
1、一开始,线程池里一个工作线程都没有
2、随着任务提交:

  1. 线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务;
  2. 线程数量达到了corePoolSize,则将任务移入队列等待;
  3. 队列已满,新建线程(非核心线程)执行任务;
  4. 队列已满,总线程数又达到了maximumPoolSize,就执行拒绝策略。

拒绝策略

如图所示:

  1. 第一个是拒绝任务(默认情况)
  2. 第二个是需要调用者自己去执行
  3. 第三个是丢掉最老的任务
  4. 第四个是把当前任务丢弃

总结

本篇文章写到这里就结束了,但是关于多线程的学习是无止尽的,在写这篇文章时,我借鉴了很多优秀的文章,在这里很衷心的感谢他们,我也希望自己秉承着“活到老学到老”的态度,不断去学习,写出更好、更有价值的文章,如果你觉得有收获的话,就留下你的吧!