docker镜像

spring-boot项目image
  1. 创建一个Spring Boot项目

  2. 写一个controller

    @RestController    
    public class DockerController {
    @GetMapping("/dockerfile")
    @ResponseBody
    String dockerfile() {
    return "hello docker" ;
    }
    }
  3. mvn clean package打成一个jar包 在target下找到”dockerfile-demo-0.0.1-SNAPSHOT.jar”

  4. 在docker环境中新建一个目录”first-dockerfile”

  5. 上传”dockerfile-demo-0.0.1-SNAPSHOT.jar”到该目录下,并且在此目录创建Dockerfile

  6. 创建Dockerfile文件,编写内容

    FROM openjdk:8    

    MAINTAINER itcrazy2016

    LABEL name="dockerfile-demo" version="1.0" author="itcrazy2016"

    COPY dockerfile-demo-0.0.1-SNAPSHOT.jar dockerfile-image.jar

    CMD ["java","-jar","dockerfile-image.jar"]
  7. 基于Dockerfile构建镜像

    docker build -t test-docker-image .
  8. 基于image创建container

    docker run -d --name user01 -p 6666:8080 test-docker-image
  9. 查看启动日志

    docker logs user01
  10. 宿主机上访问

    curl localhost:6666/dockerfile
  11. 还可以再次启动一个

    docker run -d --name user02 -p 8081:8080 test-docker-image
镜像仓库
docker hub
1. 访问 hub.docker.com 申请用户名和密码。

2. 在docker机器上登录
docker login

3. 输入用户名和密码

4. docker push itcrazy2018/test-docker-image
[注意镜像名称要和docker id一致,不然push不成功]

5. 给image重命名,并删除掉原来的
docker tag test-docker-image itcrazy2018/test-docker-image
docker rmi -f test-docker-image

6. 再次推送,刷新hub.docker.com后台,发现成功

7. 别人下载,并且运行
docker pull itcrazy2018/test-docker-image
docker run -d --name user01 -p 6661:8080 itcrazy2018/test-docker-image
阿里云docker hub
1. 阿里云docker仓库
https://cr.console.aliyun.com/cn-hangzhou/instances/repositories
搭建自己的Docker Harbor
1. 访问github上的harbor项目

2. 下载版本,比如1.7.1
https://github.com/goharbor/harbor/releases

3. 找一台安装了docker-compose,上传并解压
tar -zxvf xxx.tar.gz

4. 进入到harbor目录
修改harbor.cfg文件,主要是ip地址的修改成当前机器的ip地址
同时也可以看到Harbor的密码,默认是Harbor12345

5.安装harbor,需要一些时间
sh install.sh

6. 浏览器访问,比如39.100.39.63,输入用户名和密码即可
Image常见操作
1. 查看本地image列表    
docker images
docker image ls

2. 获取远端镜像
docker pull    

3. 删除镜像[注意此镜像如果正在使用,或者有关联的镜像,则需要先处理完]
docker image rm imageid
docker rmi -f imageid
docker rmi -f $(docker image ls)     删除所有镜像

4. 运行镜像
docker run image

5. 发布镜像
docker push
container到image

既然container是基于image之上的,想想是否能够由一个container反推出image呢?肯定是可以的,比如通过docker run运行起一个container出来,这时候对container对一些修 改,然后再生成一个新的image,这时候image的由来就不仅仅只能通过Dockerfile咯。

1. 拉取一个centos image    
docker pull centos

2. 根据centos镜像创建出一个container
docker run -d -it --name my-centos centos

3. 进入my-centos容器中
docker exec -it my-centos bash

4. 输入vim命令
bash: vim: command not found

5. 我们要做的是对该container进行修改,也就是安装一下vim命令,然后将其生成一个新的centos

6. 在centos的container中安装vim
yum install -y vim

7. 退出容器,将其生成一个新的centos,名称为"vim-centos-image"
docker commit my-centos vim-centos-image

8. 查看镜像列表,并且基于"vim-centos-image"创建新的容器
docker run -d -it --name my-vim-centos vim-centos-image

9. 进入到my-vim-centos容器中,检查vim命令是否存在
docker exec -it my-vim-centos bash vim

结论 :可以通过docker commit命令基于一个container重新生成一个image,但是一般得到image的 方式不建议这么做,不然image怎么来的就全然不知咯。

container常见操作
1. 根据镜像创建容器    
docker run -d --name -p 9090:8080 my-tomcat tomcat

2. 查看运行中的
container docker ps

3. 查看所有的container[包含退出的]
docker ps -a

4. 删除container
docker rm containerid
docker rm -f $(docker ps -a)   删除所有container

5. 进入到一个container中
docker exec -it container bash

6. 根据container生成image
docker commit

7. 查看某个container的日志
docker logs container

8. 查看容器资源使用情况
docker stats

9. 查看容器详情信息
docker inspect container

10.停止/启动容器
docker stop/start container
container资源限制

如果不对container的资源做限制,它就会无限制地使用物理机的资源,这样显然是不合适的。 查看资源情况 :docker stats

  1. 内存限制

    --memory    Memory limit 

    如果不设置 --memory-swap,其大小和memory一样

    docker run -d --memory 100M --name tomcat1 tomcat
  1. cpu限制

    --cpu-shares    权重 

    docker run -d --cpu-shares 10 --name tomcat2 tomcat
  1. 图像化资源监控

    https://github.com/weaveworks/scope

    sudo curl -L git.io/scope -o /usr/local/bin/scope 

    sudo chmod a+x /usr/local/bin/scope

    scope launch 39.100.39.63
    # 停止
    scope scope stop

    # 同时监控两台机器,在两台机器中分别执行如下命令
    scope launch ip1 ip2
底层技术支持

Container是一种轻量级的虚拟化技术,不用模拟硬件创建虚拟机。 Docker是基于Linux Kernel的Namespace、CGroups、UnionFileSystem等技术封装成的一种自 定义容器格式,从而提供一套虚拟运行环境。

Namespace:用来做隔离的,比如pid[进程]、net[网络]、mnt[挂载点]等

CGroups: Controller Groups用来做资源限制,比如内存和CPU等

Union file systems:用来做image和container分层

docker安装

1.1 在Win10上准备centos7

我们的目的仅仅是要安装一个centos7,然后在centos7上安装docker

如果搞不定vagrant+virtualbox的方式,也可以直接使用VM搭建一个centos7

或者你可以直接使用一台云服务器,上面安装了centos7

毕竟我们的目的只是为了得到一个centos7的机器,所以不必花太多精力在这个问题上折腾

使用的环境是(建议尽量使用这个版本,因为vagrant和VirtualBox会出现版本冲突问题)

win10 64位
VirtualBox-6.0.12-133076-Win
vagrant_2.2.6_x86_64
centos7
XShell6

