当前位置: 首页 > news >正文

多线程-初阶

1. 认识线程

1.1 概念

线程是什么?

一个线程就是一个“执行流”。每个线程之间都可以按照顺序执行自己的代码。多个线程之间“同时”执行着多份代码。

为什么要有线程?

并发编程成为刚需单核 CPU 的发展遇到了瓶颈.。要想提高算力,,就需要多核 CPU.。而并发编程能更充分利用多核 CPU 资源。

有些任务场景需要等待“IO”,为了让等待IO的时间能够去做一些其他的工作,也需要用到并发编程。

其次, 虽然多进程也能实现并发编程, 但是线程比进程更轻量。

创建线程比创建进程更快。

销毁线程比销毁进程更快。

调度线程比调度进程更快。

最后,线程虽然比进程轻量,但还是不满足,于是有了“线程池”(ThreadPool)和“协程”(Coroutine)。

进程和线程的区别

进程是包含线程的。每个进程至少有一个线程存在,即主线程。

进程和进程之间不共享内存空间。同一个进程的线程之间共享同一个内存空间。

进程是资源分配的最小单位,线程是系统调度的最小单位。

⼀个进程挂了⼀般不会影响到其他进程.。但是⼀个线程挂了, 可能把同进程内的其他线程⼀起带走(整个进程崩溃)。
Java的线程和操作系统线程的关系
线程是操作系统中的概念。操作系统内核实现了线程这样的机制,并且对用户层提供了一些API供用户使用。
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进⼀步的抽象和封装。

1.2 第⼀个多线程程序

每个多线程都是一个独立的执行流。

每个多线程之间是“并发”执行的。

public class ThreadDemo {private static class MyThread extends Thread{@Overridepublic void run(){while ( true){Random random = new Random();System.out.println(Thread.currentThread().getName());try {Thread.sleep(random.nextInt(10));} catch (InterruptedException e) {throw new RuntimeException(e);}}}public static void main(String[] args) {MyThread t1 = new MyThread();t1.start();Random random = new Random();while (true){System.out.println(Thread.currentThread().getName());try {Thread.sleep(random.nextInt(10));} catch (InterruptedException e) {throw new RuntimeException(e);}}}}
}

1.3 创建线程

private static class MyThread implements Runnable{@Overridepublic void run(){System.out.println("Thread is running");}public static void main(String[] args) {Thread t=new Thread(new MyThread());t.start();}
}

对比上面两种方法:

继承Thread类,直接使用this就表示当前线程对象的引用。

实现Runnable接口,this表示的是MyRunnable的引用。需要使用Thread.currentThread()。

其他变形

匿名内部类创建Thread子类对象

public static void main(String[] args) {Thread t1=new MyThread(){@Overridepublic void run(){System.out.println("aaaa");}};t1.start();
}
匿名内部类创建Runnable子类对象
Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("使⽤匿名类创建 Runnable ⼦类对象");}
});
lambda 表达式创建Runnable子类对象
Thread t3=new Thread(()->{System.out.println("使用匿名内部类创建Thread子类对象");
});
Thread t4=new Thread(()-> System.out.println("使⽤匿名类创建 Thread ⼦类对象"));

2. Thread类及常见方法

Thread类是JVM用来管理线程的一个类,换句话说,每个线程都有一个唯一的Thread对象与之相关。

2.1 Thread的常见构造方法

Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");

2.2 Thread的几个常见属性

