《java高并发编程详解》

线程的概念:现有操作系统都支持多任务的执行,对计算机来说每一个任务就是一个进程process,在每一个进程内部至少要有一个线程thread是在运行中。线程是进程中的一个实体,线程本身是不会独立存在的。进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。真正要占用CPU运行的是线程,线程是CPU分配的基本单位。

线程是程序执行的一个路径,每一个线程都有自己的局部变量表,程序计数器,以及各自的生命周期。

一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域。

线程的5个生命周期:

1.new
2.runnable
3.running
4.blocked
5.terminated
每一个线程都具备:局部变量表&程序计数器&生命周期。

线程的生命周期:new -->runnable-->running-->blocked-->terminated

new:当我们使用new 关键字创建一个线程时,此时并不处于执行状态,因为它没有调用start()
方法启动该线程。此时线程处于new状态。

注解:new 状态通过start方法进入runnable状态

runnable:线程只有调用了start方法,才是真正的在jvm中创建了一个线程。

注解:线程一经启动,并不一定立即执行,线程的运行与否和进程听从cpu的调度。这个中间状态
称为runnable,也就是说它此时具备运行的资格,但不一定立即运行。

由于线程有running状态,所以不会直接进入到blocked和terminated状态,即便是在线程执行逻辑
中调用wait/sleep方法,也必须等到cpu调度执行才可以。runnable只有意外终止或进入running状态。


running : 一旦cpu通过轮询或者其他方式从任务可执行队列中选中了线程,那么此时它才能真正的执行自己的逻辑代码。

running状态转换条件:

1.直接进入terminated状态,比如调用了stop方法或者判断某个逻辑标示
2.进入blocked状态,比如调用了sleep或者wait方法。
3.进行某个阻塞的io操作,比如因网络数据的读写而进入了blocked状态
4.获取某个锁资源,从而加入到该锁的阻塞队列中二进入了blocked状态
5.由于CPU的调度器轮询使该线程放弃执行,进入runnable状态
6.线程主动调用yield方法,放弃CPU执行权,进入runnable状态

blocked的状态流转:

1.直接进入terminated状态,比如调用stop或者意外死亡
2.线程阻塞的操作结束,比如读取了想要的数据字节进入到runnable状态
3.线程完成了指定时间到休眠,进入到了runnable状态
4.wait中的线程被其他线程notify/notifyall唤醒,进入runnable状态
5.线程获取到了某个锁资源,进入runnable状态
6.线程在阻塞过程中被打断

terminated: 线程的最终状态,该状态线程将不会切换到其他状态,整个生命周期都结束了。

1.线程运行正常结束,结束生命周期
2.线程运行出错意外结束
3.jvm crash ,导致所有的线程都结束。

《第二章:深入理解thread构造函数》

1.构造线程时,一定要起有意义的名字,有助于排查和线程的跟踪。
2.如果在构造Thread的时候,没有显示的指定一个ThreadGroup,那么子线程将会被加入父线程所在的线程组。
1.一个线程的创建肯定是由另一个线程完成的
2.被创建线程的父线程是创建他的线程。
3.所有的父线程都是main线程
程序计数器:程序计数器是线程私有的,用于存放当前线程接下来将要执行的字节码指令,分支,循环,跳转,异常处理等信息。一个处理器只执行其中一个线程中的指令,为了能够在CPU时间片轮转切换上下文之后顺利回到正确的执行位置,每条线程都需要具有一个独立的程序计数器,各个线程之间互不影响。
Java虚拟机栈:线程私有的,生命周期和线程相同,是在JVM运行时创建,在线程中,方法在执行的时候都会创建一个名为栈帧stack frame的数据结构主要用于存放局部变量表,操作栈,动态连接,方法出口等信息。方法的调用对应着栈帧在虚拟机栈中的压栈和弹栈过程。
本地方法栈:线程私有的,Java提供了调用本地方法的接口Java native interface,也就是c/c++程序,在线程的执行过程中,经常会碰到调用JNI方法的情况,比如网络通信,文件操作的底层,JVM为本地方法所划分的内存区域就是本地方法栈。
堆内存:堆内存是JVM中最大的一块内存区域,被所有线程所共享,Java在运行期间创建的所有对象几乎都放在该内存区域。堆内存一般分为新生代和老年代。
方法区:被多个线程所共享,主要存储已经被虚拟机加载的类信息,常量,静态变量等信息。
Java8元空间:Java8将方法区干掉了,取而代之的是元空间,存放的是被虚拟机栈加载的类信息,常量,静态变量。

