2022-08-02 第六小组 张宁杰 多线程
目录
多线程
知识点
创建线程
在Java中,创建线程有3种方式
(1)继承Thread类,并且重写run方法
Thead类中的run方法不是抽象方法,Thread类也不是抽象类
myThread当继承了Thread类之后,它就是一个独立的线程
要让线程启动。调用线程的Start方法
当调用start方法启动一个线程时,会执行重写的run方法的代码
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();
// 普通的对象调方法
// myThread.run();
// 线程的优先级,概率问题!做不到百分百
// 90会先跑主方法 10先跑mythread
System.out.println(3);
System.out.println(4);
}
}
调用的是start,执行的是run,为什么不直接调run?
要是直接调run()方法,就和对象调方法没有区别
(2)实现Runnable接口
@FunctionalInterface:函数式接口,可以使用箭头函数
如果想要让线程启动,必须调用Thread类中的start方法
问题:实现了Runnable接口找不到start()方法了?
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);
// start
MyThread2 myThread2 = new MyThread2();
// 如果想要让线程启动,必须调用Thread类中的start方法
// 问题:实现了Runnable接口,找不到start方法了?
Thread t = new Thread(myThread2);
t.start();
System.out.println(3);
System.out.println(4);
}
}
使用箭头函数(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);
}
}
(3)实现callable接口
callable--->FutureTask-->RunnableFuture-->Runnable-->Thread
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);
// Callable-->FutureTask-->RunnableFuture-->Runnable-->Thread
FutureTask<String> futureTask = new FutureTask<>(new MyThread3());
new Thread(futureTask).start();
System.out.println(3);
System.out.println(4);
}
}
线程的优先级
概率问题,做不到百分百,90%会先跑主方法,10%先跑myThread
守护线程
Java中提供两种类型的线程:
1、用户线程
2、守护程序线程
守护线程为用户线程提供服务,仅在用户线程运行时才需要】
守护线程对于后台支持任务非常有用
垃圾回收,大多数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就变成了守护线程
ch05.setDaemon(true);
ch05.start();
}
}
线程的生命周期
从摇篮到坟墓
NEW:这个状态主要是线程未被start()调用执行
RUNNABLE:线程正在JVM中被执行,等待来自操作系统的调度
BLOCK:阻塞,因为某些原因不能立即执行需要挂起等待
WAITING:无限期等待。object类,如果没有唤醒,则一直等待
TIME_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方法的本意就是阻塞主线程
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方法的本意阻塞主线程
}
}
线程安全
1、CPU多核缓存控制
物理内存:硬盘内存(固态硬盘,尽量不要选择混合硬盘)
CPU缓存为了提高程序运行的性能,CPU的处理速度最快,内存次之,硬盘速度最快
在CPU处理内存数据时,如果内存运行速度太慢,就会拖累CPU的速度
为了解决此问题,CPU设计了多级缓存策略
CPU分为三级缓存,每个CPU都有了l1,l2缓存,但是l3缓存是多核公用的
CPU查找数据时,CPU->l1->l2->l3->内存->硬盘
从CPU到内存,60-80纳秒
从CPU到l3,15纳秒
从CPU到l2,3纳秒
从CPU到l1,1纳秒
寄存器:0.3纳秒
进一步优化,CPU每次读取一个数据,读取的是与他相邻的64个字节的数据。(缓存行)
MESI协议
1、修改态,此缓存被动过,内容与主内存中不同,为此缓存专有
2、专有态,此缓存与主内存一致,但是其他CPU中没有
3、共享态,此缓存与主内存一致,其他缓存也有
4、无效态,此缓存无效,需要从主内存中重新读取
指令重排
四条指令,四个人在四张纸上写下【恭喜发财】
java内存模型-JMM:尽量做到硬件和操作系统之间达到一致的访问效果
public class Ch01 {
{
int a = 1; // 1
int b = 2; // 2
int c = a + b; // 3
/* 指令3不能被排到1和2前面
但是1和2之间没有依赖关系,编辑器就可以重排1和2。
不会对程序的执行顺序产生干扰。
*/
}
public static void main(String[] args) {
int [] nums = new int[]{1,2,3,4,5};
for (int i = 0; i < nums.length; i++) {
System.out.println(nums[i]);
}
}
}
使用volatile关键字来保证一个变量在一次读写操作时,避免指令重排,我们在读写操作之前加入一条指令,当CPU碰到这条指令后必须等到前面的执行执行完成才能继续执行下面
package com.jsoft.afternoon;
public class Ch02 {
private static int x = 0,y = 0;
private static int a = 0,b = 0;
private static int count = 0;
private volatile static int NUM = 1;
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
for (;;) {
Thread t1 = new Thread(() -> {
a = 1;
x = b;
});
Thread t2 = new Thread(() -> {
b = 1;
y = a;
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("一共执行了:" + count++ + "次");
if(x == 0 && y ==0){
long end = System.currentTimeMillis();
System.out.println("耗时:" +(end - start) + "毫秒,(" + x + "," + y + ")");
break;
}
a = 0;b = 0;x = 0;y = 0;
}
}
/*
我们发现测试结果中大部分感觉是正确的,(0,1)或(1,0),一个是线程1先执行,一个是线程2先执行。
按道理来说,绝对不会出现(0,0),如果出现(0,0)代表存在指令重排,乱序执行。
使用volatile关键字来保证一个变量在一次读写操作时,避免指令重排。
我们在读写操作之前加入一条指令,当CPU碰到这条指令后必须等到前面的执行执行完成才能继续执行下一条指令。
【内存屏障】。
*/
}
线程之间的可见性
Thread线程一直在高速读取缓存中的isOver,不能感知主线程已经把isOVer
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;
}
}
线程的争抢
解决问题最好的办法就是【加锁】
synchronlized同步锁,线程同步,当一个方法加上了synchronized修饰,这个方法就叫做同步方法。synchronized这个关键字属于独占式悲观锁通过JVM隐式实现只允许同一时刻只有一个线程操作资源。
tLock是Lock的默认实现方式之是基于AQS(Abstract Queued Synchronizer,队列同步器)实现的默认是通过非公平锁实现的
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);
}
}
线程安全的实现方法
(1)数据不可变,一切不可变的对象一定是线程安全的,对象的方法的实现方法的调用者,不需要再进行任何的线程安全的保障措施
如final关键字修饰的基本数据类型,字符串
只要一个不可变的对象被正确地创建出来,那外部的可见状态永远都不会改变
(2)互斥同步。加锁【悲观锁】
(3)非阻塞同步。【无锁编程】,自旋。我们会用cas来实现这种非阻塞同步
(4)无同步方案。多个线程需要共享数据,但是这些数据又可以在单独的线程中计算,得出结果。我们可以吧共享数据的可见范围限制在一个线程之内,这样就无需同步,把共享的数据拿过来。我用我的,你用你的,从而保证线程安全。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();
}
}