工作——Java高级工程师

626 阅读46分钟

B站学习资料

黑马程序员最全SSM框架教程|Spring+SpringMVC+MyBatis全套教程(spring+springmvc+mybatis)

www.bilibili.com/video/BV1WZ…

黑马程序员SpringBoot2全套视频教程,springboot零基础到项目实战(spring boot2完整版)

www.bilibili.com/video/BV15b…

SpringCloud+RabbitMQ+Docker+Redis+搜索+分布式,史上最全面的springcloud微服务技术栈课程

www.bilibili.com/video/BV1LQ…

观看记录:P41

P1 微服务技术栈导学1

  • 什么是微服务? 微服务是分布式架构的一种,所谓分布式架构就是把服务做拆分,而拆分的过程中会产生各种各样的问题需要解决,而SpringCloud仅仅解决了服务拆分中的服务治理问题。

image.png

  • 微服务技术栈 image.png

image.png

image.png

image.png

P2 微服务技术栈导学2

image.png

image.png

image.png

image.png

image.png

P4 02-认识微服务-服务架构演变

image.png

image.png

image.png

image.png

image.png

P5 03-认识微服务-微服务技术对比

image.png

image.png

image.png

P6 04-认识微服务-SpringCloud

image.png

image.png

P7 05-服务拆分-案例Demo

image.png

image.png

P8 06-服务拆分-服务远程调用

image.png

image.png

image.png

image.png

image.png

image.png

P9 07-Eureka-提供者与消费者

image.png

image.png

P10 08-Eureka-eureka原理分析

  • 服务调用出现的问题

image.png

  • Eureka的作用

image.png

image.png

image.png

P11 09-Eureka-搭建eureka服务

  • 搭建EurekaServer

image.png

image.png

P12 10-Eureka-服务注册

  • 在Eureka中注册user-service image.png

注意:有时候会发现服务没有成功注册到Eureka中,可能是因为在启动类中少了注解“ @EnableEurekaClient”,可以加上去试试。

image.png

P13 11-Eureka-服务发现

image.png

image.png

P14 12-Ribbon-负载均衡原理

  • 负载均衡流程

image.png

image.png

P15 13-Ribbon-负载均衡策略

image.png

image.png

image.png

image.png

P16 14-Ribbon-饥饿加载

image.png

  • 总结

image.png

P17 15-Nacos-认识和安装Nacos

image.png

P18 16-Nacos-快速入门

image.png

P19 17-Nacos-服务多级存储模型

image.png

  • 服务跨集群调用问题

image.png

  • 服务器集群属性

image.png

  • 总结

image.png

P20 18-Nacos-NacosRule负载均衡

image.png

image.png

P21 19-Nacos-服务实例的权重设置

image.png

P22 20-Nacos-环境隔离

  • 环境隔离-namespace

image.png

image.png

P23 21-Nacos-Nacos和Eureka的对比

image.png

image.png

image.png

P25 02-Nacos配置管理-Nacos实现配置管理

image.png

P26 03-Nacos配置管理-微服务配置拉取

image.png

image.png

image.png

image.png

P27 04-Nacos配置管理-配置热更新

image.png

image.png

image.png

P28 05-Nacos配置管理-多环境配置共享

image.png

image.png

image.png

P29 06-Nacos配置管理-nacos集群搭建

image.png

P30 07-Feign-基于Feign远程调用

image.png

image.png

image.png

image.png

image.png

image.png

P31 08-Feign-自定义配置

image.png

image.png

image.png

image.png

P32 09-Feign-性能优化

image.png

image.png

P33 10-Feign-最佳实践分析

image.png

image.png

image.png

P34 11-Feign-实现Feign最佳实践

image.png

image.png

P35 12-Gateway网关-网关作用介绍

image.png

image.png

P36 13-Gateway网关-快速入门

image.png

image.png

image.png

P37 14-Gateway网关-路由断言工厂

image.png

image.png

image.png

P38 15-Gateway网关-路由的过滤器配置

image.png

image.png

P39 16-Gateway网关-全局过滤器

image.png

image.png

image.png

image.png

P40 17-Gateway网关-过滤器链执行顺序

image.png

image.png

image.png

P41 18-Gateway网关-网关的cors跨域配置

image.png

image.png

P62 02-初识MQ-同步通讯的优缺点

image.png

image.png

image.png

image.png

P63 03-初识MQ-异步通讯的优缺点

image.png

image.png

image.png

image.png

image.png

image.png

P64 04-初识MQ-mq常见技术介绍

image.png

耗时半月,终于把牛客网上的Java面试八股文整理成了视频合集

www.bilibili.com/video/BV18S…

想要成为一名java高级工程师得具备一些什么技能?

1 转载地址

www.zhihu.com/question/42…

2 摘录1

一名高级Java开发工程师需要具备的技能很多,初级工程师、中级工程师、高级工程师。

初级工程师

一般工作一到三年,熟练掌握Java基础知识,Java多线程并发机制,JavaWeb容器原理及垃圾回收,会使用Spring,Mybatis等框架,能够代码规范,代码清晰,逻辑有序,有良好的编程习惯。

中级工程师

工作3到6年,能使用算法和多线程技术,熟练掌握反射机制,能写出常用的几大框架,深入了解底层原理,熟悉框架,单独写项目。

高级工程师

8年以上工作经验,懂源码和底层原理,能够结合业务需求造框架,深入理解高并发,多线程,jvm,操作系统等,精通数据库,算法等。

3 摘录2

高级Java程序员需要学习的知识点:

1、Core Java,Java基础,JDK的类库,很多童鞋都会说,JDK我懂,但是懂还不足够,知其然还要知其所以然,JDK的源码写的非常好,要经常查看,对使用频繁的类,比如String,集合类(List,Map,Set)等数据结构要知道它们的实现、不同的集合类有什么区别,然后才能知道在一个具体的场合下使用哪个集合类更适合、更高效,这些内容直接看源代码就OK了。

2、多线程并发编程,现在并发几乎是写服务端程序必须的技术,那对Java中的多线程就要有足够的熟悉,包括对象锁机制、synchronized关键字,concurrent包要非常熟悉。

3、I/O,Socket编程,首先要熟悉Java中Socket编程,以及I/O包,再深入下去就是Java NIO,再深入下去是操作系统底层的Socket实现,了解Windows和Linux中是怎么实现Socket的。

4、JVM要了解,拓展眼界,了解之后眼界会更宽阔,比如Java内存模型(会对理解java锁、多线程有帮助)、字节码、JVM的模型、各种垃圾收集器以及选择、JVM的执行参数(优化JVM)等,可以去Oracle网站上查看具体版本的JVM规范。

5、设计模式,如单例、模板方法、代理、适配器等等,以及在Core Java和一些Java框架里的具体场景的实现,这个可能需要慢慢积累,先了解有哪些使用场景,见的多了,自己就自然而然会去用。

6、常用数据库(Oracle、MySQL等)、SQL语句以及一般的优化。

7、JavaWeb开发的框架,如Spring、iBatis等框架,熟知原理。

8、其它一些有名的用的比较多的开源框架和包,Netty网络框架,Apache common的N多包,Google的Guava等等,也可以经常去Github上找一些代码看看。

4 摘录3

Redis、Elasticsearch、Hbase作为Java工程师必会的技术栈,尝尝作为面试的考核点。特别是互联网大厂,不仅要求面试者了解这些考核点,还需要深入理解底层原理,所以掌握Redis、Elasticsearch、Hbase成为Java工程师的必备技能。

5 摘录4

zhuanlan.zhihu.com/p/83904890

对于一个高级Java开发程序员来说,需要掌握哪些技能呢?

