Java学习笔记(多线程):基础


1、线程的创建和启动方式

1.1 继承Thread类创建线程类

让一个类继承Thread,并重写其中的run()方法,该run()方法中定义的就是线程需要做的事。所以run()方法是线程执行体。

以上是定义一个线程类,要让线程类执行还需要创建这个类的实例,并执行实例的start()方法启动该线程。定义和启动的示例代码如下。

public class FirstThread extends Thread{
    private int i;
    public void run(){
        for(; i < 20; i++){
            System.out.println(this.getName() + " " + i);
        }
    }
    public static void main(String[] args) {
        for(int j = 0; j < 100; j++){
            System.out.println(Thread.currentThread().getName() + " " + j);
            if(j == 20){
                new FirstThread().start();
                new FirstThread().start();
            }
        }
    }
}

这段代码的输出如下
在这里插入图片描述

这段代码的入口是main函数。main函数开始运行时就启动了一个主线程,从上图中可以看出主线程的名字是main。主线程启动后就开始循环输出j。当j=20时,主线程就会开启两个线程。当然这两个线程开启后不一定会马上开始执行,于是主线程和另外两个线程开始随机执行。

上面的代码中涉及到了两个方法,可能比较常用。

  • Thread.currentThread():这是Thread的静态方法,用来返回当前正在执行的线程对象。
  • getName():该方法是Thread的实例方法,返回该线程的名字。默认情况下,主线程名字是main,其它线程创建顺序不同名字依次是Thread-0,Thread-1…等。

虽然同一个进程中的子线程是可以共享变量的,但是继承于Thread类的线程类A,我们多次调用A的start()方法,即启动了多个线程,这些线程的成员变量不是共享的,每个线程各自拥有属于自己的成员变量。就像上面例子中Thread-1和Thread-0的i不是共享的。

1.2 实现Runnable接口创建线程类

实现Runnable接口,并重写其中的run()方法,该run()方法和继承Thread类中的一样。

创建该类的实例,然后将实例作为参数放到Thread类的构造函数中构造出新的线程,然后调用线程的start()方法启动线程。示例代码如下。

