功能:Java多线程

技术功能:Java多线程 功能:Java多线程Java多线程
一、介绍
在了解线程之前,还需要简单了解进程的概念。简单的来说就是一心多用
在生活之中,我们常常可以一心多用。我可以一边打游戏,一边放着音乐

特点:Java多线程

Java多线程

一、介绍

在了解线程之前,您需要简要了解进程的概念。简单来说就是多任务处理。

生活中,我们经常可以一心多用。我可以一边听音乐一边玩游戏,甚至泡脚。是的,这也可以理解为我的多线程生活。

在计算机中,同时存在上述任务,这可以称为多线程,例如

流程:比如游戏、音乐等应用都在电脑上。每个应用程序都可以算作一个进程。

线程:往往是一个游戏,有伤害计算、数据上传、图像音乐等步骤,每个执行细节也可以理解为一个线程。

所以综上所述,一个进程就是一个应用运行的进程,可以包含多个线程在运行,但至少要有一个线程,这样才能支持这个进程。

它是线程cpu调度和计算某个资源的通道。在这个通道下,cpu可以调度一些任务。

在java中,我们从Main方法运行,所以我们称它为主线程。

除了主线程之外,java还有一个后台线程在默默工作,这就是GC线程,也就是垃圾收集所在的线程。

二、Java线程的实现

1)继承Thread类

package com . ban moon . mode;

/**

*实现多线程模式。

* 1.继承线程类

* 2.实现它的运行方法

* 3.创建对象并调用start方法。

*/

公共类扩展

公共静态void main(String[]args){ 0

ExtendsModeA modeA=new ExtendsModeA();

extendsmodb modeB=new extendsmodb();

modea . start();

modeb . start();

for(int I=0;i 1000(一)

系统。out.println ('=========主线程==========');

}

}

类扩展数据包络扩展线程{

@覆盖

public void run(){ 0

for(int I=0;i 1000I){ 0

系统。out . println('==========线程A============');

}

}

}

类扩展deB扩展Thread{

@覆盖

public void run(){ 0

for(int I=0;i 1000I){ 0

系统。out . println('==========线程B============');

}

}

}

启动后,你会发现最后应该打印的主线其实是混在A线和b线之间的。

也就是说,这些线程是交替执行的,计算机实际上无法实现真正的并发,但其线程之间的切换却无法人为感知,因此给人一种并发的错觉。

哪个线程在执行中具有优先级,这与CPU调度有关,这将在后面讨论。

2)实现Runnable接口

package com . ban moon . mode;

/**

*实现多线程模式。

* 1.实现可运行的接口

* 2.构造Thread对象,并将Runnable实现对象作为参数。

* 3.调用线程对象的启动方法。

*/

公共类RunnableMode {

公共静态void main(String[]args){ 0

Thread modeA=new Thread(new RunnableModeA());

Thread modeB=new Thread(new RunnableModeB());

modea . start();

modeb . start();

for(int I=0;i 1000(一)

系统。out.println ('=========主线程==========');

}

}

类RunnableModeA实现了Runnable{

@覆盖

public void run(){ 0

for (int

i = 0; i 1000; i++) {
System.out.println("=========== 线程A ===========");
}
}
}
class RunnableModeB implements Runnable{
@Override
public void run() {
for (int i = 0; i 1000; i++) {
System.out.println("=========== 线程B ===========");
}
}
}

执行后效果与上方一致,打印的信息都是穿插打印的

由于Java只支持单继承,为了使得线程实现更具有灵活性,推荐使用Runnable接口方式

此外,Runnable还有Lanmbda的简写方式

package com.banmoon.mode;
/**
 * 实现多线程方式
 * 1、实现接口Runnable,Lambda简写方式
 */
public class RunnableModeByLambda {
    public static void main(String[] args) {
        new Thread(() - {
            for (int i = 0; i  1000; i++)
                System.out.println("=========== 线程A ===========");
        }).start();
        new Thread(() - {
            for (int i = 0; i  1000; i++)
                System.out.println("=========== 线程B ===========");
        }).start();
        for (int i = 0; i  1000; i++)
            System.out.println("=========== 主线程 ===========");
    }
}

3)实现Callable接口

