java基础概念63-多线程
一、线程VS进程
1-1、进程
进程是程序的基本执行实体。
每一个正在运行的软件都是一个进程。
一个进程可以包含多个线程。
1-2、线程
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
简单理解:应用软件中互相独立,可以同时运行的功能。
每个线程都有自己的执行路径和栈空间,它们可以并发执行,共享进程的资源。
如:内存、文件句柄等,例如,在一个浏览器进程中,可能有一个线程负责显示页面内容,另一个线程负责下载图片,还有一个线程负责处理用户的输入操作。
二、多线程的概念
多线程是指在一个程序中同时运行多个线程,这些线程可以并行或并发执行。
并行是指多个线程在多核处理器上同时执行,而并发是指多个线程在单核处理器上通过时间片轮转的方式交替执行。
2-1、多线程的应用场景
- 软件中的耗时操作
- 拷贝、迁移大文件
- 加载大量的资源文件
- 所有的聊天软件
- 所有的服务器
2-3、小结
2-4、并发VS并行
1、并发
在同一时刻,有多个指令在单个CPU上交替执行。
2、并行
在同一时刻,有多个指令在多个CPU上同时执行。
在计算机中,并发和并行可能同时发生。当2核4线程,处理4个线程的时候,就是并行;但是处理多于4个线程的时候,CPU的运行就是随机并发选取多个线程,并行执行!
三、多线程的实现方式
- 继承Thread类的方式进行实现
- 实现Runnable接口的方式进行实现
- 利用Callable接口和Future接口方式实现
3-1、继承Thread类
- 继承Thread类;
- 重写run方法;
- 使用start方法调用线程。
3-2、实现Runnable接口
- 实现Runnable接口
- 实现run方法
- 创建自定义类的对象
- 创建Thread类的对象。
3-3、 利用Callable接口和Future接口
前两种方式,重写的run方法没有返回值,不能获取到多线程运行的结果!
若是想要获取多线程的运行结果:
3-4、多线程三种实现方式的对比
四、线程中常见的成员方法
4-1、线程的名字
若是没有给线程设置名字,线程是有默认名字的,格式:Thread-x(x序号,从0开始!)
设置线程名字:
1、setName方法
2、Thread构造方法
4-2、currentThread()方法:获取当前线程
【细节】:
当JVM虚拟机启动之后,会自动的启动多条线程,其中有一条线程就叫做main线程,他的作用就是去调用main方法,并执行里面的代码。
在以前,我们写的所有的代码,其实都是运行在main线程当中。
4-3、sleep()方法
【细节】:
1、哪条线程执行到这个方法,那么哪条线程就会在这里停留对应的时间;
2、方法的参数:就表示睡眠的时间,单位毫秒
1秒= 1000秒
3、当时间到了之后,线程会自动的醒来,继续执行下面的其代码。
4-4、线程的优先级
1、线程的调度
线程调度是操作系统或 Java 虚拟机(JVM)为了合理分配 CPU 时间片给各个线程,以实现多线程并发执行而进行的一系列操作。
抢占式调度
CPU在哪个时候执行哪条现成,不确定!执行时间也不确定。都是随机的。
优先级越大,线程抢到CPU的概率越大。
java中线程的优先级1~10,默认是5。
非抢占式调度
所有的线程轮流执行,执行的时间也差不多!
2、设置线程的优先级
【注意】:
优先级越高,只是抢到CPU的概率越大,但是,不是100%。
4-5、设置守护线程
应用场景:
Java 的垃圾回收器(GC)就是一个典型的守护线程。它在后台不断运行,负责回收不再使用的内存对象,释放系统资源。当所有的非守护线程(如主线程、业务逻辑线程等)执行完毕后,JVM 会退出,此时垃圾回收线程也会自动停止运行。
在大型应用程序中,需要实时监控日志文件的变化,以便及时发现异常情况。可以创建一个守护线程,定期检查日志文件的大小、内容等信息。当应用程序的所有业务线程结束后,这个日志监控的守护线程会自动终止,不会影响 JVM 的退出。
4-6、出让线程
让出CPU的执行权,出让线程会让结果均匀一点,但不是绝对的!
4-7、插入线程
join
方法的主要作用是让当前线程等待调用 join
方法的线程执行完毕后再继续执行。简单来说,就是一个线程可以通过调用另一个线程的 join
方法来等待该线程结束。
原本main线程和土豆线程,交替在控制台打印,土豆线程插入后,土豆线程执行完,main线程再执行!
五、线程的生命周期
有没有执行权:能不能去抢CPU。
睡眠时间到了之后,线程需要重新去抢CPU,抢到了才能执行下面的代码!
六、线程安全的问题
6-1、多线程的问题
需求:3个窗口一共卖100张电影片
【注意】:
若是用implements Runnable方式,则ticket变量不用static修饰!因为此方法,只会创建一个自定义对象!
问题结果:
问题原因:
CPU在执行的时候,随时会被抢走!且线程执行具有随机性!
解决方式:将操作共享数据的代码锁起来!
6-2、同步代码块
synchronized
关键字是用于实现线程同步的重要工具,它可以保证在同一时刻只有一个线程能够访问被 synchronized
修饰的代码块或方法,从而避免多线程并发访问共享资源时可能出现的数据不一致问题。
或者是:
synchronized (MyThreadMain.class){
// 同步代码
}
【注意】:
同步代码块在while里面!
6-3、同步方法
【技巧】:
先写同步代码块,再将同步代码块中的方法抽取成同步方法中。
@Override
public void run() {
while (true) {
if (method()) break;
}
}
// 锁对象是this
private synchronized boolean method() {
if (ticket < 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(getName() + "正买卖第" + ticket + "张票");
} else {
return true;
}
return false;
}
6-4、拓展:StringBuilder和StringBuffer
字符串在拼接的时候,除了StringBuilder还有StringBuffer。两者方法一模一样!
将StringBuilder的实例用于多个线程是不安全的,如果需要线程的同步,则建议使用StringBuffer。因为StringBuffer中所有的方法都是同步方法。
所以,若是你的代码是单线程的,可以用StringBuilder,但是,若是多线程,建议用StringBuffer。
6-4、lock锁
对于同步代码块、同步方法,锁的开关是自动关闭,自动打开的,自己没办法控制。
想要手动的加锁、释放锁,使用lock方法:
示例:
public class MyThreadMain {
public static void main(String[] args) {
MyThread myThread1 = new MyThread("窗口1");
MyThread myThread2 = new MyThread("窗口2");
MyThread myThread3 = new MyThread("窗口3");
myThread1.start();
myThread2.start();
myThread3.start();
}
}
public class MyThread extends Thread {
public MyThread() {
}
public MyThread(String name) {
super(name);
}
// 0~99
static int ticket = 0;
// 因为MyThread可能创建多个,所以,要保证锁对象唯一!
static Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
//synchronized (MyThread.class) {
lock.lock();
try {
if (ticket < 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(getName() + "正买卖第" + ticket + "张票");
} else {
break;
}
} catch (Exception e) {
e.printStackTrace();
}finally {
// 无论是否break,都要执行!
lock.unlock();
}
//}
}
}
}
七、死锁
锁的嵌套!是一个错误。
八、等待唤醒机制
8-1、生产者和消费者
生产者消费者模式是一个十分经典的多线程协作的模式。
至少有三个角色:
- 生产者;
- 消费者;
- 控制生产者和消费者的执行。
1、生产者等待模式
2、消费者等待模式
3、常用方法
4、示例
/**
* 作用:控制生产者和消费者的执行
*/
public class Desk {
// 是否有面条:0-没有面条,1-有面条
public static int foodFlag = 0;
// 总个数
public static int count = 10;
// 锁对象
public static Object lock = new Object();
}
/**
* 消费者
*/
public class Cook extends Thread{
@Override
public void run() {
while (true){
synchronized (Desk.lock){
if(Desk.count == 0){
break;
}else {
// 核心逻辑
// 1、判断桌子上是否有食物
if(Desk.foodFlag == 1){
// 2、有,等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
// 3、没有,制作食物
System.out.println("厨师做了一碗面条");
// 4、修改桌子上的食物状态
Desk.foodFlag = 1;
// 5、叫醒等待的消费者开吃
Desk.lock.notifyAll();
}
}
}
}
}
}
/**
* 生产者
*/
public class Foodie extends Thread{
/**
* 1、循环
* 2、同步代码块
* 3、判断共享数据是都到了末尾(到了末尾)
* 4、判断共享数据是都到了末尾(没到末尾,执行核心逻辑)
*/
@Override
public void run() {
while(true){
synchronized (Desk.lock){
if(Desk.count == 0){
// 共享数据到了末尾
break;
}else {
// 执行核心逻辑
if(Desk.foodFlag == 0){
// 桌子上没有食物, 等待
try {
// 让当前线程和锁进行绑定
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
// 有面条
// 吃的总数-1
Desk.count--;
System.out.println("吃货正在吃买面条,还能再吃" + Desk.count + "碗!!!");
// 吃完唤醒厨师继续做
Desk.lock.notifyAll();
// 修改桌子的状态
Desk.foodFlag = 0;
}
}
}
}
}
}
/**
* 测试类
*/
public class ProduceDemo {
public static void main(String[] args) {
// 创建线程对象
Foodie foodie = new Foodie();
Cook cook = new Cook();
// 设置名字
foodie.setName("吃货");
cook.setName("厨师");
// 启动线程
foodie.start();
cook.start();
}
}
8-2、阻塞队列
【注意】:
生产者、消费者使用同一个阻塞队列。
生产者:
public class Cook02 extends Thread{
ArrayBlockingQueue<String> blockingQueue;
public Cook02(ArrayBlockingQueue<String> blockingQueue){
this.blockingQueue = blockingQueue;
}
@Override
public void run() {
while (true){
try {
//底层有锁
blockingQueue.put("面条");
// 打印语句在锁的外面,控制台输出显示可能有问题,但是不影响数据!
System.out.println("厨师放了一碗面");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
消费者:
public class Foodie02 extends Thread{
ArrayBlockingQueue<String> blockingQueue;
public Foodie02(ArrayBlockingQueue<String> blockingQueue){
this.blockingQueue = blockingQueue;
}
@Override
public void run() {
while (true){
String noddle = null;
try {
//底层有锁
noddle = blockingQueue.take();
System.out.println(noddle);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试:
public class ProduceQueueDemo {
public static void main(String[] args) {
ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(1);
Cook02 cook = new Cook02(blockingQueue);
Foodie02 food = new Foodie02(blockingQueue);
cook.start();
food.start();
}
}
九、多线程的6个状态
【注意】:
没有运行状态,因为一旦线程抢夺到CPU之后,线程就交出去了,JVM就不管了。
十、内存图
main方法运行在main线程中!
每个线程有一个自己的栈!
堆内存是唯一的!
示例:
有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池中的奖项为 {10,5,20,50,100,200,500,800,2,80,300,700};
创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1”,“抽奖箱2”
随机从抽奖池中获取奖项元素并打印在控制台上,格式如下:
每次抽的过程中,不打印,抽完时一次性打印(随机) 在此次抽奖过程中,抽奖箱1总共产生了6个奖项。
分别为:10,20,100,500,2,300最高奖项为300元,总计额为932元
在此次抽奖过程中,抽奖箱2总共产生了6个奖项。
分别为:5,50,200,800,80,700最高奖项为800元,总计额为1835元
public class Test {
public static void main(String[] args) {
ArrayList<Integer> allPrizes = new ArrayList<>();
Collections.addAll(allPrizes, 10, 5, 20, 50, 100, 200, 500, 700, 800, 40, 900, 1000);
PrizeDraw p1 = new PrizeDraw(allPrizes);
PrizeDraw p2 = new PrizeDraw(allPrizes);
PrizeDraw p3 = new PrizeDraw(allPrizes);
p1.setName("抽奖箱1");
p2.setName("抽奖箱2");
p3.setName("抽奖箱3");
p1.start();
p2.start();
p3.start();
}
}
public class PrizeDraw extends Thread{
ArrayList<Integer> allPrizes;
public PrizeDraw(ArrayList<Integer> allPrizes){
this.allPrizes = allPrizes;
}
@Override
public void run() {
ArrayList<Integer> boxList = new ArrayList<>();
while (true){
synchronized (PrizeDraw.class){
if (allPrizes.size() == 0){
// 抽完了
int max = getMax(boxList);
int sum = getSum(boxList);
System.out.println(getName() + boxList + "最大奖:" + max + ",总金额:" + sum);
break;
}else {
Collections.shuffle(allPrizes);
Integer prize = allPrizes.remove(0);
boxList.add(prize);
}
}
}
}
public int getMax(ArrayList<Integer> list){
int max = list.get(0);
for (Integer item : list){
if(item > max){
max = item;
}
}
return max;
}
public int getSum(ArrayList<Integer> list){
int sum = 0;
for (Integer item : list){
sum = sum + item;
}
return sum;
}
}