public class FirstThread implements Runnable{
    private int i;
    public void run(){
        for(; i < 20; i++){
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
    public static void main(String[] args) {
        for(int j = 0; j < 100; j++){
            System.out.println(Thread.currentThread().getName() + " " + j);
            if(j == 20){
                FirstThread runnable = new FirstThread();
                new Thread(runnable, "新线程1").start();
                new Thread(runnable, "新线程2").start();
            }
        }
    }
}

代码的执行结果如下
在这里插入图片描述

1.2.1 Runnable和Thread创建线程间的区别

这里有一点是需要注意到的,在通过继承Thread创建线程的例子中,我们通过继承Thread类定义一个线程类A。类A中有一个私有变量i。之后我多次调用A的start()方法,即创建多个线程,并对私有变量i累加并输出。可以发现结果如左图所示,通过类A创建的线程各自拥有自己的私有变量i
而通过实现Runnable接口的线程类创建的线程之间是互相共享实例变量的,所以右图中的输出两个线程的i是连续的。
在这里插入图片描述


1.3使用Callable和Furture创建线程

1.3.1、FutureTask创建线程

Future接口是用于增强Runnable接口的,比如Future中提供了获取run方法返回值的方法(在Future中,run方法就是call方法)。

但Future接口本身无法创建线程,通常我们使用Future的子类FutureTask来创建线程。FutureTask同时实现了RunnableFuture接口,也就是说,我们可以使用FutureTask创建线程(来自Runnable),同时也可以获取线程执行方法的返回值(来自Future)。

FutureTask的构造函数如下

在这里插入图片描述

其中的Callable是个函数式接口,可以看成是Runnable的增强版。Callable提供了一个call()方法作为线程执行体,并且call()方法可以有返回值并且可以抛出异常。call()方法对于Callable而言就是Runnable中的run()方法,call()方法里定义线程的逻辑。

刚刚说call()方法可以有返回值。Callable是个泛型接口,Callable,T就是call()方法的返回值的类型。

下面来看看FutureTask如何构造一个线程。FutureTask实现接口Future。

创建线程之前先定义一个FutureTask实例。

有关Lambda表达式和函数式接口可以看这篇文章https://blog.csdn.net/sinat_38393872/article/details/102664856
FutureTask<Integer> futureTask = new FutureTask((Callable<Integer>) () -> {
    int i = 0;
    for(; i < 100; i++){
        System.out.println(Thread.currentThread().getName() + "的i的值是" + i);
    }
    return i;
});

有了FutureTask实例后,就可以像Runnable那样启动一个线程,所以整个线程的demo如下所示

FutureTask<Integer> task = new FutureTask((Callable<Integer>) () -> {
    int i = 0;
    for(; i < 100; i++){
        System.out.println(Thread.currentThread().getName() + "的i的值是" + i);
    }
    return i;
});
for(int i = 0; i < 100; i++){
    System.out.println(Thread.currentThread().getName() + "的i的值是" + i);
    if(20 == i){
        //新建线程并启动
        new Thread(task, "有返回值的线程").start();
    }
}

执行结果如下图所示
在这里插入图片描述

1.3.2、Future中的方法

Future接口提供了好几个方法来控制它关联的Callable任务,其中就包括获取返回值。

  • V get():返回Callable任务里的call()方法的返回值。调用该方法会导致当前线程阻塞,必须等到该FutureTask对象代表的线程执行结束后才会得到返回值。
  • V get(long time, TimeUnit unit):返回Callable任务里的call()方法的返回值。调用该方法会导致当前线程阻塞,必须等到该FutureTask对象代表的线程执行结束后才会得到返回值。或者当阻塞时间超过了参数指定的时间之后,会直接抛出TimeoutException异常。
  • boolean cancel(boolean mayInterrupt):取消线程的执行。如果参数是true,那即使线程已经开始执行(执行了start()方法),那线程也会被强行中断;如果参数是false,且线程刚新建,还未执行start()方法,线程可以被取消,并且取消后线程再执行start()方法也是无效的。但是如果线程已经执行了start()方法,那就会中断失败。如果线程成功被取消,返回true,否则返回false。
  • boolean isCancelled():如果线程被cancel()方法成功取消掉,该方法返回true。其他方法导致线程终止,该方法返回false。
  • boolean isDone():无论是什么方法,线程被cancel()方法取消也好,正常流程结束也好,发生异常终止线程也好,只要线程任务结束了,该方法就返回true,否则返回false。









2、线程的生命周期

2.1 线程的生命周期

线程的生命周期是:新建、就绪、运行、阻塞、死亡。

2.2 新建和就绪

当new一个线程时,该线程就处于新建状态,和其它普通Java对象一样,Java虚拟机只是给它分配内存并初始化成员变量的值。这时的线程并没有表现出线程的动态特征。

线程对象调用start()方法后,线程就处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器。处于这个状态的线程还没运行,但随时可能进入运行状态,具体情况要看Java虚拟机的调度。

注意是使用start()方法启动线程,而不是run()。而且一旦直接调用了 run()方法,那这个线程对象就只会被看成是普通对象,即使之后再调用start()方法也没用了。

2.3 运行和阻塞

一个CPU在同一时间只能有一个线程处于运行状态。

发生以下情况时,线程会发生阻塞。

  • 线程调用sleep()方法,不会放弃锁。
  • 线程调用了阻塞式的I/O方法,在方法返回之前,线程被阻塞。
  • 线程试图获取一个同步监听器,而这同步监听器正被其它线程锁持有。
  • 线程在等待某个通知(notify)执行了wait()方法,该方法会放弃锁。
  • 程序调用了线程的suspend()方法将线程挂起。但这个方法容易引起死锁,所以应该尽量避免使用该方法。

正在执行的程序被阻塞之后会在何时的时间进入就绪状态。针对上面的几种情况,发生以下的事件会让线程进入就绪状态。

  • 调用sleep()的线程经过了指定时间。
  • 线程调用的阻塞式I/0方法已经返回。
  • 线程成功地获取了试图取得的同步监视器。
  • 线程正在等待某个通知时,其它线程发出了一个通知。
  • 处于挂起状态的线程调用了resume()回复方法。

2.4 线程死亡

当发生下面三种情况时,线程会死亡。

  • run()或call()方法执行完成,线程正常结束。
  • 线程抛出一个为捕获的Exception或Error。
  • 直接调用该线程的stop()方法来结束该线程,但这方法容易引起死锁。

为了测试某个线程是否已经死亡,可以调用线程对象的isAlive()方法,当线程处于就绪,运行,阻塞三种状态时,该方法返回true;当线程处于新建,死亡两种状态时,方法返回false。

对于已死亡的线程和已经启动的线程,若再次调用start()方法,会抛出IllegalThreadStateException异常。






3、控制线程

这一节讲的是如何控制控制线程的状态,比如让某个线程获得更多的执行机会(设置优先级),线程中断以后处理方式等等。

3.1 线程的 join()方法:暂停,让我先行

Thread提供了让一个线程等待另一个线程的方法,join()方法。当一个线程A的执行体中新建了另一个线程B,并启动线程B的start()方法,然后执行线程B的join()方法,那线程A会一直等待线程B执行完毕后,才会开始重新执行未完成的代码。

注意,以上的过程必须是按顺序的,线程B要先执行start()方法后才能执行join()方法。

另外,关于执行体的解释需要详细说一下。这里的执行体在代码中是指两个地方:一是主函数的psvm代码块,二是线程的run()方法里。

第一种是主程序中定义了一个线程A,执行这个线程A的join()方法时,主程序的执行会暂停,一直等到线程A结束后才开始执行。网上的很多例子介绍线程的join()方法时,举的例子都是第一种情况。
但第二种情况很少见,我担心线程A的run中执行线程B的join()方法时,出现的结果不符合预期,于是我做了以下测试。

public class Test {
    public static void main(String[] args) {
        new Thread(() -> {
            for(int i = 0; i < 20; i++){
                System.out.println("子线程A " + i);
                if(i == 10){
                    Thread thread = new Thread(() -> {
                        for(int j = 0; j < 20; j++){
                            System.out.println("子线程B " + j);
                        }
                    }, "子线程B");
                    thread.start();
                    try{
                        thread.join();
                    } catch(Exception e){}
                }
            }
        }, "子线程A").start();
    }
}

输出结果如下,线程B在线程A中执行join()方法后,线程A就立刻暂停。直到线程B执行结束以后,线程A才开始执行。在这里插入图片描述

3.2 线程中断

3.2.1 检测线程中断状态,向线程发起中断

我们可以使用interrupt()方法来向一个线程发出中断请求,这是一个实例方法。该方法执行后,线程中的中断标志位会变为true,表示线程收到了中断请求。

thread1.interrupt(); //thread1线程被中断

我们使用isInterrupted()方法来看看线程是否被中断,这也是一个实例方法true表示被中断,false表示没有被中断。

还有一个静态方法interrupted()可以检测线程是否被中断,返回值和isInterrupted()一样。不同点是,该方法检测的是当前线程的中断状态,并且执行以后会将线程的中断位设为false。

3.2.2 中断的概念

Thread.sleep()的官方文档中可以看到该方法会抛出InterruptedException异常,这里需要介绍线程中断的概念。

线程的中断和操作系统中的中断不是同一个概念,前者收到中断请求后可以不理会中断,而后者必须处理中断。

我们可以使用interrupt()方法来向一个线程发出中断请求,该方法执行后,线程中的中断标志位会变为true。我们使用isInterrupted()方法来看看线程是否被中断。

线程的中断机制可以看成是一个消息提示,中断可以不被理会。比如下面的代码,主线程执行线程1的thread1.interrupt();,thread1的isInterrupted()也返回true,表示收到中断请求了。但是线程1仍然继续执行原有逻辑。

public class InterruptTest {
    private static Integer i = 0;
    public static void main(String[] args) {
        Runnable runnable = new InterruptedRunnable();
        Thread thread1 = new Thread(() -> {
        	while(true) {
            if(!Thread.currentThread().isInterrupted()) {
                System.out.println("线程1正在执行");
            }   else {
                System.out.println("线程1收到中断请求,但是不想理这个中断");
            }
        }
        }, "线程1");
        thread1.start();
        while(true) {
            i++;
            System.out.println("主线程:" + i);
            if(i == 20) {
                thread1.interrupt();
            }
            if(i == 30) {
                return;
            }
        }
    }
}

所以说,线程的中断只是一个消息机制,如果线程逻辑不处理中断请求,那中断不会对线程的状态产生任何影响。

3.3 线程睡眠

sleep()方法可让当前正在执行的线程进入阻塞状态。下面是sleep()方法的文档
在这里插入图片描述

要注意到sleep()是静态方法不是实例方法,所以它的功能是让当前正在执行的方法进入阻塞状态

3.3.1 InterruptedException抛出的时机

当线程的中断位是true时(线程处于中断状态),此时对这个线程sleep()的话,sleep()方法会抛出InterruptedException异常,睡眠失败,并且使线程的中断位变为false(线程不处于中断状态)。

比如下面的代码,线程1一开始在中断状态下执行sleep()就抛出异常,同时清除了中断位,使之后的sleep()都没有抛出异常。

public static void main(String[] args) {
    Runnable runnable = new InterruptedRunnable();
    Thread thread1 = new Thread(() -> {
        while(true) {
            try{
                System.out.println(Thread.currentThread().getName() + "的中断状态:"+ Thread.currentThread().isInterrupted());
                Thread.sleep(99);
            }
            catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + "发生了中断异常");
                System.out.println(Thread.currentThread().getName() + "睡眠失败");
                System.out.println(Thread.currentThread().getName() + "的中断状态:"+ Thread.currentThread().isInterrupted());
            }

        }
    }, "线程1");
    thread1.start();
    thread1.interrupt();
}