采坑指南:安装过程碰到的一些问题https://gper.club/articles/7e7e7f7ff7g58gc1g6e

1.1.1 下载安装vagrant

01 访问Vagrant官网
https://www.vagrantup.com/

02 点击Download
Windows,MacOS,Linux等

03 选择对应的版本

04 傻瓜式安装

05 命令行输入vagrant,测试是否安装成功

1.1.2 下载安装virtual box

01 访问VirtualBox官网
https://www.virtualbox.org/

02 选择左侧的“Downloads”

03 选择对应的操作系统版本

04 傻瓜式安装

05 [win10中若出现]安装virtualbox快完成时立即回滚,并提示安装出现严重错误
(1)打开服务
(2)找到Device Install Service和Device Setup Manager,然后启动
(3)再次尝试安装

1.1.3 安装centos7

01 创建centos7文件夹,并进入其中[目录全路径不要有中文字符]

02 在此目录下打开cmd,运行vagrant init centos/7
此时会在当前目录下生成Vagrantfile,同时指定使用的镜像为centos/7,关键是这个镜像在哪里,我已经提前准备好了,名称是virtualbox.box文件

03 将virtualbox.box文件添加到vagrant管理的镜像中
(1)下载网盘中的virtualbox.box文件
(2)保存到磁盘的某个目录,比如D:\virtualbox.box
(3)添加镜像并起名叫centos/7:vagrant box add centos/7 D:\virtualbox.box
(4)vagrant box list 查看本地的box[这时候可以看到centos/7]

04 centos/7镜像有了,根据Vagrantfile文件启动创建虚拟机
来到centos7文件夹,在此目录打开cmd窗口,执行vagrant up[打开virtual box观察,可以发现centos7创建成功]

05 以后大家操作虚拟机,还是要在centos文件夹打开cmd窗口操作
vagrant halt 优雅关闭
vagrant up 正常启动

06 vagrant常用命令
(1)vagrant ssh
进入刚才创建的centos7中
(2)vagrant status
查看centos7的状态
(3)vagrant halt
停止/关闭centos7
(4)vagrant destroy
删除centos7
(5)vagrant status
查看当前vagrant创建的虚拟机
(6)Vagrantfile中也可以写脚本命令,使得centos7更加丰富
但是要注意,修改了Vagrantfile,要想使正常运行的centos7生效,必须使用vagrant reload
(7)使用sudo -i切换到根目录(root账号)

至此,使用vagrant+virtualbox搭建centos7完成,后面可以修改Vagrantfile对虚拟机进行相应配置

1.1.4 若想通过Xshell连接centos7

01 使用centos7的默认账号连接
在centos文件夹下执行vagrant ssh-config
关注:Hostname Port IdentityFile
IP:127.0.0.1
port:2222
用户名:vagrant
密码:vagrant
文件:Identityfile指向的文件private-key

02 使用root账户登录
vagrant ssh 进入到虚拟机中
sudo -i
vi /etc/ssh/sshd_config
修改PasswordAuthentication yes
passwd修改密码,比如abc123
systemctl restart sshd
使用账号root,密码abc123进行登录

1.1.5 Vagrantfile通用写法

# -*- mode: ruby -*-
# vi: set ft=ruby :

# All Vagrant configuration is done below. The "2" in Vagrant.configure
# configures the configuration version (we support older styles for
# backwards compatibility). Please don't change it unless you know what
# you're doing.
Vagrant.configure("2") do |config|
# The most common configuration options are documented and commented below.
# For a complete reference, please see the online documentation at
# https://docs.vagrantup.com.

# Every Vagrant development environment requires a box. You can search for
# boxes at https://vagrantcloud.com/search.
config.vm.box = "centos/7"

# Disable automatic box update checking. If you disable this, then
# boxes will only be checked for updates when the user runs
# `vagrant box outdated`. This is not recommended.
# config.vm.box_check_update = false

# Create a forwarded port mapping which allows access to a specific port
# within the machine from a port on the host machine. In the example below,
# accessing "localhost:8080" will access port 80 on the guest machine.
# NOTE: This will enable public access to the opened port
# config.vm.network "forwarded_port", guest: 80, host: 8080

# Create a forwarded port mapping which allows access to a specific port
# within the machine from a port on the host machine and only allow access
# via 127.0.0.1 to disable public access
# config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1"

# Create a private network, which allows host-only access to the machine
# using a specific IP.
# config.vm.network "private_network", ip: "192.168.33.10"

# Create a public network, which generally matched to bridged network.
# Bridged networks make the machine appear as another physical device on
# your network.
config.vm.network "public_network"

# Share an additional folder to the guest VM. The first argument is
# the path on the host to the actual folder. The second argument is
# the path on the guest to mount the folder. And the optional third
# argument is a set of non-required options.
# config.vm.synced_folder "../data", "/vagrant_data"

# Provider-specific configuration so you can fine-tune various
# backing providers for Vagrant. These expose provider-specific options.
# Example for VirtualBox:
#
# config.vm.provider "virtualbox" do |vb|
# # Display the VirtualBox GUI when booting the machine
# vb.gui = true
#
# # Customize the amount of memory on the VM:
# vb.memory = "1024"
# end
config.vm.provider "virtualbox" do |vb|
vb.memory = "4000"
vb.name= "jack-centos7"
vb.cpus= 2
end
#
# View the documentation for the provider you are using for more
# information on available options.

# Enable provisioning with a shell script. Additional provisioners such as
# Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
# documentation for more information about their specific syntax and use.
# config.vm.provision "shell", inline: <<-SHELL
# apt-get update
# apt-get install -y apache2
# SHELL
end

1.1.6 box的打包分发

01 退出虚拟机
vagrant halt

02 打包
vagrant package --output first-docker-centos7.box

03 得到first-docker-centos7.box

04 将first-docker-centos7.box添加到其他的vagrant环境中
vagrant box add first-docker-centos7 first-docker-centos7.box

05 得到Vagrantfile
vagrant init first-docker-centos7

06 根据Vagrantfile启动虚拟机
vagrant up [此时可以得到和之前一模一样的环境,但是网络要重新配置]

1.2.1 安装docker

https://docs.docker.com/install/linux/docker-ce/centos/

01 进入centos7
vagrant ssh

02 卸载之前的docker
sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine

03 安装必要的依赖
sudo yum install -y yum-utils \
device-mapper-persistent-data \
lvm2

04 设置docker仓库 [设置阿里云镜像仓库可以先自行百度,后面课程也会有自己的docker hub讲解]
sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo

[访问这个地址,使用自己的阿里云账号登录,查看菜单栏左下角,发现有一个镜像加速器:https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors]

