并发编程-彻底搞懂线程相关知识

153 阅读24分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情


1 进程和线程的基础概念

1.1 什么是进程和线程

进程是程序运行资源分配的最小单位

进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、 磁盘IO等,同一进程中的多条线程共享该进程中的全部系统资源,而进程和进程之间是相互独立的。进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。

进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。显然,程序是死的、静态的,进程是活的、动态的。进程可以分为系统进程和用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们就是处于运行状态下的操作系统本身,用户进程就是所有由你启动的进程。

线程是CPU调度的最小单位,必须依赖于进程而存在

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的、 能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

线程无处不在

任何一个程序都必须要创建线程,特别是Java不管任何程序都必须启动一个main函数的主线程了;JavaWeb开发里面的定时任务、定时器、JSP和Servlet、异步消息处理机制,远程访问接口RM等,任何一个监听事件, onclick的触发事件等都离不开线程和并发的知识。

1.2 CPU核心数和线程数的关系

多核心:也指单芯片多处理器(Chip Multiprocessors,简称 CMP),CMP 是由美国斯坦福大学提出的,其思想是将大规模并行处理器中的SMP(对称多处理器)集成 到同一芯片内,各个处理器并行执行不同的进程。这种依靠多个CPU同时并行地运行程序是实现超高速计算的一个重要方向,称为并行处理。

多线程:Simultaneous Multithreading,简称SMT。让同一个处理器上的多个线程同步执行并共享处理器的执行资源。

核心数、线程数:目前主流CPU都是多核的。增加核心数目就是为了增加线程数,因为操作系统是通过线程来执行任务的,一般情况下它们是1:1对应关系,也就是说四核CPU一般拥有四个线程。但Intel引入超线程技术后,使核心数与线程数形成1:2的关系。

1.3 CPU时间片轮转机制

我们平时在开发的时候,感觉并没有受CPU核心数的限制,想启动线程就启 动线程,哪怕是在单核 CPU 上也可以随心所欲,这是因为操作系统提供了一种CPU时间片轮转机制。

时间片轮转调度是一种最古老、最简单、最公平且使用最广的算法,又称RR调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。

CPU时间片轮转机制原理如下:

如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。 如果进程在时间片结束前阻塞或结来,则CPU当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾。

时间片轮转调度中唯一有趣的一点是时间片的长度。从一个进程切换到另一个进程是需要定时间的,包括保存和装入寄存器值及内存映像,更新各种表格和队列等。假如进程切( processwitch),有时称为上下文切换(context switch),需要5ms,再假设时间片设为20ms,则在做完20ms有用的工作之后,CPU 将花费5ms来进行进程切换。CPU时间的20%被浪费在了管理开销上了。

为了提高CPU效率,我们可以将时间片设为5000ms。这时浪费的时间只有0.1%。但考虑到在一个分时系统中,如果有10个交互用户几乎同时按下回车键,将发生什么情况?假设所有其他进程都用足它们的时间片的话,最后一个不幸的进程不得不等待5s才获得运行机会。多数用户无法忍受一条简短命令要5s才能做出响应,同样的问题在一台支持多道程序的个人计算机上也会发生。

结论可以归结如下:

时间片设得太短会导致过多的进程切换,降低了CPU效率: 而设得太长又可能引起对短的交互请求的响应变差。将时间片设为100ms通常是一个比较合理的折衷。

1.4 并发和并行

举个例子,如果有条高速公路A上面并排有8条车道,那么最大的并行车辆就是8辆此条高速公路A同时并排行走的车辆小于等于8辆的时候,车辆就可以并行运行。CPU也是这个原理,一个CPU相当于一个高速公路A,核心数或者线程数就相当于并排可以通行的车道;而多CPU就相当于并排有多条高速公路,而每个高速公路并排有多个车道。

当谈论并发的时候一定要加个单位时间,也就是说单位时间内并发量是多少? 离开了单位时间其实是没有意义的。

俗话说,一心不能二用,这对计算机也一样,原则上一个 CPU 只能分配给一个 进程,以便运行这个进程。我们通常使用的计算机中只有一个CPU,也就是说只有一颗心,要让它一心多用同时运行多个进程,就必须使用并发技术。实现并发技术相当复杂,最容易理解的是“时间片轮转进程调度算法”。