第一个是基础

比如对集合类,并发包,IO/NIO,JVM,内存模型,泛型,异常,反射,等有深入的了解,最好是看过源码了解底层的设计。比如一般面试都会问ConcurrentHashMap,CopyOnWrite,线程池,CAS,AQS,虚拟机优化等知识点,因为这些对互联网的企业是绝对重要的。

而且一般人这关都过不了,还发牢骚说这些没什么用,为什么要面试。举一例子,在使用线程池时候,因为使用了无界队列,在远程服务异常情况下导致内存飙升,怎么去解决?你要是连线程池都不清楚,你怎么去玩?

再举一例,由于对ThreadLocal理解出错,使用它做线程安全的控制,导致没能实现真的线程安全。所以作为一个拿三万的Java程序员这点基础是要有的。

第二你需要有全面的互联网主流技术相关知识

从底层说起,你起码得深入了解mysql、redis、mongodb,nginx,tomcat,rpc,jms等方面的知识。你要问需要了解到什么程度,我可以给你说个大概。首先对于MySQL,你要知道常见的参数设置,存储引擎怎么去选择,还需要了解常见的索引引擎,知道怎么去选择。知道怎么去设计表,怎么优化sql,怎么根据执行计划去调优。

高级的你需要去做分库分表的设计和优化,一般互联网企业的数据库都是读写分离,还会垂直与水平拆分,所以这个也有经验的成分在里面。

然后redis,mongodb都是需要了解原理,需要会调整参数的,而nginx和tomcat几乎都是java互联网方面必配,其实和阿里的技术栈选择有点关系。

至于rpc相关的就多了去,必须各种网络协议,序列化技术,SAO等等,你要有一个深入的理解。现在应用比较广的rpc框架,在国内就是dubbo了,可以自行搜索。至于jms相关的起码得了解原理吧,一般情况下不是专门开发中间件系统和支撑系统的需要了解太多细节,国内企业常用的主要是activeMQ和kafka。

你能对我说的都研究的比较深入,阿里p7都不是太大的问题,当然这个还需要看你的架构能力方面的面试表现了。

第三就是编程能力,编程思想,算法能力,架构能力

首先排序和查询的基本算法得会,编程思想是必须的,问你个AOP和IOC你起码得清清楚楚,设计模式不说每种都用过,但也能了解个几种吧。

编程能力这个我觉得不好去评价,但是拿一个2000W用户根据姓名年龄排序这种题目也能信手拈来。最后就是架构能力,这种不是说要你设计个多牛逼多高并发的系统,起码让你做一个秒杀系统,防重请求的设计能快速搞定而没有坑吧。

因此在这里我也给那些技术想达到这个高度甚至想往架构师发展的Java程序员提供一份详细的进阶路线图,主要针对1-5年及以上工作经验的Java开发人员,从广度到深度架构图还比较全面的,里面的技术包涵了Java高并发、微服务、源码分析、源码分析、高性能、分布式等内容,这些也是目前互联网企业比较常用的技术,那么来详细看看。

01、底层源码分析

学习Java技术体系,设计模式,流行的框架与组件,常见的设计模式,编码必备,Spring5,做应用必不可少的最新框架,MyBatis,玩数据库必不可少的组件......

02、分布式架构

高并发,高可用,海量数据,没有分布式的架构知识肯定是玩不转的,要了解分布式中的,分布式架构原理,分布式架构策略,分布式中间件,分布式架构实战等等内容。

03、微服务架构

业务越来越复杂,服务分层,微服务架构是架构升级的必由之路。比如:微服务框架,Spring Cloud,Docker与虚拟化,微服务架构。

04、性能优化

任何脱离细节的ppt架构师都是耍流氓,向上能运筹帷幄,向下能解决一线性能问题,比如:性能指标体系,JVM调优,Web调优,DB调优等等....

05、多线程并发

从架构设计,到应用层调优,再深入了解底层原理,扎实的Java基本功才能让自己变为扫地神僧:内存模型,并发模式,线程模型,锁细节等等.....

Java高级进阶——综合

Java进阶篇

Java高级进阶——JVM学习

1 综合

jvm-大白话带你认识JVM

看完这篇文章你还敢说你懂JVM吗?

jvm学习路线(简洁明了)

小白入门JVM最好教程(记得收藏)

Java JVM怎么学习啊?从哪方面入手?

清华大佬力荐的JVM学习路线+实战笔记+阿里真题,吃透吊打面试官

JVM学习全过程(跟着狂神学笔记超详细)

学习了jvm,对您的工作,技能带来了哪些正面影响?

2 视频学习资料-黑马程序员JVM完整教程,Java虚拟机快速入门,全程干货不拖沓

黑马程序员JVM完整教程,Java虚拟机快速入门,全程干货不拖沓

配套资料pan.baidu.com/s/167mob33E… 提取码:bqob

学习记录:已看P27

网友学习笔记

nyimac.gitee.io/2020/07/03/…

P1 01_什么是jvm

image.png

image.png

P2 02_学习jvm有什么用

image.png

P3 03_常见的jvm

image.png

P4 04_学习路线

image.png

P5 05_程序计数器_作用

image.png

image.png

image.png

image.png

P6 06_程序计数器_特点

image.png

P7 07_栈

image.png

P8 08_栈的演示

image.png 演示代码:

public class Test1 {
    public static void main(String[] args) throws InterruptedException{
        method1();
    }

    private static void method1(){
        method2(1,2);
    }
    private static int method2(int a,int b){
        int c=a+b;
        return c;
    }
}

P9 09_栈_问题辨析1

1 垃圾回收是否涉及栈内存?

垃圾回收不需要管理栈内存,因为栈内存无非就是一次次的方法调用产生的栈帧内存,而栈帧内存在每一次方法调用之后都会被弹出栈,也就是会被自动地回收掉,所以根本需要垃圾回收来管理我们的栈内存。

2 栈内存分配越大越好吗?

image.png -Xss是用来设置线程栈内存的大小。因为电脑物理内存的大小是一定的,栈内存划分的越大,会让线程数变少,所以栈内存不是越大越好,一般采用系统默认设置的栈内存大小就可以了。

3 方法内的局部变量是否线程安全?

那看一个变量是不是线程安全,那么就是要看这个变量对多个线程到底是共享的,还是私有的。

  • 示例代码1
/**
 * 局部变量的线程安全问题
 */
public class Test1 {
    //多个线程同时执行此方法
    static void m1(){
        int x=0;
        for(int i=0;i<5000;i++){
            x++;
        }
        System.out.println(x);
    }
}

变量x是线程私有的,所以是线程安全的。 image.png

  • 示例代码2
/**
 * 局部变量的线程安全问题
 */
public class Test1 {
    public static void main(String[] args){
        StringBuilder sb=new StringBuilder();
        sb.append(4);
        sb.append(5);
        sb.append(6);
        new Thread(()->{
            m2(sb);
        });
    }

    /**
     * sb作为方法的私有变量,是线程安全的
     */
    public static void m1(){
        StringBuilder sb=new StringBuilder();
        sb.append(1);
        sb.append(2);
        sb.append(3);
        System.out.println(sb.toString());
    }

    /**
     * sb作为一个参数可能被别的线程访问,因此不是线程安全的
     * 可以用StringBuffer,是线程安全的
     * @param sb
     */
    public static void m2(StringBuilder sb){
        sb.append(1);
        sb.append(2);
        sb.append(3);
        System.out.println(sb.toString());
    }

