栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Java并发基础学习(四)——Thread与Object中关于线程的方法

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Java并发基础学习(四)——Thread与Object中关于线程的方法

前言

前面几篇算是总结了一些Java线程中比较模棱两可的内容,在线程状态一篇博文中,我们在介绍线程状态切换的图中时,出现了notify,wait等方法,这些是Object的方法。今天这篇博客就是针对这些方法做一些梳理,当然,不是全部的Thread和Object类中的方法介绍,而是只是总结一下关于多线程相关的方法。其实在实际开发写代码的时候,很少会直接接触这些方法,但是这些方法确实很多并发工具的底层原理,因此还是有必要总结一下。

Object类中的方法 简述 wait

在某些场景,在线程执行的时候,在某些条件下,我们想让某些线程先休息一下(比如生产者消费者模式中,数据容器数据已经满了,这个时候就生产者就需要休息一下),等到某个时刻再被唤醒。这就是wait的作用,在上一篇博客的总结中,可以看到调用wait方法之后,线程进入到WAIT状态。而与之对应的,唤醒线程的方法就是notify或者notifyAll。wait是属于Object中的方法,通过对象调用wait方法的代码只能存在于同步代码块中。

进入到WAIT状态的线程,在以下4种场景下才会被唤醒

1、另一个线程调用这个对象的notify方法,且刚好被唤醒的就是本线程

2、另一个线程调用了notifyAll方法

3、本线程过了wait设置的超时时间

4、线程自身被通知中断(调用interrupt方法)

notify

与wait相对应的就是notify和notifyAll方法,notify是随机唤醒一个对应对象的等待线程。而notifyAll是唤醒指定对象的所有等待线程。

notifyAll

notifyAll上面已经介绍,唤醒对象等待队列中的所有线程

实例 1、简单的wait和notify实例
public class WaitNotifyDemo {

    public static Object object = new Object();

    static class Thread1 extends Thread{
        @Override
        public void run() {
            synchronized (object){
                System.out.println(Thread.currentThread().getName()+"线程开始执行了");
                try {
                    object.wait();//wait会释放锁
                } catch (InterruptedException e) {//线程在wait的时候,可以响应中断,响应中断之后,线程就退出WAIT状态了
                    e.printStackTrace();
                }
                //线程从wait获得锁之后,继续从wait之后开始执行,而不是从开头开始执行
                System.out.println(Thread.currentThread().getName()+"线程继续开始执行");
            }
        }
    }

    static class Thread2 extends Thread{
        @Override
        public void run() {
                synchronized (object){
                    object.notify();
                    //线程2虽然执行了notify,但是并不会立刻释放这把锁,而是执行完synchronize代码块之后,才释放这把锁
                    System.out.println("线程"+Thread.currentThread().getName()+"调用了notify");
                }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread1 thread1 = new Thread1();
        Thread2 thread2 = new Thread2();
        thread1.start();
        Thread.sleep(200);//保证wait在notify之前执行
        thread2.start();
    }
}
2、简单的wait与notifyAll的实例
public class WaitNotifyAllDemo implements Runnable{

    private static final Object resourceA = new Object();