package com.banmoon.mode;
import java.util.concurrent.*;
/**
 * 实现多线程方式
 * 1、实现Callable接口,了解
 * 2、需要定义返回值类型
 * 3、创建执行服务线程池,来进行执行
 */
public class CallableMode {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(2);
        FutureString resultA = service.submit(new CallableModeA());
        FutureString resultB = service.submit(new CallableModeB());
        System.out.println("结果A:" + resultA.get());
        System.out.println("结果B:" + resultB.get());
        for (int i = 0; i  1000; i++)
            System.out.println("=========== 主线程 ===========");
        service.shutdown();
    }
}
class CallableModeA implements CallableString{
    @Override
    public String call() throws Exception {
        String str = "=========== 线程A ===========";
        for (int i = 0; i  1000; i++)
            System.out.println(str);
        return str;
    }
}
class CallableModeB implements CallableString{
    @Override
    public String call() throws Exception {
        String str = "=========== 线程B ===========";
        for (int i = 0; i  1000; i++)
            System.out.println(str);
        return str;
    }
}

这里使用到了一个执行服务工具类Executors,它可以创建线程池,后续会讲到

三、线程状态及方法

1)状态

其实,jdk中还有一个线程状态的枚举Thread.State

和上图有些不同,但不影响理解,只是少了个就绪的状态

2)方法

Thread方法 是否静态 说明
setPriority 设置线程的优先级,优先级高的更有机会优先被CPU调度,但这个不是绝对
sleep 让当前所处的线程进行休眠,可以用来模拟网络延迟,放大同步问题
join 插队,等待正在运行的线程终止
yield 暂停当前的线程,执行其他的线程,让CPU选择再次进行选择调度

1、setPriority

package com.banmoon.status;
public class ThreadPriorityMethods {
    public static void main(String[] args) {
        Thread threadA = new Thread(new MyThread(), "线程A");
        Thread threadB = new Thread(new MyThread(), "线程B");
        Thread threadC = new Thread(new MyThread(), "线程C");
        threadA.setPriority(9);
        threadB.setPriority(5);
        threadC.setPriority(1);
        threadC.start();
        threadB.start();
        threadA.start();
    }
}
class MyThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i  10; i++) {
            System.out.println("============" + Thread.currentThread().getName() + "============");
        }
    }
}

2、sleep

