一.创建线程
在Java中,创建线程有3种方式
(1)继承Thread类,并且重写run方法
(2) Thread类中的方法不是抽象方法,Thread也不是抽象类
(3)MyThread类继承了Thread之后,他就是一个线程类
**启动线程,执行重写的run方法·4
//重写run方法
class MyThread extends Thread{
@Override
public void run() {
System.out.println("重写的run方法。。。");
}
}
**要想让线程启动,调用线程的start方法
public class Ch01 {
public static void main(String[] args) {
System.out.println(1);
MyThread myThread=new MyThread();
//当调用start方法启动一个线程时会执行重写的run方法的代码
//调用的是start,执行的是run,为什么不直接调run
myThread.start();
//普通的对象调方法,没有启动线程
//myThread.run();
//线程的优先级,概率问题,做不到百分百
//90%会先跑主方法,10%的几率会跑MyThread中的run方法
System.out.println(3);
System.out.println(4);
}
}
思考:调用的是start,执行的是run,那为什么不直接调run呢?
结论:用普通的对象调方法,不会启动线程。
如果想让线程启动,必须调用Thread中的start方法。
(4)线程的优先级:
概率问题,做不到百分百,有90%的会先跑主方法,10%的几率会跑MyThread中的run方法
二.实现Runnable接口:
创建实现类的线程对象
调用start方法
public class Ch02 {
public 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);
}
}
三.使用箭头函数(lambda)
Lambda表达式格式:()-> {}
(参数列表) -> {代码}
格式说明:
- 小括内的语法与传统方法参数列表一致,没有参数就留空,有多个参数就用逗号分隔
- 【->】 是新引入的语法格式,代表指向动作
- 大括号内的语法与传统方法体要求一致
1.小括号中书写的内容和接口中的抽象方法的参数列表一致
2.大括号中书写的内容和实现接口中的抽象方法体一致
3.箭头是固定的
public class Ch03 {
public Ch03() {
}
public static void main(String[] args) {
System.out.println(1);
(new Thread(() -> {
System.out.println(2);
})).start();
try {
Thread.sleep(1000L);
} catch (InterruptedException var2) {
var2.printStackTrace();
}
System.out.println(3);
System.out.println(4);
}
}
四.实现Callable接口
、定义一个线程任务类实现Callable接口,声明线程执行的结果类型。
2、重写线程任务类的call()方法,这个方法可以直接返回执行的结果。
3、创建一个Callable的线程任务对象。
4、把Callable的线程任务对象包装成一个未来任务对象。
5、把未来任务对象包装成线程对象。
6、调用线程start()方法,启动线程。
7、获取线程执行结果。
————————————————
优点:
线程任务只是实现了Callable接口,可以继续继承其他类,而且可以继续实现其他接口。
同一个线程任务对象可以被包装成多个线程对象。
适合多个线程去共享同一个资源。
实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立。
线程池可以放入Runnable接口和Callable接口。
可以得到线程执行结果。
————————————————
package com.zjl.study.多线程;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
/**
* Created by zjl 2022/5/28
**/
public class 创建线程方式3 {
public static void main(String[] args) {
// 3、创建一个Callable的线程任务对象。
MyCallable myCallable = new MyCallable();
// 4、把Callable的线程任务对象包装成一个未来任务对象。
FutureTask<String> futureTask = new FutureTask<>(myCallable);
// 5、把未来任务对象包装成线程对象。
Thread thread = new Thread(futureTask);
// 6、调用线程start()方法,启动线程。
thread.start();
// 7、获取线程执行结果。如果此时获取结果的任务还未执行完成,会让出CPU,直至任务执行完成才获取结果。
try {
String s = futureTask.get();
System.out.println(s);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 1、定义一个线程任务类实现Callable接口,声明线程执行的结果类型。
class MyCallable implements Callable<String> {
// 2、重写线程任务类的call()方法,这个方法可以直接返回执行的结果。
@Override
public String call() throws Exception {
return "子线程任务执行,线程名称为:" + Thread.currentThread().getName();
}
}
五.守护线程
java里面提供两种类型的线程:用户线程 , 守护线程
1.用户线程(我们创建线程时,默认的一类线程,属性 daemon = false
)
1.守护线程(属性 daemon = true
)
用户线程和守护线程的关系:
用户线程就是运行在前台的线程,
守护线程就是运行在后台的线程
一般情况下,守护线程是为用户线程提供一些服务 (典型的守护线程是垃圾回收线程 )
当不存在非守护线程时,守护线程自动销毁。
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中被执行,等待来自操作系统的调度
* 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);
}
}
七.网络安全
1.cpu多核缓存结构
cpu缓存为了提高程序运行的性能,现在cpu在很多方面对程序进行优化
cpu处理速度最快,内存次之,硬盘速度最低
在cpu处理内存数据时,如果内存运行速度太慢,就会拖累cpu的速度,为了解决这样的问题,cpu设计了多种缓存数据
2.cpu的缓存:L1 ,L2 ,L3
每个cpu都有L1 L2 缓存,但是L3缓存是多核公用的
cpu查找数据时,cpu->L1->L2->L3->内存->硬盘
从cpu到内存 60~80纳秒 从cpu到L3 15纳秒 从cpu到L2 3纳秒 从cpu到L1 1纳秒 到寄存器 0.3纳秒
3.指令重排
我们发现测试结果中大部分感觉是正确的(0,1)或(1,0)
按道理来说绝对不会出现(0,0),如果出现(0,0)代表存在指令重排,乱序执行
使用volatile关键字保证一个变量在一次读写操作时,避免指令重排
我们在读写操作之前加入一条指令,当CPU碰到这条指令时必须等到前面的指令执行完,才能执行下一条指令
public class Ch02 {
private static int x=0,y=0;
private static int a=0,b=0;
private static int count =0;
private static volatile 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)
按道理来说绝对不会出现(0,0),如果出现(0,0)代表存在指令重排,乱序执行
使用volatile关键字保证一个变量在一次读写操作时,避免指令重排
我们在读写操作之前加入一条指令,当CPU碰到这条指令时必须等到前面的指令执行完,才能执行下一条指令
【内存屏障】
* */
}
}
}
4.可见性
可见性
thread线程一直在高速读取缓存中的isOver,不能感知主线程已经把isOver
这就是线程的可见性的问题
怎么解决?
volatile能够强制改变变量的读写直接在内存中操作
5.线程争抢
说到解决多线程中资源争抢的问题,大多数第一个想到的关键字就是synchronized,它能够保证多个线程之间资源访问的同步性(即它修饰的方法或者代码块在任意时刻只能有一个线程执行)。
一个座位一个人 两个电影窗口卖票 不加锁会造成什么结果?
public class Seat implements Runnable {
private int seatNumber = 100;
@Override
public void run() {
while (true) {
if (seatNumber > 0) {
try {
Thread.sleep(30);
--seatNumber;
System.out.println(Thread.currentThread().getName() + "占用1个座位,还剩余 " + seatNumber + "个座位");
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(Thread.currentThread().getName() + ":不好意思,票卖完了!");
break;
}
}
}
public static void main(String[] args) {
Seat mr = new Seat();
Thread t1 = new Thread(mr, "A窗口");
Thread t2 = new Thread(mr, "B窗口");
t1.start();
t2.start();
}
}
6.线程安全的实现方法
第一种实现线程安全的方式:同步代码块,即用synchronized关键字
第二种方法:同步方法,也是用synchronized关键字,只是这个关键字用在方法上了,把线程共享的数据块抽象成方法,在方法上加了同步锁。
第三种方法:使用Lock锁机制,对线程不安全的代码块采用lock()加锁,使用unlock()解锁。