废话不不多说,先上代码:
public class Test {
private static int count = 0;
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new Thread() {
public void run() {
for (int i = 0; i < 200; i++) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
};
}.start();
}
try {
Thread.sleep(3000);
System.out.println("===" + count);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上述代码最终System.out是的多少?400?答案是不确定的,有可能是400,有可能比400小的数。这是因为是线程不安全造成的(可见性)。线程A修改了成某个值之后,相当于只更新了其工作内存的值,没有马上更新到主内存上。所以线程B从主内存读取的时候还是旧的值。
加上线程安全的悲观锁synchronized,能解决并发问题,代码如下:
public class Test {
private static int count = 0;
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new Thread() {
public void run() {
for (int i = 0; i < 200; i++) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (Test.class) {
count++;
}
}
};
}.start();
}
try {
Thread.sleep(3000);
System.out.println("===" + count);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
这一次,加上同步锁后,最终输出结果必然是400。Synchronized虽然确保了线程的安全,但是在性能上却不是最优的,Synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。
原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类,使用如下:
public static void main(String[] args) {
// 初始化内存值是10
final AtomicInteger value = new AtomicInteger(10);
new Thread(){
public void run() {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "..." + value.get());
value.compareAndSet(10, 20);
System.out.println(Thread.currentThread().getName() + "..." + value.get());
};
}.start();
new Thread(){
public void run() {
System.out.println(Thread.currentThread().getName() + "###" + value.get());
value.compareAndSet(10, 30);
System.out.println(Thread.currentThread().getName() + "###" + value.get());
};
}.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 结果是:
Thread-1###10
Thread-1###30
Thread-0...30
Thread-0...30
java.util.concurrent.atomic完全建立在CAS之上,CAS有三个操作数,内存值V、旧的预期值A、要修改的值B,如果 V == A, 那么 V = B,返回true;否则什么都不做返回false。
1.CAS(Compare And Swap)比较并替换,是线程并发运行时用到的一种技术
2.CAS是原子操作,保证并发安全,而不能保证并发同步
3.CAS是CPU的一个指令(需要JNI调用Native方法,才能调用CPU的指令)
4.CAS是非阻塞的、轻量级的乐观锁
5.在Java中 CAS 底层使用的就是自旋锁(是指尝试去获取锁的线程不会立即阻塞,而是采用循环的方式去获取锁,这样的好处是减少线程上下文切换消耗,缺点是循环会消耗CPU) + UnSafe类。
CAS优点:
非阻塞的轻量级的乐观锁,通过CPU指令实现,在资源竞争不激烈的情况下性能高,相比synchronized重量锁,synchronized会进行比较复杂的加锁、解锁和唤醒操作。
CAS缺点:
1、CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
2、不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。因为它本身就只是一个锁住总线的原子交换操作啊。两个CAS操作之间并不能保证没有重入现象。
3、ABA问题
线程C、D;线程D将A修改为B后又修改为A,此时C线程以为A没有改变过,java的原子类AtomicStampedReference,通过控制变量值的版本号来保证CAS的正确性。具体解决思路就是在变量前追加上版本号,每次变量更新的时候把版本号加一,那么A - B - A就会变成1A - 2B - 3A。
public static void main(String[] args) {
AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100,1);
new Thread(() -> {
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + " 的版本号为:" + stamp);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
stampedReference.compareAndSet(100, 101, stampedReference.getStamp(), stampedReference.getStamp() + 1 );
stampedReference.compareAndSet(101, 100, stampedReference.getStamp(), stampedReference.getStamp() + 1 );
},"A").start();
new Thread(() -> {
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + " 的版本号为:" + stamp);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean b = stampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
System.out.println(b); // false
System.out.println(stampedReference.getReference()); // 100
System.out.println(stampedReference.getStamp()); // 3
},"B").start();
}