java 并发实践 --ConcurrentHashMap 与 CAS

前言

最近在做接口限流时涉及到了一个有意思问题,牵扯出了关于 concurrentHashMap 的一些用法,以及 CAS 的一些概念。限流算法很多,我主要就以最简单的计数器法来做引。先抽象化一下需求:统计每个接口访问的次数。一个接口对应一个 url,也就是一个字符串,每调用一次对其进行加一处理。可能出现的问题主要有三个:

  1. 多线程访问,需要选择合适的并发容器
  2. 分布式下多个实例统计接口流量需要共享内存
  3. 流量统计应该尽可能不损耗服务器性能

但这次的博客并不是想描述怎么去实现接口限流,而是主要想描述一下遇到的问题,所以,第二点暂时不考虑,即不使用 redis。

说到并发的字符串统计,立即让人联想到的数据结构便是 ConcurrentHashpMap<String,Long> urlCounter;


volatile 疑问记录

对 java 中 volatile 关键字的描述,主要是 可见性有序性 两方面。

一个很广泛的应用就是使得多个线程对共享资源的改动变得互相可见,如下:


浅析 java 内存模型(JMM)

并发编程模型的分类

在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体)。通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。

在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写 - 读内存中的公共状态来隐式进行通信。在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。

同步是指程序用于控制不同线程之间操作发生相对顺序的机制。在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。

Java 的并发采用的是共享内存模型,Java 线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。如果编写多线程程序的 Java 程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。


ThreadLocal 的最佳实践

SimpleDateFormat 众所周知是线程不安全的,多线程中如何保证线程安全又同时兼顾性能问题呢?那就是使用 ThreadLocal 维护 SimpleDateFormat

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 SimpleDateFormatThreadTest {

static volatile AtomicInteger n = new AtomicInteger(-1);

<!-- more -->

static ThreadLocal<DateFormat> sdf ;

static {
sdf =new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
}

public static void main(String[] args) throws ParseException, InterruptedException {

Set<String> dateSet = new ConcurrentHashSet<>();
Set<Integer> numberSet = new ConcurrentHashSet<>();

Date[] dates = new Date[1000];
for (int i = 0; i < 1000; i++) {
dates[i] = sdf.get().parse(i + 1000 + "-11-22");
}

ExecutorService executorService = Executors.newFixedThreadPool(10);
for(int i=0;i<1000;i++){
executorService.execute(new Runnable() {
@Override
public void run() {
int number = n.incrementAndGet();
String date = sdf.get().format(dates[number]);
numberSet.add(number);
dateSet.add(date);
System.out.println(number+" "+date);
}
});
}
executorService.shutdown();
Thread.sleep(5000);
System.out.println(dateSet.size());
System.out.println(numberSet.size());
}

}

实践证明 sdf 的 parse(String to Date)有严重的线程安全问题,format(Date to String)有轻微的线程安全问题,虽然不太明显,但还是会出现问题,这和内部的实现有关。

简单分析下使用 ThreadLocal 的好处,1000 次转换操作,10 个线程争抢执行,如果每次都去 new 一个 sdf,可见其效率之低,而使用 ThreadLocal,是对每个线程维护一个 sdf,所以最多就只会出现 10 个 sdf,真正项目中,由于操作系统线程分片执行,所以线程不会非常的多,使用 ThreadLocal 的好处也就立竿见影了。


java trick--String.intern()

《深入理解 java 虚拟机》第二版中对 String.intern() 方法的讲解中所举的例子非常有意思

不了解 String.intern() 的朋友要理解他其实也很容易,它返回的是一个字符串在字符串常亮池中的引用。直接看下面的 demo

1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) {
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);

<!-- more -->

String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
}

两者输出的结果如下:

1
2
true
false

我用的 jdk 版本为 Oracle JDK7u45。简单来说,就是一个很奇怪的现象,为什么 java 这个字符串在类加载之前就已经加载到常量池了?

我在知乎找到了具体的说明,如下:

1
2
3
4
5
6
7
8
9
10
11
package sun.misc;

import java.io.PrintStream;

public class Version {
private static final String launcher_name = "java";
private static final String java_version = "1.7.0_79";
private static final String java_runtime_name = "Java(TM) SE Runtime Environment";
private static final String java_runtime_version = "1.7.0_79-b15";
...
}

而 HotSpot JVM 的实现会在类加载时先调用:

1
2
3
4
5
6
7
8
9
public final class System{
...
private static void initializeSystemClass() {
...
sun.misc.Version.init();
...
}
...
}

原来是 sun.misc.Version 这个类在起作用。


java trick -- intergerCache

看一段代码:

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
Integer a=100,b=100,c=150,d=150;
System.out.println(a==b);
System.out.println(c==d);
}
}

这段代码会输出什么?


使用 JPA 实现乐观锁

乐观锁的概念就不再赘述了,不了解的朋友请自行百度谷歌之,今天主要说的是在项目中如何使用乐观锁,做成一个小 demo。

持久层使用 jpa 时,默认提供了一个注解 @Version 先看看源码怎么描述这个注解的

1
2
3
4
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface Version {
}

简单来说就是用一个 version 字段来充当乐观锁的作用。
先来设计实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Created by xujingfeng on 2017/1/30.
*/
@Entity
@Table(name = "t_student")
public class Student {

@Id
@GenericGenerator(name = "PKUUID", strategy = "uuid2")
@GeneratedValue(generator = "PKUUID")
@Column(length = 36)
private String id;

@Version
private int version;

private String name;

//getter()...
//setter()...
}

使用 zkclient 操作 zookeeper 的学习过程记录

前言

最近开发的分布式 (使用 motan) 项目中使用 zookeeper 作为服务中心来提供注册服务 (@MotanService) 和发现服务(@MotanRefer), 虽然 motan 这个 rpc 框架对服务模块进行了很好的封装,但是以防以后会出现定制化的需求,以及对服务更好的监控,所以有必要了解一下 zookeeper 的基本知识和使用方法。关于 zookeeper 的知识点,网上很多的博客都已经介绍的很详尽了,我写这篇的博客的用意其实也就是将一些零散的却很精妙的博客整理出来,方便以后查阅。短篇以 cp 的方式,长篇的以 url 的方式。

zookeeper 是什么?

ZooKeeper 是一个分布式的,开放源码的分布式应用程序协调服务,是 Google 的 Chubby 一个开源的实现,是 Hadoop 和 Hbase 的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。
ZooKeeper 的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
ZooKeeper 包含一个简单的原语集,提供 Java 和 C 的接口。 ZooKeeper 代码版本中,提供了分布式独享锁、选举、队列的接口。

—- 百度百科

一开始看的云里雾里的,幸好我之前搞过一点 hadoop,对他的生态体系有所了解,这才大概知道他想说什么。提炼几个关键词,并且加入我后面学习的理解,总结一下就是 –

zookeeper 是一个组件,需要安装客户端和服务端,一般用于解决分布式开发下的一些问题。化抽象为具体,你可以把整个 zookeeper 理解成一个树形数据结构,也可以理解为一个文件系统的结构,每个叶子节点都会携带一些信息 (data),并且也可能会携带一些操作 (op)。分布式场景中,每一个客户端都可以访问到这些叶子节点,并且进行一些操作。我们所有使用 zookeeper 的场景几乎都是在 CRUD 某一个或者某些叶子节点,然后会触发对应的操作… 即 zookeeper 本身可以理解为一个 shareData。
—- 来自于博主的口胡

zookeeper 怎么学?

学一个新的中间件的最好方法是先在脑子里面有一个想法:我为什么要学他,是想解决什么问题,他大概是个什么东西,我觉得打开思路的最好方式是看几篇博客 (大多数情况你一开始看不懂,但是混个眼熟),然后看视频,这里我自己是了解过了 zookeeper 原生的 api 之后看了极客学院 的视频


Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×