05 安装docker
sudo yum install -y docker-ce docker-ce-cli containerd.io

06 启动docker
sudo systemctl start docker

07 设置docker开机启动
sudo systemctl enable docker

08 查看版本号
docker version

09 测试docker安装是否成功
查看镜像
docker images
拉取镜像
sudo docker pull hello-world
运行容器
sudo docker run --name myhelloworld hello-world
查看容器
docker ps -a

1.2.2 docker基本体验

01 创建tomcat容器
拉取tomcat镜像
docker pull tomcat

启动容器,将tomcat的8080端口映射到虚拟机的9090端口(访问时使用9090端口访问)
docker run -d --name my-tomcat -p 9090:8080 tomcat

进入到tomcat容器

02 创建mysql容器
拉取mysql镜像
docker pull mysql

启动mysql容器(设置root账号的默认密码)
docker run -d --name my-mysql -p 3301:3306 -e MYSQL_ROOT_PASSWORD=123456 --privileged mysql

03 进入到容器里面
docker exec -it containerid /bin/bash

1.2.3 可能有的疑惑

(1)docker pull在哪拉取的镜像?

​ 默认是在hub.docker.com

(2)docker pull tomcat拉取的版本是?

​ 默认是最新的版本,可以在后面指定版本”:”

(3)简单先说一下命令咯

docker pull        拉取镜像到本地
docker run 根据某个镜像创建容器
-d 让容器在后台运行,其实就是一个进程
--name 给容器指定一个名字
-p 将容器的端口映射到宿主机的端口
docker exec -it 进入到某个容器中并交互式运行

循环依赖

循环依赖:若是在spring容器中存在三个service类,分别为ServiceAServiceBServiceC,并且ServiceA依赖ServiceBServiceB依赖ServiceCServiceC依赖ServiceA,这三者形成了一种循环依赖。

spring的循环依赖一般分为三种情况,构造器循环依赖,setter循环依赖,prototype循环依赖。


构造器循环依赖:在Spring创建ServiceA的时候,首先会将beanName->serviceA放入一个正在创建bean池,发现需要依赖ServiceB,会去创建ServiceB。在创建ServiceB的时候也会将beanName->serviceB放入正在创建bean池,发现需要依赖ServiceC,会去创建ServiceC。在创建ServiceC的时候也会将beanName->serviceC放入到正在创建bean池,发现需要依赖ServiceA,会去创建ServiceA。但是此时检查正在创建bean池时,发现ServiceA已经正在创建,就会抛出异常。

构造器循环依赖示例:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:myname="http://www.test.com/schema/user"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.test.com/schema/user
http://www.test.com/schema/user.xsd">

<bean id="serviceA" class="com.study.pack.xml.circle.ServiceA">
<constructor-arg index="0" ref="serviceB"></constructor-arg>
</bean>

<bean id="serviceB" class="com.study.pack.xml.circle.ServiceB">
<constructor-arg index="0" ref="serviceC"></constructor-arg>
</bean>

<bean id="serviceC" class="com.study.pack.xml.circle.ServiceC">
<constructor-arg index="0" ref="serviceA"></constructor-arg>
</bean>
</beans>
package com.study.pack.xml.circle;

import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

public class CircleTest {
public static void main(String[] args) {
XmlBeanFactory factory = new XmlBeanFactory(new ClassPathResource("circle.xml"));
try {
ServiceA serviceA = (ServiceA)factory.getBean("serviceA");
} catch (Exception e) {
String message = e.getMessage();
System.out.println(message);
}
}
}

public class ServiceA {

private ServiceB serviceB;

public ServiceA(){}

public ServiceA(ServiceB serviceB) {
this.serviceB = serviceB;
}

public ServiceB getServiceB() {
return serviceB;
}

public void setServiceB(ServiceB serviceB) {
this.serviceB = serviceB;
}
}

package com.study.pack.xml.circle;

public class ServiceB {

private ServiceC serviceC;

public ServiceB() {}

public ServiceB(ServiceC serviceC) {
this.serviceC = serviceC;
}

public ServiceC getServiceC() {
return serviceC;
}

public void setServiceC(ServiceC serviceC) {
this.serviceC = serviceC;
}
}

package com.study.pack.xml.circle;

public class ServiceC {

private ServiceA serviceA;

public ServiceC() {}

public ServiceC(ServiceA serviceA) {
this.serviceA = serviceA;
}

public ServiceA getServiceA() {
return serviceA;
}

public void setServiceA(ServiceA serviceA) {
this.serviceA = serviceA;
}
}

setter循环依赖:

setter循环依赖示例:






prototype循环依赖:

prototype循环依赖示例:





spring扩展

spring除了可以解析自定的标签外,也可以解析用户自定义的标签。解析自定义标签需要添加一些扩展。下面以一个实例展示这个过程。

1.在resources/META-INF目录下新建文件spring-test.xsd

<?xml version="1.0" encoding="UTF-8" ?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.test.com/schema/user">
<xs:element name="user">
<xs:complexType>
<xs:attribute name="id" type="xs:string"></xs:attribute>
<xs:attribute name="userName" type="xs:string"></xs:attribute>
<xs:attribute name="email" type="xs:string"></xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

2.在resource/META-INF目录下创建spring.schema文件,并添加以下内容。

http\://www.test.com/schema/user.xsd=META-INF/spring-test.xsd

3.创建自定义bean解析器类UserBeanDefinitionParser,该类需要继承AbstractSingleBeanDefinitionParser

public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
@Override
protected Class<?> getBeanClass(Element element) {
return User.class;
}
// 从element中解析并提取相应的元素
@Override
protected void doParse(Element element, BeanDefinitionBuilder builder) {
String userName = element.getAttribute("userName");
String email = element.getAttribute("email");
if (StringUtils.hasText(userName)) {
builder.addPropertyValue("userName", userName);
}
if (StringUtils.hasText(email)) {
builder.addPropertyValue("email", email);
}
}
}

4.创建自定义namespace处理器类MyNamespaceHandler,该类需要继承NamespaceHandlerSupport

public class MyNamespaceHandler extends NamespaceHandlerSupport {
@Override
public void init() {
registerBeanDefinitionParser("user", new UserBeanDefinitionParser());
}
}

5.定义一个实体类

@Data
public class User {

private String id;
private String userName;
private String email;

@Override
public String toString() {
return this.getUserName() + "--" + this.getEmail();
}
}

6.创建一个xml文件,beans3.xml,使用自定义标签定义bean

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:myname="http://www.test.com/schema/user"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.test.com/schema/user
http://www.test.com/schema/user.xsd">

<myname:user id="testbean" userName="aaa" email="bbb"/>
</beans>

7.定义一个测试类,测试能否成功加载该bean