守护线程经常用作与执行一些后台任务,因此有时也称为后台线程,如果你希望关闭某些线程的时候,或退出JVM进程的时候,一些线程能够自动关闭,那么就使用守护线程。

《第三章:Thread api 介绍》

sleep方法:sleep方法会使当前线程进入指定毫秒数的休眠,暂停执行,虽然给定了一个休眠时间,但最终还是要以系统的定时器和调度器的精度为准,休眠有一个非常重要的特性,那就是它不会放弃monitor锁的所有权。
yield方法: 它会提醒调度器我愿意放弃当前的cpu资源,如果cpu资源不紧张,则会忽略这种提醒。调用yield 方法会使当前线程从running状态切换到runnable状态。

yield方法和sleep的区别:

1.sleep 会导致当前线程暂停指定的时间,没有cpu时间片的消耗
2.yield 只是对CPU调度器的提示,如果CPU调度没有忽略这个提示,它会导致线程上下文的切换
3.sleep 会使线程短暂block,会在给定的时间内释放CPU资源
4.yield 会使当前线程从running状态切换到runnable状态
5.sleep 几乎百分之百的完成了给定时间的休眠,而yield的提示并不能一定保证

6.一个线程sleep 另一个线程调用interrupt 会捕获到中断信号,而yield则不会。

//设置的优先级别不能小于1,不能大于10
//对于root用户,它会hint操作系统你想要设置的优先级别,否则它会被忽略。
//如果cpu比较忙,设置CPU可能会获取更多的时间片,但是空闲时优先级的高低几乎不会有任何作用。

线程的优先级:

1.对于root用户,它会hint 操作系统你想要设置的优先级别,否则它会被忽略。
2.如果Cpu比较忙,设置优先级可能会获得更多的CPU时间片,但是闲时优先级的高低几乎不会有任务作用。

设置线程上下文类加载器

public ClassLoader getContextClassLoader()获取线程上下文的类加载器(这个线程是由哪个类加载器加载的),如果是在没有修改线程上下文类加载器的情况下,则保持与父线程同样的类加载器。

public void setContextClassLoader(ClassLoader cl) 设置该线程的类加载器,这个方法可以打破Java 类机载器的父委托机制。

interrupt是线程的中断方法。如下方法会使线程进入阻塞状态,调用interrupt 将使线程结束中断。
会使线程进入阻塞的方法
1.object的wait 方法
2.object的wait(long)方法
3.object的wait(long,int)方法
4.thread的sleep(long)方法
5.thread的sleep(long,int)方法        
6.thread的join(long)方法
7.thread的join(long,int)方法
8.InterruptibleChannel的io 操作
9.seletror 的wakeup方法
10.其他方法
关闭一个线程的方式:
1.线程结束生命周期正常结束:线程运行结束,完成了自己的使命之后,就会正常退出。
2.捕获中断信号关闭线程:借助 isInterrupted()方法。【可以调用线程的interrupt()方法停止线程。】
3.使用volatile开关控制:interrupt 标识很有可能被擦除,使用private volatile boolean closed = false;【由于线程interrupt标示很有可能会被清除掉,或者逻辑中并不会调用任何中断方法, 所以使用volatile修饰的开关关闭线程也是可以的。】
4.异常退出

5.进程假死:进程依旧存在,但没有日志输出,程序也不进行任何作业。看起来死了却没死,现这种情况,绝大部分原因就是某个线程阻塞了,或者线程出现了死锁。
 

总结:可以使用jstack,jconsole,jvisualvm工具。

《第四章:线程安全和数据同步》

synchronized关键字的特点:
1。synchronized 关键字提供了一种锁机制,能够确保共享变量互拆访问,从而防止数据的不一致性问题的出现。
2。synchronized 关键字包括monitor enter 和 monitor exit 两个JVM指令,它能够保证任何时候和任何线程执行到monitor enter之前都必须从主内存之间获取数据,而不是从缓存中。而在monitor exit运行成功之后,共享变量被更新的值必须刷新到内存中去。
3。synchronized 的指令严格遵守Java-happens-before的原则,一个monitor exit 指令之前必须有 一个monitor enter 指令。
4.synchronized : synchronized关键字可以实现简单的策略来防止线程干扰和内存一致性的错误, 如果一个对象对多个线程都是可见的,那么对该对象对所有对读和写都将通过同步对方式来进行。
5.synchronized 可以对代码块和方法进行修饰,不可以对class类以及变量进行修饰。
6.synchronized具有排他性,所有的线程必须串行的经过synchronized保护的共享区域,如果synchronized 作用域越大,则代表着其效率越低,甚至还会丧失并发的优势。synchronized应该尽可能的只作用于共享资源 的读写作用域。 多个锁的交叉容易造成死锁。