ID 是线程的唯⼀标识,不同线程不会重复
名称是各种调试工具用到
状态表示线程当前所处的⼀个情况,下面我们会进⼀步说明
优先级高的线程理论上来说更容易被调度到
关于后台线程,需要记住⼀点:JVM会在⼀个进程的所有非后台线程结束后,才会结束运行。
是否存活,即简单的理解,为 run 方法是否运行结束了
线程的中断问题,下面我们进⼀步说明
public class ThreadDemo {public static void main(String[] args) {Thread thread=new Thread(()->{for (int i=0;i<10;i++){System.out.println("Thread is running");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println(Thread.currentThread().getName() + ": 我即将死去");});System.out.println(Thread.currentThread().getName()+ ": ID: " + thread.getId());System.out.println(Thread.currentThread().getName()+ ": 名称: " + thread.getName());System.out.println(Thread.currentThread().getName()+ ": 状态: " + thread.getState());System.out.println(Thread.currentThread().getName()+ ": 优先级: "+ thread.getPriority());System.out.println(Thread.currentThread().getName()+ ": 后台线程: " + thread.isDaemon());System.out.println(Thread.currentThread().getName()+ ": 活着: " + thread.isAlive());System.out.println(Thread.currentThread().getName()+ ": 被中断: " + thread.isInterrupted());thread.start();while (thread.isAlive()) {}System.out.println(Thread.currentThread().getName()+ ": 状态: " + thread.getState());}}

2.3 中断一个线程

目前常见的有以下两种方式:

1.通过共享标记来进行沟通。

2。调用interrupt()方法来通知。

示例1:使用自定义的变量来作为标志位(需要给标志位上加volatile关键字)

public class ThreadDemo {private static class MyRunnable implements Runnable {public volatile boolean isQuit = false;@Overridepublic void run() {while (!isQuit){System.out.println(Thread.currentThread().getName()+"别管我,忙着转账呢");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println(Thread.currentThread().getName()+"啊?差点坏了大事");}}public static void main(String[] args) throws InterruptedException {MyRunnable target = new MyRunnable();Thread thread = new Thread(target,"李四");System.out.println(Thread.currentThread().getName()+"让李四开始转账");thread.start();Thread.sleep(10*1000);System.out.println(Thread.currentThread().getName()+ ": ⽼板来电话了,得赶紧通知李四对⽅是个骗⼦!");target.isQuit = true;}
}

示例2:使用Thread.interrupted()或者Thread.currentThread().isInterrupted() 代替自定义标志位。

public class ThreadDemo {private static class MyRunnable implements Runnable {@Overridepublic void run() {while (!Thread.interrupted()){System.out.println(Thread.currentThread().getName()+"别管我,忙着转账呢");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();System.out.println(Thread.currentThread().getName()+"有内鬼终止交易");break;}}System.out.println(Thread.currentThread().getName()+"啊?差点坏了大事");}}public static void main(String[] args) throws InterruptedException {MyRunnable target = new MyRunnable();Thread thread = new Thread(target,"李四");System.out.println(Thread.currentThread().getName()+"让李四开始转账");thread.start();Thread.sleep(10*1000);System.out.println(Thread.currentThread().getName()+ ": ⽼板来电话了,得赶紧通知李四对⽅是个骗⼦!");thread.interrupt();}
}

Thread收到通知的方式有两种:

1. 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通
知,清除中断标志。
        
                当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法。可以选择忽略这个异常,也可以跳出循环结束线程。
2.否则,只是内部的⼀个中断标志被设置,thread 可以通过。
Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志这种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到。

2.4 等待一个线程-join()

public class ThreadDemo {public static void main(String[] args) throws InterruptedException {Runnable target =()->{for (int i = 0; i < 10; i++){try {System.out.println(Thread.currentThread().getName()+":我还在工作!");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName()+":我工作结束了!");};Thread thread1=new Thread( target,"李四");Thread thread2=new Thread( target,"王五");System.out.println("让李四先开始工作");thread1.start();thread1.join();System.out.println("让王五开始工作");thread2.start();thread2.join();System.out.println("程序结束");}
}

2.6 获取当前线程引用

public static void main(String[] args) throws InterruptedException {Thread thread= Thread.currentThread();System.out.println("Current Thread: " + thread);
}

3. 线程的状态

public static void main(String[] args) {for (Thread.State state: Thread.State.values()){System.out.println(state);}
}
NEW: 安排了工作, 还未开始行动
RUNNABLE: 可工作的。又可以分成正在工作中和即将开始工作
BLOCKED: 这几个都表示排队等着其他事情
WAITING: 这几个都表示排队等着其他事情
TIMED_WAITING: 这几个都表示排队等着其他事情
TERMINATED: 工作完成了

4. 多线程带来的风险-线程安全(重点)

4.1 观察线程不安全

public class ThreadDemo {private static int count=0;public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{for (int i = 0; i < 1000; i++){count++;}});Thread t2=new Thread(()->{for (int i = 0; i < 1000; i++){count++;}});t1.start();t2.start();//如果没有join,肯定不行,线程还没自增完,就开始打印了。t1.join();t2.join();System.out.println(count);}
}

4.2 线程安全的概念

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

4.3 线程不安全的原因

线程调度是随机的,这是线程安全问题的罪魁祸首,随即调度使一个程序在多线程环境下,执行顺序存在很多的变数。

修改共享数据

上面线程不安全的代码中,涉及到多个线程对count变量进行修改。此时count是一个多线程都能访问到的"共享数据"。

原子性

什么是原子性

我们把⼀段代码想象成⼀个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。
那我们应该如何解决这个问题呢?是不是只要给房间加⼀把锁,A 进去就把门锁上,其他人是不是就进 不来了。这样就保证了这段代码的原子性了。 有时也把这个现象叫做同步互斥,表示操作是互相排斥的。
一条Java语句不一定是原子的,也不一定只是一条指令。
比如刚才我们看到的 n++,其实是由三步操作组成的:
1. 从内存把数据读到 CPU
2. 进行数据更新
3. 把数据写回到 CPU
不保证原子性会给多线程带来什么问题
如果⼀个线程正在对⼀个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。
可见性
可见性指,一个线程对共享变量值的修改,能够及时地被其他线程看到。
Java内存模型(JMM):Java虚拟机规范中定义了Java内存模型。
目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果。
线程之间的共享变量存在主内存 (Main Memory)。
每⼀个线程都有自己的 "工作内存" (Working Memory) 。
当线程要读取⼀个共享变量的时候,会先把变量从主内存拷到工作内存,再从工作内存读取数据。
当线程要修改⼀个共享变量的时候,也会先修改工作内存中的副本, 再同步回主内存。
由于每个线程有自己的工作内存,这些工作内存中的内容相当于同⼀个共享变量的 "副本"。此时修改线程的工作内存中的值,,线程2的工作内存不⼀定会及时变化。
1) 初始情况下,两个线程的工作内存内容⼀致。
2) ⼀旦线程1修改了a的值, 此时主内存不一定能及时同步。对应的线程2的工作内存的 a 的值也不⼀定能及时同步。