    /**
     * sb作为一个返回值,可能被别的线程拿到引用,进行修改,因为也是线程不安全的
     * @return
     */
    public static StringBuilder m3(){
        StringBuilder sb=new StringBuilder();
        sb.append(1);
        sb.append(2);
        sb.append(3);
        return sb;
    }
}

结论:要判断一个变量是不是线程安全的,不仅要看它是不是方法内的局部变量,还要看它是否逃离了方法的作用范围,如果逃离了方法的作用范围,那么它有可能被别的线程使用,是线程不安全的。

P12 12_栈_内存溢出1

1 栈帧过多导致栈内存溢出

image.png 当所有栈帧占用的内存之和超过了线程的最大栈内存,线程无法为栈帧分配新的内存,就会出现栈内存溢出。

  • 测试代码
public class Test1 {
    private static int count;

    public static void main(String[] args){
        try{
            method1();
        }catch (Throwable e){
            e.printStackTrace();
            System.out.println(count);
        }
    }

    private static void method1(){
        count++;
        method1();
    }
}

设置栈内存大小

image.png

2 栈帧过大导致栈内存溢出

P13 13_栈_内存溢出2

  • 示例代码
public class Test1 {
    public static void main(String[] args) throws JsonProcessingException{
        Dept d=new Dept();
        d.setName("Market");

        Emp e1=new Emp();
        e1.setName("zhang");
        e1.setDept(d);

        Emp e2=new Emp();
        e2.setName("li");
        e2.setDept(d);

        d.setEmps(Arrays.asList(e1,e2));

        ObjectMapper mapper=new ObjectMapper();
        System.out.println(mapper.writeValueAsString(d));
    }
}

class Emp{
    private String name;
    @JsonIgnore
    private  Dept dept;

    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name=name;
    }

    public Dept getDept(){
        return dept;
    }
    public void setDept(Dept dept){
        this.dept=dept;
    }
}

class Dept{
    private String name;
    private List<Emp> emps;

    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name=name;
    }

    public List<Emp> getEmps(){
        return emps;
    }
    public void setEmps(List<Emp> emps){
        this.emps=emps;
    }
}

P14 14_线程诊断_cpu占用高

  • 定位方法
  1. 用top命令定位哪个进程对cpu的占用过高
  2. ps H -eo pid,tid,%cpu |grep 进程id(用ps命令进一步定位是哪个线程引起的cpu占用过高)
  3. jstack 进程id 可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行号。

image.png

jstack显示的线程id是十六进制的

P15 15_线程诊断_迟迟得不到结果

  • 示例代码-死锁

image.png

  • linux中运行代码

image.png

  • jstack分析结果

image.png

P16 16_本地方法栈

image.png 本地方法栈给本地方法的运行提供内存和空间。

P17 17_堆_定义

image.png

P18 18_堆_内存溢出

堆内存溢出的示例代码:

/**
 * 演示堆内存溢出
 */
public class Test1 {
    public static void main(String[] args){
        int i=0;
        try{
            List<String> list=new ArrayList<>();
            String a="hello";
            while(true){
                list.add(a);
                a=a+a;
                i++;
            }
        }catch (Throwable e){
            e.printStackTrace();
            System.out.println(i);
        }
    }
}

P19 19_堆_内存诊断_jmap

image.png

问题:jps和jmap使用有问题(windows) 解决:jps用不了是因为环境变量配置有问题;jmap用不了是因为权限问题(例如需要用管理员打开cmd才可以使用)

image.png

image.png

P20 20_堆_内存诊断_jconsole

image.png

P21 21_堆_内存诊断_jvirsualvm

image.png

image.png

P22 22_方法区_定义

image.png

image.png

image.png

P23 23_方法区_内存溢出

image.png

P24 24_方法区_内存溢出

image.png

P25 25_方法区_常量池

image.png

示例代码:

// 二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)
public class Test1 {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

使用javap工具来反编译Test1.class文件:

javap -v Test1.class

反编译结果:

D:\NoSynchronous\02.Exercise.BackEnd\00Java\itheima_java_concurrency\target\classes\com\jvm\P25>javap -v Test1.class
Classfile /D:/NoSynchronous/02.Exercise.BackEnd/00Java/itheima_java_concurrency/target/classes/com/jvm/P25/Test1.class
  Last modified 2022-8-13; size 572 bytes
  MD5 checksum 32dfa23f4f46f7061d0d94e8940bee15
  Compiled from "Test1.java"
public class com.jvm.P25.Test1
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#21         // java/lang/Object."<init>":()V
   #2 = Fieldref           #22.#23        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #24            // hello world
   #4 = Methodref          #25.#26        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #27            // com/jvm/P25/Test1
   #6 = Class              #28            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/jvm/P25/Test1;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               MethodParameters
  #19 = Utf8               SourceFile
  #20 = Utf8               Test1.java
  #21 = NameAndType        #7:#8          // "<init>":()V
  #22 = Class              #29            // java/lang/System
  #23 = NameAndType        #30:#31        // out:Ljava/io/PrintStream;
  #24 = Utf8               hello world
  #25 = Class              #32            // java/io/PrintStream
  #26 = NameAndType        #33:#34        // println:(Ljava/lang/String;)V
  #27 = Utf8               com/jvm/P25/Test1
  #28 = Utf8               java/lang/Object
  #29 = Utf8               java/lang/System
  #30 = Utf8               out
  #31 = Utf8               Ljava/io/PrintStream;
  #32 = Utf8               java/io/PrintStream
  #33 = Utf8               println
  #34 = Utf8               (Ljava/lang/String;)V
{
  public com.jvm.P25.Test1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 4: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/jvm/P25/Test1;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String hello world
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 6: 0
        line 7: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
    MethodParameters:
      Name                           Flags
      args
}
SourceFile: "Test1.java"

P26 26_方法区_运行时常量池

  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
  • 运行时常量池,常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。

P27 27_StringTable_面试题

image.png

Java高级进阶——书籍推荐

1 总结1

1.1 四大名著之《Java核心技术(第10版)》

提起Java入门必读,四大名著是少不了的。《Java 核心技术》就是其中之一,分卷一、卷二两册,卷一讲基础,卷二为进阶。全书对于Java语言的核心概念、语法、重要特性和开发方法讲解地非常细致,可以说是一部有关Java的百科全书。

1.2 四大名著之《EffectiveJava中文版(第 2 版)》

“神书”、“整本书都是精华”、“内容永不过时”、“Java 程序员必读”……这些标签都是属于《Effective Java中文版》的。

这本书不是讲理论基础的,而是讲实战的。书中介绍了78条极具实用价值的经验规则,涵盖了大多数开发人员每天所面临的问题的解决方案,并且通过代码例子进一步进行佐证,让你知其然,也知其所以然。

哪怕不是Java程序员,也能买来看看,书中绝妙的编程观是跨语言、跨平台的。

1.3 四大名著之《Java 编程思想(第 4 版)》

这同样是一本Java领域的经典书籍,从Java编程的基础知识点开始讲起,慢慢深入,一直到Java的高级特性。作者用了大量通俗易懂的代码对理论进行佐证,让读者在实际操作中可以深入理解操作符、枚举类型、接口等相关知识。

这是一本被广大读者评价为“不管什么时候阅读都会有收获”的书,不论你是否刚接触 Java,都可以在阅读中获得启迪。

1.4 《深入理解Java虚拟机:JVM 高级特性与最佳实践(第 2 版)》

用Java却不懂JVM是说不过去的,但市面上有关JVM 的书很少,这本书更是少有的国内讲解此方面备受好评的书,甚至可以说是JVM书籍最佳的读物之一。

本书整体内容更偏向实战,难易适中。从内存管理、执行子系统、程序编译与优化、高效并发等核心主题讲解JVM。第2版根据JDK1.7进行了内容升级,整体上不光适合入门,而且对于已经接触JVM的人来说,也会常读常新。

1.5 《Java语言程序设计(基础篇)(原书第 10 版)》

这应该是很多程序员,尤其是Java程序员倍感亲切的书,因为这本书可以说是他们当中很多人的入门初心。

本书从最基本的程序理论知识出发,哪怕你完全没有Java基础,也可以通过这本书了解Java基本的程序设计、语言结构、面对对象程序设计等知识。书中还配了大量的代码和课后习题,让读者在了解有关Java的基础知识的时候,也能进行实践深入理解。

非常适合零基础、Java 和编程爱好者来读。

1.6 《Java 高并发编程详解:多线程与架构设计》

全书主要分为四个部分:头一部分主要阐述Thread的基础知识;第二部分引入了 ClassLoader; 第三部分详细、深入地介绍volatile关键字的语义;第四部分也是最重要的一部分,站在程序架构设计的角度深入讲解了如何设计高效灵活的多线程应用程序。

2 总结2——史上最全的Java进阶书籍推荐

史上最全的Java进阶书籍推荐

3 总结3——阿里大牛都在读的10本Java实战书籍,Java开发进阶必备书单

阿里大牛都在读的10本Java实战书籍,Java开发进阶必备书单

Java高级进阶——多线程并发

0 相关书籍

0.1 JAVA并发编程实战

1 Java中的多线程你只要看这一篇就够了

www.jianshu.com/p/40d4c7aeb…

相关知识点

谈谈Java线程有几种状态

用多线程只有一个目的,那就是更好地利用cpu的资源,因为所有的多线程代码都可以用单线程来实现。说这个话其实只有一半对,因为反应“多角色”的程序代码,最起码每个角色要给他一个线程吧,否则连实际场景都无法模拟,当然也没法说能用单线程来实现:比如最常见的“生产者,消费者模型”。

很多人都对其中的一些概念不够明确,如同步、并发等等,让我们先建立一个数据字典,以免产生误会。

