JAVA和Nginx 教程大全

网站首页 > 精选教程 正文

Java 基础(十四)线程——下 java简单的线程实例

wys521 2024-10-29 16:59:01 精选教程 23 ℃ 0 评论


Java 线程:线程的交互

线程交互的基础知识

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

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

  • 必须从同步环境内调用 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关键字了。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表