Java 基础(十四)线程——下

Java 基础(十四)线程——下

Android小彩虹2021-07-16 15:03:56240A+A-

上周因为一些事情回了一趟长沙,所以更新晚了几天。Sorry~

Java 线程:线程的交互

线程交互的基础知识

首先我们从 Object 类中的三个方法来学习。

方法名 作用
void notify() 唤醒在此对象监视器上等待的单个线程
void notifyAll() 唤醒在此对象监视器上等待的所有线程
void wait() 使当前的线程等待,直到其他线程调用此对象的 notify()方法或 notifyAll()方法

关于 等待/通知,要记住的关键点是:

  • 必须从同步环境内调用 wait()、notify()、notifyAll()方法。线程不能调用对象上的等待或通知方法,除非它拥有那个对象的锁。
  • wait()、notify()、notifyAll()都是 Object 的实例方法。与每个对象具有锁一样,每个对象可以有一个线程列表,他们等待来自该信号。线程通过执行对象上的 wait 方法获得这个等待列表。从那时候起,它不再执行任何其他指令,直到调用对象的 notify 方法为止。如果多个线程在同一个对象上等待,则将只选择一个线程(不保证顺序)继续执行。如果没有线程等待,则不采取任何特殊操作。

敲黑板!!!上面这段话是重点。会用 wait、notify 方法的童鞋先理解这段话,不会用 wait、notify 方法的童鞋请看懂下面的例子再结合例子理解。

public static void main(String[] args) {
        Thread1 thread1 = new Thread1();
        thread1.start();
        synchronized (thread1.obj) {
            try {
                System.out.println("等待 thread1 完成计算。。。");
                //线程等待
                thread1.obj.wait();
//                thread1.sleep(1000);//思考一下,如果把上面这行代码注掉,执行这行代码
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("thread1 对象计算的总和是:" + thread1.total);
        }


    }


    public static class Thread1 extends Thread {
        int total;
        public final Object obj = new Object();

        @Override
        public void run() {
            synchronized (obj) {
                for (int i = 0; i < 101; i++) {
                    total += i;
                }
                //(完成计算了)唤醒在此对象监视器上等待的单个线程,在本例中线程 thread1 被唤醒
                obj.notify();
                System.out.println("计算结束:" + total);
            }

        }
    }
}

以上代码的两个 synchronize 代码块的锁都用 Thread1 的实例对象也是可以的,这里为了方便大家理解必须要用同一个锁,才 new 了一个 Obj 对象。

注意:当在对象上调用 wait 方法时,执行该代码的线程立即放弃它在对象上的锁。然而调用 notify 时,并不意味着这时线程会放弃其锁。如果线程仍然在完成同步代码,则线程在同步代码结束之前不会放弃锁。因此,调用了 notify 并不意味着这时该锁变得可用

上面的运行结果忘记粘贴出来了,童鞋们自行测试吧~

多个线程在等待一个对象锁时使用 notifyAll()

在多数情况下,最好通知等待某个对象的所有线程。如果这也做,可以在对象使用 notifyAll()让所有在此对象上等待的线程重新活跃。

public class ThreadMutual extends Thread{
    int total;

    public static void main(String[] args) {
        ThreadMutual t = new ThreadMutual();
        new Thread1(t).start();
        new Thread1(t).start();
        new Thread1(t).start();
        new Thread1(t).start();
        new Thread1(t).start();
        new Thread1(t).start();
        t.start();

    }

    @Override
    public void run() {
        synchronized (this) {
            for (int i = 0; i < 11; i++) {
                total += i;
            }
            //(完成计算了)唤醒在此对象监视器上等待的单个线程,在本例中线程A被唤醒
            System.out.println("计算结束:" + total);
            notifyAll();

        }

    }


    public static class Thread1 extends Thread {

        private final ThreadMutual lock;

        public Thread1(ThreadMutual lock){
            this.lock = lock;
        }

        @Override
        public void run() {
            synchronized (lock) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "得到结果:"+lock.total);
            }

        }
    }
}

