多线程并发的解决分两类:加锁和无锁。
1、加锁:使用synchronized修饰或者使用jdk的并发工具类包进行加锁
缺点:性能上稍微差点,可能会出现死锁。(当线程阻塞的时候,当抢夺资源失败到阻塞会有几万个时钟(硬件上的))
2、无锁:使用CAS 算法实现,---里面具体实现了unsafe的操作。实现原子操作
优点:性能上更好,但是并不是很多场景都能用,有时候相对复杂。简单的还可以。
下面我们说一下AtomicIntegerFieldUpdater 更新器的使用。
AtomicIntegerFieldUpdater:原子更新整型的字段的更新器
package com.zpl.nolock;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
/**
* 尤其注意这个在没有system.out的情况下出现数据脏读。
*
* @author zhangpengliang
*
*/
public class AtomicIntegerFieldUpdaterTest {
static class Student {
private String name;
public volatile int age;
public Student(String name, int age) {
super();
this.name = name;
this.age = age;
}
}
/**
* 反射获取student类中的字段age,该字段必须是volatile修饰并且不能是private.并把这个student(age)作为共享
*/
private static AtomicIntegerFieldUpdater<Student> fu = AtomicIntegerFieldUpdater
.newUpdater(Student.class, "age");
public static void main(String[] args) {
Student st = new Student("张三", 0);
java.util.List<Thread> v = new ArrayList<Thread>();
for (int i = 0; i < 5; i++) {
ThreadA a = new ThreadA(st);
Thread b = new Thread(a, "线程" + String.valueOf(i));
v.add(b);
}
for (int i = 0; i < 5; i++) {
Thread th = v.get(i);
th.start();
}
}
static class ThreadA implements Runnable {
private Student st;
public ThreadA(Student st) {
super();
this.st = st;
}
public void run() {
// fu.getAndIncrement(st);
// System.out.println("当前" + Thread.currentThread().getName()
// + "执行新增之后的结果" + fu.get(st));
// 或者
int expect = fu.get(st);
int value = expect + 1;
System.out.println(fu.get(st));
// 为什么加这一行就没有并发问题,不加就会有并发问题呢?原因是:在println里面加了一把锁。synchronized(this)
// 如果不加system.out.println的话我们可以加一个锁,把下面的作为同步代码块。
// 这里我们的compareAndSet是原子操作,但是与下面的打印并不是一体的,可能我们第一个线程增加完后,第二个线程直接过来了,而且他的expect值是改过的,这时候
// 就又可以执行原子操作增加1了。导致我们第一次打印并没有打印出真正的第一次增加完后的值。
/**
* 至于上面添加了一个System.out.println(expect),代码就可以实现同步的原因是:我们下面的system.
* out与上面那个system.out 在多个线程下执行时同步的。因为源代码中添加了锁(同步代码块)
*/
if (fu.compareAndSet(st, expect, value)) {
System.out.println("当前" + Thread.currentThread().getName()
+ "执行新增成功之后的结果" + fu.get(st));
} else {
System.out.println(
"当前" + Thread.currentThread().getName() + "执行失败");
}
}
}
}
上面代码我们需要注意的就是:Student类中的age 必须volatile修饰保证可见性,而且age不能是private修饰。
在操作中有的困惑:
这里如果不添加System.out.println().打印的结果并不是我们想要的。
但是最终的结果却仍是对的,五个线程,最总结果却不错,为什么打印的不对呢。
再看下加上system.out.println()..看下结果
这个结果却是我们想要的,为什么会出现脏读现象呢?
原因在于:当我们不加这个system.out.println时,由于compareAndSet是原子操作,不可拆分,但是。它与下面的打印信息并不是。当所有线程 2 4 0 1 3 这个五个线程打印的时候其实所有线程的compareAndSet已经执行操作完 了,当打印的时候,
有这种可能:线程0打印的时候已经执行加到4了,线程1 2 3也是.当4打印的时候值已经是5了。
但是在打印的时候又阻塞了,因为:
因为打印是同步代码块。所以阻塞了,所以打印就需要抢占当前资源。
为啥加了哪行打印就能看到了呢?
原因还是这个,所有的打印都是抢占的一个资源。我们看打印的日志:
0
1
当前线程4执行新增成功之后的结果2
当前线程1执行新增成功之后的结果1
0
当前线程0执行失败
0
当前线程2执行失败
0
当前线程3执行失败
只有拿到这打印资源后,才可以执行后面的compareAndSet加1.
但我觉得最安全的做法就是在if条件外加一个synchronized修改(同步代码块)。并能按我们设置的输出
至少结果是一部一部来的。这里只是解释了下为什么会有这个现象。就当做个备注学习下了。