并发 - Java并发基础知识

并发 - Java并发基础知识

来看看 Java 的并发

美女朋友给我看了马未都的一段视频, 说的确实不错, 不羡慕谁, 不嘲笑谁, 不嫉妒谁. 只有这样, 才能让自己自由自在的生活. 我细细琢磨了一下, 其实这三句话, 说到底都归结为最后一句话, 也就是不嫉妒谁. 说白了就是为什么别人有的我没有, 我也要有. 这种动机不能说有错, 但实际上很多时候会导致错误的结果. 不管生活怎么艰难, 毕竟都要脚踏实地的走下去. 这个星期女儿重返校园, 然后也收到了世外的录取通知, 生活就是这样一步步的前进, 无所谓好坏, 只有自己的心里怎么看待. 在生活里找点乐趣, 做点开发大概就是乐趣了吧. 最近发现B站CodeSheep连续放了几期服务器配置的视频, 相当不错, 这里整理记录一下, 毕竟天天Java不就是为了写项目么:

  1. 服务器软件大科普!

  2. 建议人手一套:个人专属多节点Linux环境打造,Linux操作系统学习实验环境安装配置视频教程

  3. 一次性备齐服务器常用工具环境和软件设施

昨天(2020年6月16日)把牙齿整了一下, 下星期一三点半去做牙套,就可以弄好一颗牙齿了. 再让医生看看右边上边的牙齿有没有蛀牙. 然后跟着程序羊的视频, 配置好了一台虚拟机. 今天如果有空的话, 看看如何部署前后端分离的项目. 前后大概中断了有一个月的开发, 现在还得继续捡起来, 等把并发也看完, 大概就可以重新再去写点什么玩意了. 经过看了操作系统的并发基本原理实现, 对于锁, 条件变量, 信号量都有了更深的了解, 而且对于阻塞也有了更深的了解. 现在就可以来看一下俺的主力语言:Java里是怎么来用这些并发概念的了.

  1. Java的多线程类 Thread

  2. 终于弄清楚了打断

  3. wait和notify

  4. join等待某个线程执行结束

  5. volatile

Java的多线程类 Thread

这个不是什么新鲜的玩意了. Thread类就是线程类, 然后有静态方法和创建实例. 创建Thread实例既可以直接使用匿名类来覆盖run()方法, 也可以给构造器传递一个实现Runnable接口的对象都行. 启动线程则是调用.start()方法, 相比基础的C语言, 由于Thread可以写在一个类里边, 所以给线程传递变量更方便. Thread真正好用的还是一系列静态方法:

  1. Thread.currentThread(), 获取当前线程的引用

  2. Thread.sleep(), 让当前线程睡眠(阻塞), 这个方法不会交出监视器锁, 也就是不会释放任何竞争资源

  3. Thread.yield(), 交出当前线程控制权, 但不会阻塞该线程, 只是将线程暂停一下. 应该优先使用sleep()方法.

  4. Thread.join(), 调用这个方法的线程会阻塞到参数中的线程运行结束为止.

后边都会逐渐使用到这些方法.

终于弄清楚了打断

有本好书还是不错的, 《Java高并发程序设计》很短的篇幅就讲清楚了线程的打断. 打断我个人理解, 可以认为是给线程传递一个消息, 即设置一个打断变量. 如果线程有逻辑来处理打断变量, 则就可以通过这个来让线程实现协作. 首先是看一下打断相关的方法:

  1. interrupt(), 设置一个线程的打断标志, 是实例方法

  2. isInterrupted(), 检测一个线程的中断标志, 是实例方法

  3. Thread.interrupted(), 检测一个线程的中断标志, 同时清除掉中断标志, 这是个静态方法

这里关键要理解, interrupt()只是设置线程一个标志, 如果线程没有处理这个标志的程序, 那么打断就没有用. 如果想要用打断, 就必须添加处理打断的程序才行, 也就是检测打断标记. 这里比较特殊的是, 打断阻塞的线程(比如sleep), 会抛出一个受检异常InterruptedException, 这个异常在被catch的时候, 会清除打断标志 所以通过打断来控制线程的时候, 除了在线程循环中检测打断标志的时候, 还必须妥善的处理在阻塞的过程中被打断的方法, 一般就是catch异常, 然后重新设置打断标志. 这样就可以利用中断去做一些事情.

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(){
        @Override
        public void run() {
            while (true) {

                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("发现中断标志位");
                    break;
                } else {
                    System.out.println("未发现中断标志位");

                }

                try{
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    //Sleep中被打断, 会抛异常并清除中断标志位
                    System.out.println("Interrupted when sleep");
                    Thread.currentThread().interrupt();
                }

                Thread.yield();
            }
        }
    };

    t1.start();
    Thread.sleep(10000);
    System.out.println("主线程打断T1");
    t1.interrupt();

}

例子里新创建一个线程, 然后进入循环, 每次检测打断标志, 然后睡2秒. 如果睡眠过程被打断, 就会重新设置自己的打断标志, 下一次循环的时候就会退出. 这里如果不是重新设置标志位, 则根本无法通过打断来让t1停止工作. 如果自己要编写打断程序, 就要这么好好设置一下.

