明明用了ConcurrentHashMap,可是始终线程不安全,

下面我们来看代码:

 public class Test40 {  

     public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
System.out.println(test());
}
} private static int test() throws InterruptedException {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String, Integer>();
ExecutorService pool = Executors.newCachedThreadPool();
for (int i = 0; i < 8; i++) {
pool.execute(new MyTask(map));
}
pool.shutdown();
pool.awaitTermination(1, TimeUnit.DAYS); return map.get(MyTask.KEY);
}
} class MyTask implements Runnable { public static final String KEY = "key"; private ConcurrentHashMap<String, Integer> map; public MyTask(ConcurrentHashMap<String, Integer> map) {
this.map = map;
} @Override
public void run() {
for (int i = 0; i < 100; i++) {
this.addup();
}
} private void addup() {
if (!map.containsKey(KEY)) {
map.put(KEY, 1);
} else {
map.put(KEY, map.get(KEY) + 1);
}
}
}

测试代码跑了10次,每次都不是800。这就很让人疑惑了,难道ConcurrentHashMap的线程安全性失效了?

查了一些资料后发现,原来ConcurrentHashMap的线程安全指的是,它的每个方法单独调用(即原子操作)都是线程安全的,但是代码总体的互斥性并不受控制。以上面的代码为例,最后一行中的:

 map.put(KEY, map.get(KEY) + 1);  

实际上并不是原子操作,它包含了三步:

  1. map.get
  2. 加1
  3. map.put

其中第1和第3步,单独来说都是线程安全的,由ConcurrentHashMap保证。但是由于在上面的代码中,map本身是一个共享变量。当线程A执行map.get的时候,其它线程可能正在执行map.put,这样一来当线程A执行到map.put的时候,线程A的值就已经是脏数据了,然后脏数据覆盖了真值,导致线程不安全

简单地说,ConcurrentHashMap的get方法获取到的是此时的真值,但它并不保证当你调用put方法的时候,当时获取到的值仍然是真值

为了使上面的代码变得线程安全,我引入了synchronized关键字来修饰目标方法,如下:

 public class Test40 {  

     public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
System.out.println(test());
}
} private static int test() throws InterruptedException {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String, Integer>();
ExecutorService pool = Executors.newCachedThreadPool();
for (int i = 0; i < 8; i++) {
pool.execute(new MyTask(map));
}
pool.shutdown();
pool.awaitTermination(1, TimeUnit.DAYS); return map.get(MyTask.KEY);
}
} class MyTask implements Runnable { public static final String KEY = "key"; private ConcurrentHashMap<String, Integer> map; public MyTask(ConcurrentHashMap<String, Integer> map) {
this.map = map;
} @Override
public void run() {
for (int i = 0; i < 100; i++) {
this.addup();
}
} private synchronized void addup() { // 用关键字synchronized修饰addup方法
if (!map.containsKey(KEY)) {
map.put(KEY, 1);
} else {
map.put(KEY, map.get(KEY) + 1);
}
} }

运行之后仍然是线程不安全的,难道synchronized也失效了?

查阅了synchronized的资料后,原来,不管synchronized是用来修饰方法,还是修饰代码块,其本质都是锁定某一个对象。修饰方法时,锁上的是调用这个方法的对象,即this;修饰代码块时,锁上的是括号里的那个对象

在上面的代码中,很明显就是锁定的MyTask对象本身。但是由于在每一个线程中,MyTask对象都是独立的,这就导致实际上每个线程都对自己的MyTask进行锁定,而并不会干涉其它线程的MyTask对象。换言之,上锁压根没有意义

理解到这点之后,对上面的代码又做了一次修改:

 public class Test40 {  

     public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
System.out.println(test());
}
} private static int test() throws InterruptedException {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String, Integer>();
ExecutorService pool = Executors.newCachedThreadPool();
for (int i = 0; i < 8; i++) {
pool.execute(new MyTask(map));
}
pool.shutdown();
pool.awaitTermination(1, TimeUnit.DAYS); return map.get(MyTask.KEY);
}
} class MyTask implements Runnable { public static final String KEY = "key"; private ConcurrentHashMap<String, Integer> map; public MyTask(ConcurrentHashMap<String, Integer> map) {
this.map = map;
} @Override
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (map) { // 对共享对象map上锁
this.addup();
}
}
} private void addup() {
if (!map.containsKey(KEY)) {
map.put(KEY, 1);
} else {
map.put(KEY, map.get(KEY) + 1);
}
} }

此时在调用addup时直接锁定map,由于map是被所有线程共享的,因而达到了让所有线程互斥的目的,线程安全达成。

修改后,ConcurrentHashMap的作用就不大了,可以直接将代码中的map换成普通的HashMap,以减少由ConcurrentHashMap带来的锁开销

最后特别补充的是,synchronized关键字判断对象是否是它属于锁定的对象,本质上是通过 == 运算符来判断的。换句话说,上面的代码中,可以采用任何一个常量,或者每个线程都共享的变量,或者MyTask类的静态变量,来代替map。只要该变量与synchronized锁定的目标变量相同(==),就可以使synchronized生效

综上,代码最终可以修改为:

 public class Test40 {  

     public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100; i++) {
System.out.println(test());
}
} private static int test() throws InterruptedException {
Map<String, Integer> map = new HashMap<String, Integer>();
ExecutorService pool = Executors.newCachedThreadPool();
for (int i = 0; i < 8; i++) {
pool.execute(new MyTask(map));
}
pool.shutdown();
pool.awaitTermination(1, TimeUnit.DAYS); return map.get(MyTask.KEY);
}
} class MyTask implements Runnable { public static Object lock = new Object(); public static final String KEY = "key"; private Map<String, Integer> map; public MyTask(Map<String, Integer> map) {
this.map = map;
} @Override
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (lock) {
this.addup();
}
}
} private void addup() {
if (!map.containsKey(KEY)) {
map.put(KEY, 1);
} else {
map.put(KEY, map.get(KEY) + 1);
}
} }

