标签:
0.1)本文部分文字转自“深入理解jvm”, 旨在学习 线程安全与锁优化 的基础知识;
0.2)本文知识对于理解 java并发编程非常有用,个人觉得,所以我总结的很详细;
【1】概述
【2】线程安全
1)线程安全定义:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的;(干货——线程安全定义)


2)绝对线程安全
2.1)在java API中标注自己是线程安全的类,大多数都不是绝对的线程安全
2.2)java.util.Vector 是一个线程安全的容器,因为它的add()方法,get()方法,size() 方法 这些方法都是被 synchronized修饰的,尽管效率低下,但确实是安全的;对Vector的测试如下:
// 对线程安全的容器 Vector的测试
public class VectorTest {
private static Vector<Integer> vector = new Vector<>();
public static void main(String[] args) {
while(true) {
for (int i = 0; i < 100; i++) {
vector.add(i);
}
Thread removeThread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
}
});
Thread printThread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
System.out.println(vector.get(i));
}
}
});
removeThread.start();
printThread.start();
// 不要同时产生过多的线程,否则会导致os 假死
while(Thread.activeCount() > 20);
}
}
}
对以上代码的分析(Analysis):
A1)运行结果: 作者说会抛出异常(但我的运行结果却没有抛出异常),按理说应该是会抛出异常的;
A2)抛出异常的原因:因为如果另一个线程恰好在错误的时间里删除了一个元素,导致序号i 已经不再可用的话,再用i 访问数组就会抛出一个 ArrayIndexOutOfBoundsException。
A3)如果要保证这段代码能够正确执行下去,修改后的代码为:
// 对线程安全的容器 Vector的测试(修改后的代码)
public class ModifiedVectorTest {
private static Vector<Integer> vector = new Vector<>();
public static void main(String[] args) {
while(true) {
for (int i = 0; i < 100; i++) {
vector.add(i);
}
Thread removeThread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (vector) { // 添加同步块,this line
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
}
}
});
Thread printThread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (vector) { // 添加同步块,this line
for (int i = 0; i < vector.size(); i++) {
System.out.println(vector.get(i));
}
}
}
});
removeThread.start();
printThread.start();
// 不要同时产生过多的线程,否则会导致os 假死
while(Thread.activeCount() > 20);
}
}
}
3.1)上述 VectorTest.java 和 ModifiedVectorTest.java 就是相对线程安全的案例;
4.1)线程兼容定义:线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用;

case1)等待可中断:指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助;1.6.3)关于synchronized 和 ReentrantLock 性能的分析:

对上图的分析(Analysis):
A1)多线程环境下 synchronized的吞吐量下降得非常严重,而 ReentrantLock 则能基本保持在同一个比较稳定的水平上;与其说ReentrantLock性能好,还不如说 synchronized还有非常大的优化余地;
A2)虚拟机在未来的性能改进中肯定也会更加偏向于原生的 synchronized,所以还是提倡在 synchronized能实现需求的情况下,优先考虑使用 synchronized 来进行同步;(干货——同步方式推荐使用synchronized)
2.4)如何使用CAS 操作来避免阻塞同步,看个荔枝:(测试incrementAndGet 方法的原子性)
// Atomic 变量自增运算测试(incrementAndGet 方法的原子性)
public class AtomicTest {
public static AtomicInteger race = new AtomicInteger(0);
public static void increase() {
// 输出正确结果,一切都要归功于 incrementAndGet 方法的原子性
race.incrementAndGet();
}
public static final int THREADS_COUNT = 20;
public static void main(String[] args) throws Exception {
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
increase();
}
}
});
threads[i].start();
}
while(Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println(race);
}
/**
* incrementAndGet() 方法的JDK 源码
* Atomically increment by one the current value.
* @return the updated value
*/
public final int incrementAndGet() {
for(;;) {
int current = get();
int next = current + 1;
if(compareAndSet(current,next)) {
return next;
}
}
}
}
2.5)CAS操作(比较并交换操作)的ABA问题:如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就说它的值没有被其他线程改变过了吗? 如果在这段期间它的值曾经被改为了B,之后又改回了A,那CAS操作就会误认为它从来没有被改变过,这个漏洞称为 CAS操作的 ABA问题;
2.6)解决方法:J.U.C 包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的version 来保证CAS的正确性。不过目前来说这个类比较鸡肋, 大部分cases 下 ABA问题 不会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效;(干货——CAS操作(比较并交换操作)的ABA问题及其解决方法)

public class LockEliminateTest {
// raw code
public String concatString(String s1, String s2, String s3) {
return s1 + s2 + s3;
}
// javac 转化后的字符串连接操作
public String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
}
对以上代码的分析(Analysis):
A1)对于 javac 转化后的字符串连接操作代码: 使用了同步,因为StringBuffer.append() 方法中都有一个同步块,锁就是sb对象。虚拟机观察变量sb,很快就会发现他的动态作用域被限制在 concatString() 方法内部;也就是所 sb 的所有引用都不会逃逸到方法之外;
A2)所以,虽然这里有锁,但是可以被安全地消除掉,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了;

// javac 转化后的字符串连接操作
public String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}


http://www.cnblogs.com/pacoson/p/5351355.html
标签:
原文地址:http://www.cnblogs.com/softidea/p/5352974.html