  • 多线程:指的是这个程序(一个进程)运行时产生了不止一个线程。
  • 并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
  • 并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有共用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反映这个系统的处理能力。

image.png

  • 线程安全:经常用来描述一段代码。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存,cpu是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果,如不加事务的转账代码。
void transferMoney(User from, User to, float amount){
    to.setMoney(to.getBalance() + amount);
    from.setMoney(from.getBalance() - amount);
}
  • 同步:Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。如上面的代码简单加入@synchronized关键字。在保证结果准确的同时,提高性能,才是优秀的程序。线程安全地优先级高于性能。

好了,让我们开始吧。我准备分成几部分来总结涉及到多线程的内容:

1、扎好马步:线程的状态

2、内功心法:每个对象都有的方法(机制)

3、太祖长拳:基本线程类

4、九阴真经:高级多线程控制类

扎好马步:线程的状态

先来两张图:

线程状态

线程状态转换

各种状态一目了然,值得一提的是“Blocked”和“Waiting”这两个状态的区别:

  • 线程在Running的过程中可能会遇到阻塞(Blocked)情况,对Running状态的线程加同步锁(Synchronized)使其进入(lock blocked pool),同步锁被释放进入可运行状态(Runnable)。从jdk源码注释来看,blocked指的是对monitor的等待(可以参考下文的图)即该线程位于等待区。

  • 线程在Running的过程中可能会遇到等待(Waiting)情况,线程可以主动调用object.wait或者sleep,或者join(join内部调用的是sleep,所以可看成sleep的一种)进入。从jdk源码注释来看,waiting是等待另一个线程完成某一个操作,如join等待另一个完成执行,object.wait()等待object.notify()方法执行。

Waiting状态和Blocked状态有点费解,我个人的理解是:Blocked其实也是一种wait,等待的是monitor,但是和Waiting状态不一样,举个例子,有三个线程进入了同步块,其中两个调用了object.wait(),进入了waiting状态,这时第三个调用了object.notifyAll(),这时候前两个线程就一个转移到了Runnable,一个转移到了Blocked。

从下文的monitor结构图来区别:每个Monitor在某个时刻,只能被一个线程拥有,该线程就是“Active Thread”,而其它线程都是“Waiting Thread”,分别在两个队列“Entry Set”和“Wait Set”里面等候。在"Entry Set"中等待的线程状态Blocked,从jstack的dump中来看是“Waiting for monitor entry”,而在“Wait Set”中等待的线程状态是Waiting,表现在jstack的dump中是“in Object.wait()”。

此外,在runnable状态的线程是处于被调度的线程,此时的调度顺序是不一定的。Thread类中的yield方法可以让一个running状态的线程转入runnable。

2 视频学习资料-黑马程序员全面深入学习Java并发编程,JUC并发编程全套教程

www.bilibili.com/video/BV16J…

3 视频学习资料-【黑马程序员】详解Java并发编程、多线程原理【配套源码+笔记】

www.bilibili.com/video/BV1EN…

学习记录:已看P74

P3 预备知识

image.png

image.png

image.png

P5 进程线程概念

image.png

image.png

上下文切换:当计算机内存有限,当一个任务不运行时,其它任务需要运行,但是内存不够了,cpu可以把暂时不用的任务与代码停掉,切换到其它进程和程序

P6 并行并发概念

并发

image.png

image.png

并行

image.png

image.png

image.png

image.png

P7 线程应用-异步调用

image.png

P8 线程应用-提升效率

image.png

P9 线程应用——提升效率-验证(需要课程资料和示例代码)

image.png

P10 线程应用——提升效率——小结

image.png

P11 创建线程——方法1

image.png

P12 创建线程——方法2

image.png

P13 创建线程——lambda简化

image.png

P14 创建线程——方法1,2-原理

image.png

P15 创建线程——方法3

image.png

P16 线程运行——现象

image.png

P17 线程运行-查看和杀死-windows

image.png

image.png

P19 线程运行-jconsole(暂时还没实践)

image.png

P20 线程运行原理——栈帧debug

image.png

P21 线程运行原理——栈帧图解

image.png

P22 线程运行原理——多线程

P23 线程运行原理——上下文切换

image.png

image.png

P24 常见方法-概述

image.png

image.png

P26 常见方法-sleep_状态

image.png

P31 常见方法-sleep应用

image.png

P32 常见方法-join

image.png

image.png

P33 常见方法-join-同步应用

image.png

示例代码:

@Slf4j(topic="c.TestJoin")
public class TestJoin {
    static int r=0;
    static int r1=0;
    static int r2=0;

    public static void main(String[] args) throws InterruptedException{
        test2();
    }

    private static void test2() throws InterruptedException{
        Thread t1=new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r1=10;
        });

        Thread t2=new Thread(()->{
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r2=20;
        });

        long start=System.currentTimeMillis();
        t1.start();
        t2.start();
        log.debug("join begin");
        t1.join();
        log.debug("t1 join end");
        t2.join();
        log.debug("t2 join end");
        long end=System.currentTimeMillis();
        log.debug("r1: {} r2: {} cost: {}",r1,r2,end-start);
    }
}

代码执行流程(两种情况,t1先执行和t2先执行):

image.png

P34 03.024-常见方法-join-限时同步

P35 03.025-常见方法-interrupt-打断阻塞

image.png

P36 03.026-常见方法-interrupt-打断正常

打断正常运行的线程,不会清空打断状态。

P37 03.027-设计模式-两阶段终止-interrupt

image.png

P38 03.028-设计模式-两阶段终止-interrupt分析

image.png

P41 03.031-常见方法-interrupt-打断park

image.png