程序死锁的因素:

1.交叉锁可导致死锁:线程a持有r1的锁等待获取r2的锁,线程b持有r2的锁,等待获取r1的锁。
2.内存不足:当并发请求系统可用内存时,如果此时系统不足,那么可能会出现死锁的问题。两个线程 T1 & T2,执行了某个任务,T1已经获取了10MB,T2获取了20MB,如果每个线程都需要获取30内存,但是 剩余都内存只剩下20MB,那么两个线程都可能在等待彼此能够释放内存。
3.一问一答式的数据交换服务端开启某个端口,等待客户端访问,客户端发起请求立即等待接收, 由于某种原因服务端错过了客户端的访问请求,仍在等待一问一答式的数据交换,那么此时客户端和 服务端都在等待彼此。
4.数据库锁:无论是数据库table级别的锁,还是row 行级别的锁,比如某个线程执行了for update 语句退出了事务,其他线程访问数据库时都将陷入死锁。
5.文件锁:某线程获得了文件锁意外退出,其他读取该文件的线程都将陷入死锁,直到系统释放文件句柄资源。
6.死循环引起的死锁:由于程序代码原因或者异常处理不当,进入了死循环。

1.同步阻塞与异步非阻塞:

同步阻塞消息处理:

图5-1所示的设计存在几个显著的缺陷,具体如下:

1.同步Event提交,客户端等待时间过长会陷人阻塞,导致二次提交Event耗时过长。
2.由于客户端提交的Event数量不多,导致系统同时受理业务数量有限,也就是系统整体的吞吐量不高。
3.这种一个线程处理一个Event的方式,会导致出现频繁的创建开启与销毁,从而增加系统额外开销。
4.在业务达到峰值的时候,大量的业务处理线程阻塞会导致频繁的CPU上下文切换,从而降低系统性能。

异步非阻塞消息处理:

客户端提交Event后会得到一个相应的工单并且立即返回,Event则会被放置在Event队列中。服务端有若千个工作线程,不断地从Event队列中获取任务并且进行异步处理,最后将处理结果保存至另外的一个结果集中,如果客户端想要获得处理结果,则可凭借工单号再次查询。

两种方式相比较,异步非阻塞的优势非常明显,首先客户端不用等到结果处理结束之后才能返回,从而提高了系统的吞吐量和并发量;其次若服务端的线程数量在一个可控的范围之内是不会导致太多的CPU上下文切换从而带来的额外开销的;再次服务端线程可以重复利用,这样就减少了不断创建线程带来的资源浪费。但是异步处理的方式同样也存在缺陷,比如客户端想要得到结果还需要再次调用接口方法进行查询。

wait & sleep 的区别:

0.wait 和sleep方法都可以使当前线程进人阻塞状态。
1.wait和sleep方法都可以使线程进人阻塞状态。
2.wait和sleep方法均是可中断方法,被中断后都会收到中断异常。
3.wait是Object的方法,而sleep是Thread特有的方法。
4.wait方法的执行必须在同步方法中进行,而sleep则不需要。
5.线程在同步方法中执行sleep方法时,并不会释放monitor的锁,而wait方法则会释放monitor的锁。
6.sleep方法短暂休眠之后会主动退出阻塞,而wait方法(没有指定wait时间)则需要被其他线程中断后才能退出阻塞。
synchronized关键字的缺陷:synchronized提供了一种排他式的数据同步机制。某个线程在获取
monitor lock 的时候可能被阻塞,而这种阻塞存在两种缺陷:
1.无法控制阻塞时长
2.阻塞不可被中断
获取线程运行时异常

在Thread类中,关于处理运行时异常的API总共有四个,如下所示:

1.public void setUncaughtExceptionHandler ( UncaughtExceptionHandler eh):为某个特定线程指定UncaughtExceptionHandler。

2.public static void setDefaultUncaughtExceptionHandler ( UncaughtExceptionHandlereh):设置全局的UncaughtExceptionHandler。

3.public UncaughtExceptionHandler getUncaughtExceptionHandler() :获取特定线程的UncaughtExceptionHandler。

4.public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler() :获取全局的UncaughtExceptionHandler。


UncaughtExceptionHandler 的介绍

线程在执行单元中是不允许抛出checked异常的,这一点前文中已经有过交代,而且线程运行在自己的上下文中,派生它的线程将无法直接获得它运行中出现的异常信息。对此,Java为我们提供了一个UncaughtExceptionHandler接口,当线程在运行过程中出现异常时,会回调UncaughtExceptionHandler接口,从而得知是哪个线程在运行时出错,以及出现了什么样的错误,示例代码如下:

