Java并发实战一:线程与线程安全

2021年7月14日 1点热度 0条评论 来源: HITSZ-YC

从零开始创建一家公司

Java并发编程是Java的基础之一,为了能在实践中学习并发编程,我们跟着创建一家公司的旅途,一起来学习Java并发编程。

进程与线程

由于我们的目标是学习并发编程,所以我不会把很多时间放在底层原理和复杂的概念上。操作系统上的进程就像是全国各地的公司,而每个公司又都有许多员工--线程。关于进程与线程的关系先了解这么多。

创建一个线程

想象你现在成立了一个互联网公司,你准备先设立一个总经理的岗位,而你自己是幕后Boss,Main线程。

public class App {
    public static void main(String[] args) throws Exception {
        Thread manager = new Thread(new manager());
        manager.start();        
    }
}
class manager implements Runnable {    
    @Override
    public void run() {
        System.out.println("我是总经理");
    }
}

让我们一步步看上面的代码,首先为了区分公司的员工和其他人,每个员工要有统一的标识,都属于 Thread 类。这个Thread就是我们公司的员工标识,只要是这个类的对象都属于你的公司。

虽然已经有了 Thread 标识,但公司的每个人的职责都不同,所以还要进一步的细分。向Thread构造函数传入不同的实现Runnable接口的对象,可以获得不同职责的员工。

不同员工的职责由不同的 run 方法实现区分。manager.start()方法就是默认调用Runnable接口中的run方法。同时Runnable之所以是个接口,是因为继承只能单一,而接口可以实现多个,就像我们公司的员工,在社会上还可能有其他位置一样。

终止线程

我们公司当然要有下班制度,重新实现如下。

public class App {
    public static void main(String[] args) throws Exception {
        Thread manager = new Thread(new manager());
        manager.start();   
        manager.interrupt();
    }
}
class manager implements Runnable {    
    @Override
    public void run() {
        while(true) {
            if(Thread.currentThread().isInterrupted()) break;
            System.out.println("正在上班中");
        }
} }

可以看到 manager 类中加了个循环表示持续的上班状态。当到下班时间时,你(Main线程)调用经理的 interrupt 方法通知他该下班了。

注意,这里的 interrupt方法不会直接结束上班状态,只是通知。而经理根据自己 run 方法的实现来决定到底怎么下班。

用  Thread.currentThread().isInterrupted() 方法来判断是否收到通知。 Thread.currentThread()代表当前对象,之所以不是当前类,是因为你有时候会想只通知某些特定的员工下班,而不是每次通知都只能让所有员工下班。

还有一个 interrupted 方法,和 isInterrupted 作用相同,都是查询当前状态,不过前一个方法查询的同时会清除状态。如果员工都是收到中断请求就下班,那二者没有什么区别。但对某些需要收到特定次数下班通知才会下班的员工来说,用 interrupted方法就特别合适。

线程休眠

当员工在上班时间却感觉疲倦怎么办,幸好我们有休息制度。

class manager implements Runnable {    
    @Override
    public void run() {
        while(true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                break;
            }
            if(Thread.currentThread().isInterrupted()) break;
            System.out.println("我是总经理");
        }
    }
}

可以看到代码中增加了静态方法 Thread.sleep 和一个异常处理机制。线程会先休眠1秒后在执行下面的步骤。

虽然 sleep 是静态方法,但是只对当前线程其作用。这样设计的原因是为了防止别的线程调用该线程的休眠方法,也就是说,只有当前线程才能控制当前线程的休眠状态。

由于线程休眠过程中无法处理中断,所以当线程休眠时收到中断请求,就会抛出异常,在异常处理中决定如何中断。

总结

关于线程的基本情况基本这么多,可以看到麻雀虽小,五脏俱全。有唯一的标识Thread,可以实现自己的方法,可以响应中断,可以进行休眠等待。一个员工的工作周期已经初步成型,接下来我们看看简单的员工之间的合作。

等待(wait)和通知(notify)

