梳理java多线程

本文详细介绍了Java中的多线程概念,包括线程调度、并发与并行的区别、多线程的实现方式、线程同步与异步、线程池的使用,以及Callable接口用于带返回值的线程。通过实例代码展示了synchronized和Lock的使用,解释了它们之间的区别,并讨论了线程的六种状态。最后,探讨了线程池ExecutorService的概念和好处,以及Java中四种线程池的使用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

线程与进程

进程:

是指一个内存中运行的应用程序,每个进程有一个独立的内存空间

线程:

进程中的执行路径,共享一个内存空间,线程之间可以自由切换,并发执行,一个进程最少有一个线程。

线程实际上是进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程。

每个线程都有自己的栈空间,共用一份堆内存。

线程调度

分时调度

所有线程轮流使用cpu的使用权,平均分配每个线程占用cpu的时间

抢占式调度

优先让优先级高的线程使用cpu,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

抢占式调度

cpu使用抢占式调度模式在多个线程间进行高速切换,看起来像是同时做多件事情。多线程并不能提高程序的运行速度,但能提高运行的效率。

并发与并行

并发:可以是一个处理器或者是多个处理器,通过来回切换运行,使得看起来像是同时发生,实际上是两个或多个事件在同一个时间段发生。

并行:一个处理器以上,两个或多个事情在同一个时刻发生(同时发生)

多线程的实现

继承Thread,继承Thread的类则须重写其run方法,run方法的调用则是执行了一条新的路径,这个执行路径的触发方式,不是调用run,而是通过thread对象的start()方法来启动任务。

public class MyThread extends Thread{
​
    /**
     * run方法是线程要执行任务的方法
     */
    public void run() {
        //这里的代码就是一条新的执行路径
        //这个执行路径的触发方式,不是调用run,而是通过thread对象的start()方法来启动任务
        for (int i=0;i<10;i++){
            System.out.println("一条大河波浪宽"+i);
        }
    }
}
public class Demo1 {
    public static void main(String[] args) {
        //一开始main函数就开启了一个线程
        MyThread m = new MyThread();
        m.start();//这个方法开启了第二个线程
        for (int i=0;i<10;i++){
            System.out.println("风吹稻花上两岸"+i);
        }
    }
}
//还可以使用匿名内部类的方式实现多线程
public class Demo1 {
    new Thread(){
        @Override
        public void run() {
            for (int i=0;i<10;i++){
                System.out.println("一条大河波浪宽"+i);
            }
        }
    }.start();
    for (int i=0;i<10;i++){
        System.out.println("风吹稻花上两岸"+i);
    }
}

实现接口Runnable

创建一个实现Runnable接口的类,创建它的一个对象(任务),new一个Thread对象把任务传进去。然后执行这个Thread的star方法。

public class MyRunnable implements Runnable{
​
    @Override
    public void run() {
        for (int i=0;i<10;i++){
            System.out.println("床前明月光"+i);
        }
    }
}
public class Demo1 {
    public static void main(String[] args) {
        MyRunnable r = new MyRunnable();    //视作创建了一个任务
        Thread t = new Thread(r);   //交给多个线程去执行
        t.start();
        for (int i=0;i<10;i++){
            System.out.println("疑是地上霜"+i);
        }
    }
}

实现runnable与继承Thread相比有如下优势:

1、通过创建任务(也就是实现Runnable的类创建出来的对象),然后给线程分配的方式来实现的多线程,更适合多个线程同时执行相同任务的情况。

2、可以避免单继承带来的局限性。(可以同时实现多个接口,但是只能继承一个父类)

3、任务与线程本身是分离的,提高了程序的健壮性。

4、后续学习的线程池技术,接受runnable类型的任务,不接受thread。

给线程设置名称

如果是Runnable则有两个方法

public static void main(String[] args) {
        new Thread(new MyRunnable(),"大河线程");
        //一种
        Thread t = new Thread(new MyRunnable());
        t.setName("这是我父亲日记里的文字。");
        //另一种
        t.start();
}
    static class MyRunnable implements Runnable{
​
        public void run(){
            System.out.println(Thread.currentThread().getName());
        }
    }