public class TestApp {
public static void main(String[] args) {
XmlBeanFactory factory = new XmlBeanFactory(new ClassPathResource("beans3.xml"));
User u1 = (User)factory.getBean("testbean");
System.out.println(u1);
}
}
  1. 输出结果
aaa--bbbf

事件监听

spring事件和事件监听器

在spring框架中有许多事件监听的应用。定义事件监听一般有三种角色:事件(event),事件监听器(listener),
事件的发布者(multicaster)。

事件:在java.util包下面有一个类 EventObject,该类只定义了一个Object:source 属性(事件源)。spring事件定义是在该类上面扩展的,ApplicationEvent -> EventObject

监听器:在java.util包下面定义了监听器的顶层接口 EventListener,该接口没有定义任何方法。spring事件监听器接口继承了该接口,ApplicationListener -> EventListener

事件发布器:在spring中有一个类 SimpleApplicationEventMulticaster,该类可以实现spring事件的发布。

spring事件发布的简单示例:

public class Test {

public static void main(String[] args) {
// 1. 定义一个事件发布器
SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster();
// 2.添加WorkEventListener和FreeEventListener事件监听器
multicaster.addApplicationListener(new WorkEventListener());
multicaster.addApplicationListener(new FreeEventListener());
// 3.发布WorkEvent和FreeEvent事件
multicaster.multicastEvent(new WorkEvent(new Object()));
multicaster.multicastEvent(new FreeEvent(new Object()));
}

// WorkEvent 事件->继承自spring的ApplicationEvent。
public static class WorkEvent extends ApplicationEvent{
public WorkEvent(Object source){
super(source);
}
}

// FreeEvent 事件->继承自spring的ApplicationEvent。
public static class FreeEvent extends ApplicationEvent{
public FreeEvent(Object source){
super(source);
}
}

// WorkEvent事件监听器->继承自spring的ApplicationListener,通过泛型可以限制该listener只可接受WorkEvent类型的事件。
public static class WorkEventListener implements ApplicationListener<WorkEvent>{
@Override
public void onApplicationEvent(WorkEvent workEvent) {
System.out.println("WorkEventListener received event->" + workEvent.getClass().getSimpleName());
}
}
// FreeEvent事件监听器->继承自spring的ApplicationListener,通过泛型可以限制该listener只可接受FreeEvent类型的事件。
public static class FreeEventListener implements ApplicationListener<FreeEvent>{
@Override
public void onApplicationEvent(FreeEvent freeEvent) {
System.out.println("FreeEventListener received event->" + freeEvent.getClass().getSimpleName());
}
}
}
// 输出结果:
// WorkEventListener received event->WorkEvent
// FreeEventListener received event->FreeEvent

由于ApplicationListener接口定义每个实现类只能接收一种事件,但对于一个监听器处理多个事件无能为力。故 spring 又在 ApplicationListener接口上扩展了一个接口SmartApplicationListener,该接口可以通过判断事件和事件源来决定是否处理该事件,更加的灵活。

public class Test {

public static void main(String[] args) {
SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster();
multicaster.addApplicationListener(new MySmartListener());
multicaster.multicastEvent(new WorkEvent(new Object()));
multicaster.multicastEvent(new FreeEvent(new Object()));
}

// 定义WorkEvent
public static class WorkEvent extends ApplicationEvent{
public WorkEvent(Object source){
super(source);
}
}
// 定义FreeEvent
public static class FreeEvent extends ApplicationEvent{
public FreeEvent(Object source){
super(source);
}
}

public static class MySmartListener implements SmartApplicationListener {
// WorkEvent和FreeEvent均可以处理
@Override
public boolean supportsEventType(Class<? extends ApplicationEvent> aClass) {
String workEventName = WorkEvent.class.getSimpleName();
String freeEventName = FreeEvent.class.getSimpleName();
return aClass.getSimpleName().equals(workEventName) || aClass.getSimpleName().equals(freeEventName);
}

// 对于属性源不加限制
@Override
public boolean supportsSourceType(Class<?> sourceType) {
return true;
}

@Override
public void onApplicationEvent(ApplicationEvent applicationEvent) {
System.out.println("MySmartListener received event " + applicationEvent.getClass().getSimpleName());
}
}
}

java开发常见问题

maven仓库地址证书过期
问题:在idea中创建了一个maven项目,在pom文件中引入几个依赖。但是这几个依赖包一直下载不下来,提示下载失败。
排查问题:
1. 推断可能是maven哪里配置出现了问题,下载失败肯定会有日志记录。
2. idea菜单的 help -> show log in explore 会直接跳转到idea日志目录。
3. 查看idea.log文件。
4. 该日志文件中出现如下的日志信息。

Caused by: java.lang.RuntimeException: org.eclipse.aether.transfer.ArtifactTransferException: Failure to transfer org.springframework:spring-context:pom:5.1.8.RELEASE from http://maven.aliyun.com/nexus/content/groups/public/ was cached in the local repository, resolution will not be reattempted until the update interval of datanucleus has elapsed or updates are forced. Original error: Could not transfer artifact org.springframework:spring-context:pom:5.1.8.RELEASE from/to datanucleus (http://maven.aliyun.com/nexus/content/groups/public/): sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
5. 该信息显示无法找到合法的证书。
解决:为该站点生成证书信息-> 运行下面的java类,然后在控制台输入1,会在该类所在项目路径下生成一个证书,文件名为 ssecacerts。 将该文件拷贝到 $JAVA_HOME/jre/lib/security 目录下。
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

public class InstallCert {
public static final String HOSTNAME = "repo.maven.apache.org";

public static void main(String[] args) throws Exception {
args = new String[]{HOSTNAME};
String host;
int port;
char[] passphrase;
if ((args.length == 1) || (args.length == 2)) {
String[] c = args[0].split(":");
host = c[0];
port = (c.length == 1) ? 443 : Integer.parseInt(c[1]);
//java keytool默认的密码是changeit
String p = (args.length == 1) ? "changeit" : args[1];
passphrase = p.toCharArray();
} else {
System.out.println("Usage: java InstallCacerts <host>[:port] [passphrase]");
return;
}

File file = new File("jssecacerts");
if (file.isFile() == false) {
File dir = new File(System.getProperty("java.home") + File.separatorChar + "lib" + File.separatorChar + "security");
//先看默认的证书库jssecacerts是否存在
file = new File(dir, "jssecacerts");
//如果不存在则使用cacerts(它随J2SDK一起发行,含有数量有限的可信任的基本证书)
if (file.isFile() == false) {
file = new File(dir, "cacerts");
}
}
System.out.println("Loading KeyStore " + file + "...");
InputStream in = new FileInputStream(file);
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(in, passphrase);
in.close();

SSLContext context = SSLContext.getInstance("TLS");
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
X509TrustManager defaultTrustManager = (X509TrustManager) tmf.getTrustManagers()[0];
SavingTrustManager tm = new SavingTrustManager(defaultTrustManager);
context.init(null, new TrustManager[]{tm}, null);
SSLSocketFactory factory = context.getSocketFactory();

System.out.println("Opening connection to " + host + ":" + port + "...");
SSLSocket socket = (SSLSocket) factory.createSocket(host, port);
socket.setSoTimeout(10000);
try {
System.out.println("Starting SSL handshake...");
socket.startHandshake();
socket.close();
System.out.println();
System.out.println("No errors, certificate is already trusted");
} catch (SSLException e) {
System.out.println();
e.printStackTrace(System.out);
}

X509Certificate[] chain = tm.chain;
if (chain == null) {
System.out.println("Could not obtain server certificate chain");
return;
}

BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));