综合来说:

并发:指应用能够交替执行不同的任务,比如单CPU核心下执行多线程并非是同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉到的速度不断去切换这两个任务,已达到"同时执行效果",其实并不是的,只是计算机的速度太快,我们无法察觉到而已。

并行:指应用能够同时执行不同的任务,例如吃饭的时候可以边吃饭边打电话,这两件事情可以同时执行。

两者区别:一个是交替执行,一个是同时执行。

1.5 高并发编程的意义、好处和要注意的问题

由于多核多线程的CPU的诞生,多线程、高并发的编程越来越受重视和关注。多线程可以给程序带来如下好处。

(1)充分利用 CPU 的资源

从上面的CPU的介绍,可以看的出来,现在市面上没有CPU的内核不使用多线程并发机制的,特别是服务器还不止一个CPU,如果还是使用单线程的技术做思路,明显就out了。因为程序的基本调度单元是线程,并且一个线程也只能在一个CPU的一个核的一个线程跑,如果你是个i3的CPU的话,最差也是双核心4线程的运算能力;如果是一个线程的程序的话,那是要浪费3/4的CPU性能;如果设计一个多线程的程序的话,那它就可以同时在多个CPU的多个核的多个线程上跑,可以充分地 利用CPU,减少CPU的空闲时间,发挥它的运算能力,提高并发量。

(2)加快响应用户的时间

比如我们经常用的迅雷下载,都喜欢多开几个线程去下载,因为多个线程下载快。

我们在做程序开发的时候更应该如此,特别是我们做互联网项目,网页的响应 时间若提升1s,如果流量大的话,就能增加不少转换量。做过高性能web前端调优的时候,要将静态资源地址用两三个子域名去加载,因为每多一个子域名,浏览器在加载你的页面的时候就会多开几个线程去加载你的页面资源,提升网站的响应速度。多线程、高并发无处不在。

(3)可以使你的代码模块化,异步化,简单化

例如我们实现电商系统,下订单和给用户发送短信、邮件就可以进行拆分, 将给用户发送短信、邮件这两个步骤独立为单独的模块,并交给其他线程去执行。 这样既增加了异步的操作,提升了系统性能,又使程序模块化,清晰化和简单化。

1.6 多线程程序需要注意的问题

(1)线程之间的安全性

从前面的章节中我们都知道,在同一个进程里面的多线程是资源共享的,也就是都可以访问同一个内存地址当中的一个变量。例如:若每个线程中对全局变量、 静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

(2)线程之间的死锁

为了解决线程之间的安全性引入了Java的锁机制,而一不小心就会产生Java线程死锁的多线程问题,因为不同的线程都在等待那些根本不可能被释放的锁,从而导致所有的工作都无法完成。假设有两个线程,分别代表两个饥饿的人,他们必须共享刀叉并轮流吃饭。他们都需要获得两个锁:共享刀和共享叉的锁。

假如线程A获得了刀,而线程B获得了叉。线程A就会进入阻塞状态来等待获得叉,而线程B则阻塞来等待线程A所拥有的刀。这只是人为设计的例子,但尽管在运行时很难探测到,这类情况却时常发生。

(3)线程太多了会将服务器资源耗尽形成死机当机

线程数太多有可能造成系统创建大量线程而导致消耗完系统内存以及CPU的“过渡切换”,造成系统的死机,那么我们该如何解决这类问题呢?

某些系统资源是有限的,如文件描述符。多线程程序可能耗尽资源,因为每个线程都可能希望有一个这样的资源。如果线程数相当大,或者某个资源的侯选线程数远远超过了可用的资源数则最好使用资源池。一个最好的示例是数据库连接池。只要线程需要使用一个数据库连接,它就从池中取出一个,使用以后再将它返回池中。资源池也称为资源库。

2 Java里的线程

2.1 Java程序天生就是多线程的

