多线程语法(二)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Jintao_Ma/article/details/53456227

多线程语法(二)

零.前言

主要是在上篇的基础上理解一些基本原理,本片目录:

一.悲观锁和乐观锁

二.数据库中的悲观锁和乐观锁

三.阻塞算法和非阻塞算法

四.阿姆达尔定律(Amdahl)

正文:

一.悲观锁和乐观锁

1.1.悲观锁和乐观锁是一种思想,前者悲观地认为每次写操作前,数据都会被修改,因此数据在取值的时候就要加上锁;而乐观锁认为每次写操作前,数据都不会被修改,因此数据在取值的时候不加锁,而只在修改数据的时候加锁。

悲观锁与乐观锁的思想如下面的形式:悲观锁与乐观锁的思想如下面的形式:

public volatile int a = 0;
	/*悲观锁*/
	public void setValue1(String key){
		synchronized(this) {
			int b = a + 1;
			a = b;
		}
	}
	/*乐观锁*/
	public void setValue2(String key){
		for(;;){
		int b = a + 1;
			synchronized(this) {
				if(b == a + 1){
					a = b;
					break;
				}else{
					continue;
				}
			}
		}
	}

上面的这个例子能体现乐观锁和悲观锁的思想。

1.2.悲观锁和乐观锁的实例

悲观锁通常情况下的实例有synchronized修饰方法时,或者lock修饰方法时。 而乐观锁实现可以是在方法体内部,使用synchronized或者lock实现,只对修改数据的代码块进行修改。

不过乐观锁一个较好的实现是CAS,首先CAS是什么?它是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令,这个指令会对内存中的共享数据做原子的读写操作。它的操作过程可以这样来描述,首先从内存中读取一个数据(读取后的数据应该放在volatile变量中,上篇讲述过),这个值被称为旧的值A,然后执行原子操作时,会再次查看内存的这个数据的值B和这个旧值A是否相等,如果相等则证明在这一次的取操作和写操作之间,内存的值从来没有被其他线程修改过,这个时候就可以把我们期望的值C赋给这个数据;如果不相等,则证明数据被其他线程修改了,那么就不赋新值,并且把此时内存中该数据的值返回。

Java中已经提供了一些乐观锁的实现,如AtomicBoolean, AtomicInteger, AtomicLong, AtomicReference等,看一下AtomicInteger的实现:

public class AtomicInteger extends Number implements java.io.Serializable {
     private volatile int value;
     /**
     * Atomically sets to the given value and returns the old value.
     *
     * @param newValue the new value
     * @return the previous value
     */
    public final int getAndSet(int newValue) {
        for (;;) {
            int current = get();
            if (compareAndSet(current, newValue))
                return current;
        }
    }
    public final int get() {
        return value;
    }
}
完全符合乐观锁的思想; 且compareAndSet就是CAS的操作的一种实现。但是会有一个ABA的问题,什么意思呢?
就是说在进行compareAndSet之前,内存的值由其他线程把A改为X,然后再改为A,本线程在进行compareAndSet的时候,发现A的值并没有变,于是就把A替换为期望的值C;
这在值是数值的时候是没问题的,不管有没有其他线程进行修改过,本线程的目的都是要修改这个1,1和1之间对于本线程来说都是一样的。如下:

新建一个类:

public class class4MultiThread6 {
	private String string = "test";
	public String getString() {
		return string;
	}
	public void setString(String string) {
		this.string = string;
	}
}

1)比较数值本身:

/*比较数值本身*/
AtomicInteger atomicInteger = new AtomicInteger(1);
atomicInteger.compareAndSet(1, 2);
atomicInteger.compareAndSet(2, 1);
atomicInteger.compareAndSet(1, 2);
System.out.println("atomicInteger:"+atomicInteger.get());

2)有的时候我们需要比较引用本身的值,那么就需要使用另一个类:

/*比较引用的值*/
class4MultiThread6 class4MultiThread6_1 = new class4MultiThread6();
class4MultiThread6_1.setString("test1");
class4MultiThread6 old = class4MultiThread6_1;
class4MultiThread6 class4MultiThread6_2 = new class4MultiThread6();
class4MultiThread6_2.setString("test2");
class4MultiThread6 class4MultiThread6_3 = new class4MultiThread6();
class4MultiThread6_2.setString("test3");
AtomicReference<class4MultiThread6> atomicReference = new AtomicReference<class4MultiThread6>(old);

/*其他线程通过把A换为B再换为A,但是A所指向的值已经改变*/
Boolean boolean1 = atomicReference.compareAndSet(old,class4MultiThread6_3);
System.out.println("boolean1:"+boolean1);
class4MultiThread6_1.setString("newtest1");
Boolean boolean2 = atomicReference.compareAndSet(class4MultiThread6_3,class4MultiThread6_1);
System.out.println("boolean2:"+boolean2);

/*比较A和A,其实引用的值已经改变,但是依然操作成功*/
Boolean booleanRef = atomicReference.compareAndSet(old,class4MultiThread6_2);
System.out.println("AtomicReference:"+booleanRef);