public static void main(String[] args) {
    Thread.setDefaultUncaughtExceptionHandler((t,e)->{
        System.out.println("线程为:"+t.getName()+"发生异常");
        e.printStackTrace();
    });
    final Thread thread = new Thread(()->{
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("这里会抛出异常:"+ 1/0);
    },"Test-exception");
    thread.start();
}

7.2.3Hook线程应用场景以及注意事项

1.Hook线程只有在收到退出信号的时候会被执行,如果在kill的时候使用了参数-9,那么Hook线程不会得到执行,进程将会立即退出,因此.lock文件将得不到清理。
2.Hook线程中也可以执行一些资源释放的工作,比如关闭文件句柄、socket链接、数据库connection等。
3.尽量不要在Hook线程中执行一些耗时非常长的操作,因为其会导致程序迟迟不能退出。
自JDK1.5起,utils包提供了ExecutorService线程池的实现,主要目的是为了重复利用线程,提高系统效率。
通过前文的学习我们得知Thread是一个重量级的资源,创建、启动以及销毁都是比较耗费系统资源的,
因此对线程的重复利用一种是非常好的程序设计习惯,加之系统中可创建的线程数量是有限的,线程数量和系统性能是一种拋物线的关系,
也就是说当线程数量达到某个数值的时候,性能反倒会降低很多,因此对线程的管理,尤其是数量的控制更能直接决定程序的性能。

8.1线程池原理

线程池,通俗的理解就是有一个池子,里面存放着已经创建好的线程,当有任务提交给线程池执行时, 池子中的某个线程会主动执行该任务。
如果池子中的线程数量不够应付数量众多的任务时,则需要自动扩充新的线程到池子中,但是该数量是有限的,就好比池塘的水界线一样。
当任务比较少的时候,池子中的线程能够自动回收,释放资源。为了能够异步地提交任务和缓存未被处理的任务,需要有一个任务队列。

一个完整的线程池应该具备如下要素。
1.任务队列:用于缓存提交的任务。
2.线程数量管理功能:一个线程池必须能够很好地管理和控制线程数量,可通过如下三个参数来实现,比如创建线程池时初始的线程数量init; 线程池自动扩充时最大的线程数量max; 在线程池空闲时需要释放线程但是也要维护一定数量的活跃数量或者核心数量core。 有了这三个参数,就能够很好地控制线程池中的线程数量,将其维护在一个合理的范围之内,三者之间的关系是init<=core<=max。
1.任务拒绝策略:如果线程数量已达到上限且任务队列已满,则需要有相应的拒绝策略来通知任务提交者。
2.线程工厂:主要用于个性化定制线程,比如将线程设置为守护线程以及设置线程名称等。
3.QueueSize :任务队列主要存放提交的Runnable,但是为了防止内存溢出,需要有limit数量对其进行控制。
4.Keepedalive时间:该时间主要决定线程各个重要参数自动维护的时间间隔。
1. execute (Runnable runnable):该方法接受提交Runnable任务。
2.shutdown():关闭并且销毁线程池。
3.getInitSize():返回线程池的初始线程数量。
4.getMaxSize():返回线程池最大的线程数量。
5.getCoreSize():返回核心线程数量。
6.getQueueSize():返回当前线程池任务数量。
7.getActiveCount():返回线程池中当前活跃的线程数量。
8.isShutdown():判断线程池是否已被销毁。
package com.thread;

import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;

/**
 * @author 云澜
 * @date 2021/7/22 3:49 下午
 */
@Slf4j
public class TryConcurrency {

    public static void main(String[] args) {
        //需求:同时进行看着新闻&听着音乐
        //这样并不能同时运行哦
        /*
       browseNews();
       enjoyMusic();*/

       //old方式: 利用多线程,实现同时进行看着新闻&听着音乐
        /*new Thread(){
            @Override
            public void run() {
                browseNews();
            }
        }.start();
        enjoyMusic();*/

       //java8方式: 利用多线程,实现同时进行看着新闻&听着音乐
        new Thread(TryConcurrency::browseNews).start();
        enjoyMusic();
    }

    /**
     *
     */
    private static void browseNews(){
        for (; ;) {
            log.info("the browse news");
            sleep(1);
        }
    }

    private static void enjoyMusic(){
        for (; ;) {
            log.info("the enjoy music");
            sleep(1);
        }
    }

    private static void sleep(int seconds){
        try {
            TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

微信小程序教程_小程序入门实战视频教程(25讲)-IT营大地_哔哩哔哩_bilibili [vidio]

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值