前言
在学习多线程之前,我们需要学习一些计算机组成的一些知识。
当去开一个虚拟机,开一个数据库,开git,开xshell,开idea, 开Nginx,开redis,启动N多个微服务。
就需要一台高性能配置的电脑(cpu8核和运行内存16G。)
为了完成特定的任务,用某种编程语言写一个软件(程序),程序要想运行就必须加载到内存中执行。
在执行程序的时候,实时的指令加载到cpu内存的指令寄存器中执行,执行过程中产生的数据要加载到数据寄存器中,ALU负责进行算术逻辑运算的操作。
系统总线(System Bus
):连接计算机系统的主要组件,用来降低成本和促进模块化。 可以通过软件来控制硬件。
进程: 一个正在执行中的程序就是一个进程,系统就会为这个进程发配独立的【运行资源】 进程是程序的一次执行过程,它有自己的生命周期 它会在启动程序时产生 运行程序时存在 关闭程序时消亡
比如:QQ,idea,腾讯会议,PDF 早期的计算机。
单进程:同一时间只能执行一个进程;计算器:同一时间只能执行一段代码。
随着计算机的发展,CPU的计算能力大幅提升, 按照时间线交替执行不同继承的方式。 每个执行一点点时间,感官上觉得这么多的进程是同时在运行。
进程更强调的是【内存资源分配】;线程更强调的是【计算的资源的分配】
因为有了线程的概念,一个进程的线程不能修改另一个线程的数据,线程之间是相互隔离的, 安全性更好。理论上,一个核在一个时间点只能跑一个线程,但是cpu同一个时间能跑16线程
- 物理cpu内核,每颗物理CPU可以有1个或多个物理内核, 通常情况下物理CPU内核数都是固定,单核CPU就只有1个物理内核
- 逻辑CPU内核,操作系统可以使用逻辑CPU来模拟真实CPU。 在没有多核心CPU情况下,单核CPU只有一个核,可以把一个CPU当做多个 CPU使用,逻辑CPU的个数就是作用的CPU物理内核数。
如何创建线程
1. 继承Thread
类,并且重写run
方法
Thread
类中的run
方法不是抽象方法,Thread
类也不是抽象类MyThread
当继承了Thread
类之后,它就是一个独立的线程。要让线程启动。调用线程的start
方法。
代码部分
class MyThread extends Thread {
@Override
public void run() {
System.out.println(2);
}
}
public class Ch01 {
public static void main(String[] args) {
System.out.println(1);
MyThread myThread = new MyThread();
myThread.start();
System.out.println(3);
System.out.println(4);
}
}
当调用start
方法启动一个线程时,会执行重写的run
方法的代码
调用的是start
(启动线程),执行的是run
(使用方法)。普通的对象调方法,myThread.run();
线程有优先级,存在概率问题!做不到百分百。可能会优先主方法,然后运行mythread
也就是说,输出结果可能为1,2,3,4
,也有可能是1,3,4,2
等等
使用箭头函数(lambda表达式)
public class Ch03 {
public static void main(String[] args) {
System.out.println(1);
new Thread(() -> System.out.println(2)).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(3);
System.out.println(4);
}
}
由于启动新线程后主线程进入了休眠Thread.sleep
,再执行3.sout 4.sout
输出结果一般为1 2 3 4
2. 实现Runnable
接口,并重写run
方法
代码部分
class MyThread2 implements Runnable {
@Override
public void run() {
System.out.println(2);
}
}
public class Ch02 {
public static void main(String[] args) {
System.out.println(1);
MyThread2 myThread2 = new MyThread2();
Thread t = new Thread(myThread2);
t.start();
System.out.println(3);
System.out.println(4);
}
}
如果想要让线程启动,必须调用Thread类中的start方法
但是Runnable
无法使用start
方法,需要先创建Thread
对象,然后将线程对象传进去。
输出与第一个方法相同,存在着优先级随机的可能性。
3. 实现Callable
接口
代码部分
class MyThread3 implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println(2);
return "call方法的返回值";
}
}
public class Ch04 {
public static void main(String[] args) {
System.out.println(1);
FutureTask<String> futureTask = new FutureTask<>(new MyThread3());
new Thread(futureTask).start();
System.out.println(3);
System.out.println(4);
}
}
一般不使用Callable
,因为它需要层层调用
Callable-->FutureTask-->RunnableFuture-->Runnable-->Thread
守护线程
Java中提供两种类型的线程:
- 用户线程
- 守护程序线程
守护线程为用户线程提供服务,仅在用户线程运行时才需要。
守护线程对于后台支持任务非常有用,他可以垃圾回收。大多数JVM线程都是守护线程。QQ,主程序等就是用户线程。
任何线程继承创建它的线程守护进程状态。由于主线程是用户线程,因此在main
方法内启动的任何线程默认都是守护线程。
创建守护线程
public class Ch05 extends Thread {
@Override
public void run() {
super.run();
}
public static void main(String[] args) {
Ch05 ch05 = new Ch05();
ch05.setDaemon(true); // ch05就变成了守护线程
ch05.start();
}
}
线程的生命周期
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。
在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。
尤其是当线程启动以后,它不可能一直"霸占"着 CPU 独自运行,所以CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。
- NEW:这个状态主要是线程未被start()调用执行
- RUNNABLE:线程正在JVM中被执行,等待来自操作系统的调度
- BLOCKED:阻塞。因为某些原因不能立即执行需要挂起等待。
- WAITING:无限期等待。Object类。如果没有唤醒,则一直等待。
- TIMED_WAITING:有限期等待,线程等待一个指定的时间
- TERMINATED:终止线程的状态,线程已经执行完毕。
public class Ch06 {
public static void sleep(int i) {
try {
// 线程休眠1秒
Thread.sleep(i);
System.out.println("哈哈哈...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
sleep(3000);
}
}
等待和阻塞两个概念有点像,阻塞因为外部原因,需要等待,
而等待一般是主动调用方法,发起主动的等待。等待还可以传入参数确定等待时间。
线程插队join
t1
线程与t2
线程同时启动,但是t1
使用了join
进行插队,此时线程t1
就进入阻塞状态,直到线程t2
完全执行完成,线程t1
才结束阻塞状态,所以t1
会先运行(也存在着不确定性)
public class Ch07 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100);
System.out.println("这是线程1---->" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100);
System.out.println("这是线程2---->" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
try {
// t1插队
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("----------------------");
// 分割线出现的位置,join方法的本意阻塞主线程
}
}
CPU多核缓存结构
物理内存:硬盘内存。(M.2固态硬盘速率目前最高,机械硬盘速率很低)
CPU缓存为了提高程序运行的性能,现在CPU在很多方面对程序进行优化。
CPU处理速度最快,内存次之,硬盘速度最低。
在CPU处理内存数据时,如果内存运行速度太慢,就会拖累CPU的速度为了解决这样的问题,CPU设计了多级缓存策略。
CPU分为三级缓存:每个CPU都有L1,L2缓存,但是L3缓存是多核公用的。
CPU查找数据时,CPU -> l1 -> l2 -> l3 -> 内存 -> 硬盘
英特尔提出了一个协议MESI协议
- 修改态,此缓存被动过,内容与主内存中不同,为此缓存专有
- 专有态,此缓存与主内存一致,但是其他CPU中没有
- 共享态,此缓存与主内存一致,其他的缓存也有
- 无效态,此缓存无效,需要从主内存中重新读取
线程安全
指令重排
指令重排是指,代码在写的时候是有顺序的,但是实际执行可能会重新排数据
{
int a = 1; // 1
int b = 2; // 2
int c = a + b; // 3
}
上面代码的情况,是有一定顺序的
执行第一行或者第二行代码,是可以不按照顺序来的,因为他们之间没有依赖关系
但是第三行代码必须依赖于第一行与第二行,一定会在他们后面执行
这就产生了指令重排,但是不会对程序的执行顺序产生干扰!
可见性
thread
线程一直在高速读取缓存中的isOver
,不能感知主线程已经把isOVer
改成了true
,这就是线程的可见性的问题。
那么该怎么解决?使用volatile
定义能够强制改变变量的读写直接在内存中操作。
public class Ch03 {
private volatile static boolean isOver = false;
private static int number = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while(!isOver){
}
System.out.println(number);
}
});
thread.start();
Thread.sleep(1000);
number = 50;
isOver = true;
}
}
线程争抢
解决线程争抢的问题最好的办法就是【加锁】,synchronized
同步锁,线程同步.
当一个方法加上了synchronized
修饰,这个方法就叫做同步方法。
public class Ch04 {
private volatile static int count = 0;
public synchronized static void add() {
count ++;
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最后的结果是:" + count);
}
}
结果输出为:20000
线程安全的实现方法
- 数据不可变。
一切不可变的对象一定是线程安全的。
对象的方法的实现方法的调用者,不需要再进行任何的线程安全的保障措施。
比如final关键字修饰的基本数据类型,字符串。
只要一个不可变的对象被正确的创建出来,那外部的可见状态永远都不会改变。 - 互斥同步。加锁。【悲观锁】
- 非阻塞同步。【无锁编程】,自旋。我们会用cas来实现这种非阻塞同步。
- 无同步方案。多个线程需要共享数据,但是这些数据又可以在单独的线程中计算,得出结果
我们可以把共享数据的可见范围限制在一个线程之内,这样就无需同步。把共享的数据拿过来,
我用我的,你用你的,从而保证线程安全。使用ThreadLocal
。
public class Ch05 {
private final static ThreadLocal<Integer> number = new ThreadLocal<>();
private static int count = 0;
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// t1内部自己用一个count
number.set(count);
for (int i = 0; i < 10; i++) {
number.set(count ++);
System.out.println("t1-----" + number.get());
}
}
});
Thread t2= new Thread(new Runnable() {
@Override
public void run() {
// t2内部自己用一个count
number.set(count);
for (int i = 0; i < 10; i++) {
number.set(count ++);
System.out.println("t2-----" + number.get());
}
}
});
t1.start();
t2.start();
}
}
制作同时卖票
通过使用synchronized
进行加锁
定义private static final Object lock = new Object();
在run()
方法的循环中,利用sleep
给予延迟
public class Ticket implements Runnable{
private static final Object lock = new Object();
private static Integer count = 100;
String name;
public Ticket(String name) {
this.name = name;
}
@Override
public void run() {
while(Ticket.count > 0){
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (Ticket.lock){
System.out.println(name + "出票一张,还剩:" + Ticket.count-- + "张!");
}
}
}
public static void main(String[] args) {
Thread one = new Thread(new Ticket("一号窗口"));
Thread two = new Thread(new Ticket("二号窗口"));
one.start();
two.start();
}
}
对火来说简单的事情,对于风来说却未必;
对鱼来说简单的事情,对鸟来说却未必;
对树根来说简单的事情,对树枝也未必。
—— 于尔克·舒比格 《当世界年纪还小的时候》