ConcurrentHashMap、synchronized与线程安全的更多相关文章

  1. java线程安全问题以及使用synchronized解决线程安全问题的几种方式

    一.线程安全问题 1.产生原因 我们使用java多线程的时候,最让我们头疼的莫过于多线程引起的线程安全问题,那么线程安全问题到底是如何产生的呢?究其本质,是因为多条线程操作同一数据的过程中,破坏了数据 ...

  2. Synchronized之三:Synchronized与线程中断、线程wait

    线程中断 见<Thread之八:interrupt中断> 正如中断二字所表达的意义,在线程运行(run方法)中间打断它,在Java中,提供了以下3个有关线程中断的方法 //中断线程(实例方 ...

  3. Java多线程-同步:synchronized 和线程通信:生产者消费者模式

    大家伙周末愉快,小乐又来给大家献上技术大餐.上次是说到了Java多线程的创建和状态|乐字节,接下来,我们再来接着说Java多线程-同步:synchronized 和线程通信:生产者消费者模式. 一.同 ...

  4. Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。

    Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离.

  5. hashmap,hashTable concurrentHashMap 是否为线程安全,区别,如何实现的

    线程安全类 在集合框架中,有些类是线程安全的,这些都是jdk1.1中的出现的.在jdk1.2之后,就出现许许多多非线程安全的类. 下面是这些线程安全的同步的类: vector:就比arraylist多 ...

  6. ConcurrentHashMap如何保证线程安全

    以前看过HashMap的内部实现,知道HashMap是使用Node数组+链表+红黑树的数据结构来实现,如下图所示.但是HashMap是非线程安全,在多线程环境不能够使用. 不过JDK在其并发包中为我们 ...

  7. notify,wait,synchronized实现线程间通知

    wait阻塞线程释放锁:notify使wait所在的线程被唤醒在次获得锁,并执行,但要等到notify所在的线程代码全部执行后! 示例代码如下: package com.vhbi.service.im ...

  8. [转]Java 对象锁-synchronized()与线程的状态与生命周期

      线程的状态与生命周期 Java 对象锁-synchronized() ? 1 2 3 4 synchronized(someObject){   //对象锁 } 对象锁的使用说明: 1.对象锁的返 ...

  9. 线程同步(使用了synchronized)和线程通讯(使用了wait,notify)

    线程同步 什么是线程同步? 当使用多个线程来访问同一个数据时,非常容易出现线程安全问题(比如多个线程都在操作同一数据导致数据不一致),所以我们用同步机制来解决这些问题. 实现同步机制有两个方法:1.同 ...

随机推荐

  1. textarea的不可拉伸和不可编辑

    不可拉伸: textarea { resize: none; } 不可编辑: 第一种方法: <textarea disabled></textarea> 第二种方法: < ...

  2. 手机浏览网页或打开App时莫名弹出支付宝领红包界面的原因及应对措施

    自从支付宝推出扫码领红包活动后,这种模式独特的赏金机制,短时间内吸引了大量的关注,但是随之也产生了很多的问题,比由于如在赏金的驱动下,微信群里铺天盖地的红包口令,朋友圈里各式各样的领红包二维码图片, ...

  3. 初学 Java Web 开发,从 Servlet 开发

    1. 基本要求:Java 编程基础 有良好的 Java 语言编程基础,这是必须的,在讨论 Web 开发技术时提了一个 Java 编程基础的问题会被鄙视的. 2. 环境准备 (Eclipse + Tom ...

  4. 【BZOJ4869】相逢是问候(线段树,欧拉定理)

    [BZOJ4869]相逢是问候(线段树,欧拉定理) 题面 BZOJ 题解 根据欧拉定理递归计算(类似上帝与集合的正确用法) 所以我们可以用线段树维护区间最少的被更新的多少次 如果超过了\(\varph ...

  5. Dapper一个和petapoco差不多的轻量级ORM框架

    我们都知道ORM全称叫做Object Relationship Mapper,也就是可以用object来map我们的db,而且市面上的orm框架有很多,其中有一个框架 叫做dapper,而且被称为th ...

  6. CSS3字体发光效果

    text-shadow 该属性为文本添加阴影效果 text-shadow: h-shadow v-shadow blur color; h-shadow: 水平阴影的位置(阴影水平偏移量),可为负值, ...

  7. 解析Visual C# 7.2中的private protected访问修饰符

    去年12月份,随着Visual Studio 2017 Update 15.5的发布,Visual C#迎来了它的最新版本:7.2. 在这个版本中,有个让人难以理解的新特性,就是private pro ...

  8. WordPress菜单“显示选项”无法显示的解决办法

    比较新版本的WordPress会出现点击“外观”——“菜单”右上角的“显示选项”无法打开的问题,而老版本的就没有这个问题,后台的其他页面中的这个 功能都可以正常使用,看来问题是因为中文版WordPre ...

  9. 发个2012年用java写的一个控制台小游戏

    时间是把杀狗刀 突然发现了12年用java写的控制台玩的一个文字游戏,有兴趣的可以下载试试哈汪~ 里面难点当时确实遇到过,在计算倒计时的时候用了多线程,当时还写了好久才搞定.很怀念那个时间虽然不会做游 ...

  10. 什么是IPFS?(三)

    前两篇介绍了IPFS的基本概念, 那么IPFS都可以用来做什么? 如果你认为仅仅是CDN, 永久的web? Are you kidding me ? IPFS可是要 --改变世界的... --变世界的 ...