一、基本概念

1、CPU核心数与线程数的关系

一般来说是1:1的关系 即1个核心对应1个线程,但我们在程序中可以创建多个线程的原因是由于CPU的时间片调度

2、CPU时间片轮转(RR调度)

把CPU的运行时间进行切片分别轮转到各个线程

3、进程和线程

进程:操作系统对资源分配的最小单位

线程:CPU调度的最小单位

进程>线程,线程不能单独存在,必须要依附于进程存在

线程数量限制:在操作系统层面Linux限制为1000,Windows限制为2000

4、并行和并发

(1)并行:同时执行(例如高速公路的4车道,并行数就是4)

(2) 并发:单位时间内的吞吐量(与并行的关键区别就在于时间限制),CPU的并发能力取决于CPU时间片的切换速度

二、多线程

1、线程的启动方式

Java中有三种线程启动的方式

(1)、继承Thread类

image

(2)、实现Runnable接口

image

(3)、实现Callable接口

image

实现Runnable和Callable接口的不同在于,实现Callable是允许有返回值的;以上三种创建线程的方式,最后都是通过Thread类进行开启,在Java中只有Thread类是线程的创建和实现类。

2、线程的结束方式

(1)、stop方式

image

stop方法被官方定义为弃用的;因为stop方法会强制退出线程,可能会导致线程中的其它资源未被正确释放等安全问题。

(2)、suspend方式

将线程挂起,同样也是被JDK弃用的;因为susbend方法不会释放锁,容易导致死锁发生

(3)、interrupt

Thread的成员方法,不会立刻导致线程退出,只会将线程的中断标志位置为true

(4)、interrupted

Thread的静态方法,返回boolean值,除具有isInterrupted正常功能外,还会重置中断标志位为false

(5)、isInterrupted

Thread的成员方法,判断线程是否被中断

一个线程退出的例子:

image

调用interrupt方法将线程标志位置为true,用isInterrupted检测当前的线程标志位;当然在实际开发当中也可以通过一些boolean标志位进行控制。

3、线程的生命周期、线程调度、等待唤醒、ThreadLocal相关

(1)生命周期

线程的五种基本状态:新建、就绪、运行、阻塞、死亡

image

当我们new一个线程并调用start方法时,该线程就从新建到就绪(Runnable)状态;当线程获取到CPU的时间片后进入到执行状态(Running);该线程调用yield方法或者时间片执行完毕后,从Running切换到Runnable状态;该线程调用wait(等待,释放锁)、sleep(休眠、不释放锁)、join(放弃执行让其他线程先执行)时会进入到阻塞(Blocked)状态;线程执行完毕后进入到死亡(Dead)状态。

(2)yield方法

使当前线程放弃时间片暂停执行,并执行其它线程;但由于CPU轮询速度较快,很可能马上又会轮询到当前线程,效果不明显

注:yield方法不会释放锁

(3)join方法

主要作用是线程的同步,使得线程从并行改为串行执行,例如线程A中执行线程B的join方法,表示线程B执行完成后才会继续执行线程A

eg:线程A、线程B、线程C三个线程,实现依次输出A、B、C中的内容

image
image

join方法的实现原理:

正常调用join方法实际上调用join(0)

public final void join() throws InterruptedException {
join(0);
}

join参数是一个delay时间,默认是0,但wait(0)不是等待0ms,而是一直等待。

void join():当前线程等该加入该线程后面,等待该线程终止。

void join(long millis):当前线程等待该线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度。

void join(long millis,int nanos):等待该线程终止的时间最长为 millis 毫秒 + nanos纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度。

join的原理为当在线程A中调用了线程B的join方法,线程A会执行wait方法,释放锁并等待线程B的执行结束