System.out.println();
System.out.println("Server sent " + chain.length + " certificate(s):");
System.out.println();
MessageDigest sha1 = MessageDigest.getInstance("SHA1");
MessageDigest md5 = MessageDigest.getInstance("MD5");
for (int i = 0; i < chain.length; i++) {
X509Certificate cert = chain[i];
System.out.println(" " + (i + 1) + " Subject " + cert.getSubjectDN());
System.out.println(" Issuer " + cert.getIssuerDN());
sha1.update(cert.getEncoded());
System.out.println(" sha1 " + toHexString(sha1.digest()));
md5.update(cert.getEncoded());
System.out.println(" md5 " + toHexString(md5.digest()));
System.out.println();
}

System.out.println("Enter certificate to add to trusted keystore or 'q' to quit: [1]");
String line = reader.readLine().trim();
int k;
try {
k = (line.length() == 0) ? 0 : Integer.parseInt(line) - 1;
} catch (NumberFormatException e) {
System.out.println("KeyStore not changed");
return;
}

X509Certificate cert = chain[k];
String alias = host + "-" + (k + 1);
ks.setCertificateEntry(alias, cert);

OutputStream out = new FileOutputStream("ssecacerts");
ks.store(out, passphrase);
out.close();

System.out.println();
System.out.println(cert);
System.out.println();
System.out.println("Added certificate to keystore 'jssecacerts' using alias '" + alias + "'");

}

private static final char[] HEXDIGITS = "0123456789abcdef".toCharArray();

private static String toHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 3);
for (int b : bytes) {
b &= 0xff;
sb.append(HEXDIGITS[b >> 4]);
sb.append(HEXDIGITS[b & 15]);
sb.append(' ');
}
return sb.toString();
}

private static class SavingTrustManager implements X509TrustManager {

private final X509TrustManager tm;
private X509Certificate[] chain;

SavingTrustManager(X509TrustManager tm) {
this.tm = tm;
}

@Override
public X509Certificate[] getAcceptedIssuers() {
throw new UnsupportedOperationException();
}

@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
throw new UnsupportedOperationException();
}

@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
this.chain = chain;
tm.checkServerTrusted(chain, authType);
}
}
}

加密算法

对称加密与非对称加密
对称加密:在对称加密中存在密钥的概念,加密时使用密钥加密,解密时使用密钥解密。
非对称加密:非对称加密中存在私钥和公钥的概念,一般加密时使用公钥加密,解密时使用私钥解密。
MD5
MD5算法可以为一段给定的字符串或者某个文本生成唯一的一个字符串(数字签名)。
// 通过apache的commons-lang实现md5
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.3.2</version>
</dependency>