wait和notify

这个东西现在也清晰多了, 说白了就是一个条件变量, 只不过这个条件变量从属于一个对象, 所以wait()和notify()是Object的方法, 这样只要选择正确的对象=正确的条件变量, 就可以让一批线程来进行想条件变量一样的等待和唤醒操作. 条件变量必须外边有一个互斥锁, 在Java中, 就体现在wait()和notify()必须先获得一个对象监视器(可以认为就是一个互斥锁), 然后再被执行. 这里用书上的例子来自己改一下, 只要在同一个对象上进行wait()和notify(), 就相当于在同一个条件变量上进行等待. 由于有了条件变量的知识, 可以知道wait()的时候实际上会释放互斥锁, 这样被notify()的线程会先获取锁, 然后再执行操作.

public class WaitAndNotify {

    final static Object object = new Object();

    public static class Thread1 extends Thread {
        @Override
        public void run() {
            synchronized (object) {
                System.out.println(System.currentTimeMillis() + ": Thread1 start!");
                try {
                    System.out.println(System.currentTimeMillis() + ": Thread1 wait for object");
                    object.wait();

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(System.currentTimeMillis() + ": Thread1 ends");
            }
        }
    }

    public static class Thread2 extends Thread {
        @Override
        public void run() {
            synchronized (object) {
                System.out.println(System.currentTimeMillis() + ": Thread2 start! To notifyall");


                object.notifyAll();
                System.out.println(System.currentTimeMillis() + ": Thread2 ends");

                try {
                    Thread.sleep(2000);

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }
    }

    public static void main(String[] args) {
        Thread1 thread1 = new Thread1();
        Thread2 thread2 = new Thread2();

        thread1.start();
        thread2.start();
    }
}

这里的1号线程会先去获得object的锁, 然后在object上进行等待, 这就是java的条件变量的使用方法. 和基础的条件变量是由一个锁和一个条件变量组成是一样的. 2号线程则会先尝试获取object的锁, 然后进行唤醒, 会唤醒在object上等待的T1, 然而T1必须等待T2释放object锁才能继续运行. 运行的结果可能出现两种情况, 一是如下所示:

1592558548766: Thread1 start!
1592558548782: Thread1 wait for object
1592558548782: Thread2 start! To notifyall
1592558548782: Thread2 ends
1592558550789: Thread1 ends

T1先获得object锁, 然后睡眠, 睡眠的时候会自动释放object锁. 之后T2开始运行, 等到T2完全结束之后, T1会从wait()方法中返回, 此时重新持有object锁, 然后就可以继续运行了. 也可能出现T2先拿到锁的情况:

1592558818118: Thread2 start! To notifyall
1592558818133: Thread2 ends
1592558820137: Thread1 start!
1592558820137: Thread1 wait for object

由于此时已经没有可以notify的Thread2, 所以Thread1会一直睡下去, 这个时候程序就会死锁. 同样从上边这个例子可以发现, Thread.sleep()不会释放竞争资源, 只是让线程暂时挂起, 而wait()方法就是真正的等待条件变量, 即会释放锁.

join等待某个线程执行结束

这个在之前操作系统的锁原理中也接触过了, 一个线程如果要等待另外一个线程, 就可以使用pthread_join(线程号). 对于面向对象的Java来说, 在一个线程中获取另外一个线程的引用, 然后调用该线程的.join()方法就可以等待那个线程结束之后再进行操作. 写一个简单的小例子就是:

public class Join {

    public volatile static int i = 0;

    public static class Thread1 extends Thread {
        @Override
        public void run() {
            for (; i < 10000000; i++) {
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {
        Thread1 thread1 = new Thread1();
        thread1.start();
        System.out.println(i);

    }
}

这里主线程会打印i的值, 如果主线程不等待Thread1, 那么在i开始自增不久就会输出i的值, 会是一个很小的值. 但是如果加上join:

public static void main(String[] args) throws InterruptedException {
    Thread1 thread1 = new Thread1();
    thread1.start();
    thread1.join();
    System.out.println(i);

}

则可以保证会打印出10000000. join的本质是让当前线程在要等待的线程上wait(), 所以使用join()需要添加受检异常InterruptedException, 可以参考之前被打断的线程的处理方式. 这个wait()和notify()的对象实际上就是被等待的线程自己, 被等待的线程自己在执行完毕的时候会唤醒所有等待其自身的线程. 可见面向对象就是用面向对象的思想封装了一层多线程, 其本质实际上就是把等待这个线程的所有线程通过一个条件变量来控制.

volatile

volatile实际上做的事情就是让虚拟机强制在每次读取和更新该变量的时候, 都从内存中更新和读取, 而不是通过高速缓存, 以免造成不可见. 所以volatile解决的就是可见性问题, 而不是原子性问题. 如果一个变量确定要被多线程更新和读取, 就将其设置为volatile即可. 目前的知识都是比较基础的Java多线程知识, 下一期补一点很少接触的多线程基础知识.

LICENSED UNDER CC BY-NC-SA 4.0
Comment