并发带来的问题

先看一个单例类,后文中都会用到:

public class SimpleWorkingHardSingleton {
private static SimpleWorkingHardSingleton simpleSingleton = new SimpleWorkingHardSingleton(); // 数量
private int count; private SimpleWorkingHardSingleton() {
count = 0;
} public static SimpleWorkingHardSingleton getInstance() {
return simpleSingleton;
} public int getCount() {
return count;
} public void addCount(int increment) {
this.count += increment;
System.out.println(this.count);
} }

使用原子变量同步

上文中,我们已经知道这个类的getCount方法对count的操作是线程不安全的,我们可以用一些原子变量来实现原子性:

public class SimpleWorkingHardSingleton {
private static SimpleWorkingHardSingleton simpleSingleton = new SimpleWorkingHardSingleton(); // 数量
private AtomicLong atomicCount = new AtomicLong(0); private SimpleWorkingHardSingleton() {
count = 0;
} public static SimpleWorkingHardSingleton getInstance() {
return simpleSingleton;
} public AtomicLong getAtomicCount() {
return atomicCount;
} public void addAtomicCount(long increment) {
this.atomicCount.getAndAdd(increment);
} }

可以看到,在这个类中,我们把count使用AtomicLong原子类。java的jdk包实现了一系列的原子类,这些原子类型的操作都是原子的。那么count的增加就不会分为3步(获取,增加,赋值)了,这个原子的操作是原子类内部实现的,我们在使用过程中只需知道这个操作过程是原子的、不可分割的即可。在使用原子类型的情况下:count变量是会达到预期的效果的。

原子变量失效情况

这里所说的原子变量的失效情况是指当类中使用了多个原子变量,如果一个操作要改变多个原子变量,那么还是会出现同步问题:

public class SimpleWorkingHardSingleton {
private static SimpleWorkingHardSingleton simpleSingleton = new SimpleWorkingHardSingleton(); // 数量
private AtomicLong atomicCount = new AtomicLong(0); private AtomicLong atomicCountCopy = new AtomicLong(0); private SimpleWorkingHardSingleton() {
count = 0;
} public static SimpleWorkingHardSingleton getInstance() {
return simpleSingleton;
} public AtomicLong getAtomicCount() {
return atomicCount;
} public AtomicLong getAtomicCountCopy() {
return atomicCountCopy;
} public void addAtomicCount(long increment) {
this.atomicCount.getAndAdd(increment);
this.atomicCountCopy.getAndAdd(increment);
}
}

这种情况下,atomicCount和atomicCountCopy各自的增加是原子的,但是两个变量都增加这个过程是两步,不是原子的。若是a、b两根线程在运行addAtomicCount方法,a线程执行完atomicCount的增加,此时a线程挂起,b线程执行,并且执行了atomicCount和atomicCountCopy的增加,那么此时atomicCountCopy就要比atomicCount小1了,因为a线程还有一半的任务没有执行呢。

java关键字synchronized实现同步

java提供了一种内置的锁机制同步代码块(synchronized block),它包括两部分:锁对象和由锁对象保护的代码块。

  1. 若synchronized修饰了一段代码,则负责保护一段代码;
    synchronized (lock) {
    // 操作或访问由lock保护的代码块
    }
  2. 若修饰了一个方法,则负责保护这个方法的全部代码,锁是当前对象;若synchronized修饰静态方法,那么同步代码块的锁是Class
public class SimpleWorkingHardSingleton {
private static SimpleWorkingHardSingleton simpleSingleton = new SimpleWorkingHardSingleton(); // 数量
private int count; private int countCopy; private SimpleWorkingHardSingleton() {
count = 0;
} public static SimpleWorkingHardSingleton getInstance() {
return simpleSingleton;
} public int getCount() {
return count;
} public int getCountCopy() {
return countCopy;
} public synchronized void addCount(int increment) {
/*
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
System.err.println(e);
}
*/
this.count += increment;
this.countCopy += increment;
System.out.println(this.count);
} }

上文代码中synchronized对整个方法进行了修饰,那么保护的代码就是方法中的全部代码;这样在多线程环境中,会有序递增地输出count。但是这样有一个潜在问题就是性能问题;

