1.什么是线程
线程(Thread)是操作系统分配处理器时间的基本单位,也是程序执行的最小单元。一个进程中可以包含多个线程,这些线程可以并行或并发地执行,共享同一进程的资源。
线程就是程序执行过程中的一个打工仔,程序执行离不开进程,线程就是进程单位下的打工仔,它的作用就是让打工效率更高。
2.什么是进程
进程(Process)是正在运行的程序的实例,他是操作系统分配资源和调度人物的基本单位。
每个进程都有自己独立的执行上下文和内存空间等,进程之间不可以直接相互访问,需要通过操作系统提供的机制(进程通信)才能进行数据交换和共享。
一个进程由一个或多个线程组成,这些线程可以并发或者并行执行不同的任务,因此,线程的存在大大增加了进程执行任务的效率和程序的响应速度。
3.进程和线程有什么关联
- 进程中包含一个或者多个线程,每个进程中都有一个main线程,它可以开辟新的子线程
- 线程不会凭空存在,每一个线程都独属于的某个进程,共享这个进程的资源。
- 线程是进程的执行单位,线程可以为进程处理并发的任务。
4.在Java中如何创建线程
创建线程有多种方式,这里只叙述了两种。
4.1继承Thread类
一个类继承了Thread类,就可以作为线程使用,一般会重写Thread里面的run()方法,写上2自己的业务逻辑(Thread类中的run()方法实际上是实现了Runnable接口里面的run()方法)。
Code:Cat类可以作为线程使用
class Cat extends Thread {
@Override
public void run() {
int i = 0;
while (true) {
//该线程每隔一秒。在控制台输出一段话
System.out.println("喵喵喵~~~~~,线程名" + Thread.currentThread().getName() + ++i);
try {
Thread.sleep(1000);//线程休眠一秒
} catch (InterruptedException e) {
e.printStackTrace();
}
if (i == 80) {
break;
}
}
}
}
/**
* 如果主线程已经结束了,如果此时程序内还有其他线程在执行,它并不会导致这个进程的结束
* 只有当所有线程结束,这个进程才会结束
*/
public class Thread01 {
public static void main(String[] args) throws InterruptedException {
//1.当运行这个程序,底层会开启一个进程,进程会执行到main方法,这个进程会开启一个主(main)线程
//2.主线程里面又会开辟一个新的线程(可以叫做子线程),也就是当执行了start()的时候
//创建Cat对象,可以当作线程使用
Cat cat = new Cat();
//会执行Cat的run()方法
cat.start();//启动线程--->会自动执行run方法
//3.当main线程执行了start开启一个新的线程的时候,main线程不会阻塞,会继续执行
int j = 0;
System.out.println("主线程开启线程后不会阻塞,会继续执行");
for (int i = 0; i < 60; i++) {
System.out.println("主线程 i=" + i);
Thread.sleep(1000);
}//4.主线程和子线程会交替执行
}
}
4.2实现Runnable接口
一个类实现了Runable接口,也可以作为一个线程使用,实现里面的run()方法编写自己的逻辑。
注意点:实现Runnable接口实现里面的run()方法后,发现在main函数里面直接调用start()方法没用,因为不存在该方法;同时也不能直接调用run()方法,因为这样还是调用一个普通函数。
解决办法:在Thread类的构造函数中,传入dog对象作为参数。这意味着该线程对象将执行dog对象中定义的任务或逻辑。
Code:Dog实现了Runnable接口就可以作为线程使用
package com.hua.runnable;
public class TestRunnableCreate {
public static void main(String[] args) {
Dog dog = new Dog();
//当使用Runnable接口实现多线程的方式不能直接调用start方法,采取以下方式实现
Thread thread = new Thread(dog);
//底层使用了静态代理设计模式
thread.start();
}
}
/**
* 实现多线程的方式二:实现一个Runnable接口
*/
class Dog implements Runnable {
@Override
public void run() {
int count = 0;
while (true) {
System.out.println("小狗汪汪叫。。。hi" + count + "-" + Thread.currentThread().getName());
count++;
try {
Thread.sleep(1000);
if (count == 10) {
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
疑问:为什么不直接调用Cat里面的run()方法呢?而是通过调用start()方法。
1.如果直接在main函数里面用Cat.run()调用方法的话,就是一个普通的调用,会在当前线程中串行执行,并没有创建出一个线程然后调用run()方法,并非并发或并行执行,而且通过Thread.currentThread().getName()得到的线程名是main线程。
2.start()方法负责完成线程的初始化工作并启动线程的执行。在调用
start()
方法后,线程会进入就绪状态,并等待系统调度来执行。3.通过调用
start()
方法,系统会为线程分配 CPU 时间片,并与其他线程并发执行,实现真正的多线程并发。总结:使用 start() 方法启动线程能够实现真正的并发执行,而直接调用 run
()
方法只是普通的方法调用,并没有创建新的线程。
5.注意点
真正实现多线程的不是run()方法,而是start()底层的start0()方法,它是一个native方法(底层由c/c++实现),由JVM自动调用,我们无法调用它。
6.线程常用方法
- setName---------//设置线程名称
- getName---------//得到该线程的名称
- start---------------//使线程开始执行;JVM底层调用该线程的start0方法
- run----------------//调用线程对象run方法
- setPriority-------//设置线程的优先级
- getPriority-------//得到线程的优先级
- sleep-------------//在指定毫秒数内让当前正在执行的线程休眠(暂停执行)
- getState---------//获取进程当前的状态(生命周期六大状态)
- interrupt---------//中断线程(如果正在休眠会中断休眠)
- yield--------------//线程礼让,让出cpu让其他线程执行先,但是不一定会让出cpu,底层调度还要取决于cpu
- join---------------//线程插队,直接让出cpu让其他线程执行,如:在t1中执行t2.join()方法后,t1会让t2执行完t2的任务后再重新执行自身的任务,此时t1的状态是阻塞,阻塞不会影响cpu的性能
6.线程的生命周期
线程的生命周期分为六个
- New(新建状态),当线程被创建(new),它就处于New状态,此时还没用start开启;
- Runnable(可运行状态),当线程调用了start()方法,就处于可运行状态,此时它已经在操作系统底层线程池中被调度,等待cpu分配时间片等资源来执行;
- Block(阻塞状态),如果一个运行中的线程被停止或者阻塞,它就处于阻塞状态了,此时该线程不能被执行,只有得到主动唤醒的通知后,它才会进入可运行状态;
- Waitting(等待状态),当线程执行需要等待条件时候,它就会处于等待状态,只有当条件满足被唤醒后,才会进入可运行状态,如果该线程此时持有锁,会释放它所持有的锁,以便其他线程继续执行;
- TimeWaitting(计时等待状态),与等待状态类似,只是该状态会有一个倒计时条件,在指定时间内线程会等待某个条件的发生,条件满足或时间到了会直接唤醒,进入可运行状态;
- Terminated(结束状态),当线程run()方法执行过程中遇到了没有捕获到到的异常时候,它就进入结束状态,此时该线程无法继续执行。
图取自度娘:
7.线程同步机制
7.1 概念(为什么要引入线程同步机制)
多个线程同时访问共享资源,从而可能出现以下问题:
竞态条件(Race Condition):由于多个线程同时访问共享资源,可能会导致数据不一致、结果错误等问题。比如,在多线程环境下对一个变量进行递增操作时,多个线程可能会读取到相同的值并执行相同的加法操作,导致最终结果不正确。
死锁:如果多个线程彼此占有对方需要的资源,则可能会出现死锁现象,导致所有线程都无法继续执行。
饥饿:某个线程可能始终无法获取到需要的资源,导致一直处于等待状态,无法执行任务。
线程同步机制可以保证在任意时刻只有一个线程可以访问共享资源,从而避免竞态条件。同时,线程同步机制还可以协调多个线程之间的执行顺序,避免出现死锁和饥饿等问题。
7.2 eg:模拟售票
public class SellTicket02 implements Runnable {
private int ticketNum = 100;
@Override
public void run() {
while (true) {
if (ticketNum <= 0) {
System.out.println("余票不足,停止售票");
break;
}
try {
Thread.sleep(50);
System.out.println(Thread.currentThread().getName() + "窗口出售了一张票,余票:" + (--ticketNum));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
开启多线程模拟多个售票窗口同时售票
public class Ticket {
public static void main(String[] args) {
SellTicket02 sellTicket02 = new SellTicket02();
Thread thread1 = new Thread(sellTicket02);
Thread thread2 = new Thread(sellTicket02);
Thread thread3 = new Thread(sellTicket02);
thread1.start();
thread2.start();
thread3.start();
}
}
这样执行会有一个问题就是售票可能会出现负数票的情况,并且会数据不同步,比如:
- 线程1读取ticketNum为100,线程2读取ticketNum为100,线程3读取ticketNum为100;
- 线程1对ticketNum进行了减一操作,更新后为99;
- 线程2对ticketNum进行了减一操作,更新后为99;
- 线程3对ticketNum进行了减一操作,更新后为99。
在以上过程中,票数的实际值应该是97,但最终结果却为99,出现了数据不一致的情况。
如果最后只有一张票的时候多个线程进入了,那么就会出现负数了。这里我们就可以使用线程同步机制,使用synchronized关键字。
改进(使用synchronized)
//使用Synchronized线程同步锁,可以实现同一时刻多个线程的操作只有一个才会进入
//其他线程必须等它操作完成后才能执行----线程同步
public class SellTicket03 implements Runnable {
private int ticketNum = 1000;
private Boolean loop = true;
public synchronized void sell() {
if (ticketNum <= 0) {
System.out.println("余票不足,停止售票");
loop = false;
return;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "窗口出售了一张票,余票:" + (--ticketNum));
}
@Override
public void run() {
while (loop) {
sell();
}
}
}
public class Ticket {
public static void main(String[] args) {
//第三种方式,同步锁加上
SellTicket03 sellTicket03 = new SellTicket03();
Thread thread1 = new Thread(sellTicket03);
Thread thread2 = new Thread(sellTicket03);
Thread thread3 = new Thread(sellTicket03);
thread1.start();
thread2.start();
thread3.start();
}
}
售票将不会出现负数的情况,,并且数据同步,没有出现异常。
7.3 互斥锁
- 互斥锁是一种用于保护共享资源的线程同步机制。它可以确保在任意时刻只有一个线程可以访问共享资源,从而避免了多个线程同时访问共享资源导致的竞态条件问题。
7.3.1基本思想
互斥锁的基本思想是:在访问共享资源之前先尝试获取锁。如果获取到了锁,就进入临界区(即访问共享资源的区域)执行相应操作;如果没有获取到锁,则等待直到获取到锁为止。当一个线程正在访问共享资源时,其他线程都会被阻塞,等待该线程释放锁之后才能继续尝试获取锁。
7.3.2实现方式--使用synchronized关键字定义互斥锁
- 方式一,直接给方法加锁
//同步方法:在方法声明中使用synchronized关键字,表示该方法为同步方法。
//当一个线程正在访问该方法时,其他线程都会被阻塞,直到该线程退出该方法。
public synchronized void sell() {
if (ticketNum <= 0) {
System.out.println("余票不足,停止售票");
loop = false;
return;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "窗口出售了一张票,余票:" + (--ticketNum));
}
- 方式二 ,给代码块添加锁,如上面的sell方法,这里可以不用this,只要是同一个对象即可
//同步块:使用synchronized关键字定义一个同步块,表示在其中的代码需要获取某个对象的锁。
//同步块可以指定锁定的对象,如果不指定则默认为this对象。
public /*synchronized*/ void sell() {
//二.给代码块加锁
synchronized (this) {//这里可以不用this,只要是同一个对象就可以
if (ticketNum <= 0) {
System.out.println("余票不足,停止售票");
loop = false;
return;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "窗口出售了一张票,余票:" + (--ticketNum));
}
}
不使用this
package com.hua.sychronized;
//使用Synchronized线程同步锁,可以实现同一时刻多个线程的操作只有一个才会生效
//其他线程必须等它操作完成后才能执行----线程同步
public class SellTicket03 implements Runnable {
private int ticketNum = 100;
private Boolean loop = true;
private Object lock = new Object();
public void sell() {
synchronized (lock) {
if (ticketNum <= 0) {
System.out.println("余票不足,停止售票");
loop = false;
return;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "窗口出售了一张票,余票:" + (--ticketNum));
}
}
@Override
public void run() {
while (loop) {
sell();
}
}
}
用的比较多的是代码块加锁,因为直接给方法加锁可能会造成性能浪费。
7.3.3 当synchronized遇到static
- 当同步锁是加在静态方法上的时候,实际上这个锁是加在当前类本身的
public synchronized static void sell(){
System.out.println("11111");
}
- 当同步锁加在静态方法内部的代码块上的时候,这个时候使用this是行不通的,要使用类对象,同时一定要记得对象必须一致。
//如果当前类叫Thread01
public static void sell(){
synchronized(Thread01.class){
System.out.println("1111111");
}
}
7.3.4 互斥锁中注意事项
1、同步方法如果没有被static修饰,默认锁对象为this;
2、如果被static修饰,默认锁对象为类名.class;
3、多个线程的锁对象必须是同一个对象;
7.4 死锁
7.4.1 概念
死锁是指两个或多个线程互相持有对方需要的资源,并且都在等待对方释放资源,导致所有线程无法继续执行的情况。简单来说,就是一种循环等待的状态。
7.4.2 产生死锁的条件
- 互斥条件:共享资源同时只能被一个线程占用。
- 请求与保持条件:一个线程已经持有了至少一个资源,并且在等待其他线程释放所需的资源。
- 不可剥夺条件:线程已经获得的资源不能被其他线程强制性地抢占。
- 循环等待条件:若干个线程形成一种头尾相接的循环等待资源关系。
7.4.3 例子
假设有两个线程A和B,以及两个资源X和Y。线程A首先获取到资源X,线程B首先获取到资源Y。然后,线程A想要获取资源Y,而线程B想要获取资源X。由于互斥条件,每个资源一次只能被一个线程持有,所以线程A无法获取到资源Y,线程B也无法获取到资源X。这时,线程A和线程B就进入了互相等待对方释放资源的状态,从而形成了死锁。
7.4.4 解决死锁的方式
- 避免死锁:通过合理的资源分配和调度策略,避免系统进入死锁状态。
- 破环互斥条件:对某些资源进行改造,使其不再是互斥的,从而避免死锁的发生。
- 破坏请求与保持条件:一次性申请所有所需资源,或者在申请资源时释放已拥有的资源,从而破坏死锁发生所需要的请求和持有的条件。
- 破坏不可剥夺条件:允许系统强制性地抢占线程所持有的资源,从而破坏死锁发生所需要的不可剥夺条件。
- 破坏循环等待条件:按照规定的顺序申请资源,避免循环等待的情况发生。
7.5 释放锁
7.5.1以下情况会发生释放锁
- 当前线程的同步方法,同步代码块执行完毕;
- 当前代码块执行遇到break或return;
- 当前线程在同步方法或同步代码块中遇到未处理的Error或Exception,导致异常结束;
- 程序中主动停止,如使用了wait()方法;
- sleep并不会释放锁,而是会将锁持有进入阻塞状态;