[译]Java Memory Model(Java内存模型)JMM与硬件内存模型

[译]Java Memory Model(Java内存模型)JMM与硬件内存模型

翻译自:Java Memory Model

  • Java内存模型
  • 硬件内存结构
  • JMM以及硬件内存结构的对应关系
    • 共享对象的可见性
    • 线程竞争情况

Java内存模型(后面简称JMM)定义了Java虚拟机如何通过计算机内存(RAM)进行工作的。JVM是整个计算机的模型,所以JVM中存在着内存模型(即JMM) – AKA(不知道怎么翻译)

如果你想要设计正确的并发程序,那理解JMM就很重要了。JMM定义了不同的线程什么时候以及怎样才能看到其他线程写入的共享变量值,并且在必要的情况下,如何去同步(synchronize)的访问这些共享的变量。

原来的JMM是不足的,所以在java 1.5的时候JMM被重新设计,并且这个模型现在依然在Java8中运行着。

阅读更多

[docker]4. docker-compose简单实用

我们知道,Docker可以把环境+应用打包成一个镜像,然后通过镜像注入参数去启动无数个容器。然而,我们之前都是一个一个去操作的,当我们需要操作多个镜像并且是多态机器上面部署的时候,这种方式会显得特别无力。所以docker-compose的诞生就是为了解决这个问题。

一. 安装

1.1MacOS

OSX系统相对简单,只要安装了docker软件,那么docker-compose就已经自带在其中。可以通过docker-compose —version查看。

阅读更多

[docker]3.dockerfile的使用以及通过maven插件构建Docker镜像

本文的目的

据我们所知,docker除了官方提供的镜像以外(通常都是一些基础环境的镜像,或者像WordPress这样著名的程序),也有很多自己的自定义的镜像,主要用来实现我们自己需要的定制需求。

Dockerfile就像是maven构建程序的pom.xml一样,通过定义一系列镜像(maven中的项目)所需要的条件,就可以轻松构建一个镜像(项目)。那么这篇文章就是为了掌握Dockerfile常规的几个语法,以便能够让我们通过定义的一系列指令构建出我们自己的定制镜像。

本文使用到的Java示例代码地址:https://github.com/WeidanLi/p-docker-demo

阅读更多

[docker]2.docker命令的使用

本文的目的

通过与git平台进行类比,快速理解和使用docker相关的命令。

1. Docker命令介绍

在Docker的使用过程中,我们需要使用Docker提供的命令与Docker进行对话,以便告诉Docker我们要完成的操作。

这些操作,可以与git或者svn的使用进行类比,因为道理都是从一个仓库中拉取相关的内容,然后进行操作。

阅读更多

[docker]1.初识Docker

本文的目的

编写Docker的教程,并不是系统的对Docker进行了解,而是通过一个Java工程师的口吻,来快速入门了解Docker的内容。

本文的目的很简单

  1. 快速了解Docker几个概念,能够通过与Java相关的类比,快速勾勒Docker的模型
  2. 在自己的系统上面安装docker成功即可,由于CentOS是一个比较好的平台了解Docker的内容,所以期待对Docker的学习能够在Linux系的平台上进行,这样能够更加快速的学习。

阅读更多

一个Java程序猿眼中的汇编

[TOC]

一、介绍

在计算机中,CPU只能识别二进制,当需要CPU做事情的时候,都需要通过二进制去指挥CPU做什么事情。古人觉得二进制实在是难以理解,所以才发明了汇编语言以及后面的高级语言,所以汇编语言可以称作为比较接近机器语言的语言了。

当然后面的古人依然觉得汇编语言难以理解,所以发明了C、C++、Java来解决难以理解的问题。所以后面的语言越来越偏于人类自然语言,并且使用面向对象的思想来设计程序。当然这期间经历的事情还挺多:C编译成汇编语言,汇编语言再翻译成二进制语言,这个过程即称为assembling。

学习汇编语言既需要下面两个概念的理解:内存模型寄存器

阅读更多

[运维]Jenkins配合gitlab、maven、docker实现自动化部署

前言

要说什么人适合这个教程,那就是懒的人。

自从Docker出现以后,越来越多的项目以集装箱的形式进行项目的部署,测试和生产都在统一环境里面去实现,可以减少很多不必要的麻烦,像docker运行的容器,不像是项目在运行,而是Linux中的服务,所以多多少少也跟微服务这个词语扯上点关系吧。

但是像项目开发完成都需要手动去上传部署可以说很浪费时间了,所以这个事情可以交给强大的Git和Jenkins来实现。

不过有一点需要注意的是,我觉得该教程适合那种有规定分支(比如master)来生产的,要是直接在master上面开发的话,就没有太大必要,因为每次提交都会触发构建,会造成机器的效率问题,以及生产容易出现bug等等。

需要

阅读这篇教程,你需要这些东西:
1. shell基础
2. git
3. maven的管理项目之间的关系


具体步骤如下:
1. 当gitlab的指定分支(比如:master分支)监听到有代码更新,通过设置webhook地址通知Jenkins。
2. Jenkins拉取gitlab-master分支的代码,联合maven中间件构建Docker镜像,上传到我们自己的DockerHub。
3. Jenkins构建完成(这时候镜像已经存储在我们的DockerHub上面了),连接到部署的机器,关闭老的项目,启动新的项目。

因为是在我自己的电脑上启动相对应的虚拟机进行实现,所以可以说一切从零开始搭建。观看教程的你如果中间哪一步企业已经构建完了,可以利用现有的环境进行稍加配置实现CI自动化部署。


机器分布:

编号 机器名称 IP 作用
1 Gitlab-server 192.168.1.50 用于存放Gitlab服务
2 Jenkins-server 192.168.1.53 用于存放Jenkins服务, Docker-registry服务