synchronized对整个方法进行了修饰,就会导致这个方法每次只有一个线程可以运行,这就会导致性能问题;假如这个方法中有一个耗时3s的io操作,我们用Thread.sleep(3000);来模拟。然而synchronized保护的代码块本不应该包含这3s的操作,因此代码应该写成:

public void addCount(int increment) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
System.err.println(e);
}
synchronized (this) {
this.count += increment;
System.out.println(this.count);
}
}

上文中两个变量不同步的情况,就可以用synchronized同步代码块来解决;而且使用synchronized要注意,先保证正确性,即可能产生并发问题的共享变量都要放在同步代码块当中;然后再追求性能,即对尽可能短的代码进行保护,也不能太过细化因为锁的使用和释放都是需要代价的。

一个稍微复杂的场景(多看例子多模仿系列)

/**
* 实现带缓存功能的因子分解
*/
public class CachedFactorizer {
private static CachedFactorizer cachedFactorizer = new CachedFactorizer();
// 上一个处理的数字
private long lastNumber;
// 上一个数字分解的结果
private long[] lastFactors;
// 处理数字的次数
private long hits;
// 缓存命中的次数
private long cacheHits; private CachedFactorizer() { } public synchronized long getHits() {
return hits;
} public synchronized double getCacheHitRatio() {
return (double)cacheHits / (double)hits;
} public static CachedFactorizer getInstance() {
return cachedFactorizer;
} public long[] factor(int target) {
// 伪代码,假装实现了因子分解
return new long[] {};
} public void doFactor(int target) {
Thread.sleep(300);
synchronized (this) {
hits++;
if (target == lastNumber) {
cacheHits++;
} else {
lastNumber = target;
lastFactors = factor(target);
}
}
}
}
  1. 其实可以在doFactor方法前用synchronized修饰,然而这样不符合性能问题;所以应该用synchronized修饰代码块即可
  2. getHits和getCacheHitRatio方法加上了synchronized修饰,用的锁就是this,所以和doFactor里面的锁是一样的;因而达到的效果是在doFactor内进行因子计算时候,getHits和getCacheHitRatio方法在阻塞状态

java锁机制的重入

当一个线程请求另一个线程持有的锁的时候,那么请求的线程会阻塞;重入的概念是:当线程去获取自己所拥有的锁,那么会请求成功;重入的原理是:为每个锁关联一个计数器和持有者线程,当计数器为0时候,这个锁被认为是没有被任何线程持有;当有线程持有锁,计数器自增,并且记下锁的持有线程,当同一线程继续获取锁时候,计数器继续自增;当线程退出代码块时候,相应地计数器减1,直到计数器为0,锁被释放;此时这个锁才可以被其他线程获得。

public class Parent {
public synchronized void do() { }
} public class Child extends Parent {
@Override
public synchronized void do() {
blabla
super.do();
}
}

如果没有重入机制,那么Child对象在执行do方法时候会发生死锁,因为它拿不到自己持有的锁

参考内容

  1. 书籍《Java并发编程实战》

