并发编程基础(下)

书接上文。上文主要讲了下线程的基本概念,三种创建线程的方式与区别,还介绍了线程的状态,线程通知和等待,join等,本篇继续介绍并发编程的基础知识。

sleep

当一个执行的线程调用了Thread的sleep方法,调用线程会暂时让出指定时间的执行权,在这期间不参与CPU的调度,不占用CPU,但是不会释放该线程锁持有的监视器锁。指定的时间到了后,该线程会回到就绪的状态,再次等待分配CPU资源,然后再次执行。

我们有时会看到sleep(1),甚至还有sleep(0)这种写法,肯定会觉得非常奇怪,特别是sleep(0),睡0秒钟,有意义吗?其实是有的,sleep(1),sleep(0)的意义就在于告诉操作系统立刻触发一次CPU竞争。

让我们来看看正在sleep的进程被中断了,会发生什么事情:

class MySleepTask implements Runnable{
    @Override
    public void run() {
        System.out.println("MyTask1");
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            System.out.println("中断");
            e.printStackTrace();
        }
        System.out.println("MyTask2");
    }
}

public class Sleep {
    public static void main(String[] args) {
        MySleepTask mySleepTask=new MySleepTask();
        Thread thread=new Thread(mySleepTask);
        thread.start();
        thread.interrupt();
    }
}

运行结果:

MyTask1
中断
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at java.lang.Thread.sleep(Thread.java:340)
    at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
    at com.codebear.MySleepTask.run(Sleep.java:10)
    at java.lang.Thread.run(Thread.java:748)
MyTask2

yield

我们知道线程是以时间片的机制来占用CPU资源并运行的,正常情况下,一个线程只有把分配给自己的时间片用完之后,线程调度器才会进行下一轮的线程调度,当执行了Thread的yield后,就告诉操作系统”我不需要CPU了,你现在就可以进行下一轮的线程调度了 “,但是操作系统可以忽略这个暗示,也有可能下一轮还是把时间片分配给了这个线程。

我们来写一个例子加深下印象:

class MyYieldTask implements Runnable {
    @Override
    public void run() {
        for (int i = 10; i > 0; i--) {
            System.out.println("我是" + Thread.currentThread().getName() + ",我分配到了时间片");
        }
    }
}

public class MyYield {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new MyYieldTask());
        thread1.start();

        Thread thread2 = new Thread(new MyYieldTask());
        thread2.start();
    }
}

运行结果:

并发编程基础(下)

当然由于线程的特性,所以每次运行结果可能都不太相同,但是当我们运行多次后,会发现绝大多数的时候,两个线程的打印都是比较平均的,我用完时间片了,你用,你用完了时间片了,我再用。

当我们调用yield后:

class MyYieldTask implements Runnable {
    @Override
    public void run() {
        for (int i = 10; i > 0; i--) {
            System.out.println("我是" + Thread.currentThread().getName() + ",我分配到了时间片");
            Thread.yield();
        }
    }
}

public class MyYield {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new MyYieldTask());
        thread1.start();

        Thread thread2 = new Thread(new MyYieldTask());
        thread2.start();
    }
}

运行结果:

并发编程基础(下)

当然在一般情况下,可能永远也不会用到yield,但是还是要对这个方法有一定的了解。

sleep 和 yield 区别

当线程调用sleep后,会阻塞当前线程指定的时间,在这段时间内,线程调度器不会调用此线程,当指定的时间结束后,该线程的状态为”就绪”,等待分配CPU资源。
当线程调用yield后,不会阻塞当前线程,只是让出时间片,回到”就绪”的状态,等待分配CPU资源。

死锁

死锁是指多个线程在执行的过程中,因为争夺资源而造成的相互等待的现象,而且无法打破这个”僵局”。