一个Java程序从main()方法开始执行,然后按照既定的代码逻辑执行,看 似没有其他线程参与,但实际上Java程序天生就是多线程程序,因为执行main()方法的是一个名称为main的线程。

  • [6] Monitor Ctrl-Break //监控 Ctrl-Break 中断信号的
  • [5] Attach Listener //内存 dump,线程 dump,类信息统计,获取系统属性等
  • [4] Signal Dispatcher // 分发处理发送给 JVM 信号的线程
  • [3] Finalizer // 调用对象 finalize 方法的线程
  • [2] Reference Handler//清除 Reference 的线程
  • [1] main //main 线程,用户程序入口

2.2 线程的启动与终止

2.2.1 线程启动

线程的启动方式有2种:

1、继承Thread,然后调用start

private static class UseThread extends Thread {
        @Override
        public void run() {
            super.run();
            //do your work
            System.out.println("This is a new thread extends Thread");
        }
    }

UseThread useThread = new UseThread();
useThread.start();
//useThread.start(); 多次启动同一个线程会报错,IllegalThreadStateException

一个线程不能重复启动,多次启动会报错。

if (started)
            throw new IllegalThreadStateException();

2、实现Runnable,然后交给Thread运行。

private static class UseRunnable implements Runnable {
        @Override
        public void run() {
            //do your work
            System.out.println("This is a new thread implements Runnable");
        }
    }

UseRunnable useRunnable = new UseRunnable();
new Thread(useRunnable).start();

ThreadRunnable的区别:

Thread才是Java里对线程的唯一抽象,Runnable只是对任务(业务逻辑)的抽象。Thread可以接受任意一个Runnable的实例并执行。

2.2.2 线程终止

线程自然终止

要么是 run 执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。

stop

暂停、恢复和停止操作对应在线程Thread的API就是suspend()resume()stop()。但是这些 API是过期的,也就是不建议使用的。不建议使用的原因主要有:以suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。正因为suspend()resume()stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方法。

interrupt

安全的中止则是其他线程通过调用某个线程A的interrupt()方法对其进行中 断操作, 中断好比其他线程对该线程打了个招呼,“A,你要中断了”,不代表线程A会立即停止自己的工作,同样的A线程完全可以不理会这种中断请求。 因为Java里的线程是协作式的,不是抢占式的。线程通过检查自身的中断标志 位是否被置为true来进行响应。

线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法 Thread.interrupted()来进行判断当前线程是否被中断,不过Thread.interrupted()会同时将中断标识位改写为false。

如果一个线程处于了阻塞状态(如线程调用了thread.sleepthread.jointhread.wait 等),则在线程在检查中断标示时如果发现中断标示为true,则会在这些阻塞方法调用处抛出InterruptedException异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为false。

不建议自定义一个取消标志位来中止线程的运行。因为run方法里有阻塞调 用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取消标志。这种情况下,使用中断会更好,因为:

  • 一、一般的阻塞方法,如sleep等本身就支持中断的检查。

  • 二、检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可 以避免声明取消标志位,减少资源的消耗。

注意:处于死锁状态的线程无法被中断。

/**
 * @Description: 安全中断线程 使用Thread
 * @CreateDate: 2022/2/22 9:41 上午
 */
public class EndThread {

    private static class UseThread extends Thread {

        public UseThread(String name) {
            super(name);
        }

        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName + " interrupt flag = " + isInterrupted());
            while (!isInterrupted()) {//线程安全中断,isInterrupted()置为true
//            while (!Thread.interrupted()) {//线程安全中断,但是isInterrupted()会被重置为false
//            while (true) {//线程不会理会interrupt,会继续一直运行
                System.out.println(threadName + " is running");
                System.out.println(threadName + " inner interrupt flag = " + isInterrupted());
            }
//            System.out.println(threadName + " interrupt flag = " + isInterrupted());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread endThread = new UseThread("endThread");
        endThread.start();
        Thread.sleep(20);
        endThread.interrupt();//中断线程,其实设置线程的表示为为true
    }
}
/**
 * @Description: 安全中断线程 实现Runnable接口
 * @CreateDate: 2022/2/22 9:41 上午
 */
public class EndRunnable {