一. 安装Gitlab环境

1. 使用Docker运行Gitlab环境:

sudo docker run --detach \
    --hostname gitlab.weidan.com \
    --publish 443:443 --publish 80:80 --publish 22:22 \
    --name gitlab \
    --restart always \
    --volume /srv/gitlab/config:/etc/gitlab \
    --volume /srv/gitlab/logs:/var/log/gitlab \
    --volume /srv/gitlab/data:/var/opt/gitlab \
    gitlab/gitlab-ce:latest

2. 增加防火墙规则

命令中看到需要几个端口,这时候需要再防火墙开放这几个端口以供Gitlab使用:

firewall-cmd --zone=public --add-port=80/tcp --permanent
firewall-cmd --zone=public --add-port=22/tcp --permanent
firewall-cmd --zone=public --add-port=443/tcp --permanent
firewall-cmd --reload

3. 修改本机hosts

修改本机hosts的方法不同系统是不同的,Windows是在C:/Windows/system32/drivers/etc/hosts,OSX是在/etc/hosts中。
按照我上面定义的hostname去添加,我这里因为ip是192.168.1.50,域名是gitlab.weidan.com
所以在hosts中增加192.168.1.50 gitlab.weidan.com即可

4. 配置项目

访问gitlab.weidan.com,进入页面,第一次进入是需要修改root密码的。修改完成以后可以添加团队、成员等等,现在就用root用户来做测试了。

首先创建一个空项目。

然后使用idea在当前的项目创建一个Git仓库并提交

push到刚刚在gitlab上创建的空项目地址。

(这一步没有写得特别完整,以后有机会再写Gitlab的使用。)

到了这一步基本上Gitlab的使用以及安装是已经成功的了。


二. 安装Docker私仓

搭建Docker私有仓库:
docker run -d -p 5000:5000 --privileged=true -v /opt/registry:/tmp/registry registry

添加防火墙规则:

firewall-cmd --zone=public --add-port=5000/tcp --permanent && firewall-cmd --reload

没有Https证书的情况下:
需要修改(或新增)docker的配置文件/etc/docker/daemon.json,添加下面的内容:

{"registry-mirrors": ["http://86d2a50b.m.daocloud.io"],"insecure-registries": ["xxx.xxx.xxx.xxx:5000"]}

"registry-mirrors": ["http://86d2a50b.m.daocloud.io"]是国内daocloud公司代理的Registry Hub仓库的地址,可以加快国内访问Registry Hub仓库的速度。
修改好之后需要重启Docker服务才能生效,执行命令:systemctl restart docker ,在push即可。

在这里尝试把registry给上传上去,先给registry打上tag,然后使用push命令,输出命令行中[output]下面的字样则表示私服搭建成功。

docker tag registry localhost:5000/registry
docker push localhost:5000/registry

[output]
The push refers to a repository [localhost:5000/registry]
aa027c305e18: Pushed
d5d6b45f829e: Pushed
9df68a0e05c1: Pushed
a6490c0c5308: Pushed
52a5560f4ca0: Pushed
latest: digest: sha256:a16a543b7301450be675f57b2d1d43d5789b2e0b01c7870abfff4e9b0803a3e8 size: 1364

三. 安装Jenkins环境

[本来想使用Docker进行安装,但是需要关联的东西太多了,所以就使用宿主机安装的方式]

Jenkins使用的是官方的war包

下载完成war包以后可以直接使用命令java -jar jenkins.war

不过由于让系统后台运行,所以使用nohup java -jar jenkins.war > jenkins.out &命令运行

系统默认使用8080作为端口,所以防火墙开放8080

添加防火墙规则:

firewall-cmd --zone=public --add-port=8080/tcp --permanent
firewall-cmd --reload

第一次进入页面是需要密码进入的,密码是一个字符串,在jenkins.out中可以查看得到,输入进去,就进入了安装插件的界面。


3.1 配置常用项目

3.1.1 安装需要的插件

需要手动安装插件:ssh plugin, Maven Integration plugin, git plugin

如图进入安装插件的界面管理插件

3.1.2 配置jdk、maven

通过如图所示的位置进入全局设置

设置jdk的路径,不需要点击自动安装(毕竟在运行Jenkins项目的时候就已经有Java环境了)
这里有个大误,我就接了很久,设置了路径以后,点击保存退出进来有一种没有设置好的感觉,但是其实点击安装jdk的时候系统已经保存了你设置的路径就已经是设置好了的。

拉到下面的maven设置同理,这里就不上截图了,我是用我自己的maven,然后设置了私服等等(私服我觉得很有必要,比如存储公司自己开发的框架包等等)


3.2 配置parent项目

像很多项目都是拥有一个父级pom文件,这时候需要把这个父级项目安装到maven指定的私有仓库里面,要不然后面的项目构建都会失败。

像父级pom文件的构建配置就要简单很多,主要做的事情就是maven的命令install,用于把父级pom安装到自己的仓库里面以供后面的构建使用。

所以只需要配置Git的地址以及构建的时候执行的maven命令即可

如图圈出来的两个部分。

这里没有配置自动构建,主要是因为这里是项目的根目录一旦监听了,每次其他项目提交文件都会触发,感觉就没有必要,二来呢就是因为父级pom文件的更新不大,手动出发构建也是可以接受的。

3.3 构建Eureka项目

构建Eureka项目和构建其他子项目的配置基本是一样的了,这里要做的事情就要比上面父级pom的安装要多了好一些。

  1. 从Git服务器拉取代码,使用Maven插件构建并且将构建完的镜像上传到Docker私服上去;
  2. 连接生产服务器,备份、清理旧版本的程序镜像,拉取刚刚构建完成的镜像并且启动;
  3. 配置Gitlab的webhook,以便让其接收到了更新以后通知Jenkins执行以上动作。