3)我们发现,这种比较只比较引用本身的值,并不知道引用所指向的值有没有改变。 这在我们确保每个线程都不修改引用所指向的值的时候,或者说即使修改了值
但是我们并不敏感,那么确实没有问题。    但是有些场景,当我们想要在确保引用和引用指向的值都是安全的,对任何一个变化都敏感,那么就需要使用下面的
类:

/*比较引用所指向的值*/
class4MultiThread6 class4MultiThread6_4 = new class4MultiThread6(); 
class4MultiThread6_4.setString("testRef1");
class4MultiThread6 class4MultiThread6_5 = new class4MultiThread6(); 
class4MultiThread6_5.setString("testRef2");
class4MultiThread6 class4MultiThread6_6 = new class4MultiThread6(); 
class4MultiThread6_6.setString("testRef3");
/*初始的时间戳设置为0*/
AtomicStampedReference<class4MultiThread6> atomicStampedReference =
new AtomicStampedReference<class4MultiThread6>(class4MultiThread6_4,0);

int stamp3 =  atomicStampedReference.getStamp();
int stamp4 =  atomicStampedReference.getStamp();		
int stampRef = atomicStampedReference.getStamp();

/*其他线程通过把A换为B再换为A,但是A所指向的值已经改变*/
/*模拟一个线程*/
Boolean boolean3 = atomicStampedReference.compareAndSet(class4MultiThread6_4, 
		class4MultiThread6_6, stamp3, stamp3+1);
System.out.println("boolean3:"+boolean3);
class4MultiThread6_4.setString("newtestRef1");
/*对stamp4自增,模拟另一个线程*/
stamp4++;
Boolean boolean4 = atomicStampedReference.compareAndSet(class4MultiThread6_6, 
		class4MultiThread6_4, stamp4, stamp4+1);
System.out.println("boolean4:"+boolean4);

/*比较A和A,其实引用的值已经改变,这时候已经无法操作成功*/
Boolean booleanStampedRef = atomicStampedReference.compareAndSet(class4MultiThread6_4, 
		class4MultiThread6_5, stampRef, stampRef+1);
System.out.println("booleanStampedRef:"+booleanStampedRef);

二.数据库中的悲观锁和乐观锁

想到了数据库中的悲观锁和乐观锁,,关于数据库中的悲观锁和乐观锁的实现,仔细学习发现内容很多,需要一个专门的主题;有一个不错的链接可以学习,http://blog.csdn.net/goldenfish1919/article/details/51750653
这里只介绍下乐观锁的一个实现,就是在数据库表中增加一个整数字段,每次数据修改的时候同以前的值进行比较,然后更新这个字段,就相当于上面的乐观锁的实现。同时

还有一个不错的插件,github的地址是:https://github.com/xjs1919/locker

三 阻塞算法和非阻塞算法

用悲观锁实现的算法(比如说synchronized和Lock)叫做阻塞算法,使用阻塞算法实现的数据结构叫做阻塞的数据结构,包括前面提到的阻塞队列等。

用乐观锁实现的算法(比如说CAS)叫做非阻塞算法,使用非阻塞算法实现的数据结构叫做非阻塞的数据结构,包括前面提到的AtomicInteger ,AtomicReference,AtomicStampedReference等。

那么,对于并发来说,什么时候使用阻塞算法或者非阻塞算法呢?
在轻度争用的时候非阻塞算法更好,因为这个时候争用的情况较少,也就是说大部分的争用一个CAS操作或者循环几下,几个CAS操作就可以,而这个时候阻塞算法因为涉及到锁肯定会比CAS消耗更多的时间。 在重度争用的时候,阻塞算法更好,因为它涉及到锁,其他线程会耐心的等待,而非阻塞算法这个时候会不断循环争用CAS操作,从而带来了更多的争用,所以吞吐率还不如锁。

四.阿姆达尔定律

刚看这个定律,以为是一个很复杂的公式或者算法;然后仔细拜读了并发编程网的这篇文章http://ifeve.com/amdahls-law/,发现内容还是比较容易理解,举例很恰当;
它的意思是,一个程序的整个过程,包括可以并行的部分,和不可以并行的部分。并行部分可以通过物理条件或者软件条件的提升,运行时间会趋近于无限小,串行部分
则只有通过代码来进行优化。

举一个例子:
一个程序中可被并行化的部分时间为T,不可被并行化的部分为1-T,那么并行化的部分当有两个CPU来运行时候,那么总时间为:T/2+1-T
当有很多个CPU来运行的时候,总时间为:T/无穷大+1-T,这样总的运行时间就趋近于1-T
串行化的部分假设通过代码的优化,时间也变短了:(1-T)/1.5这样通过通过并行化和串行化的优化就能够得到最快的时间。

加速比:
这里有一个概念,上面通过并行和串行的优化,会发现运行时间会无线接近于优化代码后的串行时间,也就是上面的(1-T)/1.5,那么加速比就是
之前的总运行时间1除以现在的最快时间(1-T)/1.5

猜你喜欢

转载自blog.csdn.net/Jintao_Ma/article/details/53456227
今日推荐