1.认识线程(Thread)
1️⃣每个线程都是一个独立的执行流,都可以单独参与cpu的调度.
2️⃣每个进程里至少包含一个线程时(及为主线程)或多个线程,同一个进程创建多个线程时,线程会共享同一份资源(内存➕ 文件描述符.)
⚠️:多个进程之间并不会共享同一份资源
3️⃣ 线程是系统调度执行的基本单位
进程是系统分配资源的基本单位
4️⃣(同一个进程)多线程时,每个线程执行先后的顺序并不确定 因为在操作系统内核中有个“调度器的模块”,实现方式时‘随机调度’,‘抢占式调度’
2.线程与进程的优劣势.
1️⃣.创建线程比创建进程更快
2️⃣ .销毁线程比销毁进程更快
3️⃣.调度线程比调度进程更快
3.创建线程(Thread)
方法一:继承Thread类
1️⃣继承Thread来创建一个线程类
重写run方法,run表示线程的入口,而mian表示主线程入口
//) 继承 Thread 来创建一个线程类.
class MyThread extends Thread{
@Override // @Override表示机器会在执行时检查一遍是否重写run方法
public void run() {
System.out.println("正在运行,run方法");
}
}
2️⃣ 创建MyThread的实例
//调用 start 方法启动线程 才是真正的调用系统的API
t.start();
根据MyThread类,来创建的实例(才是真正的线程)
Thread t = new MyThread();
3️⃣调用MyThread父类Thread中成员方法 start(作用启动线程)
//调用 start 方法启动线程 才是真正的调用系统的API
t.start();
4️⃣ sleep()方法是一个静态方法,属于Thread类,用于让当前正在执行的线程暂停执行一段时间
在重写run方法中创建sleep()方法时会报错 可以选try/catch解决此问题
在main中使用sleep方法时 也会出现报错 但是这里有两种解决方式try/catch和throws InterruptedException
引出一个问题为什么run只有一种解决方式呢?
解答:因为父类Thread中没有throws这个异常, 如果加上throws,则修改了方法签名,够不成重写了
子类run是重写的,就不可以使throws异常
//括号里面数字(毫秒单位)代表休眠10000ms==1s
Thread.sleep(10000);
整体代码:
//) 继承 Thread 来创建一个线程类.
class MyThread extends Thread{
@Override // @Override表示机器会在执行时检查一遍是否重写run方法
public void run() {
while(true) {
try {
//Thread类中sleep方法表示
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("正在运行,run方法");
}
}
}
public class ThreadDome1 {
public static void main(String[] args) throws InterruptedException {
//2.根据Mythread类,来创建实列(才是真正的线程)
//根据
Thread t =new MyThread();
Thread.sleep(1000);
//3) 调用 start 方法启动线程 才是真正的调用系统的API
t.start();
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("正在运行,main方法");
}
}
}
方法二:实现Runnable接口(可执行的)
需要搭配Thread类,才能在系统中真正创建出线程
//实现Runnable接口
class MyRunnable implements Runnable{
@Override
public void run() {
while(true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("正在运行,Thread/run");
}
}
}
public class ThreadDome2 {
public static void main(String[] args) throws InterruptedException {
//2) 创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数.
//Runnable runnable= new MyRunnable();
Thread t = new Thread(new MyRunnable());
//3.调用Thread类中的start方法
//表示线程开始
System.out.println("要开始创建线程了");
t.start();
while (true) {
Thread.sleep(1000);
System.out.println("正在运行,main");
}
}
}
方法三:实现匿名内部类,创建Thread的子类对象
匿名内部类表示没有名字,不能重复使用,用一次就扔了
其中代码中变量 t并非指向的是Thread,而是Thread的子类,具体是那个子类并不知道,因为匿名隐藏了
public class ThreadDome3 {
//1.实现匿名内部类, 创建Thread的子类对象
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(){
@Override
public void run() {
System.out.println("使用匿名类创建 Thread 子类对象");
}
};
//启动线程
t.start();
while(true){
Thread.sleep(1000);
System.out.println("运行maim");
}
}
}
方法四:实现匿名内部类,创建Runnable字类对象
public class ThreadDome4 {
public static boolean fla = true;
public static void main(String[] args) throws InterruptedException {
//实现匿名内部类,创建Runnable子类对象
Thread t = new Thread(new Runnable() {
@Override
public void run() {
//条件fla为真运行,
while (fla){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("正在运行run");
}
}
});
//启动线程
t.start();
for (int i = 0; i < 5; i++) {
Thread.sleep(1000);
System.out.println("正在运行main方法");
}
//主线程for循环执行完,fla=false,此时在执行匿名内部类run时循环条件为假跳出循环
fla=false;
}
}
方法五:lambda表达式 (推荐/常用)
这个写法相当于Runnable重写run方法, lambda代替了Runnable位置
lambda表达式,其实本质来讲,就是⼀个匿名函数。因此在写lambda表达式的时候,不需要关心方法名是什么。
实际上,我们在写lambda表达式的时候,也不需要关心返回值类型。
我们在写lambda表达式的时候,只需要关注两部分内容即可:参数列表和方法体
初始代码:
public class ThreadDome5 {
public static void main(String[] args) {
//lambda表达式这个写法代替了Runnable重新run方法.
//
Thread t = new Thread(() ->{
while(true){
System.out.println("正在运行,Thread");
try {
Thread.sleep(1001);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
//启动新创建的线程
t.start();
System.out.println("hello main");
}
}
使用多线程可以提高运行的效率
4.Thread的常用构造方法
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
5.Thread的常用属性
自己创建的线程ID默认按照Thread-0,Thread-1;
isDaemon属性作用:
守护线程也称为是否是后台线程
前台线程只要正在运行就会阻止进程结束,而后台线程并不会阻止
咱们创建的线程,默认为前台线程,会阻止进程结束,即使main(主线程)已经执行完了,只要创建的前台线程没有结束,进程就不会结束
//在start之前设置,设置为true 成为后台线程,不设置为前台线程 (不可以在start之后设置)
//前台线程会阻止线程结束, 后台线程并不会
t.isDaemon(true);
is Alive属性:
在真正创建出线程之前为false,start()只有内核中创建出PCB之后 isAlive为true
在run线程结束后内核中的线程也就结束了(内核pcb释放)isAlive为false
6.启动线程 :
1️⃣:start方法会调用到系统的API,到系统内核中创建线程
2️⃣:run方法(只是描述当前线程具体执行什么内容)
面试题:start和run的区别?
run
方法:定义了线程的执行行为,即线程启动后需要执行的具体操作。start
方法:负责启动一个新的线程,并让这个新线程去执行run
方法中定义的代码。- 关键区别在于 : run 方法本身并不会创建或启动任何新的线程,它只是一个普通的方法调用。 而 start 方法才是真正触发线程并发执行的关键。
7.中断线程:
(就是让run方法结束运行)
目前常见的有以下两种方式:
1. 通过共享的标记来进行沟通
2. 调用 interrupt() 方法来通知
方法一:自定义变量作为标志为run方法的结束条件
在main方法中设置fla为true,来结束run循环,
方法二:使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定
义标志位.(推荐)
// 终断t线程的睡眠(这种终断睡眠的方式依靠了java的异常处理机制。)
t.interrupt();
// t.stop(); //强行终止线程
//缺点:容易损坏数据 线程没有保存的数据容易丢失
可以使用t.interrupt()方法来清除标志位 因为异常报错提前唤醒sleep
8.等待线程 :
join方法:让一个线程A,等待另一个线程B执行结束 后,线程A在执行
在主线程执行t.join,就是让主线程等待t线程结束
9.获取当前线程的引用
//获取当前线程的引用
Thread.currentThread()
//输出
System.out.println(Thread.currentThread().getName());
10.线程的状态 :
1️⃣新建状态(NEW):Thread的对象已经有了,但还没有调用start方法
2️⃣结束状态(terminated):Thread对象还在,内核中的线程已经结束了
3️⃣就绪状态(Runnable):线程已经在cpu运行的,和等待cpu运行的线程
4️⃣阻塞状态(TIMED_WAITING):可能产生于slee方法/join/用户输入等待 产生阻塞
等待阻塞(WAITING):产生于wait()方法,线程会释放占用资源,等待其他线程调用notify(唤醒单个线程)或isnotify(唤醒全部线程)。
同步阻塞(BLOCKED):产生于锁竞争导
11.线程安全 :
例子:
public class ThreadDome7 {
//创建一个全局变量sum
static int sum =0;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() ->{
for (int i = 0; i < 50000; i++) {
sum++;
}
});
Thread t1 = new Thread(() ->{
for (int i = 0; i < 50000; i++) {
sum++;
}
});
t.start();
t1.start();
t.join();
t1.join();
System.out.println(sum);
}
}
问题 要是单线程中执行绝对是正确的
但是在多线程中并发执行,因为多线程中线程的调度是随机调度的,此时逻辑就出现问题 这种情况就是bug
出现线程安全的原因:
1️⃣.操作系统中,线程调度是随机调度和抢占式调度 (罪魁祸首)
2️⃣:两个线程,对同一个变量进行修改
3️⃣:内存可见性:当线程已经修改了变量的值,但其他线程并没看见而继续用之前的变量的值,导致出现线程安全问题
(可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到)
4️⃣:指令重排序问题
5️⃣:修改操作(不是原子的)
原子性:指一个操作是不可分割的,不可中断的,不受其他线程影响
12.变量对线程安全的影响
实例变量:在堆中。
静态变量:在方法区。
局部变量:在栈中。
以上三大变量中:
局部变量永远都不会存在线程安全问题。
因为局部变量不共享。(一个线程一个栈。)
局部变量在栈中。所以局部变量永远都不会共享。
实例变量在堆中,堆只有1个。
静态变量在方法区中,方法区只有1个。
堆和方法区都是多线程共享的,所以可能存在线程安全问题。
局部变量+常量:不会有线程安全问题。
成员变量:可能会有线程安全问题。
13.如何解决线程安全的问题 :
synchronized关键字: 加锁—目的:把多个操作打包成一个操作具有原子性
进行加锁的时候,需要先准备好锁对象 (加锁和解锁都是针对锁对象进行的)
当线程A对成员A进行加锁的操作时,线程B也想对成员A进行加锁 就会导致出现阻塞(BLOCKED),只有线程A执行完阻塞才结束,线程B才可以对成员A加锁
对Objeck变量进行加锁:
public class ThreadDome7 {
//随便创建一个Object变量
static Object object =new Object();
static int sum =0;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() ->{
for (int i = 0; i < 50000; i++) {
synchronized (object) {
sum++;
}
}
});
Thread t1 = new Thread(() ->{
for (int i = 0; i < 50000; i++) {
synchronized (object) {
sum++;
}
}
});
t.start();
t1.start();
t.join();
t1.join();
//这里预期应该是100000
System.out.println(sum);
}
}
synchronized中进入"{}"表示开始加锁,出"{}"表示加锁结束
synchronized的的特征:(可重入)
还有的虽然没有加锁, 但是不涉及 "修改", 仍然是线程安全的:String
14.死锁
1️⃣:加锁可以解决线程的安全问题,加锁使用不当时就会产生死锁
2️⃣:产生死锁的情况有以下情况:
1.一个线程,一把锁:(如果锁不是可重入锁)同一个线程对这把锁锁两次,就会出现死锁
2.二个线程,二把锁:这二个线程已经各锁了一把锁时,还想锁对方的锁,就会出现死锁
3.N个线程,M锁;(哲学家就餐问题)
3️⃣:产生死锁的必要条件:
1.互斥使用:获得锁的过程是互斥的,一个线程拿到这把锁,另个一个线程也想拿到,就需要阻塞等待
2.不可抢占:一个线程拿到这把锁,除非该线程主动解锁,否则别的线程不能抢走
3.请求保持:一个线程拿到一把锁后,继续尝试获取下一把锁
4.循环等待/环路等待:在发生死锁时,必然存在一个线程--资源的环形链
15.解决死锁问题
有许多中方法:
- 资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
- 只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
- 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件
- 资源有序分配法(引入加锁顺序):系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件
1️⃣: 产生死锁有四个必要条件, 我们只需破坏一个必要条件就好了
最容易破坏的是条件四 我们只需指定加锁的顺序就好了
16.保证内存可见性:volatile关键字
import java.util.Scanner;
//没有加入volatile的代码
public class ThreadDome8 {
static int fal=0;
public static void main(String[] args) {
Thread t =new Thread(() ->{
while(fal==0){
}
System.out.println("循环结束");
});
Thread t1 =new Thread(() ->{
Scanner scanner =new Scanner(System.in);
System.out.println("请输入大于0得数:");
fal= scanner.nextInt();
});
t.start();
t1.start();
}
}
//执行效果t线程并没有获取的t1线程用户输入的值,没有跳出循环
t线程没有感知不到t1线程fal的变化
1️⃣:volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了
当我们引入volatile关键字时 给fal加入volatile 就可以跳出循环了
但是volatile 不保证原子性
volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见
性.
synchronized 也能保证内存可见性
synchronized 既能保证原子性, 也能保证内存可见性.
13.wait(等待) 和 notify(通知) 关键字
1️⃣wait需要配合synchronized来使用(前提是这俩的锁是同一个对象),否则报错
2️⃣wait()方法是让当前线程等待的,即让线程释放了对共享对象的锁,不再继续向下执行。
3️⃣wait(long timeout)方法可以指定一个超时时间,过了这个时间如果没有被notify()唤醒,
则函数还是会返回。如果传递一个负数timeout会抛出IllegalArgumentException异常。
4️⃣notify()方法会让调用了wait()系列方法的一个线程释放锁,并通知其它正在等待(调用了wait()方法)的线程得到锁。
5️⃣notifyAll()方法会唤醒所有在共享变量上由于调用wait系列方法而被挂起的线程。
public class ThreadDome9 {
public static void main(String[] args) {
Object object =new Object();
Thread t =new Thread(() ->{
synchronized (object){
System.out.println(Thread.currentThread().getName() + " t wait之前.");
try {
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + " t wait之后.");
}
});
Thread t1 =new Thread(() ->{
try {
Thread.sleep(2000);
synchronized (object){
System.out.println(Thread.currentThread().getName() + " t1 notify之前");
object.notify();
System.out.println(Thread.currentThread().getName() + " t1 notify之后");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t.start();
t1.start();
}
}
//结果
Thread-0 t wait 之前
Thread-1 t1 notify 之前
Thread-1 t1 notify 之后
Thread-0 t wait 之后
join和wait区别是:
join是需要等待 调用此方法的线程结束后,才继续执行
wait是需要另一个线程通notify进行通知(不要求另一个线程必须执行完)
标签:Java,Thread,EE,start,run,线程,new,多线程,方法 From: https://blog.csdn.net/2401_83177222/article/details/142278682