明明用了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. Flex中创建Accordion报错

    1.错误描述 2.错误原因 <mx:Accordion width="100%" height="100%"> <s:NavigatorCon ...

  2. Linux显示系统日期

    Linux显示系统日期 youhaidong@youhaidong-ThinkPad-Edge-E545:~$ date 2015年 01月 21日 星期三 20:37:39 CST

  3. iOS - IM 即时通讯

    1.即时通讯技术 即时通讯(IM:Instant Messaging):又称实时通讯,支持用户在线实时交谈,允许两人或多人使用网络实时的传递文字消息.文件.语音与视频交流. 即时通讯在开发中使用的场景 ...

  4. freemarker之list遍历(八)

    1.设置数据源 /** * * @Title:student * @Description: * @param:@param name * @return: void * @throws */ pri ...

  5. Docker学习——Lepus部署

    Lepus部署(基于docker)及mysql慢查询配置 介绍 Lepus是一个由Python+PHP开发的数据库企业级监控系统,可用于MySQL/Oracle/MongoDB/Redis 下载镜像 ...

  6. [AH/HNOI2017]礼物

    \[推推公式,即求\Sigma^{n}_{i=1} (x_{i+k}-y_i+c)^2最小,c范围为[-m, m]\] \[拆开,就是\Sigma x_i^2 + \Sigma y_i^2 + n * ...

  7. CANopen--基于DS402协议的伺服电机原点回零模式实现

    硬件:Copley 驱动器ACJ-055-18+Maxon DCX-35L电机 1.原点回零方式简介: 有几种原点回零方法,每种方法建立:原点参考(限位或原点开关转换或编码器索引脉冲)运动方向以及索引 ...

  8. jdk,maven,tomcat 的环境配置

    一般的下载,我想就不用我多说什么了,mysql的配置在mysql压缩包安装方式上,这里直接贴图,下次要是忘记了可以看一眼. 这两个是直接新建的,打开方式为我的电脑,右键:属性,左侧:高级系统设置,下侧 ...

  9. java接口----继承(实现)方法

    文中"实现"一词特指接口的继承. 一个类实现多个接口时,不能出现同名的默认方法. 一个类既要实现接口又要继承抽象类,先继承后实现. 一个抽象类可以继承多个接口(implements ...

  10. Java基于TCP的Socket编程练习

    环境:Notpad ++ 6.0 + JDK 6.0.31 问题:使用套接字编写客户-服务器程序,实现客户-服务器交互计算.客户将三角形3个边的长度发给服务器,服务器把计算出的三角形的面积返回给客户. ...