面试+基础-----详细解读多线程(线程池、锁、死锁...)

本文深入探讨了Java中的多线程知识,包括线程的创建(通过Thread类、Runnable接口和Callable接口)、线程安全问题及其模拟、线程同步的三种方式(同步代码块、同步方法和Lock锁),并介绍了线程池的概念和使用。此外,还讨论了死锁现象以及volatile关键字和原子性在多线程中的作用。
摘要由CSDN通过智能技术生成

🧑🧑我的博客主页:努力敲代码的刺_🧑🧑

🐱‍🚀🐱‍🚀本文内容为多线程的相关知识🐱‍🏍🐱‍🏍

💕💕老牛亦解韶光贵,不待扬鞭自奋蹄💕💕

👇👇👇👇快来学习一下吧~~~👇👇👇👇

目录

一、多线程的概述

二、线程的创建方式

        方式1、继承Thread类

        方式2、实现Runnable接口

        方式3、实现Callable接口

三、线程安全问题的概述

四、线程安全问题的模拟

五、线程同步的方式

        方式1、同步代码块

        方式2、同步方法

        方式3、Lock锁

六、线程状态

七、线程池

        线程池的引入

        线程池的概念

        线程池的创建和原理

        Callable做线程池的任务

八、死锁

        死锁的基本概念

        必然死锁的案例

九、Volatile关键字

        volatile变量不可见性及解决方案

十、原子性

        volatile原子性的保证


一、多线程的概述

什么是进程?

程序是静止的,运行中的程序就是进程。

进程的三个特征

1、动态性:京城是运行中的程序,要动态的占用内存,CPU网络等资源。

2、独立性:进程与进程之间是相互独立的,彼此有自己的独立内存区域。

3、并发性:假如CPU是单核,同一个时刻其实内存中只有一个进程在被执行,

CPU会依次为每个进程服务,因为切换的速度非常快,给我们的感觉就是这些进程在同时执行,这就是并发性。

并行:同一个时刻有多个在执行。

那么什么是线程?

线程是属于进程的。一个进程可以包含多个线程,这就是多线程。

线程是进程中的一个独立执行单元。

线程创建开销相对于进程来说比较小。

线程也支持并发性。

线程的作用:

可以提高程序的效率,线程也支持并发性。

多线程可以解决很多业务模型。

大型高并发技术的核心技术。

二、线程的创建方式

方式1、继承Thread类

具体步骤:

1.定义一个线程类继承Thread类

2.重写run()方法

3.创建一个新的线程对象

调用线程对象的start()方法启动线程

package 线程;

public class Run1 {

	public static void main(String[] args) {
		Thread t= new MyThread();
		t.start();
		for(int i=0;i<5;i++)
			System.out.println("main线程输出:"+i);
	}
}
class MyThread extends Thread{
	@Override
	public void run() {
		for(int i=0;i<90;i++) {
			System.out.println("子线程输出:"+i);
		}
	}
	
}

继承Thread类的优缺点:

优点:编码简单。

缺点:线程类已经继承了Thread类,所以无法再继承其他类。

注意的点:

1.线程的启动必须调用start()方法,否则当成普通类处理。如果直接调用run()方法,相当于变成了普通类的执行。start()方法底层其实是给CPU注册当前线程,并且触发run方法执行。

2.建议线程先创建子线程,主线程的任务放在之后。

方式2、实现Runnable接口

具体步骤:

1.创建一个线程任务类实现Runnable接口。

2.重写run方法。

3.创建一个线程任务对象。

4.把线程任务对象包装成线程对象。

5.调用线程对象的start方法启动线程。

线程任务对象和线程对象不一样!!!

线程任务对象和线程对象不一样!!!

线程任务对象和线程对象不一样!!!

package 线程创建方式二;
//实现runnable接口
public class Run1 {

	public static void main(String[] args) {
		// 创建线程任务对象
		Runnable target = new MyRunnable();
		
		//把线程任务对象包装成线程对象
		Thread t= new Thread(target);
		
		//调用线程对象的start方法
		t.start();
		
		for(int i=0;i<=10;i++) {
			System.out.println(Thread.currentThread().getName()+"==>"+i);
		}
		
	}
}

class MyRunnable implements Runnable{

	@Override
	public void run() {
		for(int i=0;i<=10;i++) {
			System.out.println(Thread.currentThread().getName()+"==>"+i);
		}
	}
	
}

优点:

1.线程任务类只是实现了Runnable接口,可以继续继承其他类。

2.同一个线程任务对象可以被包装成多个线程对象。

3.适合多个线程去共享同一个资源。

4.实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立。

5.线程池可以放入Runnable或Callable线程任务对象。