/**
* md5加密
* @param text
* @param key
* @return
*/
public static String md5(String text, String key) {
return DigestUtils.md5Hex(text + key);
}
BASE64
BASE64属于对称加密的一种,可以使用该方式实现文本的编码和节码。java中关于base64编码和解码有三种方式实现,第一种是使用 sun.misc 包下面的 BASE64Encoder 和 BASE64Decoder。第二种方式是使用 java.util包下的工具类 Base64。第三种方式是使用 org.apache.commons.codec包下的类。
/**
* 编码
* @param text
* @return
*/
public static String encodeA(String text) {
BASE64Encoder base64Encoder = new BASE64Encoder();
byte[] bytes = null;
String res = null;
try {
bytes = text.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
if (bytes != null) {
res = base64Encoder.encode(bytes);
}
return res;
}

/**
* 节码
* @param text
* @return
*/
public static String decodeA(String text) {
BASE64Decoder base64Decoder = new BASE64Decoder();
byte[] bytes = null;
String res = null;
try {
bytes = base64Decoder.decodeBuffer(text);
res = new String(bytes, "UTF-8");
} catch (IOException e) {
e.printStackTrace();
}
return res;
}
/**
* 编码
* @param text
* @return
*/
public static String encodeB(String text) {
Base64.Encoder encoder = Base64.getEncoder();
byte[] bytes = null;
String res = null;
try {
bytes = text.getBytes("UTF-8");
byte[] encode = encoder.encode(bytes);
res = new String(encode, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return res;
}

/**
* 节码
* @param text
* @return
*/
public static String decodeB(String text) {
Base64.Decoder decoder = Base64.getDecoder();
String res = null;
byte[] decode = decoder.decode(text);
try {
res = new String(decode, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return res;
}
/**
* 编码
* @param text
* @return
*/
public static String encodeC(String text){
org.apache.commons.codec.binary.Base64 base64 = new org.apache.commons.codec.binary.Base64();
byte[] bytes = null;
String res = null;
try {
bytes = text.getBytes("UTF-8");
res = base64.encodeToString(bytes);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return res;
}

/**
* 解码
* @param text
* @return
*/
public static String decodeC(String text) {
org.apache.commons.codec.binary.Base64 base64 = new org.apache.commons.codec.binary.Base64();
String res = null;
byte[] bytes = null;
bytes = base64.decode(text);
try {
res = new String(bytes, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return res;
}
AES
AES属于一种对称加密算法,加密和解密使用同一密钥。
/**
* AES加密
* @param src
* @param key
* @return
* @throws Exception
*/
public static String encodeAes(String src, String key) throws Exception{
if (key == null || key.length() != 16) {
System.out.print("Key为空null");
return null;
}
byte[] raw = key.getBytes("UTF-8");
SecretKeySpec keySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");//"算法/模式/补码方式"
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] encrypted = cipher.doFinal(src.getBytes("utf-8"));
//此处使用BASE64做转码功能,同时能起到2次加密的作用。
return new org.apache.commons.codec.binary.Base64().encodeToString(encrypted);
}

/**
* AES解密
* @param src
* @param key
* @return
* @throws Exception
*/
public static String decodeAes(String src, String key) throws Exception{
if (key == null || key.length() != 16) {
System.out.print("Key为空null");
return null;
}
byte[] raw = key.getBytes("utf-8");
SecretKeySpec keySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
//先用base64解密
byte[] encrypted1 = new org.apache.commons.codec.binary.Base64().decode(src);
try {
byte[] original = cipher.doFinal(encrypted1);
String originalString = new String(original,"utf-8");
return originalString;
} catch (Exception e) {
System.out.println(e.toString());
return null;
}
}

环境

在spring框架中对于配置文件的配置属性,java的系统属性,以及系统的环境变量,都抽象在了Environment接口中,通过该接口可以获取我们之前配置好的一些属性。下面是关于Environment接口的简单使用。


public static void main(String[] args) {
// 1. 创建 Environment 对象
StandardEnvironment environment = new StandardEnvironment();
// 2. 在environment中存储了类型为 PropertySources类型的属性,该属性中存储 PropertySource列表
MutablePropertySources propertySources = environment.getPropertySources();
// 3. 创建第一个 PropertySource
Map<String, Object> mapA = new HashMap<String, Object>();
mapA.put("java", "version7");
PropertySource propertySourceA = new MapPropertySource("mapA", mapA);
// 4. 创建第二个 PropertySource
Map<String, Object> mapB = new HashMap<String, Object>();
mapB.put("java", "version8");
PropertySource propertySourceB = new MapPropertySource("mapB", mapB);
// 5.将 PropertySource添加到 propertySources中
propertySources.addLast(propertySourceA);
propertySources.addLast(propertySourceB);
System.out.println(environment.getProperty("java"));
// 输出 version7

propertySources.addLast(propertySourceA);
propertySources.addFirst(propertySourceB);
System.out.println(environment.getProperty("java"));
// 输出 version8
}

ClassLoader

类加载器的区别

java中提供了三种类加载器,分别是启动类加载器,扩展类加载器,系统类加载器。三种类加载器通过双亲委派机制来实现类的加载。

类加载器 名称 加载路径 实现
启动类加载器 BootStrap ClassLoader 核心库:java.lang.*,系统属性:sun.boot.class.path所指路径 c++
扩展类加载器 Extension ClassLoader 扩展库:jre\lib\ext 目录下的包,系统属性:java.ext.dirs所指路径 纯java
系统类加载器 System ClassLoader 环境变量 classpath 或系统属性 java.class.path 指定目录中加载类 纯java

双亲委派机制

类的双亲委派机制是指子类加载器加载类时,首先会检查该类是否已经被加载。若是该类未被加载,会将加载任务委托给父类加载器,父类加载器又会委托给更上一层类加载器,直到启动类加载器。启动类加载器会检查其加载路径,有则加载,无则加载任务给扩展类加载器,扩展类有会检查路径并执行加载。


自定义类加载器

在测试之前,需要将Test.java编译成Test.class文件,将该文件放如到路径D:\jar\com\study\zl\classloader下,并将项目中的Test.javaTest.class文件删除,以免该类被系统类加载器加载到。

package com.study.zl.classloader;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
* @Author long
* @Date 2019/10/13 10:24
*/
public class App {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
SelfClassLoader selfClassLoader = new SelfClassLoader("D:\\jar");
Class<?> cls = selfClassLoader.loadClass("com.study.zl.classloader.Test");
if (cls != null) {
// 创建一个cls类型的对象
Object o = cls.newInstance();
// 获取该对象的say方法
Method say = cls.getMethod("say", null);
// 调用该类型的say方法
say.invoke(o, null);
// 查看 cls 的类加载器
System.out.println(cls.getClassLoader().toString());
}
}
// 输出结果
// hello,world
// com.study.zl.classloader.SelfClassLoader@7f31245a
}



package com.study.zl.classloader;
public class Test {

public void say(){
System.out.println("hello,world");
}

}

自定义一个类加载器,该类加载器继承自ClassLoader,重写 findClass 方法来获取 Class。该方法首先会调用getData方法来获取类的字节码,然后调用父类的defineClass方法将字节码转化未 Class并返回。

package com.study.zl.classloader;

import java.io.*;

/**
* @Author long
* @Date 2019/10/13 10:40
*/
public class SelfClassLoader extends ClassLoader {

private String classpath;

public SelfClassLoader(String classpath) {
this.classpath = classpath;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = getData(name);
if (data != null) {
// 调用 defineClass 方法将字节码转化为Class
return defineClass(name, data, 0 , data.length);
}
} catch (IOException e) {
e.printStackTrace();
}
return super.findClass(name);
}


/**
* 返回类的字节码
*
* @param className
* @return
* @throws IOException
*/
private byte[] getData(String className) throws IOException {
InputStream in = null;
ByteArrayOutputStream out = null;
String path = classpath + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
try {
in = new FileInputStream(path);
out = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int len = 0;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
return out.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
in.close();
out.close();
}
return null;
}
}

资源文件的读取

在 java 中可以使用 Class 或者是 ClassLoader 读取资源文件。比如现在项目的resources目录下面有一个 application.properties文件,可以使用项目中某个类的class.getResource方法或者某个类的类加载器class.getClassLoader().getResource方法。方法所传资源文件路径是向对于resources的目录。

package com.study.zl.classloader.p2;

import java.net.URL;

/**
* @Author long
* @Date 2019/10/13 14:26
*/
public class App {
public static void main(String[] args) {
Class<App> appClass = App.class;
// 通过 class 获取资源时,必须有 / ,否则会从该类所在包下面读取
URL resource1 = appClass.getResource("/application.properties");
// 通过 ClassLoader 读取资源,所不同的是 ClassLoader 还有一个 getResources 方法
URL resource2 = appClass.getClassLoader().getResource("application.properties");
System.out.println(resource1.toString());
System.out.println(resource2.toString());
}

// 输出结果
// file:/D:/code/struct-parent/interview-ready/target/classes/application.properties
// file:/D:/code/struct-parent/interview-ready/target/classes/application.properties
}

读取Properties文件

在spring核心包中提供了一个工具类PropertiesLoaderUtils,该工具类可以将ClassLoader读取到的资源转换未Properties文件。

//  pom.xml文件引入 spring framework 核心包依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>2.5.6</version>
</dependency>

// application.properties文件加入内容
#Tue Jun 18 23:47:55 GMT 2019
server.port=8080
server.name=spring-boot-application

package com.study.zl.classloader.p2;

import org.springframework.core.io.UrlResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;

import java.io.IOException;
import java.net.URL;
import java.util.Enumeration;
import java.util.Properties;

/**
* @Author long
* @Date 2019/10/13 14:26
*/
public class App {
public static void main(String[] args) {
Class<App> appClass = App.class;
// 定义Properties对象
Properties properties = new Properties();
try {
// 读取资源集合
Enumeration<URL> urls = appClass.getClassLoader().getResources("application.properties");
while (urls.hasMoreElements()) {
// 使用工具类加载
properties.putAll(PropertiesLoaderUtils.loadProperties(new UrlResource(urls.nextElement())));
}
} catch (IOException e) {
e.printStackTrace();
}
// 输出
System.out.println(properties);
}
// 输出结果
// {server.port=8080, server.name=spring-boot-application}
}

CountDownLatch

应用场景

① 下一步的任务需要等待上一步任务执行完成才能执行的场景。比如:有一批任务交给线程池来处理,我们需要知道任务从开始到结束一共执行了多长的时间。下面的代码展示了这种场景的使用。

public void unite(JobExecutionContext jobContext) throws Exception {
Set<String> batchNoSet = getBatchSet(jobContext);
Set<String> idCardSet = updateLoanInfoFromSrc(batchNoSet);
// 任务开始之前获取到系统现在的时间
Long start = System.currentTimeMillis();
// 创建 CountDownLatch 对象,以任务个数做为该对象的初始化参数
CountDownLatch latch = new CountDownLatch(idCardSet.size());
for (String idCard : idCardSet) {
// 创建任务线程
CaseUnitedThread thread = new CaseUnitedThread(idCard.trim(), latch);
// 任务交给线程池处理->每执行一次任务,CountDownLatch对象的 state值减去1
threadPool.execute(thread);
}
// 等待归户完毕
latch.await();
logger.info("case united finish: total time = {}", System.currentTimeMillis() - start);
}
源码分析

类结构

CountDownLatch类内部定义比较简单,有一个类型为Sync:sync属性,而CountDownLatch最重要的两个方法countDownawait的内部实现是由Sync:sync来完成的。而Sync类又继承自AQS

// jdk1.8
public class CountDownLatch {
// 内部属性,相关操作会交由该对象来完成
private final Sync sync;
// 构造函数,初始化 sync 属性
public CountDownLatch(int count) {
// 参数值必须大于等于0
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}

public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}

public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

public void countDown() {
sync.releaseShared(1);
}

...
...
// 静态内部类,继承了AQS
private static final class Sync extends AbstractQueuedSynchronizer {
// 对外提供的构造函数,初始化对象时,会将 AQS 中的 state 值设置为 count
Sync(int count) {
setState(count);
}
...
// 尝试获取共享锁,若是 state 值为0,返回 1,否则返回 -1
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
// 尝试释放共享锁
protected boolean tryReleaseShared(int releases) {
// 自旋,可能多个线程同时尝试释放共享锁,同时修改 state 值,造成线程安全问题
for (;;) {
int c = getState();
// 若是 state 的值已经为 0,则尝试释放共享锁失败。
if (c == 0) {
return false;
}
// 通过 CAS 操作将原有 state 的值减去1,失败重试
int nextc = c-1;
if (compareAndSetState(c, nextc)) {
// state值为0时,返回 true
return nextc == 0;
}
}
}
}
}

countDown方法

当某个线程调用CountDownLatchcountDown方法时,内部操作的原理其实是将AQS中的state属性值减1


① 当一个线程调用countDown方法时,该方法内部会调用Sync:syncreleaseShared(int arg)方法。而releaseShared(int arg)方法则是由Sync的父类AQS定义的方法。

AQSreleaseShared(int arg)方法首先调用tryReleaseShared(int arg)方法,该方法在AQS中是一个空方法,具体逻辑在其子类Sync中实现。

SynctryReleaseShared(int arg)方法尝试释放共享锁。由于可能会有多个线程同时调用countDown方法修改state的值,故对state值的修改需要使用自旋CAS操作。若修改前读到state的值为0,直接返回false。若修改前state的值不为0,通过CAS操作将其值修改为state-1,若修改后的值为0,返回true

④ 第 ③ 方法其实是执行了两个操作,第一是将state的值减去1,第二是判断修改后的state值是否为0。若是第 ③ 步返回值为true,证明state的值已经为0,可以做一些操作来唤醒调用await方法的线程。故第 ② 步判断返回值为true的话,执行doReleaseShared()方法。


doReleaseShared释放共享锁,一段自旋操作。① 该方法首先获取到头节点,使用变量h保存。若是当前h节点不为空并且h节点不等于尾节点时,执行操作(这么判断的原因在于,若是头节点等于尾节点,就没必要唤醒线程。因为若是只有一个头节点时,没有需要唤醒的线程)。② 若节点hwaitStatus属性值为Node.SIGNAL,则CAS操作将其设置为0。若设置成功,调用unparkSuccessor方法唤醒线程。若设置失败,继续执行for循环。③ 若节点hwaitStatus属性值为0,则通过CAS操作将其设置为Node.PROPAGATE。若设置失败,继续执行for循环。若设置成功,继续执行后面的语句,即当h==head时,跳出for循环。

⑥ 在上述步骤 ⑤ 中,将waitStatus状态值由Node.SIGNAL设置为0成功后,调用了unparkSuccessor方法,该方法会唤醒传入节点的下一个节点的线程。① 该方法首先会获取到传入节点nodewaitStatus属性值,若是该值小于0,通过CAS操作将该值修改为0。② 获取到node节点的下一个节点s,该节点需要是waitStatus属性值小于等于0的。③ 调用LockSupportunpark方法唤醒s节点所对应的线程。

// CountDownLatch
public void countDown() {
// 调用sync的releaseShared方法
sync.releaseShared(1);
}
// AQS
public final boolean releaseShared(int arg) {
// tryReleaseShared
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
// AQS
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}

// Sync
protected boolean tryReleaseShared(int releases) {
// 自旋,可能多个线程同时尝试释放共享锁,同时修改 state 值,造成线程安全问题
for (;;) {
int c = getState();
// 若是 state 的值已经为 0,则尝试释放共享锁失败。
if (c == 0) {
return false;
}
// 通过 CAS 操作将原有 state 的值减去1,失败重试
int nextc = c-1;
if (compareAndSetState(c, nextc)) {
// state值为0时,返回 true
return nextc == 0;
}
}
}

// AQS-> 释放共享模式的动作
private void doReleaseShared() {
for (;;) {
// 保存头节点
Node h = head;
// 头节点不为空并且头节点不为尾节点
if (h != null && h != tail) {
// 获取到头节点的等待状态
int ws = h.waitStatus;
// 若是头节点的等待状态为SIGNAL
if (ws == Node.SIGNAL) {
// 节点的状态由 Node.SIGNAL更新为0失败,继续执行自旋
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) {
continue;
}
// 节点状态由 Node.SIGNAL更新为0成功,调用unparkSuccessor(Node node)唤醒后继节点
unparkSuccessor(h);
// 若是该节点的状态为0,并且由0设置为PROPAGATE失败,继续执行自旋
} else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) {
continue;
}
}
if (h == head) {
break;
}
}
}

private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// ws小于0的状态有三个 SIGNAL(-1) CONDITION(-2) PROPAGATE(-3)
// 若是 ws 小于0,CAS操作将其修改为0
if (ws < 0) {
compareAndSetWaitStatus(node, ws, 0);
}
// ws大于0仅有一种情况,即 CANCELLED(1)
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
// 从尾节点找到一个距离node节点最近的状态值<=0的节点
for (Node t = tail; t != null && t != node; t = t.prev) {
if (t.waitStatus <= 0) {
s = t;
}
}
}
// 若是该节点不为null,直接唤醒该节点线程
if (s != null) {
LockSupport.unpark(s.thread);
}
}

