什么是多线程?
有了多线程,我们就可以让程序同时做多件事情
多线程的作用?
提高效率
多线程的应用场景?
只要你想让多个事情同时运行就需要用到多线程
比如:软件中的耗时操作,所有的聊天软件,所有的服务器
并发和并行
并发:在同一时刻,有多个指令在单个cpu上交替执行
并行:在同一时刻,有多个指令在多个cpu上同时执行
多线程的实现方式
1. 继承Thread
类
这种方式是通过继承Thread
类,并重写其run()
方法来定义线程的任务。
步骤:
定义一个类继承Thread
类。
在类中重写run()
方法,定义线程需要执行的任务。
创建该类的实例,并调用start()
方法启动线程。
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread is running...");
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 启动线程
}
}
2. 实现Runnable
接口
这种方式是通过实现Runnable
接口的run()
方法来定义线程的任务。这种方式可以避免Java单继承的限制,更适用于需要继承其他类的情况。
步骤:
定义一个类实现Runnable
接口。
在类中实现run()
方法,定义线程需要执行的任务。
创建该类的实例,作为参数传递给Thread
类的构造函数,创建Thread
对象。
调用Thread
对象的start()
方法启动线程。
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Thread is running...");
}
}
public class Main {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread t= new Thread(myRunnable);
t.start(); // 启动线程
}
}
3. 使用Callable
接口和Future
接口
这种方式与前两种不同,Callable
接口可以返回线程执行的结果,并且可以抛出异常。它需要配合Future
接口来获取执行结果。
步骤:
定义一个类实现Callable
接口,并重写call()
方法来定义线程的任务。
使用ExecutorService
创建线程池,提交Callable
任务。
通过Future
对象获取线程的执行结果。
接口
package ThridCallabelAndFuture;
import java.util.concurrent.Callable;
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i = 0; i <= 100; i++) sum+=i;
return sum;
}
}
测试
package ThridCallabelAndFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/*
多线程第三种实现方式
特点:可以获取到多线程运行的结果
创建一个类MyCallable实现callable接口
重写call(有返回值,表示多线程的运行结果)
创建MyCallable的对象(表示多线程要执行的任务)
创建FutureTask的对象(作用管理多线程运行的结果)
创建Thread类的对象,并且启动(表示线程)
*/
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread t1 = new Thread(ft);
t1.start();
// 获取多线程运行的结果
int ans = ft.get();
System.out.println(ans);
}
}
多线程常用成员方法
String getName() 返回线程的名称
void setName(String name) 设置线程的名字(构造方法也可以设置名字)
细节:
1.如果我们没有给线程设置名字,线程也是有默认的名字的
格式:Thread-x(x序号,从0开始)
2.如果我们要给线程设置名字,可以用set方法进行设置,也可以构造方法设置
static Thread currentTread() 获取当前线程的对象
细节:
当JVM虚拟机启动之和,会自动的启动多条线程
其中一条线程叫做main线程
他的作用就是调用main方法,并执行里面的代码
在以前,我们写的所有代码都是运行在main线程中的
static void sleep(long time) 让线程休眠指定的时间,单位为毫秒
细节:
1.哪条线程执行到这个方法,那么哪条线程就会在这里停留对应的时间
2.方法的参数:表示睡眠的时间。单位是毫秒
当时间到了之后,线程就会自动结束,继续执行下面的其他代码
优先级:
setPriority(int newPriority)
final int getPriority()
默认的优先级为5
守护线程:
final void setDaemon(boolean on) 设置为守护线程
细节:
当其他的非守护线程执行完毕后,守护线程会陆续结束(不会立即结束)
出让线程/礼让线程
public static void yield()
插入线程/插队线程
public static void join()
//优先级
MyThread mh1 = new MyThread("飞机");
MyThread mh2 = new MyThread("坦克");
mh2.setPriority(Thread.MAX_PRIORITY);// 设置为10
System.out.println(mh1.getPriority());
System.out.println(mh2.getPriority());
// 守护线程
MyThread mh3 = new MyThread("女神");
MyThread mh4 = new MyThread("备胎");
// 把第四个线程设置为守护线程
// 当女神线程结束后,备胎就没有存在的必要了,会陆续结束
mh4.setDaemon(true);
mh3.start();
mh4.start();
// 插入线程
MyThread mh5 = new MyThread("土豆");
mh5.start();
mh5.join();
// 表示把mh5这个线程插入到当前线程之前
// mh5:土豆
// 当前线程:main线程
for(int i = 0; i < 10; i++){
System.out.println("main线程"+i);
}
线程的生命周期
线程的安全问题
数据竞争:当多个线程同时读取和写入共享变量时,可能会导致不可预测的结果。例如,一个线程正在更新一个变量的值,而另一个线程同时读取这个变量的值,就可能导致读取到错误的数据。
死锁:当两个或多个线程相互等待对方释放资源,从而进入无限等待的状态,这种情况称为死锁。死锁会导致程序的部分或全部停止运行。
解决:
1. 同步代码块(Synchronized Block)
同步代码块是指对一段代码进行同步控制,使用synchronized
关键字指定需要同步的代码块。它可以用来锁住一个对象,使得同一时间只有一个线程可以执行该代码块。
synchronized (锁对象) {
// 同步代码块
}
锁对象:通常使用的是共享资源的对象(如this
或类的某个静态对象)。锁对象必须要是唯一的,可以用字节码文件对象(MyThread.class)
同步代码块:在锁定的范围内,只有持有该锁对象的线程才能执行,其他线程必须等待
package synchronous;
public class MyThread extends Thread {
static int tic = 1;
// 锁对象一定要是唯一的
// static Object obj = new Object();
@Override
public void run() {
// 同步代码块
while(true){
// 如果锁对象是this,那么锁就不管用了
// 字节码文件对象一定是唯一的,可以作为锁对象
synchronized(MyThread.class){
// 同步代码块
if(tic <= 100){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(getName()+ "正在卖第"+tic+"张票");
tic++;
}else break;
}
}
System.out.println("票卖完了");
}
}
使用第二种实现方式:
package synchronous;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyRunnable implements Runnable {
static Lock lock = new ReentrantLock();
int tic = 0;
@Override
public void run() {
while (true) {
if (method()) break;
}
System.out.println("票卖完了");
}
private boolean method() {
// 自动锁
synchronized (Runnable.class) {
// 同步代码块
if (tic <= 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "正在卖第" + tic + "张票");
tic++;
} else return true;
}
return false;
}
}
// 避免死锁(避免两个锁嵌套)
package synchronous;
public class Test {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
MyRunnable mr = new MyRunnable();
Thread t4 = new Thread(mr);
Thread t5 = new Thread(mr);
Thread t6 = new Thread(mr);
t4.setName("窗口1");
t5.setName("窗口2");
t6.setName("窗口3");
// t1.start();
// t2.start();
// t3.start();
t4.start();
t5.start();
t6.start();
}
}
2. 同步方法(Synchronized Method)
同步方法是指使用synchronized
关键字修饰整个方法,使得该方法在某个时刻只能被一个线程访问。
public synchronized void 方法名() {
// 方法体
}
锁
import java.util.concurrent.locks.ReentrantLock;
public class Example {
private final ReentrantLock lock = new ReentrantLock();
public void method() {
lock.lock(); // 获取锁
try {
// 临界区代码
} finally {
lock.unlock(); // 释放锁
}
}
}
/*
1.循环
2.同步代码块
3.判断共享数据是否到了末尾(先写到了末尾)
没有到末尾,执行核心逻辑
*/
package synchronous;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyRunnable implements Runnable {
static Lock lock = new ReentrantLock();
int tic = 0;
@Override
public void run() {
while(true){
// 手动锁
lock.lock();
try {
if(tic==100){
break;
}else{
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + "正在卖第" + tic + "张票");
tic++;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
//finally是一定会被执行的
} finally {
lock.unlock();
}
}
}
}
等待唤醒机制
核心方法
wait()
:
使当前线程进入等待状态,并释放锁。
线程在调用wait()
时,必须已经持有该对象的锁(即在同步方法或同步块中调用)。
调用wait()
后,当前线程会进入等待队列,直到其他线程调用notify()
或notifyAll()
唤醒它。
notify()
:
唤醒在等待队列中等待同一个锁的一个线程。如果有多个线程在等待同一个锁,具体唤醒哪一个线程是由JVM决定的。
唤醒的线程必须重新获得该锁才能继续执行。
notifyAll()
:
唤醒所有在等待队列中等待同一个锁的线程。被唤醒的线程仍然需要竞争锁,只有获得锁的线程才能继续执行。
样例:
背景:吃货吃面,厨师做面,其中需要一个桌子作为中间人。我们需要完成的是如果桌子上面有食物就吃食物,十碗吃饱,如果没有,就等待。厨师则是判断桌子上面是否有食物,如果有,就等待,如果没有就做面条。
桌子:
package waitandnotify;
public class Desk {
// 表示是否有面条 0:没有面条 1:有面条
public static int foodFlag = 0;
// 总个数
public static int cnt = 10;
// 锁对象
public static Object lock = new Object();
}
吃货:
package waitandnotify;
public class Foodie extends Thread {
/*
1.循环
2.同步代码块
3.判断共享数据是否到了末尾(先写到了末尾)
没有到末尾,执行核心逻辑
*/
@Override
public void run() {
while(true){
synchronized(Desk.lock){
if(Desk.cnt == 0) {
break;
} else {
// 判断桌子上面是否有面条
if(Desk.foodFlag == 1){
Desk.cnt--;
if(Desk.cnt == 0){
System.out.println("吃货吃饱啦!!");
}else {
System.out.println("吃货正在吃面条还能再吃"+Desk.cnt+"碗面条");
}
Desk.foodFlag--;
// 唤醒厨师做面条
Desk.lock.notifyAll();
} else if(Desk.foodFlag == 0){
// 没有就等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
}
}
厨师:
package waitandnotify;
public class cook extends Thread {
@Override
public void run() {
while (true) {
synchronized (Desk.lock) {
if(Desk.cnt== 0){
break;
}else{
if(Desk.foodFlag==1){
// 有食物就等待必须用锁对象调用wait
// 这样就能把锁对象和线程绑定起来
// 绑定之和唤醒就知道唤醒什么线程了
// 不能直接wait();
try {
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else if(Desk.foodFlag== 0){
// 开始做面
System.out.println("厨师做了一碗面条");
Desk.foodFlag++;
Desk.lock.notify();// 唤醒这把锁绑定的线程
}
}
}
}
}
}
阻塞队列完成等待唤醒机制
阻塞队列的基本概念
阻塞队列:一种支持线程安全操作的队列,当队列为空时,从队列中取元素的操作会阻塞线程,直到队列中有元素可用;当队列已满时,向队列中插入元素的操作会阻塞线程,直到队列中有空间。
常见的阻塞队列实现:
ArrayBlockingQueue
:基于数组的有界阻塞队列,队列大小在初始化时指定,之后不可变。LinkedBlockingQueue
:基于链表的阻塞队列,可以是有界或无界的(实际上是有界的,但默认值为Integer.MAX_VALUE
)。PriorityBlockingQueue
:基于优先级堆的无界阻塞队列,元素根据其优先级进行排序。SynchronousQueue
:一种特殊的阻塞队列,不存储元素,每个put
操作必须等待一个take
操作,反之亦然。DelayQueue
:其中的元素只有在指定的延迟时间过后才能被取走。
阻塞队列的核心方法
-
插入操作:
put(E e)
:将元素插入队列,如果队列满了,线程将被阻塞,直到队列有空间。offer(E e)
:尝试将元素插入队列,返回true
表示插入成功,返回false
表示队列已满。offer(E e, long timeout, TimeUnit unit)
:在给定的时间内尝试插入元素,如果插入失败线程将被阻塞,直到成功插入或超时。
-
移除操作:
take()
:从队列中取走一个元素,如果队列为空,线程将被阻塞,直到队列有可用元素。poll()
:尝试从队列中取走一个元素,返回null
表示队列为空。poll(long timeout, TimeUnit unit)
:在给定的时间内尝试取走一个元素,如果队列为空线程将被阻塞,直到成功取出元素或超时。
情景:
厨师:
package waitandnotifyQue;
import java.util.concurrent.ArrayBlockingQueue;
public class cook extends Thread {
ArrayBlockingQueue<String> aq;
public cook( ArrayBlockingQueue<String> aq ) {
this.aq = aq;
}
@Override
public void run() {
while (true){
try {
aq.put("食物");
System.out.println("厨师做了一碗面条");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
吃货:
package waitandnotifyQue;
import java.util.concurrent.ArrayBlockingQueue;
public class Foodie extends Thread {
ArrayBlockingQueue<String> aq;
public Foodie( ArrayBlockingQueue<String> aq ) {
this.aq = aq;
}
// 不会结束
@Override
public void run() {
while (true){
try {
// take里面自带锁,如果再加上锁就会变成死锁
String food = aq.take();
System.out.println(food);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
测试:
package waitandnotifyQue;
import java.util.concurrent.ArrayBlockingQueue;
public class Test {
public static void main(String[] args) {
ArrayBlockingQueue<String> aq = new ArrayBlockingQueue<>(1); // 定义一个队列
cook c = new cook(aq);
waitandnotifyQue.Foodie f = new Foodie(aq);
c.start();
f.start();
}
}
线程池:
线程池的基本概念
线程池:一个容纳多个可复用线程的池子。线程池中的线程可以被多次复用,以执行不同的任务,从而避免了频繁的线程创建和销毁。
工作原理:
- 提交任务到线程池。
- 线程池将任务分配给空闲的线程执行。
- 如果所有线程都在忙,线程池会根据配置决定是创建新线程、将任务排队,还是拒绝任务。
- 线程执行完任务后回到线程池等待新的任务。
主要核心原理
- 创建一个池子,池子是空的。
- 提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子,下回再次提交任务时,不需要创建新的线程,直接复用已有的线程即可。
- 但是如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待。
package ThreadPoll;
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
没有上限的线程池
package ThreadPoll;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test {
public static void main(String[] args) throws InterruptedException {
/*
public static ExecutorService newCachedThreadPool() 创建一个没有上限的线程池
public static ExecutorService newFixedThreadPool(int nThreads) 创建一个有上限的线程池
*/
ExecutorService poolCached = Executors.newCachedThreadPool();
// 会创建7个线程
poolCached.submit(new MyRunnable());
poolCached.submit(new MyRunnable());
poolCached.submit(new MyRunnable());
poolCached.submit(new MyRunnable());
poolCached.submit(new MyRunnable());
poolCached.submit(new MyRunnable());
poolCached.submit(new MyRunnable());
// 销毁线程池
poolCached.shutdown();
}
}
有上限的线程池
package ThreadPoll;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test {
public static void main(String[] args) throws InterruptedException {
/*
public static ExecutorService newCachedThreadPool() 创建一个没有上限的线程池
public static ExecutorService newFixedThreadPool(int nThreads) 创建一个有上限的线程池
*/
ExecutorService poolFix = Executors.newFixedThreadPool(3);
// 只有3个线程
poolFix.submit(new MyRunnable());
poolFix.submit(new MyRunnable());
poolFix.submit(new MyRunnable());
poolFix.submit(new MyRunnable());
poolFix.submit(new MyRunnable());
poolFix.submit(new MyRunnable());
// 销毁线程池
poolFix.shutdown();
}
}