这三步得一步一步来:

3.3.1 从Git服务器拉取代码,使用Maven插件构建并且将构建完的镜像上传到Docker私服上去;

项目中配置maven插件

需要使用dockerfile-maven-plugin插件用于帮助构建Docker镜像:

<!-- docker的maven插件 -->
<plugin>
    <groupId>com.spotify</groupId>
    <artifactId>dockerfile-maven-plugin</artifactId>
    <version>1.3.6</version>
    <executions>
        <execution>
            <id>default</id>
            <goals>
                <goal>build</goal>
                <goal>push</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <!-- 配置Docker镜像名,也用于指定私服地址 -->
        <repository>192.168.1.53:5000/${project.artifactId}</repository>
        <!-- 配置项目tag为最新 -->
        <tag>latest</tag>
        <buildArgs>
            <JAR_FILE>${project.build.finalName}.jar</JAR_FILE>
        </buildArgs>
        <useMavenSettingsForAuth>true</useMavenSettingsForAuth>
    </configuration>
</plugin>

项目根目录还需要配置Dockerfile:

FROM 192.168.1.53:5000/java8
MAINTAINER WeidanLi <toweidan@126.com>

ENTRYPOINT ["/usr/bin/java", "-jar", "/usr/share/myservice/myservice.jar"]

ARG JAR_FILE
ADD target/${JAR_FILE} /usr/share/myservice/myservice.jar

在自己电脑上准备好192.168.1.53:5000/java8这个根镜像,运行mvn package看是否构建成功。


配置Jenkins构建任务

Git地址配置同上面父级pom构建。

接下来配置构建的目录,因为这里是大项目路径,而小项目是在大项目下面的,所以使用的pom不能直接指定,而是要指定构建的目录。

比如这里的spring-cloud-server项目就是在spring-cloud-server/spring-cloud-eureka/pom.xml。所以配置如图所示:

来到这里发现spring-cloud-server这个项目还没有安装到私仓,所以跟上一步一样在配置一个构建项目,或者也可以设置在构建之前先构建这个项目。

安装完父级项目,尝试手动构建一次。

当输出以下信息则表示已经构建成功了。这时候镜像也已经被上传到私服上去,可以在本机上尝试拉取。

[INFO] The push refers to a repository [192.168.1.53:5000/spring-cloud-eureka]
[INFO] Image 08fbb861f2c8: Preparing
[INFO] Image 5205d9541985: Preparing
[INFO] Image d1be66a59bc5: Preparing
[INFO] Image 08fbb861f2c8: Pushing
[INFO] Image 5205d9541985: Layer already exists
[INFO] Image d1be66a59bc5: Layer already exists
[INFO] Image 08fbb861f2c8: Pushed
[INFO] latest: digest: sha256:a42d9331b1b0ecbd382365f0e0bcc5224efe73bd5f96ace20743c05512efd555 size: 954
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:13 min
[INFO] Finished at: 2018-01-04T01:36:53-05:00
[INFO] Final Memory: 49M/118M
[INFO] ------------------------------------------------------------------------
[JENKINS] Archiving /root/.jenkins/jobs/build-eureka/workspace/spring-cloud-server/spring-cloud-eureka/pom.xml to cn.liweidan/spring-cloud-eureka/1.0.0-SNAPSHOT/spring-cloud-eureka-1.0.0-SNAPSHOT.pom
[JENKINS] Archiving /root/.jenkins/jobs/build-eureka/workspace/spring-cloud-server/spring-cloud-eureka/target/spring-cloud-eureka-1.0.0-SNAPSHOT.jar to cn.liweidan/spring-cloud-eureka/1.0.0-SNAPSHOT/spring-cloud-eureka-1.0.0-SNAPSHOT.jar
[JENKINS] Archiving /root/.jenkins/jobs/build-eureka/workspace/spring-cloud-server/spring-cloud-eureka/target/spring-cloud-eureka-1.0.0-SNAPSHOT-docker-info.jar to cn.liweidan/spring-cloud-eureka/1.0.0-SNAPSHOT/spring-cloud-eureka-1.0.0-SNAPSHOT-docker-info.jar
channel stopped
Finished: SUCCESS

在自己的电脑上尝试运行docker run 192.168.1.53:5000/spring-cloud-eureka, 电脑将会去仓库查找该镜像并且尝试运行。这时候运行成功:

OK,可以进入下一步操作,控制Linux生产机器,进行操作。


3.3.2 连接生产服务器,备份、清理旧版本的程序镜像,拉取刚刚构建完成的镜像并且启动;

这时候思路就比较清晰了,构建完成需要做的事情,就是通过ssh进行生产机器的操作,运行脚本实现:
1. 停止、删除正在运行的同名容器
2. 上传备份、清理旧的镜像
3. 运行新的镜像

这时候需要脚本来实现,因为第一次写脚本写的有点丑陋,但是估计功能还是可以实现的,先放上来看看:

# 获取当前项目所有的容器id
data=$(docker ps -a|grep $1|awk '{print $1}')

# 循环所有的线程pid,逐个关闭删除。
for var in ${data[@]}
do
echo "开始关闭以及删除:containerid=$var的容器";
docker stop $var
echo "开始删除:containerid=$var的容器"
docker rm $var
done

# 查询名字为输入的镜像,按照日期打上标签,上传备份,然后删除
DATE=$(date +%Y%m%d%H%M%s)
imageId=$(docker images|grep $1|grep latest| awk '{print $1}')
for var in ${imageId[@]}
do
imgName=$var
newImgName=$imgName:$DATE
shcmd="docker tag $imgName $newImgName"
res=$(${shcmd})
docker push ${newImgName}
docker rmi ${newImgName}
docker rmi ${imgName}:latest
echo ${newImgName} >> history.out
done