    private static class UseRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " interrupt flag is = " + Thread.currentThread().isInterrupted());
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println(Thread.currentThread().getName() + " is running");
            }
            System.out.println(Thread.currentThread().getName() + " interrupt flag is = " + Thread.currentThread().isInterrupted());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        UseRunnable useRunnable = new UseRunnable();
        Thread thread = new Thread(useRunnable, "endThread");
        thread.start();
        Thread.sleep(20);
        thread.interrupt();
    }
}
/**
 * @Description: 阻塞方法中抛出InterruptedException异常后,如果需要继续中断,需要手动再中断一次
 * @CreateDate: 2022/2/22 10:37 上午
 */
public class HandInterrupt {

    private static class UseThread extends Thread {
        public UseThread(String name) {
            super(name);
        }

        @Override
        public void run() {
            while (!isInterrupted()) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    System.out.println(Thread.currentThread().getName() + " in InterruptedException interrupt flag is " + isInterrupted());
                    interrupt();//释放资源
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " is running");
            }
            System.out.println(Thread.currentThread().getName() + " interrupt flag is " + isInterrupted());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread endThread = new UseThread("endThread");
        endThread.start();
        Thread.sleep(500);
        endThread.interrupt();
    }
}

3 深入理解Java里的线程

3.1 深入理解run()和start()

Thread类是Java里对线程概念的抽象,可以这样理解:我们通过new Thread()其实只是new出一个Thread的实例,还没有操作系统中真正的线程挂起钩来。 只有执行了start()方法后,才实现了真正意义上的启动线程。

start()方法让一个线程进入就绪队列等待分配CPU,分到CPU后才调用实现的run()方法,start()方法不能重复调用,如果重复调用会抛出异常。

run()方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方法并没有任何区别,可以重复执行,也可以被单独调用。

/**
 * @Description: Start和run的区别
 * @CreateDate: 2022/2/22 10:58 上午
 */
public class StartAndRun {

    private static class ThreadRun extends Thread {
        @Override
        public void run() {
            int i = 90;
            while (i > 0) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {

                }
                System.out.println(Thread.currentThread().getName() + " is running and the i = " + i--);
            }
        }
    }

    public static void main(String[] args) {
        ThreadRun threadRun = new ThreadRun();
        threadRun.setName("threadRun");
//        threadRun.run();//Thread.currentThread().getName()=main 表示并没有使用threadRun子线程,只是执行了业务逻辑
        threadRun.start();//hread.currentThread().getName()=threadRun 表示正常使用了threadRun子线程去执行任务
    }
}

3.2 其他的线程相关方法

yield()方法:使当前线程让出CPU占有权,但让出的时间是不可设定的。也不会释放锁资源。注意:并不是每个线程都需要这个锁的,而且执行yield()的线程不一定就会持有锁,我们完全可以在释放锁后再调用yield()方法。

所有执行yield()的线程有可能在进入到就绪状态后会被操作系统再次选中马上又被执行。

3.3 join()方法

把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。 比如在线程B中调用了线程A 的Join()方法,直到线程A执行完毕后,才会继续 执行线程B。(此处为常见面试考点)

示例: 我排队做核酸,然后我喜欢的女孩过来做核酸,我让她在我前面插个队,过了一会女孩的妈妈也来了,又让女孩的妈妈在女孩前面插队。最终结果就是我最早排队,结果最后做完核酸。

/**
 * @Description: join()的使用
 * 示例:我排队做核酸,然后我喜欢的女孩过来做核酸,我让她在我前面插个队,过了一会女孩的妈妈也来了,又让女孩的妈妈在女孩前面插队。最终结果就是我最早排队,结果最后做完核酸
 * @CreateDate: 2022/2/22 11:38 上午
 */
public class UseJoin {

    private static class PrettyGirl implements Runnable {

        private Thread thread;

        public PrettyGirl(Thread thread) {
            this.thread = thread;
        }

        public PrettyGirl() {
        }

        @Override
        public void run() {
            System.out.println("PrettyGirl开始排队做核酸...");
            try {
                if (thread != null) {
                    thread.join();//女孩妈妈插队
                }
            } catch (InterruptedException e) {

            }
            sleep(2000);//休眠2s
            System.out.println(Thread.currentThread().getName() + " PrettyGirl做完核酸");

        }
    }