方式3、实现Callable接口

引入概念:

未来任务对象:是一个Runnable对象,这样就可以被包装成线程对象。

未来任务对象可以在县城执行完毕后去得到线程执行的结果。

具体步骤:

1.定义一个线程任务类实现Callable接口,申明线程执行的结果类型。

2.重写线程任务类的call方法,这个方法可以直接返回执行的结果。

3.创建一个Callable的线程任务对象。

4.把Callable的线程任务对象包装成一个未来任务对象。

5.把未来任务对象包装成线程对象。

6.调用线程的start方法启动线程。

package 线程创建方式三;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

//实现callable接口
public class Run1 {

	public static void main(String[] args) {
		// 创建callable线程任务对象
		Callable<String> call = new MyCallable();
		
		//把任务对象包装成未来任务对象
		FutureTask<String> task = new FutureTask<>(call);
		
		//把未来任务对象包装成线程对象
		Thread t= new Thread(task);
		
		t.start();
		
		for(int i=1;i<=10;i++) {
			System.out.println(Thread.currentThread().getName()+"==>"+i);
		}
		
		try {
			//用字符串接收结果
			String rs= task.get();
			System.out.println(rs);
		} catch (InterruptedException e) {
			
			e.printStackTrace();
		} catch (ExecutionException e) {
			
			e.printStackTrace();
		}

	}

}
class MyCallable implements Callable<String>{

	@Override
	public String call() throws Exception {
		int sum=0;
		for(int i=1;i<=10;i++) {
			System.out.println(Thread.currentThread().getName()+"==>"+i);
			sum+=i;
		}
		return Thread.currentThread().getName()+"==>"+sum;
	}
	
}

三、线程安全问题的概述

一句话:多个线程操作同一个共享资源的时候可能会出现线程安全问题。

四、线程安全问题的模拟

先描述一个场景:

现在有一个账户里面存有10000,小红🤦‍♀️和小明🤦‍♂️同时来取钱,每次取10000。注意是同时哦。当其中一个人取完钱后,钱数应该减少10000。那么此时小明和小红就分别代表了两个线程,我们现在把两个线程分别叫做小明🤦‍♂️和小红🤦‍♀️当他们两个同时操作取款机的时候就有可能引发安全问题。当小明抢占CPU的速度大于小红,那么小明先取到钱,按理说此时钱数应该减少10000,但偏偏小红在钱数减少之前以极快的速度也取到了钱,那么此时两个人都取到了钱,然后CPU再执行两次减钱操作,此时余额为-10000。

这不就是bug????

我们把这种问题称为线程安全问题。

读者可以自己跑一下代码,注意多跑几次,结果是不一样的哦。~~~~~~~~

package 线程安全问题;

//账户类
public class Account {
	private double money;
	private String cardID;
	
	public void drawMoney(double money) {
		
		String name =Thread.currentThread().getName();
		if(this.money>=money) {
			System.out.println(name+"来取钱,"+"余额足够。");
			this.money-=money;
			System.out.println(name+"来取钱后,"+"余额剩余"+this.money);
			
		}else {
			System.out.println("余额不足。");
		}
		
		
	}
	
	public Account(String cardID,double money) {
		this.money = money;
		this.cardID = cardID;
	}
	public double getMoney() {
		return money;
	}
	public void setMoney(double money) {
		this.money = money;
	}
	public String getCardID() {
		return cardID;
	}
	public void setCardID(String cardID) {
		this.cardID = cardID;
	}
	

}



//线程类:创建取钱线程
public class DrawThread extends Thread{
	private Account acc;
	public DrawThread(Account acc,String name) {
		super(name);
		this.acc=acc;
	}
	
	@Override
	public void run() {
		acc.drawMoney(10000);
	}
	
}



public class Problem1 {

	public static void main(String[] args) {
		//创建一个共享资源账户对象
		Account acc = new Account("ICBC-110",10000);
		
		//创建两个对象
		Thread littleMing = new DrawThread(acc,"小明");
		littleMing.start();
		
		Thread littleHong = new DrawThread(acc,"小红");
		littleHong.start();
		

	}

}


五、线程同步的方式

线程同步的作用:就是为了解决上个场景所发生的问题,即线程安全问题。

线程同步解决线程安全问题的核心思想:

        让多个线程实现先后依次访问共享资源,这样就解决了安全问题。

拿到上个场景中,也就是说先让小明🤦‍♂️去取钱,此时小红在等待。当小明🤦‍♂️取完钱,钱数随之减少,那么当小红🤦‍♀️再来取钱的时候不满足条件(注意程序中的条件判断),那么小红🤦‍♀️就拿不到钱,余额也就自然不会为负数了。