@Slf4j(topic="c.TestInterruptPark")
public class TestInterruptPark {
    public static void main(String[] args) throws InterruptedException{
        test3();
    }

    private static void test3() throws InterruptedException{
        Thread t1=new Thread(()->{
            log.debug("park...");
            LockSupport.park();
            log.debug("unpark...");
            log.debug("打断状态:{}",Thread.currentThread().isInterrupted());

            LockSupport.park();
            log.debug("unpark...");
        },"t1");
        t1.start();

        Thread.sleep(1000);
        t1.interrupt();
    }
}

LockSupport.park()的作用也是让当前线程停下来,如果LockSupport.park()被interrupt方法打断,而且线程打断状态为true,那么再次调用LockSupport.park()是失效的,除非将LockSupport.park()设置为false,可以调用线程的interrupted()方法来实现。

P42 03.032-常见方法-过时方法

image.png

P43 03.033-常见方法-守护进程

image.png

image.png

P44 03.034-线程状态-五种

image.png

image.png

P45 03.035-线程状态-六种

image.png

image.png

P47 03.037-习题-应用之统筹-分析

image.png

P48 03.038-习题-应用之统筹-实现

解法1:

@Slf4j(topic="c.Test1")
public class Test1 {
    public static void main(String[] args){
        Thread t1=new Thread(()->{
            log.debug("洗水壶");
            sleep(1);
            log.debug("烧开水");
            sleep(5);
        },"老王");

        Thread t2=new Thread(()->{
            log.debug("洗茶壶");
            sleep(1);
            log.debug("洗茶杯");
            sleep(2);
            log.debug("拿茶叶");
            sleep(1);

            try {
                t1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("泡茶");
        },"小王");

        t1.start();
        t2.start();
    }
}

image.png

P49 03.039-第三章小结

image.png

P50 04.001-本章内容

image.png

image.png

P51 04.002-线程安全问题

image.png

image.png

image.png

P52 04.003-上下文切换-分析

image.png

image.png

image.png

image.png

出现正数的情况:

image.png

P53 04.004-临界区与竞态条件

image.png

image.png

P54 04.005-上下文切换-synchronized-解决

image.png

image.png

P55 04.006-上下文切换-synchronized-理解

image.png

P56 04.007-上下文切换-synchronized-理解

image.png

image.png

P57 04.008-上下文切换-synchronized-思考

image.png

P59 04.010-synchronized-加在方法上

image.png

image.png

P60 04.011-synchronized-加在方法上-习题1~2

image.png

image.png

P61 04.012-synchronized-加在方法上-习题3~4

image.png

image.png

P62 04.013-synchronized-加在方法上-习题5~8

image.png

image.png

image.png

image.png

P63 04.014-线程安全分析

image.png

P64 04.015-线程安全分析-局部变量

image.png

image.png

P65 04.016-线程安全分析-局部变量引用

image.png

image.png

image.png

image.png

image.png

image.png

P66 04.017-线程安全分析-局部变量-暴露引用

image.png

image.png 子类重写了method3方法,启用新线程调用list的remove方法,就会造成list被多个线程共享,造成线程不安全。

P67 04.018-线程安全分析-常见类-组合调用

image.png

image.png

image.png

P68 04.019-线程安全分析-常见类-不可变

image.png

P69 04.020-线程安全分析-实例分析1~3

image.png

image.png

image.png

P70 04.021-线程安全分析-实例分析4~7

image.png

image.png Connection这样的用法是线程不安全的。

image.png

image.png

image.png

P71 04.022-习题-卖票-读题

image.png

image.png

image.png

P72 04.023-习题-卖票-测试方法

image.png

P74 04.023-习题-转账

image.png

@Slf4j(topic = "c.ExerciseTransfer")
public class ExerciseTransfer {
    public static void main(String[] args) throws InterruptedException {
        Account a = new Account(1000);
        Account b = new Account(1000);
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                a.transfer(b, randomAmount());
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                b.transfer(a, randomAmount());
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        //查看转账2000次后的总金额
        log.debug("total:{}", (a.getMoney() + b.getMoney()));
    }

    //Random为线程安全
    static Random random = new Random();

    //随机1~100
    public static int randomAmount() {
        return random.nextInt(100) + 1;
    }
}

//账户
class Account {
    private int money;