    private static class Aunt implements Runnable {
        @Override
        public void run() {
            sleep(2000);//休眠2s
            System.out.println("Aunt开始排队做核酸...");
            System.out.println(Thread.currentThread().getName() + " Aunt做完核酸");
        }
    }

    private static void sleep(long ms) {
        try {
            TimeUnit.MILLISECONDS.sleep(ms);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("我开始排队做核酸...");

        Aunt aunt = new Aunt();
        Thread auntThread = new Thread(aunt);

        PrettyGirl prettyGirl = new PrettyGirl(auntThread);
        Thread prettyGirlThread = new Thread(prettyGirl);

        prettyGirlThread.start();
        auntThread.start();

        prettyGirlThread.join();//女孩插队

        sleep(2000);//休眠2s
        System.out.println(Thread.currentThread().getName() + " 我做完核酸");
    }

}

运行结果:

我开始排队做核酸...
PrettyGirl开始排队做核酸...
Aunt开始排队做核酸...
Thread-0 Aunt做完核酸
Thread-1 PrettyGirl做完核酸
main 我做完核酸

3.4 线程的优先级

在Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1~10,在线程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5,优先级高的线程分配时间片的数量要多于优先级低的线程。

设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。在不同的JVM以及操作系统上,线程规划会存在差异,有些操作系统只有3级优先级,有的甚至会忽略对线程优先级的设定。

3.5 守护线程

Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出,守护线程被全部中断。可以通过调用 Thread.setDaemon(true)将线程设置为Daemon线程,不过一般用不上,除非需要自己做一些内存调度管理分配工作的时候,可以用守护线程,比如垃圾回收线程就是Daemon线程。

Daemon线程被用作完成支持性工作,但是在Java虚拟机退出时Daemon线程中的finally块并不一定会执行。在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。

/**
 * @Description: 守护线程
 * @CreateDate: 2022/2/22 2:20 下午
 */
public class DaemonThread {

    private static class UseThread extends Thread {
        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + " interrupt flag is " + isInterrupted());
                while (!isInterrupted()) {
                    System.out.println(Thread.currentThread().getName() + " is running");
                }
                System.out.println(Thread.currentThread().getName() + " interrupt flag is " + isInterrupted());
            } finally {
                //守护线程中finally不一定执行,具体会不会执行是由操作系统线程调度给不给分配足够的时间片决定的
                System.out.println("---finally---");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        UseThread useThread = new UseThread();
        useThread.setDaemon(true);//设置为守护线程之后,当主线程sleep,UseThread也跟着结束
        useThread.start();
        Thread.sleep(5);
        useThread.interrupt();
    }
}

4 线程间的共享和协作

4.1 线程间的共享

4.1.2 synchronized内置锁

线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码 一步一步地执行,直到终止。但是,每个运行中的线程,如果仅仅是孤立地运行, 那么没有一点儿价值,或者说价值很少,多线程的目的是为了能够相互配合完成工作, 包括数据之间的共享,协同处理任务

Java支持多个线程同时访问一个对象或者对象的成员变量,关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。

/**
 * @Description: synchronized的使用
 * @CreateDate: 2022/2/22 3:03 下午
 */
public class SynchronizedUsage {

    private long count = 0;
    /**
     * 作为一个锁
     */
    private final Object obj = new Object();

    /**
     * 用在同步块上,用Object作为锁【同步块】
     */
    public void incCount1() {
        synchronized (obj) {//如果不加锁,每次结果可能都不一样
            count++;
        }
    }

    /**
     * 用在同步块上,用当前类对象实例作为锁【同步块】
     */
    public void incCount2() {
        synchronized (this) {
            count++;
        }
    }

    /**
     * 用在方法上【同步方法】
     */
    public synchronized void incCount3() {
        count++;
    }

    private static class Count extends Thread {

        private final SynchronizedUsage usage;

        public Count(SynchronizedUsage usage) {
            this.usage = usage;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                usage.incCount1();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynchronizedUsage usage = new SynchronizedUsage();
        //启动两个线程
        Count count1 = new Count(usage);
        Count count2 = new Count(usage);
        count1.start();
        count2.start();
        Thread.sleep(50);
        System.out.println("count = " + usage.count);
    }

}

对象锁和类锁:

对象锁是用于对象实例方法,或者一个对象实例上的;类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁

但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的是每个类的对应的class对象。类锁和对象锁之间也是互不干扰的。

实例锁和类锁演示:

/**
 * @Description: 实例锁和类锁是不同的,两者可以并行
 * @CreateDate: 2022/2/22 3:58 下午
 */
public class InstanceAndClass {