死锁的四个必要条件:

  • 互斥:指线程对于已经获取到的资源进行排他性使用,即该资源只能被一个线程占有,如果还有其他线程也想占有,只能等待,直到占有资源的线程释放该资源。
  • 请求并持有:指一个线程已经占有了一个资源,但是还想占有其他的资源,但是其他资源已经被其他线程占有了,所以当前线程只能等待,等待的同时并不释放自己已经拥有的资源。
  • 不可剥夺:当一个线程获取资源后,不能被其他线程占有,只有在自己使用完毕后自己释放资源。
  • 环路等待:即 T1线程正在等待T2占有的资源,T2线程正在等待T3线程占有的资源,T3线程又在等待T1线程占有的资源。

要想打破”死锁”僵局,只需要破坏以上四个条件中的任意一个,但是程序员可以干预的只有”请求并持有”,”环路等待”两个条件,其余两个条件是锁的特性,程序员是无法干预的。

聪明的你,一定看出来了,所谓”死锁”就是”悲观锁”造成的,相对于”死锁”,还有一个”活锁”,就是”乐观锁”造成的。

守护线程与用户线程

Java中的线程分为两类,分别为 用户线程和守护线程。在JVM启动时,会调用main函数,这个就是用户线程,JVM内部还会启动一些守护线程,比如垃圾回收线程。那么守护线程和用户线程到底有什么区别呢?当最后一个用户线程结束后,JVM就自动退出了,而不管当前是否有守护线程还在运行。
如何创建一个守护线程呢?

public class Daemon {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
        });
        thread.setDaemon(true);
        thread.start();
    }
}

只需要设置线程的daemon为true就可以。
下面来演示下用户线程与守护线程的区别:

public class Daemon {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (true){}
        });

        thread.start();
    }
}

当我们运行后,可以发现程序一直没有退出:

并发编程基础(下)
因为这是用户线程,只要有一个用户线程还没结束,程序就不会退出。

再来看看守护线程:

public class Daemon {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (true){}
        });
        thread.setDaemon(true);
        thread.start();
    }
}

当我们运行后,发现程序立刻就停止了:

并发编程基础(下)
因为这是守护线程,当用户线程结束后,不管有没有守护线程还在运行,程序都会退出。

线程中断

之所以把线程中断放在后面,是因为它是并发编程基础中最难以理解的一个,当然这也与不经常使用有关。现在就让我们好好看看线程中断。
Thread提供了stop方法,用来停止当前线程,但是已经被标记为过期,应该用线程中断方法来代替stop方法。

interrupt

中断线程。当线程A运行(非阻塞)时,线程B可以调用线程A的interrupt方法来设置线程A的中断标记为true,这里要特别注意,调用interrupt方法并不会真的去中断线程,只是设置了中断标记为true,线程A还是活的好好的。如果线程A被阻塞了,比如调用了sleep、wait、join,线程A会在调用这些方法的地方抛出”InterruptedException”。
我们来做个试验,证明下interrupt方法不会中断正在运行的线程:

class InterruptTask implements Runnable {
    @Override
    public void run() {
        CopyOnWriteArrayList copyOnWriteArrayList = new CopyOnWriteArrayList();
        try {
            long start = System.currentTimeMillis();
            for (int i = 0; i < 150000; i++) {
                copyOnWriteArrayList.add(i);
            }
            System.out.println("&#x7ED3;&#x675F;&#x4E86;,&#x65F6;&#x95F4;&#x662F;" + (System.currentTimeMillis() - start));
            System.out.println(Thread.currentThread().isInterrupted());
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

public class InterruptTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new InterruptTask());
        thread1.start();
        thread1.interrupt();
    }
}

运行结果:

&#x7ED3;&#x675F;&#x4E86;,&#x65F6;&#x95F4;&#x662F;7643
true

在子线程中,我们通过一个循环往copyOnWriteArrayList里面添加数据来模拟一个耗时操作。这里要特别要注意,一般来说,我们模拟耗时操作都是用sleep方法,但是这里不能用sleep方法,因为调用sleep方法会让当前线程阻塞,而现在是要让线程处于运行的状态。我们可以很清楚的看到,虽然子线程刚运行,就被interrupt了,但是却没有抛出任何异常,也没有让子线程终止,子线程还是活的好好的,只是最后打印出的”中断标记”为true。