执行结果如下

在这里插入图片描述

同样的场景还有线层阻塞,await()wait()方法。当线程中断时,调用这两个方法也会抛出该异常。

3.4 线程让步

yield()和sleep()方法类似,两者都是静态方法,并且都是让现正在运行的线程暂停。不过不同的是,sleep()是让线程进入阻塞状态,而yield()是让线程进入就绪状态。

两者都不会放弃锁的占用

所以执行yield()后,即使当前执行的线程转入就绪状态,但下一个瞬间它还是有可能被选中继续执行。

3.5 改变线程优先级

每个线程具有一定的优先级,优先级高的线程会获得更高的执行机会。

Thread类提供了setPriority(int newPriority)和getPriority()两个实例方法来设置线程的优先级和获取线程的优先级。优先级是int类型,从1到10,数字越大优先级越高。Thread有三个静态变量表示优先级,

  • MAX_PRIORITY:值为10。
  • MIN_PRIORITY:值为1。
  • NORM_PRIORITY:值为5。

当然,不同的操作系统的优先级不一定都是1到10,比如Windows 2000就只提供了7个优先级,所以编程时要避免直接为线程指定数字的优先级,而要用上面三个静态变量确保高移植性。

主线程的优先级默认是5,子线程的优先级默认与父线程的优先级相等,所以平时我们创建的子线程默认优先级为5。