    @Override
    public void run() {
        synchronized (resourceA){
            System.out.println(Thread.currentThread().getName()+"获取到资源锁");
            try {
                System.out.println("线程即将wait "+Thread.currentThread().getName());
                resourceA.wait();//wait释放锁
                System.out.println("线程"+Thread.currentThread().getName()+"被唤醒继续执行");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new WaitNotifyAllDemo();
        Thread thread01 = new Thread(runnable);
        Thread thread02 = new Thread(runnable);
        Thread thread03 = new Thread(()->{
            synchronized (resourceA){
                resourceA.notifyAll();
                System.out.println(Thread.currentThread().getName()+"notify all thread");
            }
        });
        thread01.start();
        thread02.start();
        Thread.sleep(500);//保证线程1和线程2都进入WAIT状态
        thread03.start();
    }
}
3、wait方法是会释放锁的
public class WaitNotifyReleaseOwnMonitor {

    //需要准备两把锁
    private static volatile Object resourceA = new Object();
    private static volatile Object resourceB = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(()->{
           synchronized (resourceA){
               System.out.println(Thread.currentThread().getName()+" get resourceA lock");
               synchronized (resourceB){
                   System.out.println(Thread.currentThread().getName()+" get resourceB lock");
                   try {
                       System.out.println(Thread.currentThread().getName()+ "release resourceA lock");
                       resourceA.wait();//线程进入wait会释放锁,但是只是释放其拥有的对象的锁
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
           }
        });

        Thread thread2 = new Thread(()->{
            //等待一下,确保线程1进入wait状态
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (resourceA){
                System.out.println(Thread.currentThread().getName()+" other thread  get resourceA lock");
                System.out.println(Thread.currentThread().getName()+" other thread try to get resourceB lock");
                synchronized (resourceB){//这个线程无法打印这行日志,因为wait只是释放指定对象的锁,并不会释放线程持有的所有锁
                    System.out.println(Thread.currentThread().getName()+" other thread get resourceB lock");
                }
            }

        });

        thread1.start();
        thread2.start();
    }
}

运行结果:

线程2,无法获得resourceB对象的锁,但是获得了resoureceA对象的锁,其实可以说明wait方法是释放锁的,但是只能释放指定对象的锁。

小结

关于wait,notify,notifyAll方法,这里做一个小结。

1、wait,notify,notifyAll,必须在synchronize修饰的代码块或者方法中运行

2、notify只能唤醒一个等待线程,这个是随机的。

3、notifyAll可以唤醒全部

这些方法都属于Object,类似的功能有Condition锁,这个我们后面会总结,同时需要注意的是,如果一个线程同时持有多把锁,则线程调用wait方法进入等待的时候,只会释放wait对象对应的那把锁,这个时候就需要注意一下锁的释放顺序。同时被重新唤醒的线程,并不会立刻进入到RUNNABLE状态,而是和其他线程一样,也需要先进入到等待队列,然后获取锁,如果没有获取到锁,依旧是BLOCKED状态(没有获取到synchronize锁的状态)。如果WAIT期间,发生异常,则线程也可以直接进入到TERMINATED状态。

关于wait的原理,可以一张图总结一下

这张图应该还算比较经典了,将抢锁与wait等待的过程都表述的很详细了。

Thread类中的方法 sleep方法

sleep与wait方法的差异,是面试中几乎都会问道的问题,但是sleep有着自己的特点。sleep方法可以让线程进入Waiting状态,并且不占用CPU资源,但是,sleep不会释放锁,直到规定时间后再执行,休眠期间sleep方法是可以响应中断的。

相比于直接用Thread.sleep(1000)这种方式,其实更推荐用TimeUnit.SECONDS.sleep(1),关于sleep的简单实例如下

public class SleepDontReleaseLock implements Runnable {

    private static final Lock lock = new ReentrantLock();

    @Override
    public void run() {
        lock.lock();
        System.out.println("线程"+Thread.currentThread().getName()+"获取到了锁");
        try {
            Thread.sleep(5000);
            System.out.println("线程"+Thread.currentThread().getName()+"已经苏醒");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        SleepDontReleaseLock sleepDontReleaseLock = new SleepDontReleaseLock();
        new Thread(sleepDontReleaseLock).start();
        new Thread(sleepDontReleaseLock).start();
    }
}

wait/notify与sleep的异同

1、两者都会让线程进入阻塞,也都能响应中断

2、不同的是wait只能用在同步方法中,而sleep不需要;wait会释放锁,而sleep不会;所属的类不同,sleep方法属于Thread类;sleep必须接受参数,wait是可以不传递参数的,wait是直到被唤醒。

join方法

某个线程加入到当前线程的流程中,需要等待加入的线程运行完成之后,在操作其他的。

实例代码

public class JoinDemo {

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"执行完毕");
        });

        Thread thread2 = new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"执行完毕");
        });
        thread1.start();
        thread2.start();
        System.out.println("主线程开始等待子线程运行完毕");
        //这里的join由子线程的对象进行调用,但含义实质是将子线程加入到主线程中
        thread1.join();//含义:子线程加入到主线程中,主线程需要等待子线程执行完毕之后再运行
        thread2.join();
        System.out.println("所有子线程执行完毕,下面主线程可以运行了");
        Thread.sleep(500);
        System.out.println("主线程执行完毕");
    }
}

有join的运行结果

如果将thread1.join()和thread2.join()两行代码注释掉,则会有如下输出结果

需要理解一下的是,虽然join方法是被子线程调用,但是实际是主线程被join,毕竟是主线程等待子线程运行完毕

join期间的中断
public class JoinInterrupt {