    private static void sleep(long ms) {
        try {
            TimeUnit.MILLISECONDS.sleep(ms);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static class SyncInstance implements Runnable {
        private final InstanceAndClass instanceAndClass;

        public SyncInstance(InstanceAndClass instanceAndClass) {
            this.instanceAndClass = instanceAndClass;
        }

        @Override
        public void run() {
            System.out.println("TestInstance is running...");
            instanceAndClass.instance();
        }
    }

    private static class SyncClass extends Thread {
        @Override
        public void run() {
            System.out.println("TestClass is running...");
            syncClass();
        }
    }

    /**
     * 对象锁(实例锁)
     */
    private synchronized void instance() {
        sleep(1000);
        System.out.println("SyncInstance is going... " + this);
        sleep(1000);
        System.out.println("SyncInstance ended " + this);
    }

    /**
     * 类锁(静态方法上使用synchronized)
     */
    private static synchronized void syncClass() {
        sleep(1000);
        System.out.println("SyncClass is going... ");
        sleep(1000);
        System.out.println("SyncClass ended");
    }

    public static void main(String[] args) {
        InstanceAndClass instanceAndClass = new InstanceAndClass();
        Thread t1 = new Thread(new SyncInstance(instanceAndClass));
        Thread t2 = new SyncClass();

        //两个线程可以并行,因为用的不是同一个锁
        t1.start();
        sleep(1000);
        t2.start();
    }
}

锁static变量和类锁演示:

/**
 * @Description: 静态对象和人类锁也是不同的,二者可以并行
 * @CreateDate: 2022/2/22 4:13 下午
 */
public class StaticObjectAndClass {

    private static void sleep(long ms) {
        try {
            TimeUnit.MILLISECONDS.sleep(ms);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static class SyncStaticObject extends Thread {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + ":SyncStaticObject is running...");
            syncStaticObject();
        }
    }

    private static class SyncClass extends Thread {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + ":SyncClass is running...");
            syncClass();
        }
    }

    /**
     * 静态对象锁
     */
    private static Object obj = new Object();

    private static void syncStaticObject() {
        synchronized (obj) {
            System.out.println(Thread.currentThread().getName() + ":SyncStaticObject is going...");
            sleep(1000);
            System.out.println(Thread.currentThread().getName() + ":SyncStaticObject ended");
        }
    }

    /**
     * 类锁(静态方法上加synchronized)
     */
    private static synchronized void syncClass() {
        System.out.println(Thread.currentThread().getName() + ":SyncClass is going...");
        sleep(1000);
        System.out.println(Thread.currentThread().getName() + ":SyncClass ended");
    }

    public static void main(String[] args) {
        Thread t1 = new SyncStaticObject();
//        Thread t2 = new SyncStaticObject();
        Thread t2 = new SyncClass();
        t2.start();
        sleep(1000);
        t1.start();
    }
}

4.1.2 错误的加锁原因和分析

/**
 * @Description: 错误的加锁原因
 * @CreateDate: 2022/3/2 3:35 下午
 */
public class ErrorSyncDemo {

    private static class Worker implements Runnable {
        private Integer i;
        private Object o = new Object();

        public Worker(Integer i) {
            this.i = i;
        }

        @Override
        public void run() {
//            synchronized (i) {//加入使用i作为锁对象进行加锁,后边有i++的操作,相当于在不断生成新的对象,所以导致每个线程加锁的是不同的Integer对象,属于错误加锁
            synchronized (o) {//使用一个固定不变的对象才能正确加锁
                Thread thread = Thread.currentThread();
                System.out.println(thread.getName() + "--@" + System.identityHashCode(i));
                i++;
                System.out.println(thread.getName() + "----" + i + "-@" + System.identityHashCode(i));

                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(thread.getName() + "----" + i + "-@" + System.identityHashCode(i));
            }
        }
    }