3.6 后台线程

后台线程是用来服务其它线程的,比如JVM就是典型的后台线程。后台线程有个特征:所有的前台线程死亡后,后台线程才会自动死亡。

前台线程创建的线程是前台线程,后台线程创建的线程是后台线程。

主线程默认是前台线程,所以我们平时创建的线程都是前台线程。

将一个线程设置为后台线程的方法是通过线程实例的setDaemon(true)方法。判断一个线程是不是后台线程可用isDaemon()方法,返回true则为后台线程,反之为前台线程。

线程一旦进入就绪状态,即执行start()之后,就不能再更改线程属性,即不能条用setDaemon()方法,否则会引起IllegalThreadStateException异常。所以线程的setDaemon()方法必须在start()方法之前执行。


4、线程的同步

多线程环境下,一个线程的执行随时会被另一个线程打断。一般情况下这没什么,等会再继续执行呗。但是如果是多个线程同时访问操作一个公共资源,那就很可能出现问题。学过操作系统的都知道,多个线程同时读一个资源完全不会有事,但只要涉及到写,那就会出现隐患,无论是同时写还是同时读写。

同步解决的就是多线程访问操作同一公共资源时可能出现的问题。解决方式是加锁,一个线程想要访问操作一个资源时,就必须先取得关于这个资源的锁才能继续。当线程取得锁,完成相关操作后,才会将锁还回去,其他线程才有机会获得锁。

Java中加锁是通过synchronized和lock,下面一一讲解。

4.1 synchronized

synchronized有两种使用方法,一种是同步代码块,另一钟是同步方法,两者的主要区别是加锁的对象不同。

4.1.1 同步代码块

先说说同步代码块,同步代码块的语法是

synchronized(obj){
···
}
其中的obj是线程之间的共享变量,如果这个变量是线程独有的,那加这个锁毫无意义。

这个语法的含义是,在线程执行括号包含的代码之前,必须先获得关于obj这个公共资源的锁后才能执行。而且obj同一时间内只能被一个线程锁住,而且只有这个线程执行完之后才会将锁释放,然后别的线程才能进入这个代码块执行。



4.1.2 同步方法

同步方法的语法是在方法(必须是实例方法),代码如下

public synchronized void test(){
    ...
}

只要将synchronized 放在限定词之后,返回值之前就行了。要记住,同步方法是对this加锁,也就是这个方法的调用者,而不是对这个方法加锁。