await

当一个线程调用一个CountDownLatch对象的await方法时,该线程会等到其state值为0时执行,否则不会执行。

① 当调用 CountDownLatchawait 方法时,会调用Sync:syncacquireSharedInterruptibly(int arg)方法,而acquireSharedInterruptibly(int arg)方法是由Sync:sync的父类AQS定义的方法。

AQSacquireSharedInterruptibly(int arg)方法首先会去判断当前线程是否被中断,若是当前线程已经被中断,则直接抛出中断异常。若是当前线程没被中断,会去调用tryAcquireShared(int arg)方法。而该tryAcquireShared(int arg)方法在AQS中是一个空方法,具体的逻辑是由子类Sync来实现的。

Sync重写父类的tryAcquireShared(int arg)方法,其方法的目的就是判断当前state的值是否为0,若是该值为0,返回1,否则返回-1

④ 步骤 ② 会根据步骤 ③ 方法返回的值进行判断。若是返回值不小于0,该方法什么也不做(不会挂起当前的线程);若是返回值小于0,证明state的值不为0,当前线程不能执行,需要执行进一步的操作,即调用doAcquireSharedInterruptibly(int arg)方法。


doAcquireSharedInterruptibly方法,添加到等待队列,AQS里面的方法。① 该方法首先会在队列中添加一个共享模式的节点。② 一段自旋操作,获取到该节点node的前一个节点p,若是p节点正好为head节点,调用tryAcquireShared方法尝试获取共享锁(state0返回1,否则返回-1),若是获取共享锁成功,调用setHeadAndPropagate方法,该方法可能会唤醒当前线程。 ③ 若是上一步操做获取共享锁失败,首先调用shouldParkAfterFailedAcquire方法判断当前线程是否可以被挂起。若是该方法返回值为true,调用parkAndCheckInterrupt方法挂起当前线程,并返回当前线程的中断标识。若是中断标识为true,立即抛出中断异常。