Java并发(二)-实现同步的更多相关文章

  1. Java并发编程:同步容器

    Java并发编程:同步容器 为了方便编写出线程安全的程序,Java里面提供了一些线程安全类和并发工具,比如:同步容器.并发容器.阻塞队列.Synchronizer(比如CountDownLatch). ...

  2. 【转】Java并发编程:同步容器

    为了方便编写出线程安全的程序,Java里面提供了一些线程安全类和并发工具,比如:同步容器.并发容器.阻塞队列.Synchronizer(比如CountDownLatch).今天我们就来讨论下同步容器. ...

  3. 8、Java并发编程:同步容器

    Java并发编程:同步容器 为了方便编写出线程安全的程序,Java里面提供了一些线程安全类和并发工具,比如:同步容器.并发容器.阻塞队列.Synchronizer(比如CountDownLatch). ...

  4. java并发:线程同步机制之Volatile关键字&原子操作Atomic

    volatile关键字 volatile是一个特殊的修饰符,只有成员变量才能使用它,与Synchronized及ReentrantLock等提供的互斥相比,Synchronized保证了Synchro ...

  5. java并发:线程同步机制之Lock

    一.初识Lock Lock是一个接口,提供了无条件的.可轮询的.定时的.可中断的锁获取操作,所有加锁和解锁的方法都是显式的,其包路径是:java.util.concurrent.locks.Lock, ...

  6. Java并发编程之同步

    1.synchronized 关键字 synchronized 锁什么?锁对象. 可能锁对象包括: this, 临界资源对象,Class 类对象. 1.1 同步方法 synchronized T me ...

  7. Java并发(二):基础概念

    并发编程的第二部分,先来谈谈发布(Publish)与逸出(Escape); 发布是指:对象能够在当前作用域之外的代码中使用,例如:将对象的引用传递到其他类的方法中,对象的引用保存在其他类可以访问的地方 ...

  8. Java并发编程:同步锁、读写锁

    之前我们说过线程安全问题可以用锁机制来解决,即线程必要要先获得锁,之后才能进行其他操作.其实在 Java 的 API 中有这样一些锁类可以提供给我们使用,与其他对象作为锁相比,它们具有更强大的功能. ...

  9. Java并发编程基础——同步

    一.synchronized 关键字 1)synchronized 锁什么?锁对象.可能锁对象包括: this, 临界资源对象,Class 类对象.如同下面例子所示: package cn.test. ...

  10. java并发值多线程同步业务场景以及解决方案

    1.20个人排队同时访问2个购票窗口,同时能购票的只有两个人,当其中一个人买票完成后,18个人中的其中一个在占用窗口进行购买. 20个人相当于20个线程,2相当于资源,当18个人等待的时候,相当于线程 ...

随机推荐

  1. 解决Axure发布分享预览的3个方法

    公司的同事制作的一个产品原型,要发给我,我当时正在客户这里,电脑上并没有Axure,客户又催得急,感到一阵无奈.这次回来之后,经过一番摸索,发现还是有办法的.这里给大家分享一下Axure发布分享预览的 ...

  2. vue路由组件传参

    在组件中使用 $route 会使之与其对应路由形成高度耦合,从而使组件只能在某些特定的 URL 上使用,限制了其灵活性. 使用 props 将组件和路由解耦: 取代与 $route 的耦合 const ...

  3. attempt to create delete event with null entity

    解决办法:删除之前判断是否为空 if(Object != null){ session.delete(Object); }

  4. css样式记忆

    text-indent: 2em;   //开头空两格: display : none;    //隐藏元素 background:#CCC;  //背景颜色 background: url(imag ...

  5. 2018.09.26 bzoj1015: [JSOI2008]星球大战starwar(并查集)

    传送门 并查集经典题目. 传统题都是把删边变成倒着加边,这道题是需要倒着加点. 处理方法是将每个点与其他点的边用一个vector存起来,加点时用并查集统计答案就行了. 代码: #include< ...

  6. 2018.07.22 bzoj3613: [Heoi2014]南园满地堆轻絮(逆序对结论题)

    传送门 做这道题有一个显然的结论,就是要使这个数列单调不减,就要使所有逆序对保证单调不减,也就是求出所有逆序对的最大差值,然后除以2然后就没了. 代码如下: #include<bits/stdc ...

  7. xampp环境 安装 用法 composer

    准备工作 1.打开PHP配置文件E:\xampp\php\php.ini确认以下模块已开启(移除前面的分号). extension=php_openssl.dll, extension=php_cur ...

  8. DIV+CSS实战(二)

    一.说明 在DIV+CSS实战(一)中,已经把框架搭建起来了,现在就需要往框架里面添加内容了.需要实现的内容如下图: 二.头部的设计(全媒体订阅) 左侧是一张图片+标题 ,右侧是登录名 和上次登录的时 ...

  9. jmeter 5.0版本更新说明(个人做个记录)

    变化   此页面仅详细说明了当前版本中所做的更改. 先前更改的历史记录中详细介绍了早期更改.   5.0版 摘要 新的和值得注意的 不兼容的变化 Bug修复 改进 非功能性变化 已知问题和解决方法 谢 ...

  10. HDU1078 FatMouse and Cheese(DFS+DP) 2016-07-24 14:05 70人阅读 评论(0) 收藏

    FatMouse and Cheese Problem Description FatMouse has stored some cheese in a city. The city can be c ...