    public static void main(String[] args) {
        //拿到主线程的引用
        Thread mainThread = Thread.currentThread();
        Thread thread = new Thread(() -> {
            try {
                //在子线程中中断主线程
                mainThread.interrupt();
                Thread.sleep(5000);

                System.out.println(Thread.currentThread().getName()+"运行完毕");
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName()+"线程中断");
                e.printStackTrace();
            }
        });
        thread.start();
        System.out.println("开始等待子线程运行");
        try {
            thread.join();//含义:thread加入到主线程中,主线程等待thread执行完成
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName()+"线程被中断了");//主线程被中断了
            e.printStackTrace();
        }
        System.out.println("主线程等待子线程执行完毕");
    }

}

上述代码在子线程中中断了主线程,而这个时候,主线程正在join,因此会响应中断,但是会有一些问题,具体运行图如下所示

可以看到,主线程虽然被中断了,也正常响应了中断,但是这个时候子线程依旧在运行。在一定的业务场景下,这种是很危险的。因此,为了规范化一点,如果用到了join方法(虽然实际开发中也用的不多,毕竟有些文档也不建议我们直接使用JDK底层的多线程方法)在响应中断的时候,也要将这个中断信号传递给子线程,上述代码中针对join部分的处理需要改成如下代码

try {
    thread.join();//含义:thread加入到主线程中,主线程等待thread执行完成
} catch (InterruptedException e) {
    System.out.println(Thread.currentThread().getName()+"线程被中断了");//主线程被中断了
    e.printStackTrace();
    thread.interrupt();//如果主线程在join期间中断,规范的操作是将这个中断传递给子线程
}

主要是第6行代码

join期间的状态
public class JoinThreadState {

    public static void main(String[] args) throws InterruptedException {
        Thread mainThread = Thread.currentThread();

        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(3000);
                System.out.println("主线程join期间的状态:" + mainThread.getState());//这里输出的是主线程join时候的状态
                System.out.println(Thread.currentThread().getName() + "运行结束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        thread.start();
        System.out.println("主线程等待子线运行完毕");
        thread.join();
        System.out.println("主线程运行完毕");
    }
}

用代码说话——join期间,主线程的状态是WAITING,不是一些资料上说的BLOCKED

join原理

join不是一个native的方法,其源码如下

public final synchronized void join(long millis)
throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

一并贴出了join方法中的注释内容,其中有一段话很重要**:由于线程运行结束的时候会调用notifyAll方法,因此不推荐写代码的时候直接通过Thread的对象调用wait,notify和notifyAll方法**。

可以看到join的底层其实就是调用的wait方法,上述的一段话已经解释了这里的wait是由谁唤醒的。网上查了一堆资料,在join的底层jvm源码中确实发现了调用notifyAll的痕迹

所有线程在运行结束的时候,会自动调用notifyAll方法,这也是java不推荐我们直接通过thread对象调用wait,notify和notifyAll方法的原因。基于此,我们可以用相关的代码替换join

public class JoinPrinciple {

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"执行完毕");
        });

        thread1.start();
        System.out.println("主线程开始等待子线程运行完毕");
        //这里的join由子线程的对象进行调用,但含义实质是将子线程加入到主线程中
        //thread1.join();//含义:子线程加入到主线程中,主线程需要等待子线程执行完毕之后再运行
        
        //thread1.join的等价代码,主线程获取到thread1这个对象锁之后,进入等待
        //最后被thread1的run方法执行结束之后,被唤醒
        synchronized (thread1){
            thread1.wait();
        }
        System.out.println("所有子线程执行完毕");
    }
}
yield方法

yield方法是我们需要总结的最后一个方法,yield的方法就是释放已经占有的CPU时间片,但是,尴尬的是yield一般不被JVM遵循,只需要了解其作用即可,实际开发中也用的很少。yield与sleep方法的区别就是,yield释放的线程,可能随时被再次调度。

生产者消费者模式

其实聊到wait和notify的方法,不得不说一下生产者和消费者设计模式,这个设计模式是解耦的最好体现

图中左边代表消费者,右边代码生产者,二者的通信是通过中间的FIFO的队列,中间的FIFO队列满了,生产者阻塞。中间的FIFO满了,消费者阻塞。这里我们用wait和notify手动实现以下这个设计模式

实例代码

public class ProducerConsumerModel {