而若没有使用Runnable直接是继承Thread类的对象则是只有后一种方法,Thread对象.setName()方法。

public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.setName("一条大河");
        myThread.start();
    }
    private static class MyThread extends Thread{
​
        public void run(){
            System.out.println(Thread.currentThread().getName());
        }
    }

线程休眠

可以通过Thread.sleep(1000);括号里是毫秒,来让线程休眠1000毫秒后再执行。

线程阻塞

所有消耗时间的操作,比如读文件,或接受用户输入,都能算是线程阻塞,意味着现在线程处于暂停状态也没能去做其他事。

线程中断

线程中断的操作是给Thread线程对象打上标记(异常),然后在线程的run方法中设置catch来捕获这个异常,在catch代码块中用return可使得直接跳出这个run方法,结束这个线程,而也可以设置选择场景在catch中决定是否要中断(不中断则不做任何操作),实际的应用中,如果要中断也要在return前释放各种资源。

package com.test4_5;
//线程的中断
//一个线程是个独立的执行路径,是否结束应该由自身决定。外部掐死总是一种能导致各种无可预料的错误,
//而如果有需要中断,应该给线程打标记,线程特殊情况下会查看标记,如果有则会触发一个异常
//程序员可以设置try catch 来决定如何结束这个线程(或不结束)。
public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(new MyRunnable());
        t1.start();
        for(int i = 0;i<5;i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //给线程t1添加中断标记,设置的catch中就能依照程序员的设计选择是否让它死亡。
        t1.interrupt();
    }
    private static class MyRunnable implements Runnable{
        //下面的sleep可能会有异常,而在这个类中无法抛出异常,因为实现了Runnable接口,Runnable父接口
        //都没有声明异常的抛出,子就不能抛出比父更大的异常。所以我们选择try catch
​
        @Override
        public void run() {
            for(int i = 0;i<10;i++){
                System.out.println(Thread.currentThread().getName()+":"+i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    //e.printStackTrace();
                    //System.out.println("发现了终端标志,如果不想让它死亡就可以什么也不做。");
                    System.out.println("发现了终端标志,选择线程死亡则执行return。");
                    return;
                }
            }
        }
​
    }
}

用户线程和守护线程

当一个进程不包含任何存活的用户线程时,进程结束。

1、什么是守护线程

在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程) ,直接创建的线程都是用户线程。

守护线程设置方式

Thread t1=new Thread(new MyRunnable());
//设置为守护线程需要在执行start之前
t1.setDaemon(true);
t1.start();

The Java Virtual Machine exits when the only threads running are all daemon threads.

这句话来自 JDK 官方文档,意思是:

当 JVM 中不存在任何一个正在运行的非守护线程时,则 JVM 进程即会退出。

可以做个对比实验,设置一个用户线程,while(true)死循环执行,当主线程执行完毕退出时,jvm不会退出,因为要等待所有用户线程结束。

而这个用户线程设置为守护线程时,当主线程退出时,JVM 检测到没有其他用户线程在运行,会随之退出运行,守护线程同时也会被回收,即使你里面是个死循环也不碍事。

2、守护线程的作用及应用场景

上面,我们已经知道了,如果 JVM 中没有一个正在运行的非守护线程,这个时候,JVM 会退出。换句话说,守护线程拥有自动结束自己生命周期的特性,而非守护线程不具备这个特点。

JVM 中的垃圾回收线程就是典型的守护线程,如果说不具备该特性,会发生什么呢?

当 JVM 要退出时,由于垃圾回收线程还在运行着,导致程序无法退出,这就很尴尬了!!!由此可见,守护线程的重要性了。

通常来说,守护线程经常被用来执行一些后台任务,但是呢,你又希望在程序退出时,或者说 JVM 退出时,线程能够自动关闭,此时,守护线程是你的首选。

有几点需要注意:

(1) thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。(2) 在Daemon线程中产生的新线程也是Daemon的。(3) 不要认为所有的应用都可以分配给Daemon来进行服务,比如读写操作或者计算逻辑。因为你不可能知道在所有的User完成之前,Daemon是否已经完成了预期的服务任务。一旦User退出了,可能大量数据还没有来得及读入或写出,计算任务也可能多次运行结果不一样。这对程序是毁灭性的。造成这个结果理由已经说过了:一旦所有User Thread离开了,虚拟机也就退出运行了。

同步与异步

字面意义应该较于数据而言更好理解一些,多个线程对所使用的数据同步,则对数据的操作是排队的,多个线程对所使用的数据异步,则是多个线程可以分开且同时操作一个数据。

同步:排队执行,效率低但是线程安全。

异步:同时执行,效率高但是数据不安全。

平时直接生成线程,然后运行都是异步,而如果寻求同步,则需要用到锁的概念。

synchronized隐式锁

底层原理可以在B站查到黑马程序员synchronized原理剖析与优化。

如何使用:

synchronized同步代码块:

synchronized(锁对象){}

需要注意,同步代码块不能在静态方法中编写。

可以理解为给同步代码块加上一道门,而第一个线程进去会领到创建的锁对象(创建的锁对象可以是随意的一个object对象),而其他线程到达同步代码块的时候则没有了这个锁对象,门会将其拦住,当第一个线程跳出同步代码块,则会抛出这个锁对象供之后的线程争夺。像下面的例子,每个线程都抢夺票,第一个线程抢夺到了锁对象以后扣票再抛出,其他线程不会处于一个每毫秒都访问是否有锁对象被抛出,而那个线程第一时间执行while循环继续抢夺锁对象,则大概率都是这一个线程抢夺到锁对象。