等待与通知这两个方法和前面介绍的最大的不同在于,由于要负责线程之间的协作,这两个方法是属于object的而不是Thread的。

什么意思呢?这使得可以调用  Main 线程中的对象的wait方法来中断 Main线程。

具体过程如下 当一个线程调用等待方法时,它会加入一个等待队列。由于有多个线程可能拥有该对象,当不同线程先后调用这个方法时,都会加入这个对象的等待队列中。

当 notify 方法被某一线程调用时,就会在这个等待队列中随机挑出一个线程唤醒(并不是先到先得)。

要注意的是 使用 wai() 和 notify 的关键字必须要加锁,代码加粗部分

public class App {
    public static void main(String[] args) throws Exception {
        Thread manager = new Thread(new manager());
        manager.start();   
        manager.interrupt();
      synchronized(manager){ manager.notify(); }
    }
}
class manager implements Runnable {    
    @Override
    public void run() {
        while(true) {
            synchronized(this) { try {
this.wait(); Thread.sleep(
1000); } catch (InterruptedException e) { break; } if(Thread.currentThread().isInterrupted()) break; System.out.println("我是总经理"); } } } }

 如果不加锁编译不会报错,但执行会会有 current thread is not owner 异常,意思是当前线程没有获得对象的锁就调用了 wait 方法,notify 的方法同理,也必须要先获取锁才能执行。

过程就是 加锁---- 等待(wait)-----释放锁 -----加锁------通知(notify)------ 继续执行。但通知后不会释放锁,所以调用 wait 方法的线程要先等该线程执行完释放锁后才能继续执行。

至于为什么要加锁呢?如果没有加锁,就会出现丢失唤醒问题,既 notify 方法早于 wait 调用,导致 wait 的线程一直接收不到唤醒信号。

为什么加锁就能避免,难道 notify 线程没有可能先执行吗?其实确实即使加锁后 notify 也可能先于 wait 执行。因为这两个方法是负责线程协作的,所以一般代码逻辑是用户来写出,用户来避免 notify 先于wait执行,但如果没有加锁,即使用户的逻辑正确也可能导致 notify 先于 wait 执行,这也是个并发问题。

等待线程结束(join)和谦让(yeild)

等待结束和谦让是另外的一种线程间协作的方式,上文提到的等待和通知是基于线程内部方法的,而等待结束是等待线程整体的,可以说是线程协作的一种补充。

看个代码

public class App {
    public static void main(String[] args) throws Exception {
        Thread manager = new Thread(new manager());
        manager.start();
        manager.join();
        }
    }
}
class manager implements Runnable {    
    @Override
    public void run() {
           Thread.sleep(1000);           
        }        
    }
}

 

在这个代码中 Main 线程会等待 manger 睡眠结束后才会继续执行,期间一直处于阻塞状态。

还有一点很有趣,join 本质是让当前线程调用该对象的 wait 方法,比如上文代码,本质是 Main 线程调用 managerwait 方法,再此对象上等待,而该被等待的线程结束后,会调用 notifyAll 方法告诉所有被等待的线程结束等待。所以最好不要在线程上调用 wait 方法,因为可能被 joinnotifyAll 意外唤醒。

最后思考一下 jionwait notify 之间的使用场景是很有意思的,wait notify 是 线程调用对象(由于join的存在,这个对象不能是线程!)的方法来进行协作,一个线程调用wait进入阻塞,另一个线程调用notify方法唤醒,一共三个对象(两个线程,一个协作对象)。而 jion 的场景则是 一个线程调用 wait 方法等待一个线程,该线程调用notifyAll 方法唤醒该线程,没有第三者。所以 jion 可以理解为两个线程的互相协作,而 wait notify 是两个线程通过一个对象进行协作,当然只是可以这样理解,具体本质还需要好好在实践生活中使用才能慢慢领会到。

 

    原文作者:HITSZ-YC
    原文地址: https://www.cnblogs.com/hitsz-yc/p/14994941.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系管理员进行删除。