一个类A有两个方法,一个加锁一个不加,代码如下

class A{
    public synchronized void a(){...}
    public void b(){...}
}

生成两个线程和A的实例对象,线程1执行实例的a方法,线程2执行实例的b方法。因为a方法加上了锁,所以线程1在执行a方法前,要先取得关于实例对象的锁;而方法b没加锁,所以线程2随时可以执行b方法。

于是写了下面的代码

class A{
    public synchronized void a(){
        int i = 0;
        for (; i < 20; i++){
            System.out.println("方法a:" + i);
        }
    }
    public void b(){
        int i = 0;
        for (; i < 100; i++){
            System.out.println("方法b:" + i);
        }
    }
}

public class Test {
    public static void main(String[] args) throws Exception{
        A a = new A();
        FutureTask<Integer> task1 = new FutureTask<>(() -> {
            //对象a的a方法没加锁,即访问a方法需要获得关于a对象的锁
            a.a();
            return;
        });
        FutureTask<Integer> task2 = new FutureTask<>(() -> {
        	//对象a的a方法没加锁,即访问a方法需要获得关于a对象的锁
            a.a();
            return;
        });
        FutureTask<Integer> task3 = new FutureTask<>(() ->{
        	//对象a的b方法没加锁,即访问b方法不需要获得关于a对象的锁
            a.b();
            return;
        });
        //线程1
        new Thread(task1).start();
        //线程2
        new Thread(task2).start();
        //线程3
        new Thread(task3).start();
    }
}

根据上面的分析,线程1和线程2会争夺锁。因为线程1先得到锁,所以线程2必须等到线程1释放锁以后(上面的例子中线程1释放锁就相当于线程1执行完成),才能有机会获得锁。所以预测线程2要等到线程1执行完成之后才会开始执行a方法。

而线程3调用的b方法不需要获得锁,所以线程3会随机执行。

运行上面的代码,执行结果验证了以上的分析。
在这里插入图片描述

可以看到,线程1和线程2是交替执行的。

但是如果把方法b也加上锁

//其余代码不变
public synchronized void b(){
    int i = 0;
    for (; i < 100; i++){
        System.out.println("方法b:" + i);
    }
}
//其余代码不变

那么执行结果就变成了严格的顺序执行,而两个线程访问的是不同方法,所以同步方法是对this加锁,而不是对方法加锁。
在这里插入图片描述

4.1.3 锁释放的时间

  • 当线程的同步区的代码执行完自动释放锁。
  • 当同步区的代码出现return或break使程序跳出同步区,则锁自动释放。
  • 当同步区的代码出现为处理的Error或Exception导致同步区代码结束时,锁自动释放。
  • 当同步区代码执行时,出现线程的wait()操作,则线程被暂停并释放锁

要注意,以下的操作并不会释放锁。

  • 程序调用了Thread.sleep()和Thread.yield()使线程暂停时,线程不会释放锁
  • 程序调用suspend()方法时,线程也不会释放锁。

4.2 Lock

Lock是一个更强大的同步机制,它的锁样式较多,甚至可允许一定程度的并发操作。synchronized就比较暴力,原本多线程对同一资源只读不写是完全没问题的,但是synchronized的锁不允许这种情况出现,而Lock则可以。

Lock是一个接口,他有很多实现类,这些实现类分别对应着不同的锁策略,比如ReentrantLock。首先,要先知道Lock锁的对象是谁。

4.2.1 Lock锁锁住的对象

Lock的使用方法是创建一个Lock对象,然后执行lock()或者tryLock()方法加锁,然后使用unLock()方法解除锁。示例如下。

Lock lock = new ReentrantLock();
lock.lock();
try{
	···
}
finally {
	lock.unLock();
}

Lock锁的对象是自己,当lock()执行时,当前线程会获得这个关于这个lock对象的锁。比如上面的示例中,线程1执行上面的代码(假设成功获得锁),那线程1就拥有关于lock对象的锁。在线程1未释放关于lock对象的锁之前,如果其它线程尝试执行lock.lock()(该lock和线程1持有的lock是同一个),那这些线程会进入阻塞,直到重新获得lock锁。

加锁后,在保护区的代码往往会用try包住,最后再finally中释放锁。这是为了防止保护区出现异常,直接使线程终止,从而没有执行到后面的lock.unLock()方法,导致线程一直占用锁让系统发生死锁。