public class Demo5 {
    public static void main(String[] args) {
​
//    错误的演示    创建了三个Ticket对象,创建多个线程,每个都有自己的count
//        Thread t1 = new Thread(new Ticket());
//        Thread t2 = new Thread(new Ticket());
//        Thread t3 = new Thread(new Ticket());
//        t1.start();
//        t2.start();
//        t3.start();
​
//    正确的演示,创建了一个Ticke对象,创建多个线程,运行同一个count
​
​
        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }
    static class Ticket implements Runnable{
        private int count = 10;
        private Object o =new Object();
        @Override
        public void run() {
            while (true) {
                System.out.println(Thread.currentThread().getName() + "测试线程是不是到这里才被等待的");
                synchronized (o) {
                    if (count > 0) {
                        System.out.println("准备卖票。");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        count--;
                        System.out.println(Thread.currentThread().getName() + "出票成功:余票" + count);
                    }else {
                        break;
                    }
                }
                //在一个线程抛出锁对象以后,让他休眠1000毫秒,使其不能立刻执行回while循环重新抢夺,
                // 以模拟其执行其他任务(如若直接循环去抢夺大概率还是这个线程,其他线程抢夺可能有个间隙)
                // ,抛出的锁对象让其他排队的线程进行抢夺
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

synchronized同步方法

:在实现Runnable接口的类中新建一个带有synchronized修饰符的方法,那么多线程通过run()方法调用这个方法就能实现同步的效果。

/*
线程安全,同步方法
 */
public class Demo6 {
    public static void main(String[] args) {
​
//    创建了一个Ticke对象,创建多个线程,运行同一个count
        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }
    static class Ticket implements Runnable{
        private int count = 10;
        boolean flag = true;
        @Override
        public void run() {
            while (flag) {
                System.out.println(Thread.currentThread().getName() + "测试线程是不是到这里才被等待的");
                flag=this.sale();
                //在一个线程抛出锁对象以后,让他休眠1000毫秒,使其不能立刻执行回while循环重新抢夺,
                // 以模拟其执行其他任务(如若直接循环去抢夺大概率还是这个线程,其他线程抢夺可能有个间隙)
                // ,抛出的锁对象让其他排队的线程进行抢夺
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        private synchronized boolean sale(){
            if (count > 0) {
                System.out.println("准备卖票。");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count--;
                System.out.println(Thread.currentThread().getName() + "出票成功:余票" + count);
                return true;
            }else {
                return false;
            }
        }
    }
}

通过在方法前设置System.out.println(Thread.currentThread().getName() + "测试线程是不是到这里才被等待的"); 的结果可以得出,三个线程都是进入了Runnable任务里run方法的while循环的,而不是被阻止在run方法外面,只是阻塞在了synchronized方法或者代码块外,所以即使有线程拿到锁对象也不影响其他线程进入run方法,其他的线程在阻塞状态BLOCKED 。

在上列代码中,synchronized修饰了方法之后,若是在执行这个方法前再写一个synchronized代码块synchronized (this){},这个就比较绕了,必须先理解,这两个门用的是同一把锁,也就是说其中一个synchronized门有人进去了那另一个也会关起来。用上面的代码举个例子,假设只有两个线程,第一时间两个人都跑进了while循环,不过线程0拿到了synchronized (this)的锁,则线程1被阻在synchronized (this)门口,当线程0跑出来冲向第二个门this.sale(),线程1已经进入synchronized (this)门唯一的锁被带走,线程0只能在this.sale()门口等待线程1抛出锁。

public void run() {
    while (flag) {
        synchronized (this){
            System.out.println(Thread.currentThread().getName() + "假设里面是个任务,需要消耗1秒时间");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //这里我们设置一个sleep,防止出现同一个人从上个门synchronized (this){}出来直接进入下一个门this.sale(),无法体现后面来的线程因为进入synchronized (this){}而将this.sale()也给关闭了。
        try {
             Thread.sleep(1000);
        } catch (InterruptedException e) {
             e.printStackTrace();
        }
        flag=this.sale();
        //在一个线程抛出锁对象以后,让他休眠1000毫秒,使其不能立刻执行回while循环重新抢夺,
        // 以模拟其执行其他任务(如若直接循环去抢夺大概率还是这个线程,其他线程抢夺可能有个间隙)
        // ,抛出的锁对象让其他排队的线程进行抢夺
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Lock显式锁

同步代码块和同步方法都是隐式锁,而存在一个方法是显式锁。创建的方法是Lock l = new ReentrantLock();

package com.test4_5;
​
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
​
/*
显式锁
 */
public class Demo7 {
    public static void main(String[] args) {
        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }
    static class Ticket implements Runnable{
        private int count = 10;
        //显式锁 l
        private Lock l = new ReentrantLock();
        @Override
        public void run() {
            while (true) {
                l.lock();
                    if (count > 0) {
                        System.out.println("准备卖票。");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        count--;
                        System.out.println(Thread.currentThread().getName() + "出票成功:余票" + count);
                    }else {
                        break;
                    }
                l.unlock();
                //在一个线程抛出锁对象以后,让他休眠1000毫秒,使其不能立刻执行回while循环重新抢夺,
                // 以模拟其执行其他任务(如若直接循环去抢夺大概率还是这个线程,其他线程抢夺可能有个间隙)
                // ,抛出的锁对象让其他排队的线程进行抢夺
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

隐式锁和显式锁的区别?

sync和lock的区别1、出身不同从sync和lock的出身(原始的构成)来看看两者的不同。

Sync:Java中的关键字,是由JVM来维护的。是JVM层面的锁。

Lock:是JDK5以后才出现的具体的类。使用lock是调用对应的API。是API层面的锁

sync是底层是通过monitorenter进行加锁(底层是通过monitor对象来完成的,其中的wait/notify等方法也是依赖于monitor对象的。只有在同步块或者是同步方法中才可以调用wait/notify等方法的。因为只有在同步块或者是同步方法中,JVM才会调用monitory对象的);通过monitorexit来退出锁的。

而lock是通过调用对应的API方法来获取锁和释放锁的。

2、使用方式不同Sync是隐式锁。Lock是显示锁

所谓的显示和隐式就是在使用的时候,使用者要不要手动写代码去获取锁和释放锁的操作。

我们大家都知道,在使用sync关键字的时候,我们使用者根本不用写其他的代码,然后程序就能够获取锁和释放锁了。那是因为当sync代码块执行完成之后,系统会自动的让程序释放占用的锁。Sync是由系统维护的,如果非逻辑问题的话话,是不会出现死锁的。

在使用lock的时候,我们使用者需要手动的获取和释放锁。如果没有释放锁,就有可能导致出现死锁的现象。手动获取锁方法:lock.lock()。释放锁:unlock方法。需要配合tyr/finaly语句块来完成。

3、等待是否可中断Sync是不可中断的。除非抛出异常或者正常运行完成

Lock可以中断的。中断方式:

1:调用设置超时方法tryLock(long timeout ,timeUnit unit)

2:调用lockInterruptibly()放到代码块中,然后调用interrupt()方法可以中断。

4、加锁的时候是否可以公平

Sync;非公平锁

lock:两者都可以的。默认是非公平锁。在其构造方法的时候可以传入Boolean值。

true:公平锁

false:非公平锁

5、锁绑定多个条件来condition

Sync:没有。要么随机唤醒一个线程;要么是唤醒所有等待的线程。

Lock:用来实现分组唤醒需要唤醒的线程,可以精确的唤醒,而不是像sync那样,不能精确唤醒线程。

6、从性能比较

synchronized是托管给JVM执行的,而lock是java写的控制锁的代码。在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。但是到了Java1.6,发生了变化。synchronize在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地。

7、从使用锁的方式比较

据查,synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。

而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。

现代的CPU提供了指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。

公平锁和非公平锁

简单的说就是线程也需要排队,先到的排前面,这叫公平锁。而不公平就是大家一块抢,谁抢到算谁的。

上面也说了,synchronized隐式锁是无法设置为公平锁的,而Lock也是默认为非公平锁,需要公平的时候创建时可以传入Boolean类型的参数(fair公平:)true则启用,false则关闭。

Lock lock = new ReentrantLock(true);

线程死锁

概念:进入锁的线程A执行完毕的前提是需要进入线程B正在处在的锁区域,而线程B恰好又需要进入线程A处在的锁区域才能执行完毕。其实中间可以有无数个线程一个等待一个,这种情况统称为线程死锁。

而避免这个问题的方法很简答, 程序员应当避免一个线程执行锁区域要调用到另一个线程执行的所区域,就不会产生这种情况。

代码演示:

/*
线程死锁
 */
public class Demo8 {
    public static void main(String[] args) {
        Culprit culprit = new Culprit();
        Police police = new Police();
        //模拟警察喊话罪犯,需要罪犯执行完fun才能结束,而双方都有线程进入了加了锁的say()
        //fun和say都加上了synchronized,只有线程出来了才能执行后面的fun,
        // 双方都在等待say执行完,好去调用fun
        new MyThread(culprit,police).start();
        //模拟罪犯喊话警察,需要警察执行完fun才能结束,而双方都有线程进入了加了锁的say()
        //fun和say都加上了synchronized,只有线程出来了才能执行后面的fun,
        // 双方都在等待say执行完,好去调用fun
        culprit.say(police);
    }
    static class MyThread extends Thread{
        private Culprit culprit;
        private Police police;
​
        public MyThread(Culprit culprit, Police police) {
            this.culprit=culprit;
            this.police=police;
        }
​
        @Override
        public void run() {
            police.say(culprit);
        }
    }
    //罪犯
    static class Culprit{
        public synchronized void say(Police p){
            System.out.println("罪犯:你放了我,我放了人质");
            p.fun();
        }
        public synchronized void fun(){
            System.out.println("罪犯被放走,罪犯也放了人质");
        }
    }
    //警察
    static class Police{
        public synchronized void say(Culprit c){
            System.out.println("警察:你放了人质,我放了你");
            c.fun();
        }
        public synchronized void fun(){
            System.out.println("警察救了人质,但是罪犯跑了");
        }
    }
}

以上代码大概率会发生死锁,而不发生死锁的概率也是有的,main线程执行罪犯喊话,调用警察fun()方法的时候,子线程new MyThread还没有启动,也就没有进入警察的锁区域,main线程成功调用fun()方法完成线程执行。

多线程通信

Object类有几个方法

wait(),等待,致当前线程等待它被唤醒,通常是 通知或 中断,可以有wait(long timeoutMillis)设置唤醒时间,wait(long timeoutMillis, int nanos),nanos额外的时间,在timeoutMillis的基础上。

notify() 随机唤醒一个在这个对象中wait了的线程,notifyAll() 唤醒所有

首先,方法使用的注意事项:1、wait()、notify/notifyAll()方法是Object的final方法,无法被重写。2、wait()使当前线程阻塞,前提是必须先获得锁,一般配合synchronized关键字使 用,即,一般在synchronized同步代码块里使用wait()、notify/notifyAll()方法。3、由于wait()、notify/notifyAll()在synchronized代码块执行,说明当前线程一定是获 取了锁的。当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状 态。只有当notify/notifyAll()被执行时候,才会唤醒一个或多个正处于等待状态的线 程,然后继续往下执行,直到执行完synchronized代码块的代码或是中途遇到wait(), 再次释放锁。也就是说,notify/notifyAll()的执行只是唤醒沉睡的线程,而不会立即释 放锁,锁的释放要看代码块的具体执行情况。所以在编程中,尽量在使用了 notify/notifyAll()后立即退出临界区,以唤醒其他线程。4、wait()需要被try…catch包围。5、notify和wait的顺序不能错,如果A线程先执行notify方法,B线程在执行wait方法, 那么B线程是无法被唤醒的。6、notify和notifyAll的区别notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。notifyAll会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操 作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll方法。

生产者和消费者,错误的示范:

/*
消费者和生产者,用以展示多线程通信
 */
public class Demo9 {
    public static void main(String[] args) {
        Food f=new Food();
        new Cook(f).start();
        new Waiter(f).start();
    }
    //厨师
    static class Cook extends Thread{
        private Food f;
        public Cook(Food f){
            this.f=f;
        }
        public void run(){
            for(int i=0;i<100;i++){
                if(i%2==0){
                    f.setNameAndTaste("老干妈小米粥","香辣味");
                }else {
                    f.setNameAndTaste("煎饼果子","甜辣味");
                }
            }
        }
    }
    //服务员
    static class Waiter extends Thread{
        private Food f;
        public Waiter(Food f){
            this.f=f;
        }
        public void run(){
            for(int i=0;i<100;i++){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                f.get();
            }
        }
    }
    //食物
    static class Food{
        private String name;
        private String taste;
​
        public void setNameAndTaste(String name,String taste){
            this.name=name;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.taste=taste;
        }
        public void get(){
            System.out.println("服务员端走菜的名称是:"+this.name+",味道是:"+this.taste);
        }
    }
}

禁用相同的睡眠时间,意图达到生产一份食物端走一份食物的目的,这样的多线程协同方式在急速的运行情况下出现问题概率非常高。

若使用 等待-唤醒 的多线程沟通方式就能完美实现这样的通信需求。

public class Demo9 {
    public static void main(String[] args) {
        Food f=new Food();
        new Cook(f).start();
        new Waiter(f).start();
    }
    //厨师
    static class Cook extends Thread{
        private Food f;
        public Cook(Food f){
            this.f=f;
        }
        public void run(){
            for(int i=0;i<100;i++){
                if(i%2==0){
                    f.setNameAndTaste("老干妈小米粥","香辣味");
                }else {
                    f.setNameAndTaste("煎饼果子","甜辣味");
                }
            }
        }
    }
    //服务员
    static class Waiter extends Thread{
        private Food f;
        public Waiter(Food f){
            this.f=f;
        }
        public void run(){
            for(int i=0;i<100;i++){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                f.get();
            }
        }
    }
    //食物
    static class Food{
        private String name;
        private String taste;
​
        //true表示可以生产
        private boolean flag=true;
​
        public synchronized void setNameAndTaste(String name,String taste){
            if (flag) {
                this.name = name;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.taste = taste;
                flag=false;
                this.notifyAll();
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        public synchronized void get(){
            if(!flag) {
                System.out.println("服务员端走菜的名称是:" + this.name + ",味道是:" + this.taste);
                flag=true;
                this.notifyAll();
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

多线程的六种状态

NEW 尚未启动的线程处于此状态。 RUNNABLE 在Java虚拟机中执行的线程处于此状态。 BLOCKED 被阻塞等待监视器锁定的线程处于此状态(排队等待执行)。 WAITING 无限期等待另一个线程执行特定操作的线程处于此状态。 TIMED_WAITING 正在等待另一个线程执行最多指定等待时间的操作的线程处于此状态。 TERMINATED 已退出的线程处于此状态。

新生的线程处于NEW 状态,若是运行则处于RUNNABLE ,排队阻塞是BLOCKED ,运行中的线程调用sleep方法就会从RUNNABLE变成BLOCKED,运行中的线程调用等待方法wait就有可能是WAITING或TIMED_WAITING 状态,而无论如何最终线程都会走向死亡状态TERMINATED 。

带返回值的线程Callable

Callable是功能接口,需要方法去实现,其中有泛型可以返回对象。

第三种多线程的方式,运行模式是主线程中调用,然后主线程中断,等待Callable返回后继续,可以同时等待很多个。

Runnable与Callable

接口定义

接口定义
//Callable接口
public interface Callable {
    V call() throws Exception;
}
//Runnable接口
public interface Runnable {
    public abstract void run();
}

Runnable 与 Callable的相同点

·都是接口·都可以编写多线程程序·都采用Thread.start()启动线程

Runnable 与 Callable的不同点

Runnable没有返回值;Callable可以返回执行结果Callable接口的call()允许抛出异常;Runnable的run()不能抛出

Callable使用步骤

1. 编写类实现Callable接口 , 实现call方法
class XXX implements Callable<T> {
    @Override
    public <T> call() throws Exception {
        return T;
    }
}
2. 创建FutureTask对象 , 并传入第一步编写的Callable类对象
    FutureTask<Integer> future = new FutureTask<>(callable);
3. 通过Thread,启动线程
    new Thread(future).start();

先不研究为什么Callable必须通过FutureTask启动,也不研究FutureTask具体是什么,现在需要知道它同时继承于Future和Runnable,所以执行star跟Runnable一样,需要使用一个new Thread(FutureTask).start来执行。

Callable获取返回值

Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行(因为主线程等着接收return出来的东西),如果不调用不会阻塞,而不调用也就不会有返回值。

FutureTask.get()执行会中断主线程的进度并且返回call()方法的返回值,但与以往执行"方法所有代码以后触发return返回"不同,并不重新执行call()方法中的代码,可以理解为之前的new Thread(FutureTask).start()已经执行了retrun,但retrun出来的数据,需要用FutureTask.get()获取

所以FutureTask.get()应该是在Callable实现类的call()方法执行完毕以后使用,所以FutureTask有个方法专门用以判断Callable实现类的call()方法是否执行完毕,isDone()返回的是布尔值。

FutureTask.cancel()方法是中途取消子线程(Callable接口的线程),返回值是布尔值,true代表取消成功,false是取消失败(一般情况下是子线程执行完了,没有办法取消了)

实验代码

package com.test4_5;
​
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
​
public class Demo10 {
    public static void main(String[] args) {
        MyCallable c = new MyCallable();
        FutureTask<Integer> task=new FutureTask<Integer>(c);
        //FutureTask具体是什么先不研究,但它同时继承于Future类和Runnable类,所以执行star跟Runnable一样
        new Thread(task).start();
        /*
        以下这段try/catch重点是task.get(),FutureTask.get()执行会中断主线程的进度并且返回call()方法的返回值,
        但与以往执行"方法所有代码以后触发return返回"不同,这里并不重新执行call()方法中的代码,
        可以理解为之前的new Thread(FutureTask).start()已经执行了retrun,但retrun出来的数据,
        需要用FutureTask.get()获取。以下代码可以注释掉查看中断主线程的对比结果。
         */
        try {
            System.out.println("从Callable获取到的值是"+task.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        for (int i=0;i<10;i++){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
    static class MyCallable implements Callable<Integer>{
​
        @Override
        public Integer call() throws Exception {
            for (int i=0;i<10;i++){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
            return 100;
        }
    }
}

线程池ExecutorService

概念

开发过程中不可避免创建大量线程,很多线程完成的事情很小用时很短,而如果每一个我们都走创建线程-创建任务-执行任务-关闭线程 这些步骤,我们将消耗大量资源和时间在创建线程和关闭线程上。

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间. 线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。

javaEE后端开发本身就有了多线程池的概念,不需要一一去创建,但学习这个概念是很重要的。

线程池的好处

降低资源消耗。提高响应速度。提高线程的可管理性。

java中四种线程池

缓存型(非定长)线程池

演示代码

public class Demo11 {
    /**
     * 缓存线程池
     * 执行流程:
     * * 1. 判断线程池是否存在空闲线程
     * * 2. 存在则使用
     * * 3. 不存在,则创建线程 并放入线程池, 然后使用
     * @param args
     */
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        //指挥线程池执行新的任务
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"一条大河");
            }
        });
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"波浪宽");
            }
        });
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"风吹稻花上两岸");
            }
        });
        //以上因为排列靠近,执行速度快,从输出的结果看来,线程名都不同,说明往线程池里加了三个线程
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //中断一秒的目的是使得线程池里的线程处于闲置的状态,而后再次执行相同操作可以看出,这次线程名用的是之前创建好的线程
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"风吹稻花上两岸");
            }
        });
    }
}

定长型线程池

演示代码

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
​
public class Demo12 {
    /**
     ** 定长线程池.
     * * (长度是指定的数值)
     * * 执行流程:
     * * 1. 判断线程池是否存在空闲线程
     * * 2. 存在则使用
     * * 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
     * * 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
     */
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        //这里在线程池里放入两个线程,但调用三个线程任务,每个执行3秒,可以知道第三个将需要等待三秒以后可得到运行,因为其需要等待。
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"一条大河");
            }
        });
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"波浪宽");
            }
        });
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"风吹稻花上两岸");
            }
        });
    }
}