# 下载最新标签容器启动
docker run -d -p 8000:8000 192.168.1.53:5000/${1}

脚本使用:
1. 该脚本用于存放在一个文件中,命名为deploy.sh
2. 设置执行权chmod +x ./deploy.sh
3. 执行的时候需要带上项目名,如./deploy.sh spring-cloud-eureka

先在生产机上尝试运行./deploy.sh spring-cloud-eureka看是否能成功.查看Docker进程,有spring-cloud-eureka就说明已经启动成功。

第一步,使用Jenkins所在的机器通过ssh连接到生产环境
可以通过生成证书免密登陆
不过我就比较low用的是sshpass命令:yum install sshpass
这样就可以通过指定密码跳过密码的验证,调试一下sshpass -p 'password' ssh root@ipaddress echo "HelloWorld"
如果能显示HelloWorld则表示连接成功并且打印了。

这时候就可以通过这个脚本告诉生产机器,调用上面的脚本执行部署动作。我的生产机器的IP是192.168.1.46
只要在项目构建完成的时候,执行上面命令就行了。

然后立即构建当前任务,查看console情况。发现构建成功,这时候可以上生产机子上看docker进程。


3.3.2 配置连接gitlab和jenkins

终于剩下最后一步了,就是连接Gitlab和Jenkins,以便能够让Gitlab收到更新就通知Jenkins执行刚刚上面的步骤

在系统管理安装GitLab hook/Gitlab Authentication plugin/gitlab plugin插件

然后在Jenkins构建项目那里能够看到图中的选项,点击高级并生成token

在Gitlab项目设置里面图中所在的地方填入,关闭ssl并且有push事件的时候就触发即可

Gitlab提供测试按钮:测试,在网页的上端出现200即表示已经连接成功

发送完成功的请求之后,Jenkins会出现构建任务,表示已经完成了构建。


四. 总结

花了两天的时间,电脑开了三台虚拟机,Gitlab在运行的时候明显很缓慢,终于弄好了这个过程。

不过这个教程还有些不足地方我需要指出来的:
1. 监听的是整个项目,我提交了不是eureka的东西,还是给我构建了eureka项目,这个是我最最最不希望看到的。
查看:http://blog.csdn.net/csfreebird/article/details/7723107
2. 还没有配置公司自己框架的时候的构建
3. docker私服没有开启权限认证

所以解决这三个问题,大概就可以完成自动化部署的配置了。

[jdk8] jdk8函数式编程思考

零、导读

(一)文章中心

我用jdk8已经半年多了,基本操作已经入门,但是jdk8魅力并非集合流以及线程等等,它要传输的更加是编程方式的改变,要知道像javascript是可以传递函数的,而在以前的jdk版本中,虽然可以但是并非十分的方便。现在官方已经可以通过传递行为的方式传递给函数,让编程中可以对一些基本的动作更加一步的抽象,而不需要写更多的重复代码。

文章中会提到常用的几个函数式接口,这几个接口无疑带来了很多方便。像之前用jdk8的时候写的第一篇文章《[jdk8]Lambda、JDK8接口使用、自定义函数接口》的时候还懵懵懂懂这个过程,所以这篇算是响应第一篇的使用了。

阅读更多

[多线程]Java多线程05_Lock类使用

一. Lock介绍

jdk5中新增了Lock对象,可以用来更加精确的控制线程之间的安全以及线程之间的通讯。

二. ReentrantLock类的使用

通过生产者和消费者来示范ReentrantLock类的使用,这个例子中包含了Lock加锁解锁的使用以及线程之间的通讯

通过创建5个生产者和5个消费者,对同一个资源进行操作,一个生产者一次生产5个,一个消费者一次生产一个。

/**
 * 资源类,包含创建资源和消费资源的方法
 * @author liweidan
 * @date 2017.12.19 下午2:05
 * @email toweidan@126.com
 */