5、线程通信

线程的执行虽然是由系统来决定怎么调度,但我们还是有一些权利来影响系统的决策。如何影响就是线程通信。

5.1 传统的线程通信

传统的线程通信是通过Object类提供的wait()、notify()、notifyAll()三个方法。上面提到synchronized有两种使用方法,一种是同步代码块,另一种是同步方法。这两者加锁的对象分别是synchronized括号里的对象和this。

我们就调用被锁住的对象的wait()、notify()、notifyAll()三个方法即可实现线程通信。

这三个方法的效果如下

5.1.1 wait方法

使占用该锁的线程等待,等待会让该线程放弃对锁的占用,并且等待(也就是阻塞)不是就绪状态,线程在唤醒之前都不会再次占用锁。

这里线程进入的是锁对象的等待空间中。

其它线程调用该锁的notify()方法或者notifyAll()通知唤醒该线程。该方法有两种重载形式:不带参数,线程无线等待;带参数的,有毫秒和毫微秒级别的参数。

  • notify():唤醒在这个锁上处于等待状态的一个线程。如果有多个线程,那是选择任意一个线程唤醒。
  • nofityAll():唤醒在这个锁上等待的所有线程。

5.1.2 代码调用的位置

上面的三个方法只能在sychronized的同步区代码调用。

5.2 使用Condition控制线程通信

如果系统是通过Lock来控制线程同步的,那才可以使用Condition。在某些时候,线程需要满足一定的条件才可以把任务完成。同时我们希望如果尚不满足条件,那就让线程一直等待。直到条件满足线程再去完成任务。并且在线程等待的过程中,我们不想让线程一直占用锁。这时候就可以使用Condition来控制线程通信,告诉线程什么时候等待(同时释放锁),什么时候重新执行代码。

Condition必须依附于某个Lock,可以通过Lock的实例方法newCondition()获得Condition实例。如上所述,Condition就是用来控制线程通信,能做到的事就两个,让线程阻塞,让线程重新回到就绪状态。

5.2.1 Condition的用法

Condition有三个方法来控制线程。

5.2.1.1 await()方法

使当前线程阻塞,阻塞的线程会暂时放弃锁的权限。这个放弃锁的权限与unLock()不一样。unLock()是对Lock对象加层锁,ReentrantLock是可以连续多次lock()的。只是unLock(),那线程还有可能继续占有这个锁,但是await()会让线层直接放弃锁的占用。

尽管await()会让线程放弃对锁的占用,但是很多关于锁的信息并没有被删除。比如在线程1阻塞前,线程1对lock的lock.getHoldCount()等于3。等线程1被唤醒后,lock.getHoldCount()还是等于3。

某个锁的await()使线程阻塞以后,线程是不会主动苏醒的。直到其它线程执行这个锁的signal()或者signalAll()之后,才能将锁唤醒。

await()的设计就是为了让线程满足一定条件后才执行后面的代码,所以使用时往往会使用while来一直检验条件,直到通过。

while(如果条件不满足) {
	lock.await();
}


5.2.1.2 signal()方法

随机唤醒在某个锁上被阻塞的线程。

使用时要注意,唤醒的只是一个随机的线程。如果这个线程不执行唤醒操作,那在这个锁上被阻塞的其他线程就有可能永远被阻塞,造成死锁。

5.2.1.3 signalAll()方法

随机唤醒在某个锁上被阻塞的所有线程。

5.3 阻塞队列控制线程通信

Java5提供了一个BlockingQueue接口,虽然该接口是Queue的子接口,但它的主要作用并不是作为一个容器,而是一个线程通信的工具。

阻塞队列有这样一个特征:当生产者试着想阻塞队列中放入元素时,如果队列已满,则生产者线程阻塞;当消费者试图从阻塞队列中获取元素时,如果队列为空,则消费者线程阻塞。

放入元素和获取元素分别对应着以下几个方法

  • 在队列尾部插入元素:add(E e),offer(E e)和put(E e)。正常情况下,这三个方法效果相同。但队列已满时,这三个方法分别会抛出异常,返回false,阻塞队列。
  • 在队列头部取出元素:remove(),poll()和take(),正常情况下,这三个方法效果相同。但队列为空时,这三个方法分别会抛出异常,返回false,阻塞队列。