    public static void main(String[] args) {
        Worker worker = new Worker(1);
        for (int i = 0; i < 5; i++) {
            new Thread(worker).start();
        }
    }
}

使用i进行错误加锁

Thread-0--@649390855
Thread-0----2-@678629793
Thread-1--@678629793
Thread-1----3-@461034646
Thread-2--@461034646
Thread-2----4-@1296921190
Thread-3--@1296921190
Thread-3----5-@706980030
Thread-4--@706980030
Thread-4----6-@536000283
Thread-0----6-@536000283
Thread-1----6-@536000283
Thread-2----6-@536000283
Thread-3----6-@536000283
Thread-4----6-@536000283

使用Object进行有效加锁

Thread-0--@589803175
Thread-0----2-@769676467
Thread-0----2-@769676467
Thread-4--@769676467
Thread-4----3-@1871778907
Thread-4----3-@1871778907
Thread-3--@1871778907
Thread-3----4-@1478646373
Thread-3----4-@1478646373
Thread-2--@1478646373
Thread-2----5-@484097906
Thread-2----5-@484097906
Thread-1--@484097906
Thread-1----6-@1419896858
Thread-1----6-@1419896858

4.1.3 volatile-最轻量的同步机制

volatile保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值对其他线程来说是立即可见的。

/**
 * @Description: volatile提供可见性
 * @CreateDate: 2022/3/2 5:30 下午
 */
public class VolatileDemo {

    private volatile static boolean ready;
    private static int number;

    private static class PrintThread extends Thread {
        @Override
        public void run() {
            System.out.println("PrintThread is running...");
            while (!ready) {//ready=false 死循环
                System.out.println("number = " + number);
            }
        }
    }

    public static void main(String[] args) {
        new PrintThread().start();
        sleep(1000);
        number = 2;
        ready = true;//此处将ready置为true,子线程PrintThread就能感知到ready的变化,从而停止死循环
        sleep(3000);
        System.out.println("main is ended!");
    }