如果没有调用interrupt方法,中断标记为false:

class InterruptTask implements Runnable {
    @Override
    public void run() {
        CopyOnWriteArrayList copyOnWriteArrayList = new CopyOnWriteArrayList();
        try {
            long start = System.currentTimeMillis();
            for (int i = 0; i < 500; i++) {
                copyOnWriteArrayList.add(i);
            }
            System.out.println("&#x7ED3;&#x675F;&#x4E86;,&#x65F6;&#x95F4;&#x662F;" + (System.currentTimeMillis() - start));
            System.out.println(Thread.currentThread().isInterrupted());
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

public class InterruptTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new InterruptTask());
        thread1.start();
    }
}

运行结果:

&#x7ED3;&#x675F;&#x4E86;,&#x65F6;&#x95F4;&#x662F;1
false

在介绍sleep,wait,join方法的时候,大家已经看到了,如果中断调用这些方法而被阻塞的线程会抛出异常,这里就不再演示了,但是还有一点需要注意,当我们catch住InterruptedException异常后,”中断标记”会被重置为false,我们继续做实验:

class InterruptTask implements Runnable {
    @Override
    public void run() {
        CopyOnWriteArrayList copyOnWriteArrayList = new CopyOnWriteArrayList();
        try {
            long start = System.currentTimeMillis();
            TimeUnit.SECONDS.sleep(3);
            System.out.println("&#x7ED3;&#x675F;&#x4E86;,&#x65F6;&#x95F4;&#x662F;" + (System.currentTimeMillis() - start));
        } catch (Exception ex) {
            System.out.println(Thread.currentThread().isInterrupted());
            ex.printStackTrace();
        }
    }
}

public class InterruptTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new InterruptTask());
        thread1.start();
        thread1.interrupt();
    }
}

运行结果:

false
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at java.lang.Thread.sleep(Thread.java:340)
    at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
    at com.codebear.InterruptTask.run(InterruptTest.java:20)
    at java.lang.Thread.run(Thread.java:748)

可以很清楚的看到,”中断标记”被重置为false了。

还有一个问题,大家可以思考下,代码的本意是当前线程被中断后退出死循环,这段代码有问题吗?

Thread th = Thread.currentThread();
while(true) {
  if(th.isInterrupted()) {
    break;
  }

  try {
    Thread.sleep(100);
  }catch (InterruptedException e){
    e.printStackTrace();
  }
}

本题来自 极客时间 王宝令 老师的 《Java并发编程实战》

代码是有问题的,因为catch住异常后,会把”中断标记”重置。如果正好在sleep的时候,线程被中断了,又重置了”中断标记”,那么下一次循环,检测中断标记为false,就无法退出死循环了。

isInterrupted

这个方法在上面已经出现过了,就是 获取对象线程的”中断标记”。

interrupted

获取当前线程的”中断标记”,如果发现当前线程被中断,会重置中断标记为false,该方法是static方法,通过Thread类直接调用。

并发编程基础到这里就结束了,可以看到内容还是相当多的,虽说是基础,但是每一个知识点,如果要深究的话,都可以牵扯到”操作系统”,所以只有深入到了”操作系统”,才可以说真的懂了,现在还是仅仅停留在Java的层面,唉。