那么我们自然而然的也就引入了锁🔒的概念。

形象一点说,也就是当小明进来取钱后,先把房间门锁起来,此时小红进不来,就只能等小明取完钱之后她再进去取钱。

方式1、同步代码块

作用:把出现线程安全问题的核心代码给上锁🔒,每次只能一个线程进入,执行完毕后以后自动解锁,其他线程才可以进来执行。

格式:synchronized(锁对象){

        //访问共享资源的核心代码

}

锁对象:理论上可以是任意的“唯一对象”即可。

原则上:锁对象建议使用共享资源。

                在实例方法中建议用this作文锁对象,此时this正好是共享资源。

                 在静态方法中建议用类名.        class字节码作为锁对象。

下来看一下案例,我们继续利用上个案例,就是在取钱的方法中加了一个锁,其他的代码与上个案例完全一致,为了节省空间就不再给出。

public void drawMoney(double money) {
		
		String name =Thread.currentThread().getName();
		//锁住账户:this代指账户	
		synchronized(this) {
			if(this.money>=money) {
				System.out.println(name+"来取钱,"+"余额足够。");
				this.money-=money;
				System.out.println(name+"来取钱后,"+"余额剩余"+this.money);
				
			}else {
				System.out.println("余额不足。");
			}
		}
	}

方式2、同步方法

作用与同步代码块是一致的。

用法:直接给方法加上一个修饰符synchronized。

原理:同步方法的原理和同步代码块的底层原理其实是完全一样的,只是同步方法是把整个方法的代码都锁起来了。同步方法其实底层也是有锁对象的。

如果方法是实例方法:同步方法默认用this作为锁对象。

如果方法是静态方法,同步方法默认同类名.class作为锁对象。

下面是同步方法的上锁方式,和同步代码块略有不同,其他代码也是完全一致的,小伙伴们自行体会。

public synchronized void drawMoney(double money) {
		
		String name =Thread.currentThread().getName();
		if(this.money>=money) {
			System.out.println(name+"来取钱,"+"余额足够。");
			this.money-=money;
			System.out.println(name+"来取钱后,"+"余额剩余"+this.money);
			
		}else {
			System.out.println("余额不足。");
		}
		
		
	}

方式3、Lock锁

核心思想就是当速度最快的线程执行核心代码后,先对核心代码进行上锁,此时别的线程无法访问核心代码,当此线程执行完之后,再对核心代码进行解锁,如此往复。

public void drawMoney(double money) {
		
		String name =Thread.currentThread().getName();
		lock.lock();
		try {
			if(this.money>=money) {
				System.out.println(name+"来取钱,"+"余额足够。");
				this.money-=money;
				System.out.println(name+"来取钱后,"+"余额剩余"+this.money);
				
			}else {
				System.out.println("余额不足。");
			}
		}catch(Exception e) {
			e.printStackTrace();
			}finally {
				lock.unlock();
			}
	}

六、线程状态

 七、线程池

线程池的引入

我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是会产生问题:

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大江西系统的效率,因为频繁创建线程的和销毁线程需要时间,线程也属于宝贵的系统资源。

那么有没有一种办法是的线程可以复用:就是执行完一个任务,并不被销毁,而是可以继续执行其他任务。

在Java中可以通过线程池来达到这样的效果。

线程池的概念

线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

线程池的创建和原理

package 线程池的创建;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo1 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		//创建一个线程池,容量为3
		ExecutorService pools  = Executors.newFixedThreadPool(3);
		
		//添加线程任务让线程池处理
		Runnable target = new MyRunnable();
		pools.submit(target);
		pools.submit(target);
		pools.submit(target);
		pools.submit(target);
		
	}

}

class MyRunnable implements Runnable{

	@Override
	public void run() {
		// TODO Auto-generated method stub
		for(int i=0;i<5;i++) {
			System.out.println(Thread.currentThread().getName()+"==>"+i);
		}
		
	}
	
}

Callable做线程池的任务

package callable做线程池的任务;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Demo1 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		//创建一个线程池,容量为2
		ExecutorService pools  = Executors.newFixedThreadPool(2);
		
		Future<String> t1=pools.submit(new MyCallable(100));
		Future<String> t2=pools.submit(new MyCallable(200));
		Future<String> t3=pools.submit(new MyCallable(300));
		try {
			String rs1 = t1.get();
			String rs2 = t2.get();
			String rs3 = t3.get();
			System.out.println(rs1);
			System.out.println(rs2);
			System.out.println(rs3);
			
		}catch(Exception e) {
			
			
			e.printStackTrace();
		}
		
		
	}

}


	
class MyCallable implements Callable<String>{
	
