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

上周由于一些事情回了一趟长沙,因此更新晚了几天。Sorry~html

Java 线程:线程的交互

线程交互的基础知识

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

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

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

  • 必须从同步环境内调用 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 对象。github

注意:当在对象上调用 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 的代码还没执行。而后, 就形成了某个线程一直处于等待状态。
一般,解决上面问题的最佳方式是利用某种循环,该循环检查某个条件表达式,只有当正在等待的事情尚未发生的状况下,它才继续等待。api

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 解决方案,天天上班以前刷一个算法题,真的巨爽,强烈推荐想打好基础去大厂的小伙伴们一块儿刷题。

相关文章
相关标签/搜索