单线程线程池

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
​
public class Demo13 {
    /**
     * 单线程线程池.
     * 执行流程:
     * 1. 判断线程池 的那个线程 是否空闲
     * 2. 空闲则使用
     * 4. 不空闲,则等待 池中的单个线程空闲后 使用
     */
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
​
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"一条大河");
            }
        });
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"波浪宽");
            }
        });
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"风吹稻花上两岸");
            }
        });
    }
}

周期性任务定长线程池

package com.test4_5;
​
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
​
public class Demo14 {
    /**
     * 周期任务 定长线程池.
     * 执行流程:
     * 1. 判断线程池是否存在空闲线程
     * 2. 存在则使用
     * 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
     * 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
     *
     * 周期性任务执行时:
     * 定时执行, 当某个时机触发时, 自动执行某任务 .
     */
    public static void main(String[] args) {
        ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
        /**
        * 定时执行
        * 参数1. runnable类型的任务
        * 参数2. 时长数字
        * 参数3. 时长数字的单位
        */
​
        service.schedule(new Runnable() {
        @Override
            public void run() {
            System.out.println("俩人相视一笑~ 嘿嘿嘿");
            }
        },5,TimeUnit.SECONDS);
​
​
        /**
         * 周期执行
         * 参数1. runnable类型的任务
         * 参数2. 时长数字(延迟执行的时长)
         * 参数3. 周期时长(每次执行的间隔时间)
         * 参数4. 时长数字的单位
         */
       service.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
            System.out.println("俩人相视一笑~ 嘿嘿嘿");
            }
        },5,2, TimeUnit.SECONDS);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值