public class Library {
    /** 资源锁 */
    ReentrantLock lock = new ReentrantLock();
    /** 生产者通知 */
    Condition producerCond = lock.newCondition();
    /** 消费者通知 */
    Condition consumerCond = lock.newCondition();
    /** 资源 */
    List<String> stringList = new ArrayList<>();
    /**
     * 生产方法
     */
    public void produce() {
        while (true) {
            try {
                /** 开始加锁 */
                lock.lock();
                /** 如果为空才进入生产 */
                if (stringList.isEmpty()) {
                    for (int i = 0; i < 5; i++) {
                        stringList.add("String - " + i);
                    }
                    System.out.println(Thread.currentThread().getName() + ":生产完成");
                }
                /** 唤醒所有的消费者 */
                consumerCond.signal();
                /** 抢到线程的生产者开始睡眠 */
                producerCond.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
    /**
     * 消费方法
     */
    public void consume() {
        while (true) {
            try {
                /** 开始加锁 */
                lock.lock();
                /** 叛空,如果没有资源提醒消费者生产 */
                if (stringList.isEmpty()) {
                    /** 如果已经没有了则唤醒一个生产者 */
                    producerCond.signal();
                    /** 然后该线程进入休眠 */
                    consumerCond.await();
                } else {
                    /** 消费一个 */
                    stringList.remove(stringList.size() - 1);
                    System.out.println(Thread.currentThread().getName() + ":消费完成");
                    if (stringList.isEmpty()) {
                        /** 如果已经没有了则唤醒一个生产者 */
                        producerCond.signal();
                    } else {
                        /** 如果还有资源,唤醒其他消费者 */
                        consumerCond.signal();
                    }
                    /** 该线程进入休眠 */
                    consumerCond.await();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}

/**
 * 消费者演示Lock类
 * @author liweidan
 * @date 2017.12.19 下午2:05
 * @email toweidan@126.com
 */
public class Consumer implements Runnable {
    private Library library;
    public Consumer(Library library) {
        this.library = library;
    }
    @Override
    public void run() {
        library.consume();
    }
}

/**
 * 生产者演示Lock类
 * @author liweidan
 * @date 2017.12.19 下午2:04
 * @email toweidan@126.com
 */
public class Producer implements Runnable {
    private Library library;
    public Producer(Library library) {
        this.library = library;
    }
    @Override
    public void run() {
        library.produce();
    }
}

private static void startConsumerAndProducer() {
    Library library = new Library();
    Thread[] consumerThreads = new Thread[5];
    Thread[] producerThreads = new Thread[5];
    for (int i = 0; i < 5; i++) {
        consumerThreads[i] = new Thread(new Consumer(library), "Consumer" + i);
        producerThreads[i] = new Thread(new Producer(library), "Producer" + i);
    }
    for (int i = 0; i < 5; i++) {
        consumerThreads[i].start();
        producerThreads[i].start();
    }
}

// 结果:
Producer4:生产完成
Consumer1:消费完成
Consumer4:消费完成
Consumer0:消费完成
Consumer3:消费完成
Consumer2:消费完成
Producer2:生产完成
Consumer3:消费完成
Consumer2:消费完成
Consumer1:消费完成
Consumer4:消费完成
Consumer0:消费完成
Producer3:生产完成
......

可以看到,一个生产线程完成以后可以准确的有四个消费者消费,而且也不会像notifyAll()方法一样盲目的唤醒所有线程,控制粒度更加精细。

(一)公平锁和非公平锁

锁Lock分为公平锁以及非公平锁,两者的特点很好理解:

  1. 公平锁:公平所表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得
  2. 非公平锁:谁先抢到锁就是谁的。

可以看到上面的例子,消费者基本都有按照一定的顺序来做业务,而非公平锁则只要在创建锁的使用使用ReentrantLock lock = new ReentrantLock(false);的构造方法来构建。

(二)Lock类部分方法的使用

  1. int getHoldCount() 查询当前线程锁定(调用lock()方法)的次数
  2. int getQueueLength() 查询当前正等待获取锁定线程的估计数
  3. int getWaitQueueLength(Condition condition) 获取等待一个条件锁的线程数(多少个线程调用了condition.await()方法)
  4. boolean hasQueueThread(Thread thread) 查询指定的线程是否正在等待获取此锁定
  5. booleanhasQueueThreads() 查询是否有线程正在等待此锁定
  6. boolean hasWaiters(Condition condition) 查询是否有现成正在等待与此锁有关的条件
  7. boolean isFair() 查询该锁是否是公平锁
  8. boolean isHeldByCurrentThread() 查询当前线程是否保持此锁定(是否调用了lock()方法)
  9. boolean isLocked() 查询此锁是否由任意线程锁定着(是否有线程调用了lock()方法)
  10. boolean lockInterruptibly()lock()方法作用一样,只不过是如果当前线程被中断了就会抛出异常
  11. boolean tryLock() 调用的时候如果没有其他线程持有锁则锁定,锁定成功返回true
  12. boolean tryLock(long timeout, TimeUnit unit) 指定在等待时间内尝试获取锁

(三)Condition类部分方法的使用

  1. awaitUninterruptibly() 调用此方法如果线程在wait状态则不被标记interrupted状态
  2. awaitUntil(Date deadLine) 线程在指定时间到达前可以被唤醒。

三. ReentrantReadWriteLock类的使用

这是一个把读写锁分离的锁

使用也很简单:
1. 通过ReentrantReadWriteLock lock = new ReentrantReadWriteLock()
2. 通过lock.readLock().lock()对读取操作进行加锁
3. 通过lock.writeLock().lock()对写入操作进行加锁

特性:
1. 读写、写写互斥
2. 读读不互斥

示例读读不互斥

/**
 * 
 * @author liweidan
 * @date 2017.12.19 下午5:12
 * @email toweidan@126.com
 */
public class Library {
    public List<String> stringList;
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    public void read() {
        /** 锁定读取锁 */
        lock.readLock().lock();
        /** 开始读取数据 */
        Iterator<String> iterator = stringList.iterator();
        while (iterator.hasNext()) {
            String next = iterator.next();
            System.out.println(Thread.currentThread().getName() + ": " + next);
        }
        /** 解锁 */
        lock.readLock().unlock();
    }
}

/**
 * 写进程
 * @author liweidan
 * @date 2017.12.19 下午5:09
 * @email toweidan@126.com
 */
public class ReadThread implements Runnable {
    private Library library;
    public ReadThread(Library library) {
        this.library = library;
    }
    @Override
    public void run() {
        library.read();
    }
}

/**
 * 
 * @author liweidan
 * @date 2017.12.19 下午5:10
 * @email toweidan@126.com
 */
public class ThreadStart {
    public static void main(String[] args) {
        startReadThreads();
    }
    private static void startReadThreads() {
        Library aLibrary = new Library();
        aLibrary.stringList = new ArrayList<>(5);
        for (int i = 0; i < 10; i++) {
            aLibrary.stringList.add("String" + i);
        }
        for (int i = 0; i < 5; i++) {
            new Thread(new ReadThread(aLibrary)).start();
        }
    }
}

// 结果: 
...
Thread-3: String8
Thread-3: String9
Thread-0: String5
Thread-0: String6
Thread-0: String7
Thread-0: String8
Thread-0: String9
Thread-1: String5
Thread-1: String6
Thread-1: String7
Thread-1: String8
Thread-1: String9
...
这是乱序打印的

但是如果加上去写的操作那么就是互斥的了:

/**
 * 
 * @author liweidan
 * @date 2017.12.19 下午5:12
 * @email toweidan@126.com
 */
public class Library {
    public List<String> stringList;
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

     ......

    public void write() {
        /** 锁定读取锁 */
        lock.writeLock().lock();
        /** 开始读取数据 */
        for (int i = 0; i < 10; i++) {
            stringList.add("StringO" + i);
        }
        /** 解锁 */
        lock.writeLock().unlock();
    }
}

/** 启动线程用于写数据以及读数据 */
private static void startReadThreads() {
    Library aLibrary = new Library();
    aLibrary.stringList = new ArrayList<>(5);
    for (int i = 0; i < 5; i++) {
        new Thread(new WriteThread(aLibrary)).start();
        new Thread(new ReadThread(aLibrary)).start();
    }
}

// 结果:
Thread-5: StringO8
Thread-5: StringO9
WriteThread: StringO0
WriteThread: StringO1
WriteThread: StringO2
WriteThread: StringO3
WriteThread: StringO4
WriteThread: StringO5
WriteThread: StringO6
WriteThread: StringO7
WriteThread: StringO8
WriteThread: StringO9
Thread-7: StringO0
Thread-7: StringO1
Thread-7: StringO2
Thread-7: StringO3
Thread-9: StringO0
Thread-9: StringO1

// 可以看到读库是乱序的,但是写库是一定在一起的

四. 总结

这一篇写起来发现特别简单,因为复用了之前的东西,只是用法不同了,所以并没有写多少。

  1. Lock类的使用,其中分为普通的锁以及读写锁
  2. 普通锁的使用以及公平锁和非公平锁的选择
  3. Condition决定唤醒的对象,不需要像以前那样全部唤醒
  4. 读写锁分开读和写两个操作,提高线程的性能。

[多线程]Java多线程04_多个线程协作以及线程私有数据

一、线程间的通讯

前面说完了线程的工作、线程的安全等等,那么多个线程的协同工作,线程之间的通讯就是接下来的话题了。

线程之间的等待通知,简而言之就是一个线程做完了自己该做的一件事情,然后通知另外一个线程继续需要的业务操作,实现一个任务由多个线程异步进行从而提升软件性能。

$seq
线程A->线程A: 执行任务
Note right of 线程B: 等待线程A执行完成通知
线程A->线程B: 执行完成,通知线程B启动
Note right of 线程B: 继续执行任务
$

(一)什么时候用到

线程之间的通知需要应用在有对象级别锁的时候,比如前几个说的synchronized方法或者代码块,从而使用锁来进行通知(这时候,Objectwait()notify()以及notifyAll()方法就派上用场了)。

在锁对象调用wait()的时候,它会释放当前占有的锁,线程进入阻塞状态,等待其他线程调用该对象所的notify方法才继续占领锁运行,如果没有调用notify方法则这些线程会一直阻塞下去。如果有多个线程进入等待,则由线程规划器挑选出阻塞状态的线程继续运行。

二、线程通讯实战

(一)简单例子

创建一个线程,执行的时候进入了等待状态,而主线程在3秒以后唤醒他让他继续执行。


/** * 一个进入等待的线程 * @author liweidan */ public class WaitThread implements Runnable { private Object aLock; public WaitThread(Object aLock) { this.aLock = aLock; } @Override public void run() { try { synchronized (aLock) { System.out.println("线程开始进入等待"); aLock.wait(); System.out.println("线程被唤醒,结束了运行"); } } catch (InterruptedException e) { e.printStackTrace(); } } } public class ThreadStart { public static void main(String[] args) { startWaitDemo(); } public static void startWaitDemo() { try { /** 创建一个公共锁 */ Object aLock = new Object(); /** 通过已有的锁创建线程 */ WaitThread waitThread = new WaitThread(aLock); /** 启动线程 */ new Thread(waitThread).start(); /** aLock需要在另外一个线程的监视器中执行 */ synchronized (aLock) { /** 主线程休息3秒 */ Thread.sleep(3000L); /** 唤醒阻塞的线程 */ aLock.notify(); } } catch (InterruptedException e) { e.printStackTrace(); } } } // 结果: 线程开始进入等待 线程被唤醒,结束了运行

注意:
1. aLock一定需要是监视器状态的时候,也就是在synchronized代码块中调用;
2. wait()调用以后,线程会立即释放锁。

(二)wait()立即释放锁而notify()不立即释放

  1. wait()执行完成以后,可以看到线程直接在执行处停了下来并且释放锁。
  2. notify()执行完以后只是通知正在阻塞的线程说可以继续执行下去,但是如果notify()后面还有其他业务代码,则本线程会把后面的业务代码执行完成才算是把锁释放,这时候其他阻塞线程才会开始执行。

1. Thread生命周期

虽然说用时序图来表示不太符合常规,但是因为其箭头的清晰可见,所以我还是用时序图来表示。

线程一共有四个状态:新的线程、运行的线程、暂停的线程以及销毁的线程

$seq
新的线程->运行的线程: start()
新的线程->销毁的线程: stop()
运行的线程->暂停的线程: suspend() / sleep() / wait()
暂停的线程->运行的线程: resume()
运行的线程->运行的线程: yield()
运行的线程->销毁的线程: stop()或者run()结束
$

(三)几个需要注意的地方

  1. 当一个线程正处于wait状态的时候,调用interrupt()方法会抛出InterruptedException
  2. 方法wait(long)作用是:进入wait多少秒以后没有其他线程唤醒则该线程自动唤醒
  3. 通知过早:线程监听器在调用wait()之前就已经由其他线程调用notify(),则该通知无效,线程将一直等待下去
  4. wait()执行的条件发生变化的时候,需要注意条件的设置,以免产生错乱。

(四)生产者与消费者

生产者与消费者模式是最经典的案例了,意思就是生产者生产完数据以后要通知消费者来消费,但是如果生产者或者消费者是多个的情况下,可能出现以下几种情况:

生产者或者消费者使用notify()唤醒的可能是同类(生产者唤醒生产者,消费者唤醒消费者),那么在判断资源的时候可能出现都是空,导致所有线程都在等待,这时候程序就进行不下去了。可以使用notifyAll()来解决。

因为有几种情况:一对一,一对多,多对一。这里就只示范多对多情况,因为解决了这个最复杂的情况,其他貌似都不会太难了。

/**
 * 生产者线程
 * @author liweidan
 * @date 2017.12.18 下午4:28
 * @email toweidan@126.com
 */
public class ProducerThread implements Runnable {
    /** 资源 */
    private List<String> library;
    public ProducerThread(List<String> library) {
        this.library = library;
    }
    @Override
    public void run() {
        synchronized (library) {
            while (true) {
                if (library.isEmpty()) {
                    for (int i = 0; i < 5; i++) {
                        System.out.println("生产者生产: " + "library" + i);
                        library.add("library" + i);
                    }
                    /** 生产完成,通知全部消费者以及生产者 */
                    System.out.println("完成生产,通知全部");
                    library.notifyAll();
                } else {
                    /** 如果资源不为空则进入等待 */
                    try {
                        System.out.println("资源不为空,生产者进入等待");
                        library.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

/**
 * 消费者线程
 * @author liweidan
 * @date 2017.12.18 下午4:28
 * @email toweidan@126.com
 */
public class ConsumerThread implements Runnable {
    /** 资源 */
    private List<String> library;
    public ConsumerThread(List<String> library) {
        this.library = library;
    }
    @Override
    public void run() {
        synchronized (library) {
            while (true) {
                if (!library.isEmpty()) {
                    /** 移除最后一个 */
                    System.out.println("消费者消费: " + library.get(library.size() - 1));
                    library.remove(library.size() - 1);
                    /** 消费完成,通知全部消费者以及生产者 */
                    library.notifyAll();
                } else {
                    /** 如果资源不为空则进入等待 */
                    System.out.println("消费者:资源为空进入等待");
                    try {
                        library.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

public static void startProdAndCous() {
    List<String> lib = new ArrayList<>();
    /** 5个生产者、5个消费者 */
    for (int i = 0; i < 5; i++) {
        new Thread(new ConsumerThread(lib)).start();
        new Thread(new ProducerThread(lib)).start();
    }
}

// 结果:
......
资源不为空,生产者进入等待
消费者消费: library4
消费者消费: library3
消费者消费: library2
消费者消费: library1
消费者消费: library0
消费者:资源为空进入等待
生产者生产: library0
生产者生产: library1
生产者生产: library2
生产者生产: library3
生产者生产: library4
完成生产,通知全部
资源不为空,生产者进入等待
资源不为空,生产者进入等待
消费者消费: library4
消费者消费: library3
消费者消费: library2
消费者消费: library1
消费者消费: library0
消费者:资源为空进入等待
消费者:资源为空进入等待
......

可以看到,结果已经十分明显,可以实现资源的生产与消费。

这里还有个问题,就是唤醒的时候没有分清楚唤醒谁,在接下来的Lock可以实现。

三、线程之间数据的数据通讯

jdk提供了两对类,分别用于传输数据流以及字节流,对应IO流来学习即可,分别是:
1)PipedInputStream和PipedOutputStream
2)PipedReader和PipedWriter

/**
 * 读的线程
 * @author liweidan
 * @date 2017.12.18 下午5:08
 * @email toweidan@126.com
 */
public class ReadThread implements Runnable {
    private PipedInputStream inputStream;
    public ReadThread(PipedInputStream inputStream) {
        this.inputStream = inputStream;
    }
    @Override
    public void run() {
        try {
            while (true) {
                byte[] arr = new byte[1024];
                int len = inputStream.read(arr);
                while (len != -1) {
                    System.out.println("收到数据,开始读取数据: ");
                    String str = new String(arr, 0, len);
                    System.out.println(str);
                    len = inputStream.read(arr);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

/**
 * 写的线程
 * @author liweidan
 * @date 2017.12.18 下午6:00
 * @email toweidan@126.com
 */
public class WriteThread implements Runnable {
    private PipedOutputStream outputStream;
    public WriteThread(PipedOutputStream outputStream) {
        this.outputStream = outputStream;
    }
    @Override
    public void run() {
        try {
            while (true) {
                Scanner scanner = new Scanner(System.in);
                String str = scanner.next();
                outputStream.write(str.getBytes());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

/**
 * 
 * @author liweidan
 * @date 2017.12.18 下午6:05
 * @email toweidan@126.com
 */
public class ThreadStart {
    public static void main(String[] args) {
        try {
            PipedOutputStream outputStream = new PipedOutputStream();
            PipedInputStream inputStream = new PipedInputStream();
            inputStream.connect(outputStream);
            new Thread(new ReadThread(inputStream)).start();
            new Thread(new WriteThread(outputStream)).start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

// 结果: 
dsasd
收到数据,开始读取数据: 
dsasd
HelloWorld
收到数据,开始读取数据: 
HelloWorld

两个重点:
1. 当inputStreamwhile里面已经”枯竭”的时候,就会继续等待,等有数据进来的时候,再把它打印出来。
2. 通过inputStream.connect(outputStream);这个方法连接两个流

PS:貌似开发中比较少用这个?

PipedReader和PipedWriter使用与字节流相似。

四、等待线程运行——join使用

作用:创建线程后,调用线程的join方法。那么调用的线程会停下来(或指定停下来多长时间),等待线程执行完成输出结果,调用线程才继续运行。

代码所示主线程创建了A线程后,等待A线程执行完成再继续执行主线程。

/**
 * Join使用
 * @author liweidan
 * @date 2017.12.19 上午9:54
 * @email toweidan@126.com
 */
public class JoinDemo implements Runnable {
    @Override
    public void run() {
        try {
            /** 随机休眠时间 */
            double v = Math.random() * 3;
            Thread.sleep((long) v);
            /** 输出结果 */
            System.out.println("wait in " + v + "ms");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

private static void startJoinDemo() {
    try {
        Thread aThread = new Thread(new JoinDemo());
        aThread.start();
        aThread.join();
        System.out.println("Main end in " + System.currentTimeMillis());
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

// 结果: 
wait in 1.9918097432208672ms
Main end in 1513648665010  // 可以看到Main end 总是在线程执行完成后的输出

当然也会有坑:当我们通过join指定等待时间的时候,有可能多个线程之间互相抢夺资源但是主线程抢到了发现时间已经足够了就提前给执行后面的逻辑。

在这个例子中,我把join线程修改为睡眠2秒,然后再启动其他线程,多个线程进行抢夺资源。

/**
 * Join使用
 * @author liweidan
 * @date 2017.12.19 上午9:54
 * @email toweidan@126.com
 */
public class JoinDemo implements Runnable {
    @Override
    public void run() {
        try {
            /** 随机休眠时间 */
            double v = Math.random() * 3;
            /** 指定休息2秒 */
            Thread.sleep(2000);
            /** 输出结果 */
            System.out.println("wait in " + v + "ms");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

private static void startJoinDemo02() {
    try {
        Thread aThread = new Thread(new JoinDemo());
        Thread otherThread = new Thread(() -> {
            try {
                Thread.sleep(2000L);
                System.out.println("other end");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        aThread.start();
        otherThread.start();
        aThread.join(1000L);
        System.out.println("Main end in " + System.currentTimeMillis());
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

// 结果: 
Main end in 1513649551229 // main提前结束了。
other end
wait in 0.8592433100474021ms

运行图解:

$seq
主线程->aThread: 创建A线程
主线程->otherThread: 创建其他线程
主线程->aThread: 启动A线程
主线程->otherThread: 启动其他线程
主线程->aThread: 等待1秒
Note right of aThread: aThread等待2秒
Note right of 主线程: 主线程发现已经停够1秒,打印
$

五、线程私有变量存储: ThreadLocal和InheritableThreadLocal

我们可以这么理解,每个线程执行的空间是相互隔离,当有需要在线程内部进行数据传递的时候可以通过ThreadLocal以及InheritableThreadLocal(可以获取父线程的值)来做数据,相当于一个线程之间的小数据库,用于存储相应的数据的。像我们项目就是在每次请求进来设置当前线程的值是用户类,然后在后面的各个分层如果需要拿到用户信息,即可快速获取。

举个例子:创建两个线程,分别调用同一个方法,这个方法从ThreadLocal获取值,证明每个线程拿到的值是当前线程设置的。

/**
 * 线程私有变量
 * @author liweidan
 * @date 2017.12.19 上午10:47
 * @email toweidan@126.com
 */
public class ThreadHolder {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    public static String set(String value) {
        threadLocal.set(value);
        return value;
    }
    public static String get() {
        return threadLocal.get();
    }
}

/**
 * 用于给线程调用,证明不同线程输出来的值是不一样的
 * @author liweidan
 * @date 2017.12.19 上午10:52
 * @email toweidan@126.com
 */
public class ServicceA {
    public void test() {
        String v = ThreadHolder.get();
        System.out.println("ServiceA: " + v + ", Current: " + Thread.currentThread().getName());
    }
}

/**
 * A线程
 * @author liweidan
 * @date 2017.12.19 上午10:45
 * @email toweidan@126.com
 */
public class ThreadA implements Runnable {
    private ServicceA servicceA;
    public ThreadA(ServicceA servicceA) {
        this.servicceA = servicceA;
    }
    @Override
    public void run() {
        ThreadHolder.set("AThread value");
        System.out.println(ThreadHolder.get());
        new ServicceA().test();
    }
}

/**
 * B线程
 * @author liweidan
 * @date 2017.12.19 上午10:46
 * @email toweidan@126.com
 */
public class ThreadB implements Runnable {
    private ServicceA servicceA;
    public ThreadB(ServicceA servicceA) {
        this.servicceA = servicceA;
    }
    @Override
    public void run() {
        ThreadHolder.set("BThread value");
        System.out.println(ThreadHolder.get());
        new ServicceA().test();
    }
}

// 结果
AThread value
ServiceA: AThread value, Current: Thread-0
BThread value
ServiceA: BThread value, Current: Thread-1

通过开启两个不同的线程,但是注入相同的一个对象,在对象里面获得的值都是各自线程设置的,说明这个类是可以隔离各个线程之间的值,用于存放需要经常获取的值进行计算,以便于在同一个线程当中可以随处使用。

InheritableThreadLocal则可以在线程中创建子线程然后获取到对应的父线程的值。但是需要注意的是当子线程已经获取到值的时候,父线程对值进行修改,子线程并不会响应后面的修改,所以在项目中,最好是一旦设置了这个值就不要对其作出修改,以免业务发生错误。

六、总结

  1. 线程之间通过wait()以及notify()相互通知执行,需要注意唤醒的同类以免造成软件相互等待1
  2. 线程之间通过PipedStream进行数据通讯
  3. ThreadLocal的使用