计算结束:55
Thread-5得到结果:55
Thread-6得到结果:55
Thread-4得到结果:55
Thread-3得到结果:55
Thread-2得到结果:55
Thread-1得到结果:55

注意:上面的代码如果线程 t 如果第一个 start,则会发生很多意料之外的情况,比如说notifyAll 已经执行了,wait 的代码还没执行。然后, 就造成了某个线程一直处于等待状态。
通常,解决上面问题的最佳方式是利用某种循环,该循环检查某个条件表达式,只有当正在等待的事情还没有发生的情况下,它才继续等待。

Java 线程:线程的调度与休眠

Java 线程的调度是 Java 多线程的核心,只有良好的调度,才能充分发挥系统的性能,提高程序的执行效率。

这里要明确一点,不管程序员怎么编写调度,只能最大限度的影响线程执行的次序,而不能做到精准控制。

线程休眠的目的是时线程让出 CPU 的最简单的做法之一,线程休眠时,会将 CPU资源交给其他线程,以便能轮换执行,当休眠一定时间后,线程会苏醒,进入准备状态等待执行。

线程休眠的方法是 Thread.sleep(),是个静态方法,那个线程调用了这个方法,就睡眠这个线程。

public class Test {
    public static void main(String[] args) {
        Thread t1 = new MyThread1();
        Thread t2 = new Thread(new MyRunnable());
        t1.start();
        t2.start();
    }
}

class MyThread1 extends Thread {
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println("线程1第" + i + "次执行!");
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class MyRunnable implements Runnable {
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println("线程2第" + i + "次执行!");
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果:

线程2第0次执行!
线程1第0次执行!
线程1第1次执行!
线程2第1次执行!
线程1第2次执行!
线程2第2次执行!

Java 线程:线程的调度-优先级

与线程休眠类似,线程的优先级仍然无法保证线程的执行次序。只不过,优先级高的线程获取 CPU 资源的概率较大,低优先级的并非没有机会执行。

线程的优先级用1-10之间的整数表示,数值越大优先级越高,默认为5.

public class Test {
    public static void main(String[] args) {
        Thread t1 = new MyThread1();
        Thread t2 = new Thread(new MyRunnable());
        t1.setPriority(10);
        t2.setPriority(1);

        t2.start();
        t1.start();
    }
}

class MyThread1 extends Thread {
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("线程1第" + i + "次执行!");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class MyRunnable implements Runnable {
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("线程2第" + i + "次执行!");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果:

线程2第0次执行!
线程1第0次执行!
线程1第1次执行!
线程2第1次执行!
线程1第2次执行!
线程2第2次执行!
线程1第3次执行!
线程2第3次执行!
线程1第4次执行!
线程2第4次执行!
线程1第5次执行!
线程2第5次执行!
线程1第6次执行!
线程2第6次执行!
线程1第7次执行!
线程2第7次执行!
线程1第8次执行!
线程2第8次执行!
线程1第9次执行!
线程2第9次执行!

我们可以看到,每隔50ms 打印一次,优先级高的线程1大概率先执行。

Java 线程:线程的调度-让步

线程的让步含义就是使当前运行着的线程让出 CPU 资源,但是给谁不知道,只是让出,线程回到可执行状态。

线程让步使用的是静态方法 Thread.yield(),用法和 sleep 一样,作用的是当前执行线程。

public class Test {
    public static void main(String[] args) {
        Thread t1 = new MyThread1();
        Thread t2 = new Thread(new MyRunnable());

        t2.start();
        t1.start();
    }
}

class MyThread1 extends Thread {
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("线程1第" + i + "次执行!");
        }
    }
}

class MyRunnable implements Runnable {
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("线程2第" + i + "次执行!");
            Thread.yield();
        }
    }
}

运行结果:

线程1第0次执行!
线程2第0次执行!
线程1第1次执行!
线程2第1次执行!
线程1第2次执行!
线程1第3次执行!
线程1第4次执行!
线程1第5次执行!
线程1第6次执行!
线程1第7次执行!
线程1第8次执行!
线程1第9次执行!
线程2第2次执行!
线程2第3次执行!
线程2第4次执行!
线程2第5次执行!
线程2第6次执行!
线程2第7次执行!
线程2第8次执行!
线程2第9次执行!