    public Account(int money) {
        this.money = money;
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    //转账-线程不安全
    public void transfer(Account target, int amount) {
        if (this.money >= amount) {
            this.setMoney(this.getMoney() - amount);
            target.setMoney(target.getMoney() + amount);
        }
    }

    //转账-线程安全
//    public void transfer(Account target, int amount) {
//        synchronized (Account.class) {
//            if (this.money >= amount) {
//                this.setMoney(this.getMoney() - amount);
//                target.setMoney(target.getMoney() + amount);
//            }
//        }
//    }
}

4 如何评价《Java 并发编程艺术》这本书?

zhuanlan.zhihu.com/p/399270598

Java编程思想(第4版)

第21章 并发

到目前为止,你学到的都是有关顺序编程的知识。即程序中的所有事物在任意时刻都只能执行一个步骤。

编程问题中相当大的一部分都可以通过使用顺序编程来解决。然而,对于某些问题,如果能够并行地执行程序中的多个部分,则会变得非常方便甚至非常必要,因为这些部分要么看起来在并发地执行,要么在多处理器环境下可以同时执行。

并行编程可以使程序执行速度得到极大提高,或者为设计某些类型的程序提供更易用的模型,或者两者皆有。但是,熟练掌握并发编程理论和技术,对于到目前为止你在本书中学习到的所有知识而言,是一种飞跃,并且是通向高级主题的中介。本章只能作为一个介绍,即便融会贯通了本章的内容,也绝不意味着你就是一个优秀的并发程序员了。

正如你应该看到的,当并行执行的任务彼此开始产生互相干涉时,实际的并发问题就会接踵而至。这可能会以一种微妙而偶然的方式发生,我们可以很公正地说,并发“具有可论证的确定性,但是实际上具有不可确定性”。这就是说,你可以得出结论,通过仔细设计和代码审查,编写能够正确工作的并发程序是可能的。但是,在实际情况中,更容易发生的情况是所编写的并发程序在给定适当条件的时候,将会工作失败。这些条件可能从来都不会实际发生,或者发生的不是很频繁,以至于在测试过程中不会碰上它们。实际上,你可能无法编写出能够针对你的并发程序生成故障条件的测试代码。所产生的故障经常是偶尔发生的,并且经常是以客户抱怨的形式出现的。这是研究并发问题的最强理由:如果视而不见,你就会遭其反噬。

因此,并发看起来充满了危险,如果你对它有些畏惧,这可能是件好事。尽管Java SE5在并发方面做出了显著的改进,但是仍旧没有像编译器验证或检查型异常这样的安全网,在你犯错误的时候告知你。使用并发时,你得自食其力,并且只有变得多疑而自信,才能用Java编写出可靠的多线程代码。

21.1 并发的多面性

并发编程令人困惑的一个主要原因是:使用并发时需要解决的问题有多个,而实现并发的方式也有多种,并且在这两者之间没有明显的映射关系(而且通常只具有模糊的界线)。因此,你必须理解所有这些问题和特例,以便有效地使用并发。

用并发解决问题大体上可以分为“速度”和“设计可管理性”两种。

21.1.1 更快地执行

速度问题初听起来很简单:如果你想要一个程序运行得更快,那么可以将其断开为多个片段,在单独的处理器上运行每个片段。并发时用于多处理器编程的基本工具。当前,Moore定律已经有些过时了(至少对于传统芯片是这样),速度提高是以多核处理器的形式而不是更快的芯片的形式出现的。为了使程序运行得更快,你必须学习如何利用这些额外的处理器,而这正是并发赋予你的能力。

如果你有一台多处理器的机器,那么就可以在这些处理器之间分布多个任务,从而可以极大地提高吞吐量。这是使用强有力的多处理器Web服务器的常见情况,在为每个请求分配一个线程的程序中,它可以将大量的用户请求分不到多个CPU上。

但是,并发通常是提高运行在单处理器上的程序的性能。

这听起来有些违背直觉。如果你仔细考虑一下就会发现,在单处理器上运行的并发程序开销确实应该比该程序的所有部分都顺序执行的开销大,因为其中增加了所谓上下文切换的代价(从一个任务切换到另一个任务)。表面上看,将程序的所有部分当做单个的任务运行好像是开销更小一些,并且可以节省上下文切换的代价。

使这个问题变得有些不同的是阻塞。如果程序中的某个任务因为该程序控制范围之外的某些条件(通常是I/O)而导致不能继续执行,那么我们就说这个任务或线程阻塞了。如果没有并发,则整个程序都将停止下来,直至外部条件发生变化。但是,如果使用并发来编写程序,那么当一个任务阻塞时,程序中的其他任务还可以继续执行,因此这个程序可以保持继续向前执行。事实上,从性能的角度看,如果没有任务会阻塞,那么在单处理器机器上使用并发就没有任何意义。

在单处理器系统中的性能提高的常见示例是事件驱动的编程。实际上,使用并发最吸引人的一个原因就是要产生具有可响应的用户界面。考虑这样一个程序,它因为将执行某些长期运行的操作,所以最终用户输入会被忽略,从而成为不可响应的程序。如果有一个“退出”按钮,那么你肯定不想在你写的每一段代码中都检查它的状态。因为这会产生非常尴尬的代码,而我们也无法保证程序员不会忘记这种检查。如果不使用并发,则产生可响应用户界面的唯一方式就是所有的任务都周期性地检查用户输入。通过创建单独的执行线程来响应用户的输入,即使这个线程在大多数时间里都是阻塞的,但是程序可以保证具有一定程序的可响应性。

程序需要连续执行它的操作,并且同时需要返回对用户界面的控制,以便使程序可以响应用户。但是传统的方法在连续执行其操作的同时,返回对程序其余部分的控制。事实上,这听起来就像是不可能之事,好像CPU必须同时位于两处一样,但是这完全是并发造成的一种错觉(在多处理器系统中,这就不只是一种幻觉了)。

实现并发最直接的方式是在操作系统级别使用进程。进程是运行在它自己的地址空间内的自包容的程序。多任务操作系统可以通过周期性地将CPU从一个进程切换到另一个进程,来实现同时运行多个进程(程序),尽管这使得每个进程看起来在其执行过程中都是歇歇停停。进程总是很吸引人,因为操作系统通常会将进程互相隔离开,因此它们不会彼此干涉,这使得用进程编程相对容易一些。与此相反的是,像Java所使用的这种并发系统会共享注入内存和I/O这样的资源,因此编写多线程程序最基本的困难在于协调不同线程驱动的任务之间对这些资源的使用,以使得这些资源不会同时被多个任务访问。

这里有一个利用操作系统进程的简单示例。在编写本书时,我会有规律地创建本书当前状态的多个冗余备份副本。我会在本地目录中保存一个副本,在记忆棒上保存一个副本,在Zip盘上保存一个副本,还会在远程FTP站点上保存一个副本。为了自动化这个过程,我还编写了一个小程序(用Python写的,但是其概念是相同的),它会把本书压缩成一个文件,其文件名中带有版本号,然后执行复制操作。最初,我会顺序执行所有复制操作,在启动下一个复制操作之前先等待前一个操作的完成。但随后我意识到,每个复制操作会依存储介质I/O速度的不同而花费不同的时间。既然我在使用多任务操作系统,那就可以将每个复制操作当作单独的进程来启动,并让它们并行地运行,这样可以加速整个程序的执行速度。当一个进程受阻时,另一个进程可以继续向前运行。

这是并发的理想示例。每个任务都作为进程在其自己的地址空间中执行,因此任务之间根本不可能互相干涉。更重要的是,对进程来说,它们之间没有任何彼此通信的需求,因为它们都是完全独立的。操作系统会处理确保文件正确复制的所有细节,因此,不会有任何风险,你可以获得更快的程序,并且完全免费。

有些人走的更远,提倡将进程作为唯一合理的并发方式,但遗憾的是,对进程通常会有数量和开销的限制,以避免它们在不同的并发系统之间的可应用性。

某些编程语言被设计为可以将并发任务彼此隔离,这些语言通常被称为函数型语言,其中每个函数调用都不会产生任何副作用(并因此而不能干涉其它函数),并因此可以当作独立的任务来驱动。Erlang就是这样的语言,它包含针对任务之间彼此通信的安全机制。如果你发现程序中某个部分必须大量使用并发,并且你在试图构建这个部分时碰到了过多的问题,那么你可以考虑使用Erlang这类专门的并发语言来创建这个部分。

Java采取了更加传统的方式,在顺序型语言的基础上提供对线程的支持。与在多任务操作系统中交叉外部进程不同,线程机制是在由执行程序表示的单一进程中创建任务。这种方式产生的一个好处是操作系统的透明性,这对Java而言,是一个重要的设计目标。例如,在OSX之前的Macintosh操作系统版本(Java第一个版本的一个非常重要的目标系统)不支持多任务,因此,除非在Java中添加多线程机制,否则任何并发的Java程序都无法移植到Macintosh和类似的平台之上,这样就会打破“编写一次,到处运行”的要求。

21.1.2 改进代码设计

在单CPU机器上使用多任务的程序在任意时刻仍旧只在执行一项工作,因此从理论上讲,肯定可以不用任何任务而编写出相同的程序。但是,并发提供了一个重要的组织结构上的好处:你的程序设计可以极大地简化。某些类型的问题,例如仿真,没有并发的支持是很难解决的。

大多数人都看到过至少一种形式的仿真,例如计算机游戏或电影中计算机生成的动画。仿真通常涉及许多交互式元素,每一个都有“其自己的想法”。尽管你可能注意到了这一点,但是在单处理器机器上,每个仿真元素都是由这个处理器驱动执行的,从编程的角度看,模拟每个仿真元素都有其自己的处理器并且都是独立的任务,这种方式要容易的多。

完整的仿真可能涉及非常大量的任务,这与仿真中的每个元素都可以独立动作这一事实相对应——这其中包含门和岩石,而不仅仅只是精灵和巫师。多线程系统对可用的线程数量的限制通常都会是一个相对较小的数字,有时就是数十或数百这样的数量级。这个数字在程序控制范围之外可能会发生变化——它可能依赖于平台,或者在Java中,依赖于Java的版本。在Java中,通常要假定你不会获得足够的线程,从而使得可以为大型仿真中的每个元素都提供一个线程。

解决这个问题的典型方式是使用协作多线程。Java的线程机制是抢占式的,这表示调度机制会周期性地中断线程,将上下文切换到另一个线程,从而为每个线程都提供时间片,使得每个线程都会分配到数量合理的时间去驱动它的任务。在协作式系统中,每个任务都会自动地放弃控制,这要求程序员要有意识地在每个任务中插入某种类型的让步语句。协作式系统的优势是双重的:上下文切换的开销通常要比抢占式系统要低廉许多,并且对可以同时执行的线程数量在理论上没有任何限制。当你处理大量的仿真元素时,这可以是一种理想的解决方案。但是注意,某些协作式系统并未设计为可以在多个处理器之间分布任务,这可能会非常受限。

在另一个极端,当你用流行的消息系统工作时,由于消息系统涉及分布在整个网络中的多台独立的计算机,因此并发就会成为一种非常有用的模型,因为它是实际发生的模型。在这种情形中,所有的进程都彼此完全独立地运行,甚至没有任何可能去共享资源。但是,你仍旧必须在进程间同步信息,使得整个消息系统不会丢失信息或在错误的时刻混进信息。即使你没有打算在眼前大量使用并发,理解并发也会很有用,因为你可以掌握基于消息机制的架构,这些架构在创建分布式系统时是更主要的方式。

并发需要付出代价,包含复杂性代价,但是这些代价与在程序设计、资源负载均衡以及用户方便使用方面的改进相比,就显得微不足道了。通常,线程使你能够创建更加松散耦合的设计,否则,你的代码中各个部分都必须显式地关注那些通常可以由线程来处理的任务。

21.2 基本的线程机制

并发编程使我们可以将程序分为多个分离的、独立运行的任务。通过使用多线程机制,这些独立任务(也被称为子任务)中的每一个都将由执行线程来驱动。一个线程就是在进程中的一个单一的顺序控制流,因此,单个进程可以拥有多个并发执行的任务,但是你的程序使得每个任务都好像有其自己的CPU一样。其底层机制是切分CPU时间,但通常你不需要考虑它。

线程模型为编程带来了便利,它简化了在单一程序中同时交织在一起的多个操作的处理。在使用线程时,CPU将轮流给每个任务分配其占用时间。每个任务都觉得自己在一直占用CPU,但事实上CPU时间是划分成片段分配给了所有任务(例外情况是程序确实运行在多个CPU之上)。线程的一大好处是可以使你从这个层次抽身出来,即代码不必知道它是运行在具有一个还是多个CPU的机器上。所以,使用线程机制是一种建立透明的、可扩展的程序的方法,如果程序运行的太慢,为机器增添一个CPU就能很容易地加快程序的运行速度。多任务和多线程往往是使用多处理器系统的最合理方式。

21.2.1 定义任务

线程可以驱动任务,因此你需要一种描述任务的方式,这可以由Runnable接口来提供。要想定义任务,只需实现Runnable接口并编写run()方法,使得该任务可以执行你的命令。例如,下面的LiftOff任务将显示发射之前的倒计时:

public class LiftOff implements Runnable{
   protected int countDown=10; //Default
   private static int taskCount=0;
   private final int id=taskCount++;
   public LiftOff(){}
   
