明明用了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. Linux显示USB设备

    Linux显示USB设备 youhaidong@youhaidong-ThinkPad-Edge-E545:~$ lsusb -tv /: Bus 08.Port 1: Dev 1, Class=ro ...

  2. CF371 D Searching Rectangles

    基本思路就是二分 每条边分别二分求 c++11 用fflush(sdtout) 不行 囧啊 #include<bits/stdc++.h> using namespace std; int ...

  3. 异常-----freemarker.template.TemplateException

    一,案例一 1.1.错误描述 五月 30, 2014 11:33:57 下午 freemarker.log.JDK14LoggerFactory$JDK14Logger error 严重: Templ ...

  4. [HAOI2008]木棍分割

    题目大意 网址:https://daniu.luogu.org/problemnew/show/P2511 题目大意: 有n根木棍, 第i根木棍的长度为Li,n根木棍依次连结了一起, 总共有n-1个连 ...

  5. NOIWC2018滚粗记

    Day0 \(THUWC\)刚刚结束..顺利的滚粗了 Day1 整理一下自己咸鱼的心 下午到学校坐车出发 这次队伍浩大THUWC只有4个 又到了雅礼洋湖这个地方 路上突然多了许多有关\(NOI\)的标 ...

  6. iOS逆向工程,(狗神)沙梓社大咖免费技术分享。

    序言 简介:本文针对于广大iOS开发者,作为一名开发者,仅仅专注于一门语言可能已经不适用现在的市场需求,曾经因高薪和需求量巨大,而火爆一时的移动端开发者(Android,ios),如今的路却是不再那么 ...

  7. Python标准库概览

    Python标准库通常被称为"自带的电池",自然地提供了广泛的功能,涵盖了大概200个左右的包与模块.不断有高质量的包或模块被开发出来,极大的丰富了标准库.但有些模块放在标准库中很 ...

  8. linux 下oracle 11g静默安装(完整版)

    1.操作系统及Oracle版本Linux版本:CentOS release 6.5Oracle版本:Oracle Database 11g Release 2 (11.2.0.1.0) for Lin ...

  9. js获取本机的网络IP地址

    JavaScript是一门脚本语言,是不能操作文件,读取本地信息的,所以想要获取IP,还需要借助后端技术.方法如下: //获取本机的网络ip地址 function jsonpCallback(res) ...

  10. 网页中输出HTML代码

    >>>>>> mustache.js的解决方案 var entityMap = { '&': '&', '<': '<', '>' ...