Ruiyeclub

  • HOME
  • ARCHIVES
  • ABOUT
  • LINKS
🐤SpringBoot学习入门Demo,持续更新中...: https://github.com/ruiyeclub/SpringBoot-Hello

Java并发编程(一):线程基础知识以及synchronized关键字

Posted on 2022-02-03

1.线程与多线程的概念:在一个程序中,能够独立运行的程序片段叫作“线程”(Thread)。多线程(multithreading)是指从软件或者硬件上实现多个线程并发执行的技术。

2.多线程的意义:多线程可以在时间片里被cpu快速切换,资源能更好被调用、程序设计在某些情况下更简单、程序响应更快、运行更加流畅。

2.如何启动一个线程:继承Thread类、实现Runnable接口、实现Callable接口

3.为什么要保证线程的同步?:java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。

4.基本的线程同步:使用synchronized关键字、特殊域变量volatile、wait和notify方法等。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class T {
private int count=10;
private Object o=new Object();

public void m(){
synchronized (o){
//任何线程要执行下面的代码,必须先拿到o的锁
count--;
System.out.println(Thread.currentThread().getName()+"count="+count);
}
}
}

假设这段代码中有多个线程,当第一个线程执行到m方法来的时候,sync锁住了堆内存中的o对象,这个时候第二个线程是进不来的,它必须等第一个线程执行完,锁释放掉才可以接着执行。这里有个锁的概念叫做互斥锁,sync是一种互斥锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class T1 {
private static int count =10;

public synchronized static void m(){
//这里等同于synchronized(T3.class)
count--;
System.out.println(Thread.currentThread().getName()+"count="+count);
}

public static void mm(){
synchronized (T1.class){
//思考:这里写成synchronized(this)是否可以?
count--;
}
}
}

思考这里,注意sync修饰的是一个静态方法和静态的属性,静态修饰的方法和属性是不需要new出对象来就可以访问的,所以这里没有new出对象,sync锁定的是T3.class对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class T2 implements Runnable{
private int count =10;

@Override
public /*synchronized*/ void run(){
count--;
System.out.println(Thread.currentThread().getName()+"count="+count);
}

public static void main(String[] args) {
T2 t=new T2();
for (int i=0;i<5;i++){
new Thread(t,"THREAD"+i).start();
}
}
}