    public static void main(String[] args) {
        EventStorage storage = new EventStorage();
        Producer producer = new Producer(storage);
        Consumer consumer = new Consumer(storage);

        new Thread(producer).start();
        new Thread(consumer).start();
    }


}

//生产者
class Producer implements Runnable {

    private EventStorage storage;

    public Producer(EventStorage storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            storage.put();
        }
    }
}

//消费者
class Consumer implements Runnable{

    private EventStorage storage;

    public Consumer(EventStorage storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            storage.take();
        }
    }
}

//容器
class EventStorage{
    private int maxSize;
    private linkedList storage;

    public EventStorage(){
        maxSize = 10;
        storage = new linkedList<>();
    }

    
    public synchronized void put(){

        while(storage.size() == maxSize){//如果仓库已经满了,则循环等待,wait释放锁
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        storage.add(new Date());
        System.out.println("仓库里有了"+storage.size()+"个产品");
        notify();//通知消费者取元素,这里为了简单,只有一个消费者,因此直接notify
    }

    
    public synchronized void take(){
        while(storage.size() == 0){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("拿到了"+storage.poll()+",现在仓库还剩下:"+storage.size());
        notify();//通知生产者,生产数据
    }

}
一些面试问题

1、两个线程交替打印0~100的奇偶数?

通过synchronize关键是,可以通过如下代码实现

public class WaitNotifyPrintOddEvenSync {

    private static int count;

    private static final Object lock = new Object();

    //建立两个线程,一个只处理偶数,一个只处理奇数(用位运算处理)
    //线程完成自己工作的同时,还需要考虑其他线程的输出,这里用synchronize来通信

    public static void main(String[] args) {
        Thread thread01 = new Thread(() -> {
            while(count<100){
                synchronized (lock){
                    if((count&1) == 0){
                        System.out.println(Thread.currentThread().getName()+": 输出偶数"+(count++));
                    }
                }
            }
        },"偶数");

        Thread thread02 = new Thread(() -> {
            while(count<100){
                synchronized (lock){
                    if((count&1) != 0){
                        System.out.println(Thread.currentThread().getName()+": 输出奇数"+(count++));
                    }
                }
            }
        },"奇数");

        thread01.start();
        thread02.start();
    }
}

利用wait和notify可以通过如下代码实现

public class WaitNotifyPrintOddEveWait {

    private static int count = 0;
    private static final Object lock = new Object();

    //1.拿到锁,我们就打印
    //2.打印完,唤醒其他线程,自己休眠
    static class TurningRunner implements Runnable{

        @Override
        public void run() {
            while(count<=100){
                synchronized (lock){
                    //拿到锁就直接打印
                    System.out.println(Thread.currentThread().getName()+":"+count++);
                    //打印完成之后,唤醒其他线程,然后自己休眠
                    lock.notify();
                    if(count<=100){
                        try {
                            //打印完了之后,count还是小于100,就自己休眠
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new TurningRunner(),"偶数").start();
        //Thread.sleep(100);可以休眠一下,防止奇偶错乱
        new Thread(new TurningRunner(),"奇数").start();
    }
}

2、手写消费者生产者设计模式?

上述相关内容已经有相关解答

3、wait为什么需要在同步代码块中使用,而sleep不需要?

答:为了让通信变得更加可靠,如果wait和notify不是放在同步代码块中,则很大程度上其执行顺序可以任意,毕竟线程的启动顺序也是随机的,这样很容易在某些线程wait了之后,无法得到notify,造成死锁或者长时间等待。而sleep则不同,只是简单的针对自己单独线程的

4、wait,notify,notifyAll为什么定义在Object中,而sleep定义在Thread中?

Java中的一个锁,其实就是一个对象,Java中一个线程中其实可以持有多把锁,而且这些锁直接可以相互配合,如果将wait,notify,notifyAll方法定义在Thread类中,则并不能实现多把锁互相配合的场景。同时本身wait和notify也是基于锁而定义的。而sleep是针对线程的,并不针对锁的操作,因此sleep可以定义在Thread中

5、如果调用Thread对象中的wait方法会怎么样?

这个在博客中已经总结,Java线程本身在运行完成的时候,会自动调用notify和notifyAll的方法,如果直接调用Thread对象的wait方法,可能会干扰到原有的线程通信逻辑。

总结

关于stop,suspend,resume这些过时的方式,这里就不再总结了

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/324546.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号