Original: https://www.cnblogs.com/CodeBear/p/10817779.html
Author: CodeBear
Title: 并发编程基础(下)

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/572178/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • kubeadm 搭建 K8s

    kubeadm 搭建 K8s 本篇主要记录一下 使用 kubeadm 搭建 k8s 详细过程 ,环境使用 VirtualBox 构建的3台虚拟机 1.环境准备 操作系统:Cento…

    Java 2023年6月9日
    085
  • DDD-CQRS的落地案例

    摘要 在之前的文章DDD-CQRS能解什么问题中,阐述了什么是CQRS。但是并没有业务需求可以应用CQRS。最近需要处理一个文本增量更新的业务,经过需求分析后,尝试使用CQRS来解…

    Java 2023年6月8日
    0104
  • java中的多线程

    多线程基础 进程:进程就是运行中的程序,当被关闭的时候,这段进程也关闭。比如我们玩玩游戏,打开游戏操作系统会为该进程分配一个空间,当退出游戏是,进程也就结束了 线程:线程是由进程创…

    Java 2023年6月6日
    0102
  • 【手把手】光说不练假把式,这篇全链路压测实践探索

    Hello,大家好呀,前两篇文章,我们说了下关于全链路压测的意义、整体架构,以及5种压测的方案。 前面两篇基本都属于比较理论的内容,今天这篇咱们来点实践的东西,手把手带你搞出一个压…

    Java 2023年6月15日
    0105
  • 减少Symantec Endpoint Protection 12误报的方法

    最近安装了Symantec Endpoint Protection 12。结果很多文件都遭了殃。。 VB 写的报毒。C# 写的也不放过。。。 Symantec 向卡巴小红伞360 …

    Java 2023年5月29日
    070
  • SpringCloud01

    SpringCloud01 淘宝架构演进之路 https://mp.weixin.qq.com/s?__biz=MzU0OTk3ODQ3Ng==&mid=224748542…

    Java 2023年6月13日
    077
  • 为什么不建议使用自定义Object作为HashMap的key?

    此前部门内的一个线上系统上线后内存一路飙高、一段时间后直接占满。协助开发人员去分析定位,发现内存中某个Object的量远远超出了预期的范围,很明显出现内存泄漏了。 结合代码分析发现…

    Java 2023年6月7日
    083
  • 设计模式学习笔记(十九)观察者模式及应用场景

    观察者模式(Observer Design Pattern),也叫做发布订阅模式(Publish-Subscribe Design Pattern)、模型-视图(Model-Vie…

    Java 2023年6月6日
    0100
  • K8S 使用deploy部署nginx

    K8S 使用deployment 部署nginx服务 deploy文件如下: [root@k8s-master ~]# cat deploy.yaml apiVersion: ap…

    Java 2023年5月30日
    055
  • HTML页面打印

    <style media=print>.Noprint{display:none;}style> <object id="WebBrowser&q…

    Java 2023年6月16日
    073
  • 校招总结

    第一次出去校招,面试比想象中困难。按理说之前面试经验不少,不该有什么问题,但校招和平时面试确实不一样,平时面试一天就面一两个,时间不是问题,聊一两个小时都可以,校招一天要面十多个,…

    Java 2023年6月16日
    065
  • ThreadLocal 详解

    一、ThreadLocal 简介 ThreadLocal实例通常作为静态的私有的(private static)字段出现在一个类中,这个类用来关联一个线程。ThreadLocal是…

    Java 2023年6月13日
    072
  • AOP spring boot 使用AOP面向切面编程

    关于AOP AOP(Aspect-OrientedProgramming,面向方面编程),可以说是OOP(Object-Oriented Programing,面向对象编程)的补充…

    Java 2023年6月5日
    083
  • 程序员都遇到过哪些误解?

    程序员: 为计算机编写代码的人,按照现代企业研发部的岗位,分为:开发工程师,运维工程师,架构师,数据工程师,算法工程师等; 误解: 即事实是另外一种情况,而因为环境的复杂性或者消息…

    Java 2023年6月8日
    096
  • MySQL JOIN的使用

    JOIN的使用 JOIN 理论 MySQL 七种 JOIN 的 SQL 编写 环境搭建 创建部门表 CREATE TABLE tbl_dept ( id INT NOT NULL …

    Java 2023年6月5日
    088
  • 【Java面试】大厂裁员,小厂倒闭,如何搞定面试官Java SPI是什么?有什么用?

    “Java SPI是什么?有什么用?”这是阿里p6面试过程中,第二面的时候遇到的一个真实的问题。如果你不理解SPI,建议你看完整个视频。大家好,我是Mic,…

    Java 2023年6月16日
    091
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球