上面代码中开启了五个线程,执行run方法对count进行减一的操作,这里会出现线程抢占资源的问题。当第一个线程在执行run方法时,减减的过程中,第二个线程也进入了方法,同样也在执行减减,可能会出现第二个线程减完的时候,第一个线程才输出count,这时候就出现了线程重入。处理方法可以加上synchronized关键字,只有等第一个线程执行完毕,第二个线程才可以进入,即同一时刻只能有一个线程来对它进行操作,也就是原子性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class Account {

/**
* 账号持有人和账号余额
*/
String name;
double balance;

/**
* 设置账号余额,线程睡眠目的是先让它读数据
* @param name
* @param balance
*/
public synchronized void set(String name,double balance){
this.name=name;

try {
Thread.sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
this.balance=balance;
}

public double getBalance(String name){
return this.balance;
}

public static void main(String[] args) {
Account a=new Account();
new Thread(()->a.set("zhangsan",100.0)).start();

try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(a.getBalance("zhangsan"));

try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(a.getBalance("zhangsan"));
}
}

这是一个小demo,代码中只对set方法加了锁,没有对get方法加锁,这个时候会出现脏读现象。解决方法是读和写的方法都加锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class T3 {

synchronized void m1(){
System.out.println("m1 start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
m2();
}

synchronized void m2() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m2");
}
}

这个案例中,m1和m2方法都已经加上了锁,当执行m1的时候再去执行m2,这样是可以的。一个同步方法可以调用另外一个同步方法,也就是说sync获得的锁是可重入锁。还有个概念是死锁,死锁是指多个线程抢占资源而造成的一种互相等待。举个例子,一个箱子需要两把钥匙才可以打开,钥匙分别在两个人手中,这两个人互相抢占另外一个人的钥匙,导致箱子打不开。


Docker快速上手之部署SpringBoot项目

Posted on 2022-02-01

Docker是基于Go语言实现的云开源项目。

Docker的主要目标是“Build,Ship and Run Any App,Anywhere”,也就是通过对应用组件的封装、分发、部署、运行等生命周期的管理,使用户的APP(可以是一个WEB应用或数据库应用等等)及其运行环境能够做到“一次封装,到处运行”。
image.png
Linux 容器技术的出现就解决了这样一个问题,而 Docker 就是在它的基础上发展过来的。将应用运行在 Docker 容器上面,而 Docker 容器在任何操作系统上都是一致的,这就实现了跨平台、跨服务器。只需要一次配置好环境,换到别的机子上就可以一键部署好,大大简化了操作。

一、我所理解的Docker

我喜欢将Docker比喻成”方便面”,为什么说是方便面,之前部署项目环境的配置十分麻烦,换一台机器,就要重来一次,费力费时。举个栗子,我们部署一个SpringBoot项目,我们需要在服务器上面配置项目的运行环境,要安装各种各样的软件JDK/MySQL/Redis/nginx,安装和配置这些东西十分麻烦,下次需要换个服务器重新部署又得重新安装一遍,简直要命。而Docker能将项目连带着运行环境一同部署过去,就好像泡面,泡面所需的调料包还有工具都附加在了里面。一句话总结Docker的好处,Docker解决了运行环境和配置问题,方便做持续集成并有助于整体发布的容器虚拟化技术。

二、Docker与虚拟机的区别

Docker容器 虚拟机(VM)
操作系统 与宿主机共享OS 宿主机OS上运行虚拟机OS
存储大小 镜像小,便于存储与传输 镜像庞大(vmdk、vdi等)
运行性能 几乎无额外性能损失 操作系统额外的CPU、内存消耗
移植性 轻便、灵活,适用于Linux 笨重,与虚拟化技术耦合度高
硬件亲和性 面向软件开发者 面向硬件运维者

三、Docker里面三个重要的概念Dockerfile、镜像(image)、容器(Container)

1.Dockerfile是用来构建Docker镜像的构建文件,是由一系列命令和参数构成的脚本。

Dockerfile体系结构:

1
2
3
4
5
6
7
8
9
10
11
12
FROM  基础镜像,当前新镜像是基于哪个镜像的
MAINTAINER  镜像维护者的姓名和邮箱地址
RUN  容器构建时需要运行的命令
EXPOSE  当前容器对外暴露出的端口
WORKDIR  指定在创建容器后,终端默认登录进来的工作目录,一个落脚点
ENV  用来在构建镜像过程中设置环境变量
AD  将宿主机目录下的文件拷贝进镜像且ADD命令会自动处理URL和解压tar压缩包
COPY  类似ADD,拷贝文件和目录到镜像中。将从构建上下文目录中<源路径>的文件/目录复制到新的一层的镜像内的<目标路径>位置
VOLUME  容器数据卷,用于数据保存和持久化工作
CMD  指定一个容器启动时要运行的命令。Dockerfile中可以有多个CMD指令,但只要最后一个生效,CMD会被docker run之后的参数替换
ENTRYPOINT  指定一个容器启动时要运行的命令。ENTRYPOINT的目的和CMD一样,都是在指定容器启动程序及参数
ONBUILD  当构建一个被继承的Dockerfile时运行命令,父镜像在被子继承后父镜像的onbuild被触发

2.镜像是一种轻量级、可执行的独立软件包,用来打包软件运行环境和基于运行环境开发的软件,它包含运行某个软件所需的所有内容,包括代码、运行时、库、环境变量和配置文件。

3.容器是用镜像创建的运行实例。它可以被启动、开始、停止、删除。每个容器都是相互隔离的、保证安全的平台。可以把容器看做是一个简易版的Linux环境(包括root用户权限、用户空间和网络空间等)和运行在其中的应用程序。

四、安装Docker

1.安装Docker依赖包

1
yum install -y yum-utils device-mapper-persistent-data lvm2

2.设置阿里云镜像源

1
yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
1
3.安装 Docker-CE
1
yum install docker-ce

4.查看是否安装成功

1
docker version

image.png

五、使用Docker安装MySQL

1.使用Docker拉取Mysql镜像(这里安装的是MySQL5.6版本)

1
docker pull mysql:5.6

2.安装mysql命令

1
docker run -d -p 3306:3306 --name mysql -v /ray/mysql/conf:/etc/mysql/conf.d -v /ray/mysql/logs:/logs -v /ray/mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=admin -d mysql:5.7

命令说明:

1
2
3
4
5
6
7
-p 3306:3306  将主机的3306端口映射到docker容器的3306端口
--name mysql  运行服务器名字
-v /ray/mysql/conf:/etc/mysql/conf.d  将主机/ray/mysql目录下的conf/my.cnf挂载到容器的/etc/mysql/conf.d
-v /ray/mysql/logs:/logs  将主机/ray/mysql目录下的logs目录挂载到容器的/logs
-v /ray/mysql/data:/var/lib/mysql  将主机/ray/mysql目录下的data目录挂载到容器的/var/lib/mysql
-e MYSQL_ROOT_PASSWORD=admin  初始化root用户的密码
-d mysql:5.7  后台程序运行mysql5.7

3.进入mysql

1
docker exec -it MySQL运行成功后的容器ID /bin/bash

然后即可登录MySQL,创建数据库,或者使用Navicat等工具创建数据库。

六、使用Docker安装Redis

1.使用Docker拉取Redis镜像(这里安装的是Redis3.2版本)

1
docker pull redis:3.2

2.安装redis命令(这里不对命令做解释)

1
docker run -p 6379:6379 -v /ray/redis/data:/data -v /ray/redis/conf/redis.conf:/usr/local/etc/redis/redis.conf -d redis:3.2 redis-server /usr/local/etc/redis/redis.conf --appendonly yes

加密码--requirepass "admin"

七、Docker部署SpringBoot项目

1.将项目打包成jar包(假设名字为myblog.jar),并编写一个Dockerfile文件,文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Docker image for springboot file run
# VERSION 0.0.1
# Author: Ray
# 基础镜像使用java
FROM java:8
# 作者
MAINTAINER Ray <185048761@qq.com>
# VOLUME 指定了临时文件目录为/tmp。
# 其效果是在主机 /var/lib/docker 目录下创建了一个临时文件,并链接到容器的/tmp
VOLUME /tmp
# 将jar包添加到容器中并更名为app.jar
ADD myblog.jar app.jar
# 运行jar包
RUN bash -c 'touch /app.jar'
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
#暴露8080端口
EXPOSE 80

2.将这两个文件一并上传至服务器中的同一个目录下面,进入该文件夹后执行此命令构建镜像:
image.png

1
docker build -t myblog.jar .

3.生成docker容器,并运行

1
docker run -d -p 80:80 myblog.jar

等一会儿,SpringBoot项目跑起来了后,就可以使用浏览器通过80端口进行访问了


JVM基础快速入门篇

Posted on 2022-01-31

Java是一门可以跨平台的语言,但是Java本身是不可以实现跨平台的,需要JVM实现跨平台。javac编译好后的class文件,在Windows、Linux、Mac等系统上,只要该系统安装对应的Java虚拟机,class文件都可以运行。达到”一次编译,到处运行”的效果。

一、JVM是什么?

而JVM到底是什么呢?引用百度百科对JVM的介绍:

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

二、JVM架构知识

1.JVM主要包含类装载器、运行时数据区(内存模型)、执行引擎。里面内存模型有可以细分包括本地方法栈、堆、栈(线程)、方法区(元空间)、程序计数器。
如图所示:
image.png

1.类装载器的作用就是负责加载class文件,class文件在文件开头有特定的文件标示,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由执行引擎(Execution Engine)决定。类加载器又有四大类加载器:

启动类加载器(Bootstrap ClassLoader):负责加载JRE核心类库,像JRE中的rt.jar等(C/C++);

扩展类加载器(Extension ClassLoader):负责加载JRE扩展目录ext中的jar包;

系统类加载器(Application ClassLoader):负责加载ClassPath路径下的类包;

自定义的类加载器(User ClassLoader):只加载指定目录下的jar和class,想加载其它位置的类或jar时得自行定义类加载器;

2.JVM中,对象都是在堆中分配内存空间的,栈只用于保存局部变量和临时变量,如果是对象,只保存引用,实际内存还是在堆中。栈的特点是先进后出,假设一个main方法,里面执行了一个方法,我们是后执行的那个方法即那个线程,但是执行完毕后,那个线程是先销毁的。每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表(存放局部变量的)、操作数栈(存放需要运算的数据)、动态链接、方法出口(执行完方法 回到main方法的位置)等信息,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。即一个方法对应一块栈帧内存区域(存放本方法的局部变量)。

3.方法区存储已被虚拟机加载的类信息、常量(堆中的对象常量地址)、静态变量、即时编译器编译后的代码等数据。

4.本地方法栈和栈的作用类似,但是它服务的对象是native方法,该方法得由c语言来实现。

5.程序计数器作用是当CPU多线程切换的时候,切回到当前线程的时候,回到程序计数器计数的位置,继续执行。

三、堆内存的结构模型

新生代包括Eden加上2个survivor区,执行minor gc之后,大多数的对象会被回收,活着的进入s0,再次minor gc,活着的对象eden+s0->s1,再次minor gc,eden+s1->s0…这里虚拟机采取了分代收集的思想来管理内存,JVM给每个对象一个对象年龄计数器,分代年龄达到15后即对象被执行了15次minor gc后移入老年代。除此之外,s区装不下的大对象也会直接进入老年代。

最后,老年代存的就是一些大对象和需要连续内存空间的对象(静态变量、缓存、线程池等),老年代满了的话,会执行Full GC垃圾收集。官网对Full GC的解释中介绍了个词”Stop-The-World”,它会把所有的应用线程停掉,这时候系统会卡掉。JVM虚拟机调优的目标就是减少full gc即减少STW。
image.png

四、垃圾回收机制

堆里面存放new的对象和数组,Java优于其他语言一个很重要的原因就是它能自动处理垃圾对象,也就是有垃圾回收机制(GC)。有了垃圾回收机制有几点好处编程简单,系统不容易出错。

1.什么是垃圾?

我们把没有任何引用指向的对象或者一堆对象(循环引用)定义为垃圾。

2.系统如何定位垃圾

  • 引用计数算法-简单且高效但是主流的Java虚拟机里并没有选用引用计数算法来管理内存,因为它不能解决循环引用的问题。
  • 根可达分析算法-将”GC Roots”对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象。线程栈的本地变量、静态变量、本地方法栈的变量等等都可以称作GC Roots跟节点。
    如图:
    image.png

3.常见的垃圾回收算法

  • 复制算法-没有碎片 浪费内存空间(目前使用新生代使用的是复制算法)
  • 标记清除-位置不连续 产生内存碎片
  • 标记压缩-没有碎片 效率偏低
    复制算法它将可用内存按容量分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

五.常见垃圾回收器

新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:CMS、Serial Old、Parallel Old
整堆收集器: G1

jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)


1…45

  •   GitHub
  •   Ray4j.top
  •   Springboot-Hello
  •    Search
© 2022 — 2025 Cr.    |   
UV PV
TOP