package com.banmoon.status;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ThreadSleepMethods {
    public static void main(String[] args) {
        new Thread(() - {
            int i = 0;
            SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
            while (i++10){
                String dateStr = sdf.format(new Date());
                System.out.println(dateStr + "============ 线程A ============");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

3、join

package com.banmoon.status;
public class ThreadJoinMethods {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() - {
            for (int i = 0; i  200; i++) {
                System.out.println("============= 线程A"+ i +" =============");
            }
        });
        thread.start();
        for (int i = 0; i  1000; i++) {
            if(i==100)
                thread.join();
            System.out.println("============= 主线程"+ i +" =============");
        }
    }
}

4、yield

package com.banmoon.status;
public class ThreadYieldMethods {
    public static void main(String[] args) {
        Thread thread = new Thread(() - {
            for (int i = 0; i  1000; i++) {
                if(i==200)
                    Thread.yield();
                System.out.println("============= 线程A"+ i +" =============");
            }
        });
        thread.start();
        for (int i = 0; i  1000; i++) {
            if(i==500)
                Thread.yield();
            System.out.println("============= 主线程"+ i +" =============");
        }
    }
}

四、synchronized关键字

1)并发数据问题

看下列代码,总共有10张票,创建3个线程,每个线程都去取票,直到票数小于0,则退出

package com.banmoon.sync;
/**
 * 不安全的买票服务
 */
public class TicketServer implements Runnable{
    private int ticketNum = 10;
    /**
     * 取票
     */
    @Override
    public void run() {
        // 获取当前线程名
        String name = Thread.currentThread().getName();
        while (true){
            if(ticketNum=0)
                return;
            System.out.println(name + ":取到了第" + ticketNum + "张票");
            // 模拟网络延迟
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ticketNum--;
        }
    }
    public static void main(String[] args) {
        TicketServer ticketServer = new TicketServer();
        new Thread(ticketServer, "A").start();
        new Thread(ticketServer, "B").start();
        new Thread(ticketServer, "C").start();
    }
}

预期将会是,10,9,8,7…直到取完票,但真实的结果是

我执行了很多遍,结果远远没有出现我预期的模样。

这是因为cpu调度线程太快了,当取票完成,但票数还没有减一的时候,其他的线程读取了没有减票前的票数,所以导致出现的问题,此类问题被称为并发问题,也称线程安全问题

2)synchronized介绍

此关键字保证了访问同个资源时出现的并发问题。

他的工作原理是对指定对象进行加锁,对此锁表示占有。导致其他线程进不来,可以查看下面示例的代码

synchronized对取票检票进行限制,这把锁就是ticketServer。当A线程获取锁,进入代码执行时,其他线程必须进行等待,直到A线程完成逻辑释放锁后,CPU再重新进行调度,看谁运气好能获取到这一次的锁。

在这个等待锁的线程状态,也被称为同步阻塞状态。

package com.banmoon.sync;
public class SyncTicketServer implements Runnable{
    private int ticketNum = 10;
    /**
     * 取票
     */
    @Override
    public void run() {
        // 获取当前线程名
        String name = Thread.currentThread().getName();
        while (true){
            // 注意,锁住的是this对象哦,也就是32行创建出来的ticketServer
            synchronized (this){
                if(ticketNum=0)
                    return;
                System.out.println(name + ":取到了第" + ticketNum + "张票");
                // 模拟网络延迟
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticketNum--;
            }
        }
    }
    public static void main(String[] args) {
        SyncTicketServer ticketServer = new SyncTicketServer();
        new Thread(ticketServer, "A").start();
        new Thread(ticketServer, "B").start();
        new Thread(ticketServer, "C").start();
    }
}

执行结果

3)死锁

诚然,synchronized可以解决同步问题,但他的缺点需要了解

  • 效率:线程处于同步阻塞中,效率上不去,但这是没有办法的
  • 死锁:当代码考虑不周时,将会出现死锁问题

下列示例代码展示了死锁,有两个线程,手中持有自己的一把锁,这又想获取对方手中的锁时,两个线程相持不下,都处于同步阻塞阶段,导致出现的死锁。

package com.banmoon.sync;
public class DeadLock {
    public static void main(String[] args) {
        Thread threadA = new Thread(new ThreadA(), "ThreadA");
        Thread threadB = new Thread(new ThreadB(), "ThreadB");
        threadA.start();
        threadB.start();
    }
}
class ThreadA implements Runnable{
    @Override
    public void run() {
        synchronized (ThreadA.class){
            System.out.println(Thread.currentThread().getName() + "已持有锁:ThreadA");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (ThreadB.class){
                System.out.println(Thread.currentThread().getName() + "已持有锁:ThreadA");
            }
        }
    }
}
class ThreadB implements Runnable{
    @Override
    public void run() {
        synchronized (ThreadB.class){
            System.out.println(Thread.currentThread().getName() + "已持有锁:ThreadB");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (ThreadA.class){
                System.out.println(Thread.currentThread().getName() + "已持有锁:ThreadA");
            }
        }
    }
}

真实的情况远比上述要复杂的多,但死锁的基本概念就是如此。

4)不同的修饰位置

我们现在知道,synchronized锁住的是对象,也就是获取到了对象的锁,但处于不同的修饰位置,获取哪个对象的锁也是不一致的。

简单可以分为下面几个修饰位置

1、修饰this时

package com.banmoon.sync;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
/**
 * sync修饰对象时
 */