// CountDownLatch
// 1.委托给sync的acquireSharedInterruptibly方法
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// AQS
// 2.尝试获取,若获取不到在调用doAcquireSharedInterruptibly方法
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
// 若是当前线程已经被中断,直接抛出中断异常
if (Thread.interrupted()) {
throw new InterruptedException();
}
// 若是当前 state的值不为0
if (tryAcquireShared(arg) < 0) {
doAcquireSharedInterruptibly(arg);
}
}
// AQS
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
// CountDownLatch-Sync
protected int tryAcquireShared(int acquires) {
// 若是当前 state 的值为0,返回1,否则返回-1
return (getState() == 0) ? 1 : -1;
}

// AQS->获取共享锁的方法
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
// 将当前线程封装成为 Node 节点,添加到等待队列(此时waitStatus值为0)
final Node node = addWaiter(Node.SHARED);
// 判断是否执行失败的标识,失败时会将该节点清除
boolean failed = true;
try {
for (;;) {
// 获取 node 节点的前一个节点p
final Node p = node.predecessor();
// 若是节点p为等待队列的头节点
if (p == head) {
// 若是p为头节点,尝试一下获取共享锁(具体逻辑由Sync实现)
int r = tryAcquireShared(arg);
// r >= 0 证明 state 的值已经为 0,即获取到了共享锁。CountDownLatch:Sync返回1
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null;
failed = false;
return;
}
}
// 1. 当获取锁失败后判断是否需要挂起线程
// 2. 若是需要挂起线程,则挂起线程并检查中断
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
throw new InterruptedException();
}
}
} finally {
// 若是执行失败,调用 cancelAcquire(Node node)方法将这个节点从等待队列中删除
if (failed) {
cancelAcquire(node);
}
}
}
// 在自旋操作期间成功获取到共享锁执行的操作(此处传入的propagate值为1,节点为当前节点新加入队列的节点)
private void setHeadAndPropagate(Node node, int propagate) {
// 首先保留原始头节点,然后将当前的节点设置为头节点
Node h = head;
setHead(node);
// 已经将node节点设置为了头结点,需要唤醒下一个节点啦
// 1.若是传入的值大于0
// 2.若是传入的值小于等于0,原始头结点为null
// 3.若是传入的值小于等于0,原始头节点不为null,原始头结点的waitStatus状态值小于0
// 4.给h重新赋了一次值后h为null
// 5.给h重新赋了一次值后h不为null,其waitStatus状态值小于0
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
// 获取传入节点的下一个节点s
Node s = node.next;
// 若是s为null或者是s节点是共享模式,执行doReleaseShared方法
if (s == null || s.isShared()) {
doReleaseShared();
}
}
}


private void doReleaseShared() {
for (;;) {
// 首先保存头结点
Node h = head;
// 若是头结点不为空并且头结点不等于尾节点(证明不只有一个节点)
if (h != null && h != tail) {
// 获取头结点的waitStatus状态值
int ws = h.waitStatus;
// 若是该状态值为SIGNAL
if (ws == Node.SIGNAL) {
// 尝试将节点h的waitStatus状态值由SIGNAL修改为0
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) {
continue;
}
// 若是修改成功,执行unparkSuccessor方法,唤醒当前节点的下一个节点
unparkSuccessor(h);
// 若是该状态值为0,将其设置为PROPAGATE
} else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) {
continue;
}
}
if (h == head) {
break;
}
}
}

// 唤醒node节点的下一个节点
private void unparkSuccessor(Node node) {
// ws小于0,存在三种状态
// SIGNAL(-1) CONDITION(-2) PROPAGATE(-3)
int = node.waitStatus;
if (ws < 0) {
compareAndSetWaitStatus(node, ws, 0);
}
// 获取node节点的下一个节点
Node s = node.next;
// 去除一些无用的节点
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev) {
if (t.waitStatus <= 0) {
s = t;
}
}
}
// 唤醒操作
if (s != null) {
LockSupport.unpark(s.thread);
}
}