5. synchronized 关键字 - 监视器锁 monitor lock

5.1 synchronized特性

1) 互斥

synchronized会起到互斥效果,某个线程执行到某个对象的synchronized 中时,其他线程如果也执行到同一个对象 synchronized 就会阻塞等待。
进入synchronized 修饰的代码块,相当于加锁
退出synchronized 修饰的代码块, 相当于 解锁
2) 可重入
synchronized 同步块对同⼀条线程来说是可重入的,不会出现自己把自己锁死的问题;
按照之前对于锁的设定,第⼆次加锁的时候,就会阻塞等待。直到第⼀次的锁被释放,才能获取到第二个锁,但是释放第⼀个锁也是由该线程来完成,结果这个线程已经躺平了,啥都不想干了,也就无法进行解锁操作。这时候就会死锁
Java 中的 synchronized 是可重入锁,因此没有上面的问题
for (int i = 0; i < 50000; i++) {synchronized (locker) {synchronized (locker) {count++;}}
}
在可重入锁的内部,包含了 "线程持有者" 和 "计数器" 两个信息。
如果某个线程加锁的时候, 发现锁已经被人占用,但是恰好占用的正是自己,那么仍然可以继续获取到锁,并让计数器自增。
解锁的时候计数器递减为 0 的时候,才真正释放锁。(才能被别的线程获取到)

5.2 synchronized 使用示例

synchronized 本质上要修改指定对象的 "对象头"。从使用角度来看,synchronized也势必要搭配⼀个具体的对象来使用。
private Object lock = new Object();
public void method(){synchronized ( lock){}
}

锁当前对象

public void method(){synchronized ( this){}
}

直接修饰方法

public synchronized void method(){}

5.3 Java 标准库中的线程安全类

线程不安全:

ArrayList
LinkedList
HashMap
TreeMap
HashSet
TreeSet
StringBuilder
但是还有⼀些是线程安全的。使用了⼀些锁机制来控制
Vector (不推荐使用)
HashTable (不推荐使用)
ConcurrentHashMap
StringBuffer
还有的虽然没有加锁,但是不涉及 "修改",仍然是线程安全的、
String

6. volatile 关键字

volatile 能保证内存可见性

volatile修饰的变量,能够保证"内存可见性"。

代码示例