public final void join(long millis) throws InterruptedException {
synchronized(lock) {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
//传入的是0 默认执行这个判断
while (isAlive()) {//判断线程是否存活 native方法返回的boolean变量
lock.wait(0);//执行wait方法 释放锁 并等待
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
lock.wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
}

(4)wait、notify、notifyAll方法

wait、notify、notifyAll被用于线程之间的协作,等待和唤醒;这几个方法都是Object而不是Thread的

注:为什么wait、notify、notifyAll要被设计成为Object下的方法而不是Thread的?

wait、notify、notifyAll使用的前提必须在同步代码块中,但Synchronized中的锁可以为任意对象,因此wait notify notifyAll放在所有类的父类Object中,方便管理。

wait用于线程的等待,与sleep的区别在于wait会释放锁,sleep不会释放锁

notify和notifyAll用于线程的唤醒,不会释放锁;notify用于唤醒一个线程,notifyAll会通知所有线程

等待通知的标准范式

1)等待方

a 获取对象锁(同步)

b 检查条件 条件不满足 wait

c 条件满足 执行业务代码

2)通知方

a 获取对象的锁(同步)

b 修改条件

c 通知等待方

以一个生产者消费者为例:

package com.hzf.demo;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.LinkedBlockingQueue;
public class MainTest {
public static void main(String[] args) throws InterruptedException,
ExecutionException {
LinkedBlockingQueue linkedBolckingQueue = new LinkedBlockingQueue<>(
10);
new ConsumerThread(linkedBolckingQueue).start();
new ProducterThread(linkedBolckingQueue, 10).start();
}
private static class ConsumerThread extends Thread {
private LinkedBlockingQueue mQueue;
private ConsumerThread(LinkedBlockingQueue queue) {
this.mQueue = queue;
}
@Override
public void run() {
super.run();
while (true) {
//获取同步锁
synchronized (mQueue) {
while (!mQueue.isEmpty()) {
System.out.println("消费了1个");
mQueue.remove();
mQueue.notify();
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
mQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
private static class ProducterThread extends Thread {
private LinkedBlockingQueue mQueue;
private int mMaxSize;
private ProducterThread(LinkedBlockingQueue queue, int maxSize) {
this.mQueue = queue;
this.mMaxSize = maxSize;
}
@Override
public void run() {
super.run();
while (true) {
//获取同步锁
synchronized (mQueue) {
while (mQueue.size() == mMaxSize) {
try {
mQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mQueue.add(1);
System.out.println("生产了1个");
// 如果生产量达到最大值 notify消费者消费
mQueue.notify();
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}

(5)ThreadLocal的使用

4、Java线程锁的简单介绍

Java锁分类.png

(1)可重入锁&不可重入锁

可重入锁:一个线程在外层方法中获取到了锁,进入内层方法后不需要再次获取锁

Synchronized和ReentrantLock都是可重入锁

(2)独享锁&共享锁

ReentrantLock是独享锁;ReentrantReadWriteLock是共享锁

独享锁也叫排它锁,指一个线程持有该锁之后,其它线程无法获取到该锁,持有锁的线程可以读、写数据

共享锁是指该锁可以被多个线程持有,以ReentrantReadWriteLock为例,当为读操作时,所有的线程都可以并发执行进行读操作,但无法修改数据;当为写操作时,所有其它的读写线程都会被排斥,无法进行读写操作。

(3)公平锁&非公平锁

ReentrantLock可以通过其构造方法指定当前锁为公平还是非公平锁

ReentrantLock.png

公平锁指先申请的线程先拿到锁,按照申请顺序获取锁

非公平锁不一定按照申请的顺序获取锁

非公平锁的有点在于减少唤起线程的开销,整体的吞吐效率高,但处于等待的线程有可能会被饿死

(4)乐观锁&悲观锁

乐观锁和悲观锁是一种广义上的概念,乐观锁认为自己在操作数据的同时没有其它线程在修改数据,仅仅在更新数据的时候去判断有没有别的线程更改过当前数据(CAS算法,Java的原子类递增等操作都是通过CAS实现的);悲观锁认为自己在使用数据的同时一定会有别的线程修改数据,因此在获取数据的时候要先加锁,确保数据不会被其它线程锁修改,Java的Synchronized和Lock的实现类都是悲观锁。

(5)偏向锁、轻量级锁、重量级锁

偏向锁:是指同一段代码被一个线程多次获取,那么这个线程可以自动获取到锁,降低获取锁的代价;

在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。

轻量级锁:是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

重量级锁:升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。