public class SyncDemo1{
    private void printOne(){
        synchronized (this){
            try {
                Thread.sleep(2000);
                String format = StrUtil.format("{}:当前this对象:{},时间:{}", Thread.currentThread().getName(), this, DateUtil.now());
                System.out.println(format);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    private void printTwo(){
        synchronized (this){
            String format = StrUtil.format("{}:当前this对象:{},时间:{}", Thread.currentThread().getName(), this, DateUtil.now());
            System.out.println(format);
        }
    }
    public static void main(String[] args) {
        SyncDemo1 syncDemo1 = new SyncDemo1();
        new Thread(() - {
            syncDemo1.printOne();
        }, "线程A").start();
        new Thread(() - {
            syncDemo1.printTwo();
        }, "线程B").start();
//        System.out.println(syncDemo1);// 指的就是31行创建出来的对象
    }
}

如上,因为只创建一个实例,所以他们锁定的只是this

如果不信,可以再new SyncDemo1(),让线程B去调用这个实例对象的printTwo(),保证你看到不同的结果

2、修饰XXX.class时

package com.banmoon.sync;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
/**
 * 当修饰class对象时
 */
public class SyncDemo2 {
    private void printOne(){
        synchronized (SyncDemo2.class){
            try {
                Thread.sleep(2000);
                String format = StrUtil.format("{}:时间:{}", Thread.currentThread().getName(), DateUtil.now());
                System.out.println(format);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    private void printTwo(){
        synchronized (SyncDemo2.class){
            String format = StrUtil.format("{}:时间:{}", Thread.currentThread().getName(), DateUtil.now());
            System.out.println(format);
        }
    }
    public static void main(String[] args) {
        SyncDemo2 syncDemo2 = new SyncDemo2();
        new Thread(() - {
            syncDemo2.printOne();
        }, "线程A").start();
        new Thread(() - {
            syncDemo2.printTwo();
        }, "线程B").start();
//        System.out.println(SyncDemo2.class);// class对象具有唯一性
    }
}

这个结果与上面一致,因为锁住的都是同一个对象

3、修饰成员方法时

在这里我做了对比,判断修饰this和修饰成员方法时,锁住的对象是否是同一个

package com.banmoon.sync;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
/**
 * 修饰普通方法,锁住的到底是什么
 */
public class SyncDemo3 {
    private synchronized void printOne(){
        try {
            Thread.sleep(2000);
            String format = StrUtil.format("{}:时间:{}", Thread.currentThread().getName(), DateUtil.now());
            System.out.println(format);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    private void printTwo(){
        synchronized ("Don't write that"){
//        synchronized (this){// 测试锁住的是否是this
            String format = StrUtil.format("{}:时间:{}", Thread.currentThread().getName(), DateUtil.now());
            System.out.println(format);
        }
    }
    public static void main(String[] args) {
        SyncDemo3 syncDemo3 = new SyncDemo3();
        new Thread(() - {
            syncDemo3.printOne();
        }, "线程A").start();
        new Thread(() - {
            syncDemo3.printTwo();
        }, "线程B").start();
    }
}

当23行放开进行使用,会发现线程B会被线程A卡住,说明修饰成员方法时,获取的就是当前对象的锁

4、修饰静态方法时

在这里进行了对比,判断修饰静态方法、修饰this、修饰当前class对象时,获取的是什么对象的锁

package com.banmoon.sync;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
public class SyncDemo4 {
    private static synchronized void printOne(){
        try {
            Thread.sleep(2000);
            String format = StrUtil.format("{}:时间:{}", Thread.currentThread().getName(), DateUtil.now());
            System.out.println(format);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    private void printTwo(){
        synchronized (this){
//        synchronized (SyncDemo4.class){// 判断获取的是否是SyncDemo4.class对象
            String format = StrUtil.format("{}:时间:{}", Thread.currentThread().getName(), DateUtil.now());
            System.out.println(format);
        }
    }
    public static void main(String[] args) {
        SyncDemo4 syncDemo4 = new SyncDemo4();
        new Thread(() - {
            syncDemo4.printOne();
        }, "线程A").start();
        new Thread(() - {
            syncDemo4.printTwo();
        }, "线程B").start();
    }
}

如果线程B马上打印,那说明获取的不是同一把锁。要是线程B被线程A卡住了,那说明确实是一把锁了

总结

  • 修饰代码块时:锁住的是括号中的对象
    • this:指向的是当前对象,也就是实现重写run方法的类实例化出来的对象
    • XXX.class:就是这个class对象,我比较喜欢使用,因为class对象具有唯一性
  • 修饰方法时:
    • 成员方法:成员方法所在的类所创建出来的对象,也就是谁调用了这个方法,获取的就是谁的锁
    • 静态方法:当前方法所在类的class对象,XXX.class

注意:不要修饰什么乱七八糟的对象,比如字符串对象,就像上面写的Don't write that。有时候,自己都分不清两个是不是同一个对象,还敢乱写在代码中。

5)异常释放锁

package com.banmoon.sync;
public class SyncException {
    public static void main(String[] args) throws InterruptedException {
        new Thread(() - {
            synchronized (SyncException.class){
                for (int i = 0; i  20; i++) {
                    if(i==10)
                        i = 1/0;// 异常
                    System.out.println("线程A:" + i);
                }
            }
        }).start();
        Thread.sleep(3000);
        synchronized (SyncException.class){
            for (int i = 0; i  10; i++) {
                System.out.println("主线程");
            }
        }
    }
}

如下运行结果,线程中出现异常时,当前持有的锁会立即释放。所以一定要准确的捕获异常,可以试试将异常捕获,保证线程的安全。

五、线程通信

在上述synchronized的代码案例中,线程获取了锁后,都是一条路走到黑的,除了异常没捕获的那次。

线程通信,主要是线程在获取锁后,主动将锁放弃,让其他线程也来喝喝汤,cpu大哥觉得你很懂事,cpu很欣慰。

1)主要方法

由于synchronized获取的是对象的锁,所以有关线程之间的阻塞唤醒,都来自Object

方法名 功能
public final void wait() 释放当前锁,本线程进入睡眠,从运行状态进入阻塞状态
需要等待其他线程唤醒
public final native void wait(long timeout) 释放当前锁,本线程进入睡眠,从运行状态进入阻塞状态,
一段时间后本线程自动醒来
public final native void notify() 唤醒其他任意一个线程,将它从阻塞状态拉回到就绪状态
public final native void notifyAll() 唤醒其他所有线程,将它们从阻塞状态拉回到就绪状态

2)简单示例

package com.banmoon.wait;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        new Thread(() - {
            synchronized (Demo1.class){
                for (int i = 1; i = 10; i++) {
                    try {
                        System.out.println(StrUtil.format("线程A:{},时间:{}", i, DateUtil.now()));
                        if(i==5){
                            System.out.println("睡一会,释放锁");
                            Demo1.class.wait();
                        }
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
        // 等待3秒
        Thread.sleep(5000);
        System.out.println("主线程:唤醒");
        Demo1.class.notify();
        synchronized (Demo1.class){
            System.out.println("主线程:唤醒");
            Demo1.class.notify();
        }
    }
}

执行结果,主线程唤醒后,线程A继续走他未走完的路

大家可以放开上述代码的第29,30行,运行后惊奇的发现居然出了异常

这个异常是什么原因呢,在执行wait()、notify()、notifyAll()方法时,必须要持有锁。而且唤醒还一定要持有相同对象的锁,也就是使用synchronized获取同样的对象的锁,并使用该对象进行唤醒
其实很好理解,一个持有锁的线程,怎么可能会被没有持有同样锁的线程唤醒呢

3)生产者消费者模式

通过一个中间容器,来设置该容器的最大容量作为生产者线程的结束

package com.banmoon.wait;
import cn.hutool.core.util.StrUtil;
import java.util.LinkedList;
import java.util.Queue;
public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        Container container = new Container();
        new Thread(new Consumer(container)).start();
        Thread.sleep(1000);
        new Thread(new Producer(container)).start();
    }
}
/**
 * 生产者
 */
class Producer implements Runnable{
    private Container container;
    public Producer(Container container) {
        this.container = container;
    }
    @Override
    public void run() {
        int i = 0;
        while (true){
            try {
                container.put(i);
                System.out.println(StrUtil.format("生产者:生产了{},当前数量:{}", i, container.queue.size()));
                i++;
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
/**
 * 消费者
 */
class Consumer implements Runnable{
    private Container container;
    public Consumer(Container container) {
        this.container = container;
    }
    @Override
    public void run() {
        while (true){
            try {
                Integer i = container.get();
                System.out.println("消费者:消费了" + i);
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
/**
 * 容器
 */
class Container{
    public QueueInteger queue;
    public int MAX_SIZE = 10;
    public Container() {
        this.queue = new LinkedList();
    }
    public synchronized Integer get(){
        try {
            if(queue.size()==0){
                notifyAll();
                wait();
            }
            return this.queue.poll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }
    public synchronized void put(Integer integer){
        try {
            if(queue.size()=MAX_SIZE){
                notifyAll();
                wait();
            }
            this.queue.add(integer);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

4)信号灯法

设置一个标志位flag,来控制线程之间的通信状态。简单改造上面的生产消费者,使通过flag来进行通信。

package com.banmoon.wait;
import cn.hutool.core.date.DateUtil;
public class Demo3 {
    public static void main(String[] args) {
        Demo3Flag flag = new Demo3Flag();
        new Thread(new Demo3Producer(flag)).start();
        new Thread(new Demo3Consumer(flag)).start();
    }
}
class Demo3Producer implements Runnable{
    private Demo3Flag flag;
    public Demo3Producer(Demo3Flag flag) {
        this.flag = flag;
    }
    @Override
    public void run() {
        for (int i = 0; i  100; i++){
            flag.production();
        }
    }
}
class Demo3Consumer implements Runnable{
    private Demo3Flag flag;
    public Demo3Consumer(Demo3Flag flag) {
        this.flag = flag;
    }
    @Override
    public void run() {
        for (int i = 0; i  100; i++){
            flag.consumption();
        }
    }
}
class Demo3Flag{
    // 标志位 true:生成,false:消费
    private boolean flag;
    public Demo3Flag() {
        this.flag = true;
    }
    public void setFlag(boolean flag) {
        this.flag = flag;
    }
    public synchronized void production(){
        if(!this.flag){
            try {
                wait();
            } catch (InterruptedException interruptedException) {
                interruptedException.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + ":正在生产..." + DateUtil.now());
        // 生产完成,标志位设置为可以消费
        this.flag = false;
        notifyAll();
    }
    public synchronized void consumption(){
        if(this.flag){
            try {
                wait();
            } catch (InterruptedException interruptedException) {
                interruptedException.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + ":正在消费..." + DateUtil.now());
        // 消费完成,标志位设置为可以继续生产
        this.flag = true;
        notifyAll();
    }
}

5)虚假唤醒问题

在线程通信中,如果使用不当,将会出现虚假唤醒的问题,运行下列代码来进行查看

package com.banmoon.wait;
/**
 * 虚假唤醒,问题演示
 */
public class Demo4 {
    public static void main(String[] args) {
        MyDemo4 myDemo4 = new MyDemo4();
        new Thread(() - {
            for(int i = 0; i  10; i++) myDemo4.increment();
        }, "线程A").start();
        new Thread(() - {
            for(int i = 0; i  10; i++) myDemo4.decrement();
        }, "线程B").start();
        new Thread(() - {
            for(int i = 0; i  10; i++) myDemo4.increment();
        }, "线程C").start();
        new Thread(() - {
            for(int i = 0; i  10; i++) myDemo4.decrement();
        }, "线程D").start();
    }
}
class MyDemo4{
    private int number = 0;
    public synchronized void increment(){
        try {
            if(number==1)
                wait();
            number++;
            System.out.println(Thread.currentThread().getName() + ":" + number);
            notifyAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public synchronized void decrement(){
        try {
            if(number==0)
                wait();
            number--;
            System.out.println(Thread.currentThread().getName() + ":" + number);
            notifyAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

正常预想的结果,ABCD四个线程使得number在0和1之间反复横跳,但实际上的结果,却大大出乎我所料

这个原因很好解释,因为wait方法有个特性,在摔倒就在哪里爬起来,然后继续向前走。简单的描述下出现问题的步骤,

  1. A线程,number+1
  2. C线程,判断后进行wait
  3. A线程,判断后进行wait
  4. B线程,number-1,唤醒其他线程,此时A,C被唤醒
  5. A线程,number+1
  6. C线程,number+1
  7. 一直持续下去…

好的,到第6步就已经出现问题了,记住上面说的,在哪里摔倒就在哪里爬起来。

第二步时,C线程在判断完成后进入等待,直到第六步被CPU调度,因为判断已经完成,所以直接进入了number+1的逻辑。

像上述这种现象被称为虚假唤醒

解决虚假唤醒

既然是由于被唤醒后没有判断导致,所以我们这里只需要将if改为while,让线程唤醒后的第一件事就是判断条件

修改为while后的执行结果

六、最后

思考一下如何用3条线程循环输出ABC

package com.banmoon.question;
/**
 * 使用线程循环输出ABC
 */
public class Question1 {
    public static void main(String[] args) throws InterruptedException {
        Object a = new Object();
        Object b = new Object();
        Object c = new Object();
        ABC A = new ABC("A", c, a);
        ABC B = new ABC("B", a, b);
        ABC C = new ABC("C", b, c);
        new Thread(A).start();
        Thread.sleep(10);// 保证初始ABC的启动顺序
        new Thread(B).start();
        Thread.sleep(10);
        new Thread(C).start();
        Thread.sleep(10);
    }
}
class ABC implements Runnable{
    private String name;
    // 下一个获取的对象
    private Object prev;
    // 当前对象
    private Object self;
    public ABC(String name, Object prev, Object self) {
        this.name = name;
        this.prev = prev;
        this.self = self;
    }
    @Override
    public void run() {
        while (true){
            // 获取上个对象的锁
            synchronized (prev) {
                // 获取当前对象的锁
                synchronized (self) {
                    System.out.print(name);
                    // 唤醒当前对象锁,也就是下个线程的上个对象锁
                    self.notifyAll();
                }
                try {
                    Thread.sleep(50);
                    // 释放上个对象的锁
                    prev.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

个人博客
欢迎来登录我的个人博客

入我相思门,知我相思苦,
长相思兮长相忆,短相思兮无穷极,
早知如此绊人心,何如当初莫相识。

内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/157178.html

(0)

相关推荐

  • miniui datagrid的客户端分页解决方法是怎样的

    技术miniui datagrid的客户端分页解决方法是怎样的这期内容当中小编将会给大家带来有关miniui datagrid的客户端分页解决方法是怎样的,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大

    攻略 2021年11月16日
  • pythonopencv图像处理实例(pythonopencv获取图像)

    技术Python+OpenCV数字图像处理中如何进行ROI区域的提取本篇文章给大家分享的是有关Python+OpenCV数字图像处理中如何进行ROI区域的提取,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章

    攻略 2021年12月17日
  • 指甲油怎么卸掉,卸指甲油不伤指甲的方法

    技术指甲油怎么卸掉,卸指甲油不伤指甲的方法每个女孩子都爱美,经常会看见女孩们指甲盖上涂一层美甲以此来增加自己的自信度,下面为卸指甲油不伤指甲的方法介绍指甲油怎么卸掉:1、涂指甲油之前在自己的指甲上涂一层宝宝的粘假指甲胶水

    生活 2021年10月26日
  • Linux 安装软件的三种方法

    技术Linux 安装软件的三种方法 Linux 安装软件的三种方法一、Linux 安装软件的三种方法
    rpm安装、yum 安装、源代码编译安装区别1、rpm 安装类似于window的安装包,下载后直接安

    礼包 2021年12月17日
  • 稳压二极管工作原理,稳压二极管的工作原理是什么

    技术稳压二极管工作原理,稳压二极管的工作原理是什么稳压管的工作原理:稳压管也是一种晶体二极管,它是利用PN结的击穿区具有稳定电压的特性来工作的。稳压管在稳压设备和一些电子电路中获得广泛的应用。把这种类型的二极管称为稳压管

    生活 2021年10月24日
  • 怎么设置vivado中ip核的位置(vivado怎么打开查看端口的窗口)

    技术Vivado中IP是如何控制端口的可见与不可见Vivado中IP是如何控制端口的可见与不可见,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。测试平台Viv

    攻略 2021年12月22日