    private static void sleep(long ms) {
        try {
            TimeUnit.MILLISECONDS.sleep(ms);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

不加volatile时,子线程无法感知主线程修改了ready的值,从而不会退出循环, 而加了volatile后,子线程可以感知主线程修改了ready的值,迅速退出循环。 但是volatile不能保证数据在多个线程下同时写时的线程安全。

/**
 * @Description: volatile在多线程下是不安全的
 * @CreateDate: 2022/3/2 5:40 下午
 */
public class VolatileNoSafeDemo {

    private volatile long count = 0;

    private void incCount() {
        count++;
    }

    private static class Count extends Thread {
        private VolatileNoSafeDemo noSafeDemo;

        public Count(VolatileNoSafeDemo noSafeDemo) {
            this.noSafeDemo = noSafeDemo;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                noSafeDemo.incCount();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileNoSafeDemo noSafeDemo = new VolatileNoSafeDemo();
        //启动两个线程
        Count count1 = new Count(noSafeDemo);
        Count count2 = new Count(noSafeDemo);
        count1.start();
        count2.start();
        Thread.sleep(50);
        System.out.println("result:" + noSafeDemo.count);//结果并不是20000,可见写时线程不安全
    }
}

volatile最适用的场景:一个线程写,多个线程读。

4.2 ThreadLocal

4.2.1 与synchonized的比较

ThreadLocalsynchonized都用于解决多线程并发访问。但是ThreadLocalsynchronized有本质的差别。synchronized是利用锁的机制,使变量或代码块在某一时刻仅仅能被一个线程访问。而ThreadLocal为每个线程都提供了变量的副本,使得每个线程在某一时间访问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。

4.2.2 ThreadLocal的使用

ThreadLocal类接口很简单,只有 4 个方法:

  • void set(Object value) 设置当前线程的线程局部变量的值。
  • public Object get() 该方法返回当前线程所对应的线程局部变量。
  • public void remove() 将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是 JDK 5.0 新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动 被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它 可以加快内存回收的速度。
  • protected Object initialValue() 返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

public final static ThreadLocal<String> RESOURCE = new ThreadLocal<String>()

RESOURCE代表一个能够存放String类型的ThreadLocal对象。 此时能够并发访问这个变量,对它进行写入、读取操作,都是线程安全的。

/**
 * @Description: ThreadLocal的使用
 * @CreateDate: 2022/3/2 7:04 下午
 */
public class ThreadLocalUseDemo {

    private static ThreadLocal<Integer> intThreadLocal = new ThreadLocal<Integer>() {

        @NonNull
        @Override
        protected Integer initialValue() {
            return 1;
        }
    };

    public static class TestThread implements Runnable {
        int id;

        public TestThread(int id) {
            this.id = id;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + ":start");
            Integer i = intThreadLocal.get();
            i = +id;
            intThreadLocal.set(i);
            System.out.println(Thread.currentThread().getName() + ":" + intThreadLocal.get());

        }
    }

    /**
     * 启动3个线程
     */
    public void startThreadArray() {
        Thread[] threads = new Thread[3];
        for (int i = 0; i < 3; i++) {
            threads[i] = new Thread(new TestThread(i));
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
        }
    }

    public static void main(String[] args) {
        ThreadLocalUseDemo demo = new ThreadLocalUseDemo();
        demo.startThreadArray();
    }
}

结果:

Thread-0:start
Thread-2:start
Thread-0:0
Thread-1:start
Thread-1:1
Thread-2:2

4.2.3 引发的内存泄漏分析

1、强弱软虚引用

Object o = new Object()这个o,我们可以称之为对象引用,而new Object()我们可以称之为在内存中产生了一个对象实例。

当写下o=null时,只是表示o不再指向堆中object的对象实例,不代表这个对象实例不存在了。

强引用就是指在程序代码之中普遍存在的,类似Object obj=new Object()这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象实例。

软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象实例列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来实现软引用。

弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象实例只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时, 无论当前内存是否足够,都会回收掉只被弱引用关联的对象实例。在JDK1.2之 后,提供了WeakReference类来实现弱引用。

虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象实例是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象实例被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

2、内存泄漏的现象

当我们启用了ThreadLocal以后会发生内存泄漏。

3、分析

根据我们前面对ThreadLocal的分析,我们可以知道每个Thread维护一个ThreadLocalMap,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object,也就是说ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。仔细观察ThreadLocalMap,这个map是使用ThreadLocal的弱引用作为Key的,弱引用的对象在GC时会被回收。 因此使用了ThreadLocal 后,引用链如图所示:

图中的虚线表示弱引用。

这样,当把Threadlocal变量置为null以后,没有任何强引用指向Threadlocal实例,所以Threadlocal将会被GC回收。这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些 key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而这块value永 远不会被访问到了,所以存在着内存泄露。

只有当前Thread结束以后,Current Thread就不会存在栈中,强引用断开, Current ThreadMap value将全部被GC回收。最好的做法是不在需要使用ThreadLocal变量后,都调用它的remove()方法,清除数据。

4.3 线程间的协作

4.3.1 等待/通知机制

是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者 notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait() notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

wait()调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回。需要注意,调用wait()方法后,会释放对象的锁。

wait(long)超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回。

wait(long,int)对于超时时间更细粒度的控制,可以达到纳秒。

notify()通知一个在对象上等待的线程,使其从wait()方法返回,而返回的前提是该线程获取到了对象的锁,没有获得锁的线程重新进入WAITING状态。

notifyAll()通知所有等待在该对象上的线程。

等待和通知的标准范式:

等待方遵循如下原则。

1)获取对象的锁。

2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。

3)条件满足则执行对应的逻辑。

通知方遵循如下原则。

1)获得对象的锁。

2)改变条件。

3)通知所有等待在对象上的线程。

在调用wait()notify()系列方法之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法、notify()系列方法。进 入wait()方法后,当前线程释放锁,在从wait()返回前,线程与其他线程竞争重新获得锁,执行notify()系列方法的线程退出调用了notifyAll()synchronized代码块的时候后,他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出synchronized代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。

notify和notifyAll应该用谁?

尽可能用notifyall(),谨慎使用notify(),因为notify()只会唤醒一个线程,无法确保被唤醒的这个线程一定就是我们需要唤醒的线程。

关注木水小站 (zhangmushui.cn)和微信公众号【木水Code】,及时获取更多最新技术干货。