public class CounterDemo {static class Counter {public int flag = 0;}public static void main(String[] args) {Counter counter = new Counter();Thread t1 = new Thread(() -> {while (counter.flag == 0) {// do nothing}System.out.println("循环结束!");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("输⼊⼀个整数:");counter.flag = scanner.nextInt();});t1.start();t2.start();}
}

t1读的是自己工作内存中的内容。

当t2对flag变量进行修改,此时t1感知不到flag的变化。

如果给flag加上volatile

static class Counter {public volatile int flag = 0;
}

volatile 和 synchronized 有着本质的区别。synchronized 能够保证原子性,volatile保证的是内存可见性。

public class CounterDemo {static class Counter {public volatile int flag = 0;}public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread(() -> {for (int i=0;i<1000;i++){counter.flag++;}System.out.println("循环结束!");});Thread t2 = new Thread(() -> {for (int i=0;i<1000;i++){counter.flag++;}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.flag);}
}

最终count的值仍然无法保证是2000。

7 wait和notify

由于线程之间是抢占式执行的,因此线程之间执行的先后顺序难以预知。但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序。
完成这个协调工作,主要涉及到三个方法
wait() / wait(long timeout): 让当前线程进入等待状态。
notify() / notifyAll(): 唤醒在当前对象上等待的线程。

7.1 wait()方法

wait做的事情:

使当前执行代码的线程进行等待。(把线程放到等待队列中)

释放当前锁

满足一定条件时被唤醒,重新尝试获取这个锁。

wait要搭配synchronized来使用。脱离了synchronized使用wait会抛出异常。

wait结束等待的条件:

其他线程调用该对象的notify方法。

wait等待时间超时。

其他线程调用该等待线程的interrupted的方法,导师wait抛出InterruptedException异常。、

代码示例:

public static void main(String[] args) throws InterruptedException {Object obj = new Object();synchronized ( obj){System.out.println("等待中");obj.wait();System.out.println("被唤醒");}
}
这样在执行到object.wait()之后就⼀直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就需要使用到了另外⼀个方法唤醒的方法notify()。

7.2 notify()方法

notify方法是唤醒等待的线程。
方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其
它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
如果有多个线程等待,则有线程调度器随机挑选出⼀个呈 wait 状态的线程。(并没有 "先来后到")。
在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
代码示例:
public class WaitTaskDemo {static class WaitTask implements Runnable{private Object locker;public WaitTask(Object locker) {this.locker = locker;}@Overridepublic void run() {synchronized (locker){
//                while ( true){try {System.out.println("wait 开始");locker.wait();System.out.println("wait 结束");} catch (InterruptedException e) {e.printStackTrace();}
//                }}}}static class NotifyTask implements Runnable {private Object locker;public NotifyTask(Object locker) {this.locker = locker;}@Overridepublic void run() {synchronized (locker) {System.out.println("notify 开始");locker.notify();System.out.println("notify 结束");}}}public static void main(String[] args) throws InterruptedException {Object lock = new Object();Thread t1=new Thread(new WaitTask(lock));Thread t2=new Thread(new NotifyTask(lock));t1.start();Thread.sleep(1000);t2.start();}
}

7.3 notifyAll()方法

public class WaitTaskDemo {static class WaitTask implements Runnable{private Object locker;public WaitTask(Object locker) {this.locker = locker;}@Overridepublic void run() {synchronized (locker){
//                while ( true){try {System.out.println("wait 开始");locker.wait();System.out.println("wait 结束");} catch (InterruptedException e) {e.printStackTrace();}
//                }}}}static class NotifyTask implements Runnable {private Object locker;public NotifyTask(Object locker) {this.locker = locker;}@Overridepublic void run() {synchronized (locker) {System.out.println("notify 开始");locker.notify();System.out.println("notify 结束");}}}public static void main(String[] args) throws InterruptedException {Object lock = new Object();Thread t1=new Thread(new WaitTask(lock));Thread t3=new Thread(new WaitTask(lock));Thread t4=new Thread(new WaitTask(lock));Thread t2=new Thread(new NotifyTask(lock));t3.start();t4.start();Thread.sleep(1000);t2.start();
//        t1.start();
//        Thread.sleep(1000);
//        t2.start();}
}

只能唤醒一个线程。

将notify修改为notifyAll

@Override
public void run() {synchronized (locker) {System.out.println("notify 开始");locker.notifyAll();System.out.println("notify 结束");}
}

能同时唤醒3个wait中的线程。

注意: 虽然是同时唤醒 3 个线程,但是这 3 个线程需要竞争锁,所以并不是同时执行,而仍然是有先有后的执行。

8. 阻塞队列实现

public class BlockingQueue {private int[] items=new int[1000];private volatile int size=0;private volatile int head=0;private volatile int tail=0;public void put(int value) throws InterruptedException {synchronized ( this){// 此处最好使⽤ while.// 否则 notifyAll 的时候, 该线程从 wait 中被唤醒,// 但是紧接着并未抢占到锁. 当锁被抢占的时候, 可能⼜已经队列满了// 就只能继续等待while (size==items.length){wait();}items[tail]=value;tail=(tail+1)%items.length;size++;notifyAll();}}public int take() throws InterruptedException {int ret=0;synchronized (this){while (size==0){wait();}ret=items[head];head=(head+1)%items.length;size--;notifyAll();}return ret;}public synchronized int size(){return size;}public static void main(String[] args) throws InterruptedException {BlockingQueue queue = new BlockingQueue();Thread customer=new Thread(()->{try {int value=queue.take();} catch (InterruptedException e) {e.printStackTrace();}},"消费者");customer.start();Thread producer=new Thread(()->{Random random = new Random();try {queue.put(random.nextInt());} catch (InterruptedException e) {e.printStackTrace();}},"生产者");producer.start();customer.join();producer.join();}
}

9. 定时器

定时器类似于一个"闹钟",达到一个设定的事件之后,就执行某个指定好的代码。

标准库中的定时器

标准库中提供了⼀个 Timer 类。Timer 类的核心方法为 schedule。
schedule 包含两个参数。第⼀个参数指定即将要执行的任务代码,第二个参数指定多长时间之后
执行(单位为毫秒)。
public static void main(String[] args) {Timer timer=new Timer();timer.schedule(new TimerTask(){@Overridepublic void run() {System.out.println("定时任务执行了");}},3000);
}

10. 线程池

线程池的最大好处就是减少每次启动,销毁线程的损耗。

标准库中的线程池

public static void main(String[] args) {//创建出10个线程的线程池ExecutorService pool = Executors.newFixedThreadPool(10);//可以注册一个任务到线程池中pool.submit(new Runnable() {@Overridepublic void run() {System.out.println("hello");}});
}

实现线程池

public class MyThreadPoolDemo {static class MyThreadPool{private BlockingQueue<Runnable> queue=new LinkedBlockingQueue<>();//通过这个方法,来把任务添加到线程中public void submit(Runnable runnable) throws InterruptedException {queue.put(runnable);}//n标识线程池里面有几个线程//创建了一个固定数量的线程池public MyThreadPool(int n){for (int i = 0; i < n; i++){Thread t=new Thread(()->{while (true) {try {//取出任务,并执行Runnable runnable=queue.take();runnable.run();} catch (InterruptedException e) {e.printStackTrace();}}});t.start();}}}public static void main(String[] args) throws InterruptedException {MyThreadPool pool=new MyThreadPool(4);for (int i = 0; i < 1000; i++){pool.submit(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"正在执行任务");}});}}}

http://www.dtcms.com/a/393741.html

相关文章:

  • 在 R 语言中,%>% 是 管道操作符 (Pipe Operator),它来自 magrittr 包(后被 dplyr 等 tidyverse 包广泛采用)
  • IMX6ULL学习笔记_Boot和裸机篇(1)--- SEGGER Embedded Studio 和 Uboot 环境搭建
  • 纯JS代码录制网页中的视频(可多线操作)
  • Javase 基础加强 —— 11 线程池
  • 分布式锁-Redis实现
  • 对于ModelScope的AI模型git部署感悟
  • [论文阅读] 人工智能 + 软件工程 | 从“人工扒日志”到“AI自动诊断”:LogCoT框架的3大核心创新
  • 【软考中级 - 软件设计师 - 应用技术】软件工程案例分析之软件测试实践
  • AI:读《老人与海》有感
  • 定制开发开源AI智能名片S2B2C商城小程序:产业互联网时代的创新商业模式
  • .env与.gitignore:现代软件开发中的环境管理与版本控制防护
  • 理解重参数化
  • css 给文本添加任务图片背景
  • CSS中的选择器、引入方式和样式属性
  • CSS 入门与常用属性详解
  • Linux 下 PostgreSQL 安装与常用操作指南
  • 【Linux】CentOS7网络服务配置
  • 使用C++编写的一款射击五彩敌人的游戏
  • 【LeetCode hot100|Week3】数组,矩阵
  • linux-环境配置-指令-记录
  • 自学嵌入式第四十四天:汇编
  • RTX 4090助力深度学习:从PyTorch到生产环境的完整实践指南——模型部署与性能优化
  • PythonOCC 在二维平面上实现圆角(Fillet)
  • Unity 性能优化 之 实战场景简化(LOD策略 | 遮挡剔除 | 光影剔除 | 渲染流程的精简与优化 | Terrain地形优化 | 主光源级联阴影优化)
  • [GXYCTF2019]禁止套娃1
  • 【论文阅读】-《Triangle Attack: A Query-efficient Decision-based Adversarial Attack》
  • 云微短剧小程序系统开发:赋能短剧生态,打造全链路数字化解决方案
  • 《从延迟300ms到80ms:GitHub Copilot X+Snyk重构手游跨服社交系统实录》
  • 力扣2132. 用邮票贴满网格图
  • Halcon学习--视觉深度学习