   public LiftOff(int countDown){
     this.countDown=countDown;
   }
   public String status(){
     return "#"+id+"("+(countDown>0?countDown:"Liftoff!")+").";
   }
   public void run(){
     while(countDown-->0){
       System.out.print(status());
       Thread.yield();
     }
   }
}

标识符id可以用来区分任务的多个实例,它是final的,因为它一旦被初始化之后就不希望被修改。

任务的run()方法通常总会有某种形式的循环,使得任务一直运行下去直到不再需要,所以要设定跳出循环的条件(有一种选择是直接从run()返回)。通常,run()被写成无限循环的形式,这就意味着,除非有某个条件使得run()终止,否则它将运行下去。(在本章后面将会看到如何安全地终止线程)。

在run()中对静态方法Thread.yield()的调用是对线程调度器(Java线程机制的一部分,可以将CPU从一个线程转移给另一个线程)的一种建议,它在声明:“我已经执行完生命周期中最重要的部分了,此刻正是切换给其它任务执行一段时间的大好时机”。这完全是选择性的,但是这里使用它是因为它会在这些示例中产生更加有趣的输出:你更有可能会看到任务换进换出的证据。

在下面的实例中,这个任务的run()不是由单独的线程驱动的,它是在main()中直接调用的(实际上,这里仍旧使用了线程,即总是分配给main()的那个线程):

// concurrency/MainThread.java

public class MainThread{
  public static void main(String[] args){
    LiftOff launch=new LiftOff();
    launch.run();
  }
}

当从Runnable导出一个类时,它必须具有run()方法,但是这个方法并无特殊之处——它不会产生任何内在的线程能力。要实现线程行为,你必须显式地将一个任务附着到线程上。

21.2.2 Thread类

将Runnable对象转变为工作任务的传统方式是把它提交给一个Thread构造器,下面的示例展示了如何使用Thread来驱动LiftOff对象:

// concurrency/BasicThreads.java
// The most basic use of the Thread class
public class BasicThreads{
  public static void main(String[] args){
    Thread t=new Thread(new LiftOff());
    t.start();
    System.out.println("Waiting for LiftOff");
  }
}

Thread构造器只需要一个Runnable对象。调用Thread对象的start()方法为该线程执行必需的初始化操作,然后调用Runnable的run()方法,以便在这个新线程中启动该任务。尽管start()看起来是产生了一个对长期运行方法的调用,但是从输出中可以看到,start()迅速地返回了,因为Waiting for LiftOff消息在倒计时完成之前就出现了。实际上,你产生的是对LiftOff.run()方法的调用,并且这个方法还没有完成,但是因为LiftOff.run()是由不同的线程执行的,因此你仍旧可以执行main()线程中的其它操作(这种能力并不局限于main()线程,任何线程都可以启动另一个线程)。因此,程序会同时运行两个方法,main()和LiftOff.run()是程序中与其它线程“同时”执行的代码。

你可以很容易地添加更多的线程去驱动更多的任务。下面,你可以看到所有任务彼此之间是如何互相呼应的:

// concurrency/MoreBasicThreads.java
// Adding more threads
public class MoreBasicThreads{
  public static void main(String[] args){
    for(int i=0;i<5;i++)
      new Thread(new LiftOff()).start();
    System.out.println("Waiting for LiftOff");
  }
}

输出说明不同任务的执行在线程被换进换出时混在了一起。这种交换是由线程调度器自动控制的。如果在你的机器上有多个处理器,线程调度器将会在这些处理器之间默默地分发线程。

这个程序一次运行的结果可能与另一次运行的结果不同,因为线程调度机制是非确定性的。事实上,你可以看到,在某个版本的JDK与下个版本之间,这个简单程序的输出会产生巨大的差异。例如,较早的JDK不会频繁对时间切片,因此线程1可能会首先循环到尽头,然后线程2会经历其所有循环,等等。这实际上与调用一个例程去同时执行所有的循环一样,只是启动所有线程的代价要更加高昂。较晚的JDK看起来会产生更好的时间切片行为,因此每个线程看起来都会获得更加正规的服务。通常,Sun并未提及这些种类的JDK的行为变化,因此你不能依赖于任何线程行为的一致性。最好的方式是在编写使用线程的代码时,尽可能地保守。

当main()创建Thread对象时,它并没有捕获任何对这些对象的引用。在使用普通对象时,这对于垃圾回收来说是一场公平的游戏,但是在使用Thread时,情况就不同了。每个Thread都“注册”了它自己,因此确实有一个对它的引用,而且在它的任务退出其run()并死亡之前,垃圾回收器无法清除它。你可以从输出中看到,这些任务确实运行到了结束,因此,一个线程会创建一个单独的执行线程,在对start()的调用完成之后,它仍旧会继续存在。

21.2.3 使用Executor

Java SE5的java.util.concurrent包中的执行器(Executor)将为你管理Thread对象,从而简化了并发编程。Executor在客户端和任务执行之间提供了一个间接层;与客户端直接执行任务不同,这个中介对象将执行任务。Executor允许你管理异步任务的执行,而无需显式地管理线程的生命周期。Executor在Java SE5/6中是启动任务的优选方法。

我们可以使用Executor来代替在MoreBasicThreads.java中显式地创建Thread对象。LiftOff对象知道如何运行具体的任务,与命令设计模式一样,它暴露了要执行的单一方法。ExecutorService(具有服务生命周期的Executor,例如关闭)知道如何构建恰当的上下文来执行Runnable对象。在下面的示例中,CachedThreadPool将为每个任务都创建一个线程。注意:ExecutorService对象是使用静态的Executor方法创建的,这个方法可以确定其Executor类型:

//: concurrency/CachedThreadPool.java

import java.util.concurrent.*;

public class CachedThreadPool{
  public static void main(String[] args){
    ExecutorService exec=Executors.newCachedThreadPool();
    for(int i=0;i<5;i++)
      exec.execute(new LiftOff());
    exec.shutdown();
  }
}

非常常见的情况是,单个的Executor被用来创建和管理系统中所有的任务。

对shutdown()方法的调用可以防止新任务被提交给这个Executor,当前线程(在本例中,即驱动main()的线程)将继续运行在shutdown()被调用之前提交的所有任务。这个程序将在Executor中的所有任务完成之后尽快退出。

你可以很容易地将前面示例中的CachedThreadPool替换为不同类型的Executor。FixedThreadPool使用了有限的线程集来执行所提交的任务:

// concurrency/FixedThreadPool.java

import java.util.concurrent.*;

public class FixedThreadPool{
  public static void main(String[] args){
    ExecutorService exec=Executors.newFixedThreadPool(5);
    for(int i=0;i<5;i++)
      exec.execute(new LiftOff());
    exec.shutdown();
  }
}

有了FixedThreadPool,你就可以一次性预先执行代价高昂的线程分配,因而也就可以限制线程的数量了。这可以节省时间,因为你不用为每个任务都固定地付出创建线程的开销。在事件驱动的系统中,需要线程的事件处理器,通过直接从池中获取线程,也可以如你所愿地尽快得到服务。你不会滥用可获得的资源,因为FixedThreadPool使用的Thread对象的数量是有界的。

注意,在任何线程池中,现有线程在可能的情况下,将会被自动复用。

尽管本书将使用CachedThreadPool,但是也应该考虑在产生线程的代码中使用FixedThreadPool。CachedThreadPool在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程,因此它是合理的Executor的首选。只有当这种方式会引发问题时,你才需要切换到FixedThreadPool。

SingleThreadExecutor就像是线程数量为1的FixedThreadPool。这对于你希望在另一个线程中连续运行的任何事物(长期存活的任务)来说,都是很有用的,例如监听进入的套接字连接的任务。它对于希望在线程中运行的短任务也同样很方便,例如,更新本地或远程日志的小任务,或者是事件分发线程。

如果向SingleThreadExecutor提交了多个任务,那么这些任务将排队,每个任务都会在下一个任务开始之前运行结束,所有的任务将使用相同的线程。在下面的示例中,你可以看到每个任务都是按照它们被提交的顺序,并且是在下一个任务开始之前完成的。因此,SingleThreadExecutor会序列化所有提交给它的任务,并会维护它自己(隐藏)的悬挂任务队列。

// concurrency/SingleThreadExecutor.java
import java.util.concurrent.*;

public class SingleThreadExecutor{
  public static void main(String[] args){
    ExecutorService exec=Executors.newSingleThreadExecutor();
    for(int i=0;i<5;i++)
      exec.execute(new LiftOff());
    exec.shutdown();
  }
}

作为另一个示例,假设你有大量的线程,那它们运行的任务将使用文件系统。你可以用SingleThreadExecutor来运行这些线程,以确保任意时刻在任何线程中都只有唯一的任务在运行。在这种方式中,你不需要在共享资源上处理同步(同时不会过度使用文件系统)。有时更好的解决方案是在资源上同步(你将在本章稍后学习),但是SingleThreadExecutor可以让你省去只是为了维持某些事物的原型而进行的各种协调努力。通过序列化任务,你可以消除对序列化对象的需求。

21.2.4 从任务中产生返回值

Runnable是执行工作的独立任务,但是它不返回任何值。如果你希望任务在完成时能够返回一个值,那么可以实现Callable接口而不是Runnable接口。在Java SE5中引入的Callabel是一种具有类型参数的泛型,它的类型参数表示的是从方法call()(而不是run())中返回的值,并且必须使用ExecutorService.submit()方法调用它,下面是一个简单示例:

// concurrency/CallableDemo.java
import java.util.concurrent.*;
import java.util.*;

class TaskWithResult implements Callable<String>{
  private int id;
  public TaskWithResult(int id){
    this.id=id;
  }
  public String call(){
    return "result of TaskWithResult "+id;
  }
}

public class CallableDemo {
    public static void main(String[] args){
        ExecutorService exec= Executors.newCachedThreadPool();
        ArrayList<Future<String>> results=new ArrayList<Future<String>>();
        for(int i=0;i<10;i++)
            results.add(exec.submit(new TaskWithResult(i)));
        for(Future<String> fs:results)
            try{
                // get() blocks until completion;
                System.out.println(fs.get());
            }catch (InterruptedException e){
                System.out.println(e);
                return;
            }catch (ExecutionException e){
                System.out.println(e);
            }finally{
                exec.shutdown();
            }
    }
}

submit()方法会产生Future对象,它用Callable返回结果的特定类型进行了参数化看。你可以用你isDone()方法来查询Future是否已经完成。当任务完成时,它具有一个结果,你可以调用get()方法来获取该结果。你也可以不用isDone()进行检查就直接调用get(),在这种情况下,get()将阻塞,直至结果准备就绪。你还可以在试图调用get()来获取结果之前,先调用具有超时的get(),或者调用isDone()来查看任务是否完成。

21.3 共享受限资源

21.4 终结任务

21.5 线程之间的协作

21.6 死锁

21.7 新类库中的构件

21.8 仿真

21.9 性能调优

21.10 活动对象

21.11 总结

深入学习MySQL

数据库进阶——HBase

c.biancheng.net/view/6499.h…

马士兵:并发编程底层笔记

1 计算机的组成

image.png