Java 线程:线程的调度-合并

线程的合并的含义就是将几个并行线程的线程合并为一个单线程,应用场景是当一个线程必须等待另一个线程执行完毕才能执行,使用 join 方法。

public class Test {
    public static void main(String[] args) {
        Thread t1 = new MyThread1();
        t1.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("主线程第" + i + "次执行!");
            if (i > 2) try {
                //t1线程合并到主线程中,主线程停止执行过程,转而执行t1线程,直到t1执行完毕后继续。
                t1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class MyThread1 extends Thread {
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println("线程1第" + i + "次执行!");
        }
    }
}

运行结果:

主线程第0次执行!
主线程第1次执行!
主线程第2次执行!
主线程第3次执行!
线程1第0次执行!
线程1第1次执行!
线程1第2次执行!
主线程第4次执行!
主线程第5次执行!
主线程第6次执行!
主线程第7次执行!
主线程第8次执行!
主线程第9次执行!

不逼逼了,线程 join 只有第一次有效。这里我也很懵逼,我以为线程1第***这句话的打印次数应该是(10-3)*3 次的。
这里我们来回顾一下上篇文章说的线程的基本知识,线程是死亡之后就不能重新启动了对吧。我们再来理解一下 join 的概念当一个线程必须等待另一个线程执行完毕才能执行,我们在主线程中join 线程 t1,所以直到 t1执行完毕,才能再次执行主线程。当 i=4 的时候再次执行 t1.join()时,t1 线程已经是处于死亡状态,所以不会再次执行 run 方法。因此 t1线程里面 run 方法的打印语句只执行了三次。为了验证我们的猜想,我建议去阅读以下源码。

以下是 Java8 Thread#join() 方法的源码。

public final void join() throws InterruptedException {
    this.join(0L);
}

public final synchronized void join(long var1) throws InterruptedException {
    long var3 = System.currentTimeMillis();
    long var5 = 0L;
    if(var1 < 0L) {
        throw new IllegalArgumentException("timeout value is negative");
    } else {
        if(var1 == 0L) {
            while(this.isAlive()) {
                this.wait(0L);
            }
        } else {
            while(this.isAlive()) {
                long var7 = var1 - var5;
                if(var7 <= 0L) {
                    break;
                }

                this.wait(var7);
                var5 = System.currentTimeMillis() - var3;
            }
        }

    }
}

public final native boolean isAlive();

我们可以看到 t1调用 join 方法的时候调用了重载的方法,并且传了参数0,然后关键来了while(this.isAlive())条件一直满足的情况下,调用了 this.wait(0),这里的 this 相当于对象 t1。

我们来思考一下,t1.wait()到底是哪个线程需要 wait?给你们三秒钟时间。

3...
2...
1...

好了,我直接说了,大家记住,t1只是个对象,这里不能当成是 t1线程 wait,主线程里面通过对象 t1作为锁,并调用了 wait 方法,其实是主线程 wait 了。while 的判断条件是线程 t1.isAlive(),注意,这里是判断线程 t1是否存活,如果存活,则主线程一直 wait(0),直到 t1 线程执行结束死亡。这样可以了解了吧,再来思考一下如果在 Android 主线程里面调用 join 方法可能会造成什么问题?

这个问题很简单,我就不说答案了。

Java 线程:线程的调度-守护线程

守护线程与普通线程写法上基本没啥区别,调用线程对象的方法 setDaemon(true),则可以将其设置为守护线程。

守护线程的使用情况较少,但并非无用,举例来说,JVM 的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用的时候,使用数据库连接池,连接池本身也包含着很多后台现场,监控连接个数、超时时间、状态等等。

  • setDaemon(boolean on)

将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java虚拟机退出。该方法必须在启动线程前调用。

public class ThreadDaemon {

