ConcurrentHashMap、synchronized与线程安全
明明用了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);
实际上并不是原子操作,它包含了三步:
- map.get
- 加1
- 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与线程安全的更多相关文章
- java线程安全问题以及使用synchronized解决线程安全问题的几种方式
一.线程安全问题 1.产生原因 我们使用java多线程的时候,最让我们头疼的莫过于多线程引起的线程安全问题,那么线程安全问题到底是如何产生的呢?究其本质,是因为多条线程操作同一数据的过程中,破坏了数据 ...
- Synchronized之三:Synchronized与线程中断、线程wait
线程中断 见<Thread之八:interrupt中断> 正如中断二字所表达的意义,在线程运行(run方法)中间打断它,在Java中,提供了以下3个有关线程中断的方法 //中断线程(实例方 ...
- Java多线程-同步:synchronized 和线程通信:生产者消费者模式
大家伙周末愉快,小乐又来给大家献上技术大餐.上次是说到了Java多线程的创建和状态|乐字节,接下来,我们再来接着说Java多线程-同步:synchronized 和线程通信:生产者消费者模式. 一.同 ...
- Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离.
- hashmap,hashTable concurrentHashMap 是否为线程安全,区别,如何实现的
线程安全类 在集合框架中,有些类是线程安全的,这些都是jdk1.1中的出现的.在jdk1.2之后,就出现许许多多非线程安全的类. 下面是这些线程安全的同步的类: vector:就比arraylist多 ...
- ConcurrentHashMap如何保证线程安全
以前看过HashMap的内部实现,知道HashMap是使用Node数组+链表+红黑树的数据结构来实现,如下图所示.但是HashMap是非线程安全,在多线程环境不能够使用. 不过JDK在其并发包中为我们 ...
- notify,wait,synchronized实现线程间通知
wait阻塞线程释放锁:notify使wait所在的线程被唤醒在次获得锁,并执行,但要等到notify所在的线程代码全部执行后! 示例代码如下: package com.vhbi.service.im ...
- [转]Java 对象锁-synchronized()与线程的状态与生命周期
线程的状态与生命周期 Java 对象锁-synchronized() ? 1 2 3 4 synchronized(someObject){ //对象锁 } 对象锁的使用说明: 1.对象锁的返 ...
- 线程同步(使用了synchronized)和线程通讯(使用了wait,notify)
线程同步 什么是线程同步? 当使用多个线程来访问同一个数据时,非常容易出现线程安全问题(比如多个线程都在操作同一数据导致数据不一致),所以我们用同步机制来解决这些问题. 实现同步机制有两个方法:1.同 ...
随机推荐
- Caused by: java.lang.ClassNotFoundException: com.mchange.v2.ser.Indirector
1.错误描述 usage: java org.apache.catalina.startup.Catalina [ -config {pathname} ] [ -nonaming ] { -help ...
- JSP常见的7个动作指令
JSP常见的7个动作指令 1.jsp:forward指令 执行页面转向,将请求处理转发到下一个页面 2.jsp:param指令 用于传递参数 3.jsp:include指令 用于动态 ...
- class-支持向量机SVM全析笔记
support vector machines,SVM是二类分类模型.定义在特征空间上间隔最大的线性分类器,由于包括核技巧实质上成为非线性分类器.学习策略是间隔最大化,可形式化为求解凸二次规划问题(c ...
- java暴力递归回溯算法
今天这个问题是我之前一直想解决的,还记得以前第一次上蓝桥杯的课的时候,也就是大一高数期中模拟考试那天,下午去上蓝桥杯课,遇到这道题,当时写了写,根本没有思路,然后就给大一的模拟考试去了.印象深刻啊,一 ...
- MyEclipse 2014专业版的破解--Windows系统的软件安装
一.破解前的准备 MyEclipse2014破解包: 您可以到计算机相关专业所用软件---百度云链接下载中找到链接地址进行下载. 二.破解步骤 1.打开破解文件资源包 2.执行run.bat 3.输入 ...
- CSS3动画--过渡效果
CSS3动画--过渡效果 transition 设置四个过渡属性 transition-property 过渡的名称 tr ...
- 【BZOJ3531】旅行(树链剖分,线段树)
[BZOJ3531]旅行(树链剖分,线段树) 题面 Description S国有N个城市,编号从1到N.城市间用N-1条双向道路连接,满足 从一个城市出发可以到达其它所有城市.每个城市信仰不同的宗教 ...
- POJ 2516 Minimum Cost (费用流)
题面 Dearboy, a goods victualer, now comes to a big problem, and he needs your help. In his sale area ...
- log4j日志输出性能优化-缓存、异步
转载 1.log4j已成为大型系统必不可少的一部分,log4j可以很方便的帮助我们在程序的任何位置输出所要打印的信息,便于我们对系统在调试阶段和正式运行阶段对问题分析和定位.由于日志级别的不同,对系 ...
- 关于java多态的理解
要理解多态,就必须有一个大的理解方向,不然很容易绕进去. 首先知道多态的释义:多态性是指一个名词可以有多种语义. 对于java的多态性学习者来说,就是必须要知道多个同名方法在不同情况下的使用规则. j ...