	private int n;
	public MyCallable(int n) {
		this.n=n;
	}
	@Override
	public String call() throws Exception {
		int sum=0;
		for(int i=1;i<=n;i++) {
			sum+=i;
		}
		return Thread.currentThread().getName()+"  end ="+sum;
	}
	
}

八、死锁

死锁的基本概念

死锁是这样一种情形:多个线程同时被阻塞,他们中的一个或者全部都在等待某个资源被释放。由于线程被无限期的阻塞,因此程序不可能正常终止。

Java死锁产生的四个必要条件

1.互斥使用:当一个资源被一个线程使用时,别的线程不能使用。

2.不可抢占:资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。

3.请求和保持:当资源请求者在请求其他的资源的同时保持对原有的资源的占有。

4.循环等待:存在一个等待循环队列,p1要p2的资源,p2要p1的资源,这样就形成了一个环路。有点类似于三角债。

必然死锁的案例

package 必然死锁的案例;

public class Lock1 {
	public static Object  resources01= new Object();
	public static Object  resources02= new Object();

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		new Thread(new Runnable() {
			public void run() {
				synchronized(resources01) {
					System.out.println("线程1占用资源1请求占用资源2");
					try {
						Thread.sleep(1000);
					}catch(Exception e) {
						e.printStackTrace();
					}
					synchronized(resources02) {
						System.out.println("线程1成功占用资源2");
					}
				}
				
			}
		}).start();
		
		new Thread(new Runnable() {
			public void run() {
				synchronized(resources02) {
					System.out.println("线程2占用资源2请求占用资源1");
					try {
						Thread.sleep(1000);
					}catch(Exception e) {
						e.printStackTrace();
					}synchronized(resources01) {
						System.out.println("线程2成功占用资源1");
					}
				}
			}
		}).start();
		

	}

}

九、Volatile关键字

volatile变量不可见性及解决方案

volatile变量不可见性:

多个线程访问共享变量,会出现一个线程修改变量的之后,其他线程看不到最新值的情况。

在下面的案例中,在run方法中已经修改了flag的值,但是当线程跑起来之后,主函数中依旧认为flag为false,所以不会执行while代码块。

package volatile关键字;

public class Demo1 {
	public static void main(String args[]) {
		Visibility t = new Visibility();
		t.start();
		
		while(true) {
			if(t.isFlag()) {
				System.out.println("主线程进入执行");
			}
		}
	}
	

}

class Visibility extends Thread{
private  boolean flag = false;
	
	@Override
	public void run() {
		try {
			Thread.sleep(1000);
		}catch(Exception e) {
			e.printStackTrace();
		}
		//线程中修改变量
		flag = true;
		
	}

	public boolean isFlag() {
		return flag;
	}

	public void setFlag(boolean flag) {
		this.flag = flag;
	}
	
}



所以要么加锁,要么加上volatile关键字。

		while(true) {
			synchronized(Visibility.class) {
				if(t.isFlag()) {
					System.out.println("主线程进入执行");
				}
			}		}

private  volatile boolean flag = false;

十、原子性

概述:所谓的原子性是指再一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。

先看如下代码:

我们在run方法中写一个100次的循环,然后又创建了100的线程,按理说count的值最后应该是10000,但是由于原子性的出现,count的值有时候并不是10000。其实原子性有点类似于线程安全问题。小伙伴们可以自己多跑几次,count的值是不唯一的。

package 原子性保证;

public class VolatileAtomic1 {

	public static void main(String[] args) {
		Runnable target = new MyRunnable();
		for(int i=1;i<=100;i++) {
			new Thread(target).start();
		}
	}
}
class MyRunnable implements Runnable{
	private volatile int count=0;
	@Override
	public void run() {
		for(int i=1;i<=100;i++) {
			count++;
			System.out.println("count===>"+count);
		}
		
	}
	
}

volatile原子性的保证

使用原子类之后count的值就一直是10000.

package 原子性;

import java.util.concurrent.atomic.AtomicInteger;

public class VolatileAtomic1 {

	public static void main(String[] args) {
		Runnable target = new MyRunnable();
		for(int i=1;i<=100;i++) {
			new Thread(target).start();
		}
	}
}	
class MyRunnable implements Runnable{
	//创建一个原子类对象
	private AtomicInteger at = new AtomicInteger();
	
	//incrementAndGet() 先增1再取值
	
	private  int count=0;
	@Override
	public void run() {
			for(int i=1;i<=100;i++) {
				System.out.println("count===>"+at.incrementAndGet());
		}
		
	}
	
}

本期就到这里啦,下期再见~~~

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值