  • PC:Program Counter,程序计数器,存储指令。
  • Registers:寄存器,存储数据。
  • ALU:数学逻辑计算单元,做计算。

2 cpu和内存之间的三级缓存

image.png

image.png

3 cache line

image.png

缓存一致性协议

4 缓存行填充的编程技巧

image.png

计算机网络微课堂(有字幕无背景音乐版)学习笔记

P1 1.1 计算机网络在信息时代的作用

image.png

P2 1.2 因特网概述

image.png

image.png

image.png

image.png

image.png

image.png

image.png

面试题汇总(公司)

1 面试提纲

1、自我介绍

2、技术问题: 在项目中的角色,使用了哪些技术栈, linux熟练程度 docker的熟练程度 nginx的使用,负载均衡 springcloud的熟练程度,和springboot的区别 redis的作用 mysql如何实现高可用性 nas+nfs 了解过oss没,oss和nas的区别 什么是Web Service(Web服务)?介绍一下你了解的Java领域的Web Service框架 对于用户权限管理了解的怎么样,用过什么框架没 假如你的项目出现性能瓶颈了,你觉得可能会是哪些方面,怎么解决问题 解决mybatis在插入大量数据时的内存溢出问题

3、有没有自己开发过什么小工具或者小网站

2、你为什么要离开原来的公司

3、你对于我们公司了解多少

4、你对加班的看法

5、出差是否接受

6、薪资要求

2 网络收藏

常见docker面试问题

Java知识点整理:Spring、MySQL

Java工程师面试时经常会被问到的66个问题