多线程
1.什么是多线程
我们需要明白什么是多线程的前提需要厘清什么是多线程和生命是多进程,要不然这个疑问一直贯穿我们的学习,下面是我多对线程和多进程的理解
多线程:在一个程序中同时执行多个任务
多进程·:在一个系统中执行多个程序
2.为什么使用到多线程
单线程:就比如我们在食堂中一个阿姨卖饭300份,使用单线程就可以做到安全,可以随时知道还有几份饭没有卖完,但是缺点是速度慢,需要学生一个个的排队购买
单线程卖饭的示例代码如下:
public class SingleThreadExample {
private static final int TOTAL_FOOD = 300;
public static void main(String[] args) {
Auntie auntie = new Auntie();
int soldFood = 0;
while (soldFood < TOTAL_FOOD) {
auntie.sellFood();
soldFood++;
}
System.out.println("单线程卖饭完成。");
}
private static class Auntie {
public void sellFood() {
System.out.println(Thread.currentThread().getName() + " 卖出了一份饭。");
}
}
}
多线程:使用多线程可以做到多个阿姨卖300份饭,速度快,学生不需要等待,但是缺点是不安全,不能随时知道准确还剩下几份饭,刚刚还有100,下一瞬间就还有80,造成总有几个学生排队但是吃不上饭,这个像不像我们抢票嘛,明明看见有票但是自己就是抢不到
多线程卖饭如下:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MultiThreadExample {
private static final int TOTAL_FOOD = 300;
private static final int NUM_AUNTIES = 5;
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(NUM_AUNTIES);
Auntie[] aunties = new Auntie[NUM_AUNTIES];
for (int i = 0; i < NUM_AUNTIES; i++) {
aunties[i] = new Auntie();
executorService.execute(aunties[i]);
}
executorService.shutdown();
while (!executorService.isTerminated()) {
// 等待所有阿姨卖饭完成
}
System.out.println("多线程卖饭完成。");
}
private static class Auntie implements Runnable {
private static final Object lock = new Object();
private static int soldFood = 0;
@Override
public void run() {
while (soldFood < TOTAL_FOOD) {
synchronized (lock) {
if (soldFood < TOTAL_FOOD) {
sellFood();
soldFood++;
}
}
}
}
private void sellFood() {
System.out.println(Thread.currentThread().getName() + " 卖出了一份饭。");
}
}
}
3.在java中使用到多线程
在Java中,可以通过两种方式创建线程:继承Thread类和实现Runnable接口。继承Thread类需要重写run()方法,实现Runnable接口需要实现run()方法,并将该Runnable对象传递给Thread类的构造方法。
多线程使用方法一:
创建一个Thread的子类,实现子类的run方法,在主方法中实例化该子类,并且调用该实例化子类的start方法
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("我要进行卖饭了");
// 线程执行的代码
}
}
多线程使用方法二:
实现Runnable接口需要实现run()方法,并将该Runnable对象传递给Thread类的构造方法。
public class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行的代码
System.out.println("我要进行卖饭了");
}
}
Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
4.使用多线程需要注意的问题
在多线程编程中,可能会遇到共享资源的竞争和冲突问题。为了避免这些问题,可以使用同步机制,如synchronized关键字和Lock接口来确保线程安全。
当我们同时去操作一个共享变量时,如果仅仅是读取还好,但是如果同时写入内容,就会出现问题!好比说一个银行,如果我和我的朋友同时在银行取我账户里面的钱,难道取1000还可能吐2000出来吗?我们需要一种更加安全的机制来维持秩序,保证数据的安全性!
比如我们可以来看看下面这个问题:
实际上,当两个线程同时读取value的时候,可能会同时拿到同样的值,而进行自增操作之后,也是同样的值,再写回主内存后,本来应该进行2次自增操作,实际上只执行了一次,所以造成这种效果的原因是线程竞争这个变量。
解决上面的问题有很多种方法比如下面代码(这个部分不涉及多线程锁问题可以进行跳过是作者新增的)
public class MyThread {
private static AtomicInteger value = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) value.incrementAndGet();
System.out.println("线程1完成");
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) value.incrementAndGet();
System.out.println("线程2完成");
});
t1.start();
t2.start();
Thread.sleep(1000);
System.out.println(value);
}
}
我们运行多次都是得到这种情况:
我们稍微进行修改了代码使用value.incrementAndGet()代替value++反而得到正确的结果那么一样都是自增的操作为什么案例二可以得到正确结果呢,听我慢慢给你分析
value.incrementAndGet() 和 i++ 在实现上有一些重要的区别:
-
原子性: value.incrementAndGet() 是原子操作,确保在多线程环境下自增操作的原子性。它使用底层的CAS(Compare and Swap)操作,确保只有一个线程能够成功修改值。而 i++ 不是原子操作,它包含了读取、自增和赋值三个步骤,可能会在多线程环境下导致竞争条件和数据不一致的问题。
-
线程安全性:由于 value.incrementAndGet() 是原子操作,它可以在多线程环境下安全地执行自增操作,而不需要额外的同步措施。而 i++ 在多线程环境下是不安全的,需要通过同步机制(如 synchronized 关键字或 AtomicInteger 等原子类)来保证线程安全。
-
返回值: value.incrementAndGet() 会返回自增后的新值,而 i++ 会返回自增之前的旧值。这是因为 value.incrementAndGet() 是原子操作,能够立即返回自增后的值,而 i++ 需要先返回旧值再进行自增操作。
综上所述, value.incrementAndGet() 是一种更加可靠和线程安全的方式来实现自增操作,特别适用于多线程环境。而 i++ 在多线程环境下需要额外的同步机制来确保线程安全,并且可能会出现竞争条件和数据不一致的问题。
但是这个方法只能解决一些比较简单的多线程问题,需要解决比较复杂的多线竞争问题还是需要学习下面的知识进行解决
synchronized关键字来创造一个线程锁解决上面出现的问题
通过synchronized关键字来创造一个线程锁,首先我们来认识一下synchronized代码块,它需要在括号中填入一个内容,必须是一个对象或是一个类,我们在value自增操作外套上同步代码块:
private static int value = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
synchronized (Main.class){ //使用synchronized关键字创建同步代码块
value++;
}
}
System.out.println("线程1完成");
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
synchronized (Main.class){
value++;
}
}
System.out.println("线程2完成");
});
t1.start();
t2.start();
Thread.sleep(1000); //主线程停止1秒,保证两个线程执行完成
System.out.println(value);
}
我们发现,现在得到的结果就是我们想要的内容了,因为在同步代码块执行过程中,拿到了我们传入对象或类的锁(传入的如果是对象,就是对象锁,不同的对象代表不同的对象锁,如果是类,就是类锁,类锁只有一个,实际上类锁也是对象锁,是Class类实例,但是Class类实例同样的类无论怎么获取都是同一个),但是注意两个线程必须使用同一把锁!
当一个线程进入到同步代码块时,会获取到当前的锁,而这时如果其他使用同样的锁的同步代码块也想执行内容,就必须等待当前同步代码块的内容执行完毕,在执行完毕后会自动释放这把锁,而其他的线程才能拿到这把锁并开始执行同步代码块里面的内容(实际上synchronized是一种悲观锁,随时都认为有其他线程在对数据进行修改,后面在JUC篇教程中我们还会讲到乐观锁,如CAS算法)这里就不进行一一阐述了
synchronized关键字也可以作用于方法上,调用此方法时也会获取锁:
private static int value = 0;
private static synchronized void add(){
value++;
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) add();
System.out.println("线程1完成");
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) add();
System.out.println("线程2完成");
});
t1.start();
t2.start();
Thread.sleep(1000); //主线程停止1秒,保证两个线程执行完成
System.out.println(value);
}
我们发现实际上效果是相同的,只不过这个锁不用你去给,如果是静态方法,就是使用的类锁,而如果是普通成员方法,就是使用的对象锁。通过灵活的使用synchronized就能很好地解决我们之前提到的问题了。
死锁
其实死锁的概念在操作系统中也有提及,它是指两个线程相互持有对方需要的锁,但是又迟迟不释放,导致程序卡住
死锁,通俗一点,占着茅坑不拉屎
我们发现,线程A和线程B都需要对方的锁,但是又被对方牢牢把握,由于线程被无限期地阻塞,因此程序不可能正常终止。我们来看看以下这段代码会得到什么结果:
public static void main(String[] args) throws InterruptedException {
Object o1 = new Object();
Object o2 = new Object();
Thread t1 = new Thread(() -> {
synchronized (o1){
try {
Thread.sleep(1000);
synchronized (o2){
System.out.println("线程1");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
synchronized (o2){
try {
Thread.sleep(1000);
synchronized (o1){
System.out.println("线程2");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
然后我们的程序会一直等待,直到卡死,所以,我们在编写程序时,一定要注意,不要出现这种死锁的情况。
wait和notify方法
其实我们之前可能就发现了,Object类还有三个方法我们从来没有使用过,分别是wait()、notify()以及notifyAll(),他们其实是需要配合synchronized来使用的(实际上锁就是依附于对象存在的,每个对象都应该有针对于锁的一些操作,所以说就这样设计了)当然,只有在同步代码块中才能使用这些方法,正常情况下会报错,我们来看看他们的作用是什么:
public static void main(String[] args) throws InterruptedException {
Object o1 = new Object();
Thread t1 = new Thread(() -> {
synchronized (o1){
try {
System.out.println("开始等待");
o1.wait(); //进入等待状态并释放锁
System.out.println("等待结束!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
synchronized (o1){
System.out.println("开始唤醒!");
o1.notify(); //唤醒处于等待状态的线程
for (int i = 0; i < 50; i++) {
System.out.println(i);
}
//唤醒后依然需要等待这里的锁释放之前等待的线程才能继续
}
});
t1.start();
Thread.sleep(1000);
t2.start();
}
我们可以发现,对象的wait()方法会暂时使得此线程进入等待状态,同时会释放当前代码块持有的锁,这时其他线程可以获取到此对象的锁,当其他线程调用对象的notify()方法后,会唤醒刚才变成等待状态的线程(这时并没有立即释放锁)。注意,必须是在持有锁(同步代码块内部)的情况下使用,否则会抛出异常!
notifyAll其实和notify一样,也是用于唤醒,但是前者是唤醒所有调用wait()后处于等待的线程,而后者是看运气随机选择一个。
ThreadLocal的使用
既然每个线程都有一个自己的工作内存,就比如现在我们定义多个线程锁ip,限制一个ip5s内访问次数为10次,超过就封禁。
我们可以使用ThreadLocal类,来创建工作内存中的变量,它将我们的变量值存储在内部(只能存储一个变量),不同的线程访问到ThreadLocal对象时,都只能获取到当前线程所属的变量。
p public static void main(String[] args) throws InterruptedException {
ThreadLocal<String> local = new ThreadLocal<>(); //注意这是一个泛型类,存储类型为我们要存放的变量类型
Thread t1 = new Thread(() -> {
local.set("11111"); //将变量的值给予ThreadLocal
System.out.println("访问次数已经设定");
System.out.println(local.get()); //尝试获取ThreadLocal中存放的变量
});
Thread t2 = new Thread(() -> {
System.out.println(local.get()); //尝试获取ThreadLocal中存放的变量
});
t1.start();
Thread.sleep(3000); //间隔三秒
t2.start();
}
运行结果如下:
上面的例子中,我们开启两个线程分别去访问ThreadLocal对象,我们发现,第一个线程存放的内容,第一个线程可以获取,但是第二个线程无法获取,我们再来看看第一个线程存入后,第二个线程也存放,是否会覆盖第一个线程存放的内容:
public static void main(String[] args) throws InterruptedException {
ThreadLocal<String> local = new ThreadLocal<>(); //注意这是一个泛型类,存储类型为我们要存放的变量类型
Thread t1 = new Thread(() -> {
local.set("ywsnb"); //将变量的值给予ThreadLocal
System.out.println("线程1变量值已设定!");
try {
Thread.sleep(2000); //间隔2秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1读取变量值:");
System.out.println(local.get()); //尝试获取ThreadLocal中存放的变量
});
Thread t2 = new Thread(() -> {
local.set("yyds"); //将变量的值给予ThreadLocal
System.out.println("线程2变量值已设定!");
});
t1.start();
Thread.sleep(1000); //间隔1秒
t2.start();
}
我们发现,即使线程2重新设定了值,也没有影响到线程1存放的值,所以说,不同线程向ThreadLocal存放数据,只会存放在线程自己的工作空间中,而不会直接存放到主内存中,因此各个线程直接存放的内容互不干扰。
我们发现在线程中创建的子线程,无法获得父线程工作内存中的变量:
public static void main(String[] args) {
ThreadLocal<String> local = new ThreadLocal<>();
Thread t = new Thread(() -> {
local.set("ywsnb");
new Thread(() -> {
System.out.println(local.get());
}).start();
});
t.start();
}
我们可以使用InheritableThreadLocal来解决:
public static void main(String[] args) {
ThreadLocal<String> local = new InheritableThreadLocal<>();
Thread t = new Thread(() -> {
local.set("ywsnb");
new Thread(() -> {
System.out.println(local.get());
}).start();
});
t.start();
}
在InheritableThreadLocal存放的内容,会自动向子线程传递。
定时器
我们有时候会有这样的需求,我希望定时执行任务,比如3秒后执行,其实我们可以通过使用Thread.sleep()来实现:
public static void main(String[] args) {
new TimerTask(() -> System.out.println("我是定时任务!"), 3000).start(); //创建并启动此定时任务
}
static class TimerTask{
Runnable task;
long time;
public TimerTask(Runnable runnable, long time){
this.task = runnable;
this.time = time;
}
public void start(){
new Thread(() -> {
try {
Thread.sleep(time);
task.run(); //休眠后再运行
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
我们通过自行封装一个TimerTask类,并在启动时,先休眠3秒钟,再执行我们传入的内容。那么现在我们希望,能否循环执行一个任务呢?比如我希望每隔1秒钟执行一次代码,这样该怎么做呢?
public static void main(String[] args) {
new TimerLoopTask(() -> System.out.println("我是定时任务!"), 3000).start(); //创建并启动此定时任务
}
static class TimerLoopTask{
Runnable task;
long loopTime;
public TimerLoopTask(Runnable runnable, long loopTime){
this.task = runnable;
this.loopTime = loopTime;
}
public void start(){
new Thread(() -> {
try {
while (true){ //无限循环执行
Thread.sleep(loopTime);
task.run(); //休眠后再运行
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
现在我们将单次执行放入到一个无限循环中,这样就能一直执行了,并且按照我们的间隔时间进行。
但是终究是我们自己实现,可能很多方面还没考虑到,Java也为我们提供了一套自己的框架用于处理定时任务:
public static void main(String[] args) {
Timer timer = new Timer(); //创建定时器对象
timer.schedule(new TimerTask() { //注意这个是一个抽象类,不是接口,无法使用lambda表达式简化,只能使用匿名内部类
@Override
public void run() {
System.out.println(Thread.currentThread().getName()); //打印当前线程名称
}
}, 1000); //执行一个延时任务
}
我们可以通过创建一个Timer类来让它进行定时任务调度,我们可以通过此对象来创建任意类型的定时任务,包延时任务、循环定时任务等。我们发现,虽然任务执行完成了,但是我们的程序并没有停止,这是因为Timer内存维护了一个任务队列和一个工作线程:
public class Timer {
/**
* The timer task queue. This data structure is shared with the timer
* thread. The timer produces tasks, via its various schedule calls,
* and the timer thread consumes, executing timer tasks as appropriate,
* and removing them from the queue when they're obsolete.
*/
private final TaskQueue queue = new TaskQueue();
/**
* The timer thread.
*/
private final TimerThread thread = new TimerThread(queue);
...
}
TimerThread继承自Thread,是一个新创建的线程,在构造时自动启动:
public Timer(String name) {
thread.setName(name);
thread.start();
}
而它的run方法会循环地读取队列中是否还有任务,如果有任务依次执行,没有的话就暂时处于休眠状态:
public void run() {
try {
mainLoop();
} finally {
// Someone killed this Thread, behave as if Timer cancelled
synchronized(queue) {
newTasksMayBeScheduled = false;
queue.clear(); // Eliminate obsolete references
}
}
}
/**
* The main timer loop. (See class comment.)
*/
private void mainLoop() {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
// Wait for queue to become non-empty
while (queue.isEmpty() && newTasksMayBeScheduled) //当队列为空同时没有被关闭时,会调用wait()方法暂时处于等待状态,当有新的任务时,会被唤醒。
queue.wait();
if (queue.isEmpty())
break; //当被唤醒后都没有任务时,就会结束循环,也就是结束工作线程
...
}
newTasksMayBeScheduled实际上就是标记当前定时器是否关闭,当它为false时,表示已经不会再有新的任务到来,也就是关闭,我们可以通过调用cancel()方法来关闭它的工作线程:
public void cancel() {
synchronized(queue) {
thread.newTasksMayBeScheduled = false;
queue.clear();
queue.notify(); //唤醒wait使得工作线程结束
}
}
因此,我们可以在使用完成后,调用Timer的cancel()方法以正常退出我们的程序
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
timer.cancel(); //结束
}
}, 1000);
}