一、基本概念
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方式
imagestop方法被官方定义为弃用的;因为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 imagejoin方法的实现原理:
正常调用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<Integer> linkedBolckingQueue = new LinkedBlockingQueue<>(
10);
new ConsumerThread(linkedBolckingQueue).start();
new ProducterThread(linkedBolckingQueue, 10).start();
}
private static class ConsumerThread extends Thread {
private LinkedBlockingQueue<Integer> mQueue;
private ConsumerThread(LinkedBlockingQueue<Integer> 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<Integer> mQueue;
private int mMaxSize;
private ProducterThread(LinkedBlockingQueue<Integer> 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中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。
一篇美团技术团队关于锁的讲解:https://tech.meituan.com/2018/11/15/java-lock.html
网友评论