在java中并发也是一个老生常谈的东西了,在我的理解并发也就是保证线程安全的情况下,多个线程操作相同的资源,是否并发取决于它的3种特性 :
- 原子性:一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其它线程干扰。
- 有序性:有序性即程序执行的顺序按照代码的先后顺序执行。
- 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
但是普通的没进行同步操作的多线程是不安全的。举个例子比如说int类型的自增函数i++;
int count = 0;
private void test() {
//创建10个子线程
List<Thread> threads = new ArrayList<Thread>();
for (int j = 0; j < 100; j++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
count++;
}
}
});
threads.add(thread);
}
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
try {
// 等待所有子线程执行完成 才运行主线程显示
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Log.e("count",count+"");
}
按正常的逻辑应该就是,创建100个线程并且每个线程都对i操作,最终的结果应该是为100000,但是你测试后会发现并不是这个结果,并且每次结果都不同。
造成这样的原因就是这是个不安全的线程,不安全的原因是这个成员变量i可能发生非原子性的操作,不同的线程同时在改变这个变量的值,造成了变量发生错误。
为了避免这种情况发生我们可以进行原子操作,在 java 中可以通过锁和循环 CAS 的方式来实现原子操作。
使用循环 CAS 实现原子操作
介绍:
- CAS(Compare And Swap)比较并替换,是线程并发运行时用到的一种技术
- CAS是原子操作,保证并发安全,而不能保证并发同步
- CAS是CPU的一个指令(需要JNI调用Native方法,才能调用CPU的指令)
- CAS是非阻塞的、轻量级的乐观锁
CAS 优点:
- 非阻塞的轻量级的乐观锁,通过CPU指令实现,在资源竞争不激烈的情况下性能高,省去了锁的开销,加大了系统的吞吐量
CAS 缺点:
- 循环时间长开销大。自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。
- 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性
- ABA 问题(几率很小)。因为 CAS 需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是 A,变成了 B,又变成了 A,那么使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。
对于上面那种更新基本类型int,最简单的方法就是使用原子变量,就是JDK 1.5开始提供了java.util.concurrent.atomic包里关于原子的操作类分别适用于4种原子类型:
- 原子更新基本类型
- 原子更新数组
- 原子更新引用
- 原子更新属性(字段)
操作类:
- AtomicBoolean:原子更新布尔类型
- AtomicInteger:原子更新整型
- AtomicLong:原子更新长整型
这里我们就是用AtomicInteger的incrementAndGet()方法使int数加1,。
// static int count = 0;
static AtomicInteger atomicInteger;
private void test() {
atomicInteger = new AtomicInteger(0);
List<Thread> threads = new ArrayList<Thread>();
for (int j = 0; j < 100; j++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
// count++;
// 以原子方式将当前值加 1。
//addCount()
atomicInteger.incrementAndGet();
}
}
});
threads.add(thread);
}
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
try {
// 等待所有子线程执行完成 才运主线程
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// Log.e("count",count+"");
Log.e("count", atomicInteger + "");
}
我们运行5次结果如下:
我们可以看到每一次的输出都是100000符合我们预先并发的效果,这就解决了int的原子性。其实它内部使用的是Unsafe类的CAS操作来保证原子性。
我举个例子:
//使用CAS 实现自增
private void addCount() {
for (;;) {
int count = atomicInteger.get();
boolean b = atomicInteger.compareAndSet(count , ++count );
if (b) {
break;
}
}
}
这是一个无限循环体,不断地获取目前的值,并且把比自己大1的值不断地赋值给自己,如果成功就跳出,不成功会不停地循环直到成功,不成功的原因就是就是赋值的时候别个线程也正在修改这个变量的值。当然源码内部也是这样的操作。
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return U.compareAndSwapInt(this, VALUE, expect, update);
}
源码使用的是CAS操作Unsafe类的compareAndSwapInt()方法。
使用锁(Synchronized)实现原子操作:
Java中的每一个对象都可以作为锁 (依赖JVM)
1.对于普通同步方法,锁是当前实例对象
2.对于静态同步方法,锁是当前类的Class对象
3.对于同步方法块,锁是Synchronized括号里配置的对象
补充:对于synchronized方法或者synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象。
备注:
- 同步方法和静态同步方法依靠的是方法修饰符上的ACC_SYNCHRONIZED实现。JVM根据该修饰符来实现方法的同步。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
- 同步代码块是使用monitorenter和monitorexit指令实现的,会在同步块的区域通过监听器对象去获取锁和释放锁,从而在字节码层面来控制同步scope.
private void test() {
List<Thread> threads = new ArrayList<Thread>();
for (int j = 0; j < 100; j++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
addCount();
}
}
});
threads.add(thread);
}
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
try {
// 等待所有子线程执行完成 才运主线程
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Log.e("count",count+"");
}
//上锁
private synchronized void addCount() {
count++;
}
上面代码就是我用Synchronized锁住了addCount()这个方法,使得同一时间只有一个线程在对其操作,保证了原子性。
输出的结果如下:
小结
原子操作的两种方法Synchronized和CAS,锁在java并发中也是一个元老级人物了,在线程使用一个资源时为其加锁,这样其他的线程便不能访问那个资源了,直到解锁后才可以访问。对于整形、布尔型和长整型变量的原子增减操作时,可以AtomicInteger、AtomicBoolean和AtomicLong等类来实现原子操作,当想要保证其他类型对象原子性时,也可以使用由Unsafe类提供的其他原子性语句,这样能在线程安全时相比其他提供更好的性能。当然synchronized是java提供的又简单方便性能又好的东西,用的也很普遍,所以大家也可以常用;CAS呢,如果线程数太大的话,多个线程都在循环调用CAS接口,虽然不会让引起线程阻塞,但是cpu消耗太严重,所以在性能上不如synchronized了。