java并发学习与总结
线程安全性
在对一个类分析的时候,经常会碰到XX是不是线程安全的,那么线程安全的核心是正确性,也就是说在多线程的环境下,代码能够正确执行并且按照规范得到想要的结果,那么这就是线程安全的。
比如i++
这个操作,看起来是一句话,但实际上他是三个操作:读取i的值,将i的值加1,将加一后的值赋值给i,完成这个自增的操作。在单线程下,我们无需考虑这三步操作会出现什么情况,因为一定是顺序执行,不会有任何线程会干扰我们。但是在多线程下,一旦有两个以上的线程同时进行i++
的操作,那么很有可能是这样的
在这种顺序下,A和B读取到的i是同一个值,因此他们执行结束后,i依然只被加了一次,这样这个程序的正确性就没有得到保证,因此不是线程安全的。
这里类似于多进程中的安全问题,在进程对资源的访问,因为CPU调度而导致非原子操作被分到两次执行,可能也会导致执行结果的正确性得不到保证
原子性
原子性能够保证一个操作是线程安全的,因此原子性的操作要么完全执行,要么完全不执行,在这个操作中间可能会有一些同步的操作,来保证这个操作的原子性,但是调用者不用关心,对于他们来说,这一个操作就是一次完成的
java.util.concurrent.atomic包
顾名思义,在并发包concurrent中的atomic包,atomic就指的是院子的,那么这个包中的就是可以保证原子操作的一些工具类,
是一个小型工具包,支持单个变量上的无锁线程安全编程
class | description |
---|---|
AtomicBoolean | 一个 boolean值可以用原子更新。 |
AtomicInteger | 可能原子更新的 int值。 |
AtomicIntegerArray | 一个 int数组,其中元素可以原子更新 |
AtomicIntegerFieldUpdater | 基于反射的实用程序,可以对指定类的指定的 volatile int字段进行原子更新。 |
AtomicLong | 一个 long值可以用原子更新。 |
AtomicLongArray | 可以 long地更新元素的 long数组。 |
AtomicLongFieldUpdater | 基于反射的实用程序,可以对指定类的指定的 volatile long字段进行原子更新。 |
AtomicMarkableReference | AtomicMarkableReference维护一个对象引用以及可以原子更新的标记位。 |
AtomicReference | 可以原子更新的对象引用。 |
AtomicReferenceArray | 可以以原子方式更新元素的对象引用数组。 |
AtomicReferenceFieldUpdater | 一种基于反射的实用程序,可以对指定类的指定的 volatile volatile引用原子更新。 |
AtomicStampedReference | AtomicStampedReference维护对象引用以及可以原子更新的整数“印记”。 |
DoubleAccumulator | 一个或多个变量一起维护使用提供的功能更新的运行的值 double 。 |
DoubleAdder | 一个或多个变量一起保持初始为零 double和。 |
LongAccumulator | 一个或多个变量,它们一起保持运行 long使用所提供的功能更新值。 |
LongAdder | 一个或多个变量一起保持初始为零 long总和。 |
使用这个工具包中的类,可以实现我们上述的i++的原子操作:
AtomicInteger i = new AtomicInteger();
i.incrementAndGet();
这样子,返回值就是自增后的值,而又能保证原子性
在java并发编程实战中也提到了:
在实际情况中,应尽可能地使用现有的线程安全对象(例如AcomicLong)来管理类的状态,与非线程安全的对象相比,判断线程安全对象的可能状态及其状态转换情况要更为容易,从而也更容易维护和验证线程安全性
synchronized关键字
java提供了一种内置的锁机制来支持原子性:同步代码块(synchronized block)。这种属于互斥锁,同一时间有且只有一个线程可以进入同步代码块中执行,
使用方法:
①标记方法
public sychronized void do(){...}
经过这个关键字标记的方法,当有线程在其中执行时,其他线程的请求都会被阻塞,直到该线程离开此代码块,将会释放其他线程,随机选择一个线程进入代码块
②锁Class代码块
//在方法体中需要加锁的代码:
sychronized(lock){
}
其中的lock可以是Class对象,可以是任意的java对象,因为任意java对象都是可以用作一个实现同步的锁,但是在静态代码块中,就要使用Class对象来进行,非静态可以使用this来做同步锁
可重入性:
假如一个线程试图进入一个加过同步锁的方法,而这个方法的锁由自己持有,那么他就会成功获得进入的许可,因为内置的同步锁的操作粒度是“线程”,通过为每个锁关联一个获取技术值和所有者线程的记录来进行的,当被调用后,会将调用线程和计数记录下来,因此当同一个线程请求,这种方法就会允许进入,可以防止请求自己所持有的锁而导致的死锁
Volatile关键字
Volatile关键字修饰的变量会被认为是共享变量,在编译期运行的时候会主要的这个变量是共享的,去解决编译器的重排序问题而导致的不可见性,那什么叫不可见性
public class NoVisibility{
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread{
public void run(){
while(!ready)
Thread.yield();
System.out.println(number);
}
}
public static void main(String[] args){
new ReaderThread().start();
number = 42;
ready = true;
}
}
这段代码会有三种运行可能:
① 大多数正常运行,输出42(我测试了许多次都是42)
② 输出0,ready被设置为true后,线程输出number的值,但是由于内存重排序,number在ready后执行而导致线程对ready的改变不可见,输出原来的0
③ 无限循环,ReaderThread线程获得ready的副本为false,在main线程中ready设为true,但是读线程可能永远看不到这个改变
第二三种的不可见的问题。。。说实话我没遇见过,自己去测试生成了10000条线程,依然没有遇到过,出现在修改了缓存而延迟修改主存导致的其他线程对该变量的不可见。这个应该是属于编译器的问题,使用volatile可以防止这种问题的出现