BlockingQueue以及其实现类如下图所示
在这里插入图片描述

  • ArrayBlockingQueue:基于数组实现的BlockingQueue队列。
  • LinkedBlockingQueue:基于链表实现的BlockingQueue队列。
  • PriorityBlockingQueue:这并不是个标准的阻塞队列。和PriorityQueue类似,该队列调用remove()、poll()和take()方法来取出元素时,并不是取出队列中存在时间最长的元素,而是队列中值最小的元素。对于大小的判定是根据元素实现的Conparable接口来判断。
  • SynchronousQueue:同步队列。该队列的存取操作必须交替执行。
  • DelayQueue:他是特殊的BlockingQueue,底层基于PriorityBlockingQueue实现。但是DelayQueue要求集合元素都实现Delay接口(接口里有一个long getDelay()方法),DelayQueue根据集合元素的getDelay()方法的返回值进行排序。

5.4 线程组和未处理的异常

Java可以以组为单位来管理线程,使用ThreadGroup。我们直接对ThreadGroup进行操作,就相当于对属于这个组的所有线程进行相同的操作。

我们创建的线程其实都属于某一个线程组,及时我们没有显式指定。默认情况下,子线程属于父线程的组,这个逻辑和设置后台线程的逻辑差不多。

显式设置线程所属的组只能通过线程的构造函数,所以,线程一旦被创建,那线程所属的组就永远不能被改变。下面是唯一的三个为线程指定线程组的方法。

  • Thread(ThreadGroup group, Runnable target):以target的run()方法作为线程执行体创建新线程,属于group线程组。
  • Thread(ThreadGroup group, Runnable target, String name):以target的run()方法作为线程执行体创建新线程,属于group线程组,线程名为name。
  • Thread(ThreadGroup group, String name):创建新线程,线程名为name,属于group线程组。

我们可以通过getThreadGroup()来获取线程所属的线程组。

线程组有两个构造器,ThreadGroup(String name)和ThreadGroup(ThreadGroup parentGroup, String name)。这第一个构造器很容易理解,第二个构造器是创建一个输入parentGroup的子线程组,子线程组的名字为name。

线程组创建后不允许改名,只能通过getName()获取名字。

线程组有以下几个常用方法

  • int activeGroup():返回此线程组中活动线程的数目。
  • interrupt():中断该线程中的所有线程。
  • isDaemo():判断该线程组是否是后台线程组。
  • setDaemon(boolean daemon):把该线程组设置成后台线程组,当后台线程组的最后一个线程执行结束或被销毁后,后台线程自动销毁。
    setMaxPriority(int pri):设置线程组的最高优先级。

5.4.1、使用UncaughtExceptionHandler捕获线程的异常

当创建一个线程以后,这个新创建的线程在执行过程中有可能发生异常。这种异常传统的try-catch无法捕捉到。比如下面的代码无法捕捉到线程1向上抛出的异常。

try{
    Thread arrayException = new Thread(() -> {
        int[] buffer = new int[3];
        for (int i = 0; i < 5; i++) {
            buffer[i] = 0;
        }
    }, "线程1");
    arrayException.start();
} catch (Exception e) {
    System.out.println("捕捉到执行过程中的异常");
}

try-catch只是捕捉执行的代码,上面lambda只是定义了线程1的执行,start()也只是启动线程,他们都没有去执行for循环。那么只要定义执行没有语法错误,线程的启动没有发生异常,那上面的代码就不会捕捉到任何异常。

想要捕捉到线程1执行时抛出的异常就要利用UncaughtExceptionHandler

我们可以实现UncaughtExceptionHandler中的uncaughtException(Thread t, Throwable e)方法,在该方法中定义如何处理异常。其中,抛出异常的线程,e是线程抛出的异常。之后在线程启动前使用setUncaughtExceptionHandler(UncaughtExceptionHandler eh) 设置属于自己的异常处理器,当这个线程执行发生异常时就会异常处理器捕捉处理

下面看使用示例。

//定义线程
public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        if(e instanceof ArrayIndexOutOfBoundsException) {
            System.out.println("捕获了" + t.getName() + "的数组越界异常");
        }
        if(e instanceof IllegalArgumentException) {
            System.out.println("捕获了" + t.getName() + "的参数错误异常");
        }
    }
}

之后定义线程然后抛出异常,查看异常处理器是否捕捉到并处理。

ExceptionHandler exceptionHandler = new ExceptionHandler();
//数组越界异常捕获
Thread arrayException = new Thread(() -> {
    int[] buffer = new int[3];
    for (int i = 0; i < 5; i++) {
        buffer[i] = 0;
    }
}, "线程1");
//参数错误异常捕获
Thread argumentException = new Thread(() -> {
    throw new IllegalArgumentException();
}, "线程2");