    public static void main(String[] args) {
        Thread t1 = new MyCommon();
        Thread t2 = new Thread(new MyDaemon());
        t2.setDaemon(true);        //设置为守护线程
        t2.start();
        t1.start();
    }
}

class MyCommon extends Thread {
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("线程1第" + i + "次执行!"+"——————活着线程数量:"+Thread.currentThread().getThreadGroup().activeCount());
            try {
                Thread.sleep(7);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class MyDaemon implements Runnable {
    public void run() {
        for (long i = 0; i < 9999999L; i++) {
            System.out.println("后台线程第" + i + "次执行!");
            try {
                Thread.sleep(7);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

啥也别说了,来看结果吧:

后台线程第0次执行!
线程1第0次执行!——————活着线程数量:4
后台线程第1次执行!
线程1第1次执行!——————活着线程数量:4
后台线程第2次执行!
线程1第2次执行!——————活着线程数量:4
后台线程第3次执行!
线程1第3次执行!——————活着线程数量:4
后台线程第4次执行!
线程1第4次执行!——————活着线程数量:4
后台线程第5次执行!

从上面的结果我们可以看出,前台线程是包装执行完毕的,后台线程还没有执行完毕就退出了。也就是说除了守护线程以为的其他线程执行完之后,守护线程也就结束了。

然后,我们来看看,为什么活着的线程数量会是4,明明只开了两个子线程呀,加上 main 线程也才三个,那再加一个垃圾回收线程吧哈哈哈哈。

这个问题也是我在学习过程中困扰了很久的问题。之前纠结的是,main 线程执行完了,如果还有子线程在运行。那么 main 线程到底是先结束还是等待子线程执行结束之后再结束?main 线程结束是不是代表程序退出?

然后我就 Debug 线程池里面所有的线程,发现里面有一个叫 DestoryJavaVM 的线程,然后我也不知道这是个什么东西,遂问了一下度娘,度娘告诉我~

DestroyJavaVM:main执行完后调用JNI中的jni_DestroyJavaVM()方法唤起DestroyJavaVM线程。 JVM在Jboss服务器启动之后,就会唤起DestroyJavaVM线程,处于等待状态,等待其它线程(java线程和native线程)退出时通知它卸载JVM。线程退出时,都会判断自己当前是否是整个JVM中最后一个非deamon线程,如果是,则通知DestroyJavaVM线程卸载JVM

大概就是酱紫吧,4个线程分别是两个我手动开的子线程,一个DestroyJavaVM ,还有一个大概是垃圾回收线程吧,哈哈哈哈,如果不对,请务必拍砖~

Java 线程:线程的同步-同步方法\同步块

上一篇已经就同步问题做了详细的讲解。

对于多线程来说,不管任何编程语言,生产者消费者模型都是最经典的。这里我们拿一个生产者消费者模型来深入学习吧~

实际上,应该是“生产者-消费者-仓储”模型,离开了仓储,生产者消费者模型就显得没有说服力。

对于此模型,应该明确以下几点:

  • 生产者仅仅在仓储未满时候生产,仓满则停止生产
  • 消费者仅仅在仓储有产品时候才能消费,仓空则等待
  • 当消费者发现仓储没产品可消费时候会通知生产者生产
  • 生产者在生产出可消费产品时候,应该通知等待的消费者去消费

此模型将要的知识点,我们上面都学过了,直接撸代码吧~

public class Model {
    public static void main(String[] args) {
        Godown godown = new Godown(30);
        Consumer c1 = new Consumer(50, godown);
        Consumer c2 = new Consumer(20, godown);
        Consumer c3 = new Consumer(30, godown);
        Producer p1 = new Producer(10, godown);
        Producer p2 = new Producer(10, godown);
        Producer p3 = new Producer(10, godown);
        Producer p4 = new Producer(10, godown);
        Producer p5 = new Producer(10, godown);
        Producer p6 = new Producer(10, godown);
        Producer p7 = new Producer(40, godown);

        c1.start();
        c2.start();
        c3.start();
        p1.start();
        p2.start();
        p3.start();
        p4.start();
        p5.start();
        p6.start();
        p7.start();
    }
}

/**
 * 仓库
 */
class Godown {
    public static final int max_size = 100;//最大库存量
    public int curnum;    //当前库存量

    Godown() {
    }

    Godown(int curnum) {
        this.curnum = curnum;
    }

    /**
     * 生产指定数量的产品
     *
     * @param neednum
     */
    public synchronized void produce(int neednum) {
        //测试是否需要生产
        while (neednum + curnum > max_size) {
            System.out.println("要生产的产品数量" + neednum + "超过剩余库存量" + (max_size - curnum) + ",暂时不能执行生产任务!");
            try {
                //当前的生产线程等待
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //满足生产条件,则进行生产,这里简单的更改当前库存量
        curnum += neednum;
        System.out.println("已经生产了" + neednum + "个产品,现仓储量为" + curnum);
        //唤醒在此对象监视器上等待的所有线程
        notifyAll();
    }

    /**
     * 消费指定数量的产品
     *
     * @param neednum
     */
    public synchronized void consume(int neednum) {
        //测试是否可消费
        while (curnum < neednum) {
            try {
                //当前的生产线程等待
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //满足消费条件,则进行消费,这里简单的更改当前库存量
        curnum -= neednum;
        System.out.println("已经消费了" + neednum + "个产品,现仓储量为" + curnum);
        //唤醒在此对象监视器上等待的所有线程
        notifyAll();
    }
}

/**
 * 生产者
 */
class Producer extends Thread {
    private int neednum;                //生产产品的数量
    private Godown godown;            //仓库

    Producer(int neednum, Godown godown) {
        this.neednum = neednum;
        this.godown = godown;
    }

    public void run() {
        //生产指定数量的产品
        godown.produce(neednum);
    }
}

/**
 * 消费者
 */
class Consumer extends Thread {
    private int neednum;                //生产产品的数量
    private Godown godown;            //仓库

    Consumer(int neednum, Godown godown) {
        this.neednum = neednum;
        this.godown = godown;
    }

    public void run() {
        //消费指定数量的产品
        godown.consume(neednum);
    }
}

已经消费了20个产品,现仓储量为10
已经生产了10个产品,现仓储量为20
已经生产了10个产品,现仓储量为30
已经生产了10个产品,现仓储量为40
已经生产了10个产品,现仓储量为50
已经消费了30个产品,现仓储量为20
已经生产了40个产品,现仓储量为60
已经生产了10个产品,现仓储量为70
已经消费了50个产品,现仓储量为20
已经生产了10个产品,现仓储量为30

在本例中,要说明的是当发现不能满足生产者或消费条件的时候,调用对象的 wait 方法,wait 方法的作用是释放当前线程的所获得的锁,并调用对象的 notifyAll()方法,通知(唤醒)该对象上其他等待的线程,使其继续执行。这样,整个生产者、消费者线程得以正确的协作执行。

Java 线程:volatile 关键字

Java 语言包含两种内在同步机制:同步块(方法)和 volatile 变量。这两种机制的提出都是为了实现代码线程的安全性。其中 volatile 变量的同步性较差(但有时它更简单并且开销更地),并且其使用也容易出错。

首先考虑一个问题,为什么变量需要volatile来修饰呢?
要搞清楚这个问题,首先应该明白计算机内部都做什么了。比如做了一个i++操作,计算机内部做了三次处理:读取-修改-写入。
同样,对于一个long型数据,做了个赋值操作,在32系统下需要经过两步才能完成,先修改低32位,然后修改高32位。

假想一下,当将以上的操作放到一个多线程环境下操作时候,有可能出现的问题,是这些步骤执行了一部分,而另外一个线程就已经引用了变量值,这样就导致了读取脏数据的问题。

通过这个设想,就不难理解volatile关键字了。

更多的内容,请参考《Java理论与实践:正确使用 Volatile 变量》一文,写得很好。

参考资料

Java线程详解
JDK 中文文档

推荐

这两天在逛 github 的时候无意发现了这个项目LeetCode 算法与 java 解决方案,每天上班之前刷一个算法题,真的巨爽,强烈推荐想打好基础去大厂的小伙伴们一起刷题。

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1

联系我们