Yuchuan Weng.Miko Tech-Blog.

Java并发编程笔记(五)锁的优化及其注意事项2

2017/08/17 Share

让普通变量也享受原子操作:AtomicIntegerFieldUpdater/Long/Reference

普通变量通过该类也可以享受原子操作CAS的线程安全特性。使用如下:
public static final AtomicIntegerFieldUpdater<T> field = AtomicIntegerFieldUpdater.newUpdater(T.class,"FieldName");
来进行构造,也可以使用cas的incrementAndGet()方法进行自增等操作。
但是使用该类需要注意以下几点:
1、Updater只能修改其可见范围,例如必须是Public的变量 否则private 是不可行的。
2、为了确保变量被正确的读取,它必须是volatile类型的。
3、由于是要对对象实例中的偏移量直接赋值,那么它不支持static修饰的变量。
满足以上条件 既可以享受cas操作来操作变量了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class AtomicFieldUpdaterDemo{
//模拟进行多线程投票操作
//自定义一个内部类
public static class Student {
int id;
//为保证其可见性 给updater使用 此处必须声明为volatile
volatile int scores;
}
//构建AtomicIntegerFieldUpdater对象 指定scores变量 注意该变量可见性大于private 且必须为volatile 不能为static
//变量
public static final AtomicIntegerFieldUpdater<Student> updater=
AtomicIntegerFieldUpdater.newUpdater(Student.class, "scores");
//构建总分变量和保持不变的学生对象
static AtomicInteger count = new AtomicInteger(0);
final static Student stu = new Student();
public static class FieldCAS implements Runnable {
public void run() {
// TODO Auto-generated method stub
//给与其60%的概率自增
if(Math.random()>0.4){
updater.getAndIncrement(stu);
count.getAndIncrement();
}
}
}
//启动1000个线程执行
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[1000];
for(int i=0;i<threads.length;i++){
threads[i]= new Thread(new FieldCAS());
threads[i].start();
}
for(int i=0;i<threads.length;i++){
threads[i].join();
}
//60%以上概率 600分以上 由于是线程安全的 故总分和个人加分总相等
System.out.println("该名学生个人得分: "+stu.scores+"总分为:"+count);
}
}

#让线程间互相帮助:细看SynchronousQueue 的实现
在线程池的介绍中,提到了一个非常特殊的等待队列,synchrononusQueue,他任何一个的写要对应一个读,与其说是队列不如说是一个数据交换的 通道。其内部实现了大量的无锁工具。源码可以看出其put()和take()两个方法实现了一个共同的方法,Transfer.transfer() 从字面上看 这就是数据传递的意思
Object transfer(Object e,boolean timed,long nanos)
当参数e为非空时 表示当前操作传递给一个消费者,反之表示当前操作需要请求一个信息。返回非空表示正常提供,为空表示失败(超时或者中断) time为是否觉得存在指定的时长。
SynchronousQueue内部会维护一个线程等待队列,生产者放入数据如果没有消费者接受那么数据本身和线程对象都会打包在队列中等待,(因为synchronousQueue容积为0 没数据可以放入) transfer()函数实现是SynchronousQueue的核心 它大体上分为三个步骤:
1、如果等待队列为空(或者队列中节点的类型和本次操作是一致的),那么将当前操作压入等待队列中等待,比如等待队列中读线程等待本次操作也是读,因此两个读都会等待。
2、如果等待队列的操作和本次操作是互补的(读与写)那么就插入一个完成状态的节点 并且让他匹配到一个等待节点上,接着弹出这两个节点 并且使得对应的两个线程执行。
3、如果线程发现等待队列的节点就是“完成的”,那么帮助这个节点完成任务,其流程与步骤二一致。
从整个数据传递过程可以看到,参与工作线程不仅仅是竞争资源的关系更重要的是它们还会彼此协助,这种模式可以很大程度上地减少饥饿现象,提高系统整体的并行度。

关于死锁的问题

如果应用较为复杂,频发使用无锁开发也会极大地增加难度,使用锁就得对死锁进一步的提防。死锁就是线程间(2个及以上)互相占用对方资源且不释放资源,继续请求对方占有的资源造成的相互循环等待现象。实际环境中死锁不占用CPU,相关进程不再工作,并且CPU占用率为0,不过这种表面现象只能用来猜测问题,使用jps命令查看线程Pid,使用jstack pid 查看具体线程信息 会自动发现并打印出死锁信息。(避免死锁除了使用无锁外 还可以使用重入锁。重入锁的显示等待和相应中断 可以很好地规避死锁问题)。

CATALOG
  1. 1. 让普通变量也享受原子操作:AtomicIntegerFieldUpdater/Long/Reference
  2. 2. 关于死锁的问题