arrayException.setUncaughtExceptionHandler(exceptionHandler);
argumentException.setUncaughtExceptionHandler(exceptionHandler);

arrayException.start();
argumentException.start();


最后输出如下,成功捕获异常并处理。

在这里插入图片描述

不过,我们也只能捕捉到unchecked异常,checked异常要求方法写throws语句,但是线程的建立都要通过继承或者实现,无法自主在run()方法上加上throws语句。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面是Java线程编程学习笔记之十二:生产者—消费者模型的相关内容和代码。 ## 生产者—消费者模型简介 生产者—消费者模型是一种常见的多线程并发模型,它涉及到两个角色:生产者和消费者。生产者负责生产数据,消费者负责消费数据。生产者和消费者通过一个共享的缓冲区进行通信,生产者将数据放入缓冲区,消费者从缓冲区获取数据。 在多线程编程中,生产者—消费者模型的实现有多种方式,本文将介绍一种基于Java的实现方式。 ## 生产者—消费者模型的实现 ### 1. 定义共享缓冲区 共享缓冲区是生产者和消费者进行通信的桥梁,它需要实现以下功能: - 提供一个put方法,允许生产者将数据放入缓冲区; - 提供一个take方法,允许消费者从缓冲区获取数据; - 当缓冲区已满时,put方法应该等待; - 当缓冲区为空时,take方法应该等待。 以下是一个简单的共享缓冲区的实现: ```java public class Buffer { private int[] data; private int size; private int count; private int putIndex; private int takeIndex; public Buffer(int size) { this.data = new int[size]; this.size = size; this.count = 0; this.putIndex = 0; this.takeIndex = 0; } public synchronized void put(int value) throws InterruptedException { while (count == size) { wait(); } data[putIndex] = value; putIndex = (putIndex + 1) % size; count++; notifyAll(); } public synchronized int take() throws InterruptedException { while (count == 0) { wait(); } int value = data[takeIndex]; takeIndex = (takeIndex + 1) % size; count--; notifyAll(); return value; } } ``` 上面的Buffer类使用一个数组来表示缓冲区,size表示缓冲区的大小,count表示当前缓冲区中的元素数量,putIndex和takeIndex分别表示下一个可写和可读的位置。put和take方法都是同步方法,使用wait和notifyAll来进行线程间的等待和通知。 ### 2. 定义生产者和消费者 生产者和消费者都需要访问共享缓冲区,因此它们都需要接收一个Buffer对象作为参数。以下是生产者和消费者的简单实现: ```java public class Producer implements Runnable { private Buffer buffer; public Producer(Buffer buffer) { this.buffer = buffer; } public void run() { try { for (int i = 0; i < 10; i++) { buffer.put(i); System.out.println("Produced: " + i); Thread.sleep((int)(Math.random() * 1000)); } } catch (InterruptedException e) { e.printStackTrace(); } } } public class Consumer implements Runnable { private Buffer buffer; public Consumer(Buffer buffer) { this.buffer = buffer; } public void run() { try { for (int i = 0; i < 10; i++) { int value = buffer.take(); System.out.println("Consumed: " + value); Thread.sleep((int)(Math.random() * 1000)); } } catch (InterruptedException e) { e.printStackTrace(); } } } ``` 生产者在一个循环中不断地向缓冲区中放入数据,消费者也在一个循环中不断地从缓冲区中获取数据。注意,当缓冲区已满时,生产者会进入等待状态;当缓冲区为空时,消费者会进入等待状态。 ### 3. 测试 最后,我们可以使用下面的代码来进行测试: ```java public class Main { public static void main(String[] args) { Buffer buffer = new Buffer(5); Producer producer = new Producer(buffer); Consumer consumer = new Consumer(buffer); Thread producerThread = new Thread(producer); Thread consumerThread = new Thread(consumer); producerThread.start(); consumerThread.start(); } } ``` 在上面的代码中,我们创建了一个缓冲区对象和一个生产者对象和一个消费者对象,然后将它们分别传递给两个线程,并启动这两个线程。 运行上面的代码,我们可以看到生产者和消费者交替地进行操作,生产者不断地向缓冲区中放入数据,消费者不断地从缓冲区中获取数据。如果缓冲区已满或者为空,生产者和消费者会进入等待状态,直到缓冲区中有足够的空间或者有新的数据可用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值