零零碎碎的东西总是记不长久,仅仅学习别人的文章也只是他人咀嚼后留下的残渣。无意中发现了这个 每日一道面试题 ,想了想如果只是简单地去思考,那么不仅会收效甚微,甚至难一点的题目自己可能都懒得去想,坚持不下来。所以不如把每一次的思考、理解以及别人的见解记录下来。不仅加深自己的理解,更要激励自己坚持下去。
在介绍多线程中的同步之前,我们先来了解下并发编程。
//线程1: context = loadContext(); //语句1 inited = true; //语句2 //线程2: while(!inited ){ sleep() } doSomethingwithconfig(context); 复制代码
线程1中语句1和语句2没有数据依懒性,inited仅是一个标记变量,所以这两个语句可能发生指令重排序。当语句2在语句1之前执行时,这是恰好线程2启动,标记变量init为true则线程2认为初始化已经完成,而此时语句1并没有执行,就会造成问题。
volatile是java的一个关键字,一旦一个共享变量(类的成员变量、静态变量)被volatile关键字修饰,就具备有两层含义
这就保证了可见性与有序性,但是volatile并不保证可见性。看下面一段代码
public class Main { private volatile static int test = 0; private volatile static int count = 10; public static void main(String[] args) { for(int i=0;i<10;i++){ Main mm = new Main(); new Thread(new Runnable() { @Override public void run() { for(int j=0;j<10000;j++){ mm.increase(); } count--; } }).start(); } while (count > 0){}//所有线程执行完毕 System.out.println("最后的数据为" + test); } private void increase(){test++;} } 复制代码
运行后你会发现,每一次的结果都小于100000。这是因为 test++
这个操作,它不是原子性的,与test本身这个变量无关。
test++
经过三个原子操作,读取test变量值、test变量进行加一操作、将操作后的变量值写入工作内存。当线程1执行到前两步时,线程2开始读取test变量值,当线程1三个步骤执行完毕时,虽然此时test的值会立马更新到线程2,但是线程2已经在此之前进行了读取变量值的操作,所以实际上两个线程只让test加了一次。
所以说,volatile只进行一些简单的同步操作,比如上面提到的标记变量
volatile boolean inited = false; //线程1: context = loadContext(); //语句1 inited = true; //语句2 //线程2: while(!inited ){ sleep() } doSomethingwithconfig(context); 复制代码
并发编程中的单例模式
class Singleton { private volatile static Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) instance = new Singleton(); } } return instance; } } 复制代码
这个虽然有synchronized关键字来保证单线程访问,但是这里面其实是 instance=new Singleton()
指令重排序的问题,这一步有三个原子性操作
synchronized同样是java中的一个关键字。它通过锁机制实现同步,要执行代码,则必须要获得锁,只有获得锁对象的线程才能执行锁住的代码,其他线程没有获得锁只能阻塞。锁有 对象锁 和 类锁 。同步有两种表现形式: 同步代码块 和 同步方法
class Test{ public void testMethod(){ synchronized(this){ ... } } } 复制代码
class Test{ public void testMethod(){ synchronized(Test.class){ ... } } } 复制代码
class Test{ public void testMethod(){ synchronized(o){ ... } } } 复制代码
class Test{ public synchronized static void testMethod(){ ... } } 复制代码
class Test{ public synchronized void testMethod(){ ... } } 复制代码
ReentrantLock是一个类,它的同步方法与synchronized大致相同。
基本用法
ReentrantLock lock = new ReentrantLock(); //参数默认false,不公平锁 ..................... lock.lock(); //如果被其它资源锁定,会在此等待锁释放,达到暂停的效果 try { //操作 } finally { lock.unlock(); //释放锁 } 复制代码
ReentrantLock通过lock方法与unlock方法显式的获取锁与释放锁,与synchronized隐式的获取锁不同。当线程执行到lock.lock()方法时,会尝试获取锁,获取到锁则执行下去,获取不到则会阻塞。unlock()方法则会释放当前线程所持有的锁,如果没有锁可以释放可能会发生异常。
显式的获取锁虽然比隐式的自动获取锁麻烦了不少,但多了许多可控制的情况。我们可以中断获取锁、延迟获取锁等一些操作。
当许多线程在队列中等待锁时,cpu会随机挑选一个线程获得锁。这样就会出现饥饿现象,即优先级低的线程不断被优先级高的线程抢占锁资源,以至于很长时间获得不到锁,这就是不公平锁。RenntrantLock可以使用公平锁,即cpu调度按照线程先后等待的顺序获得锁,避免饥饿现象。但是执行效率会比较低,因为需要维护一个有序队列。synchronized是不公平锁。
ReentrantLock lock = new ReentrantLock(true); 复制代码
通过在创建对象时传入boolean对象表示使用什么锁,默认为false不公平锁。
可以看出,ReentrantLock实现了许多更高级的功能,不过却多了点复杂性。在性能上来说,竞争不激烈时,两者的性能是差不多的,不过当竞争激烈时,即有大量线程等待获取锁,ReentrantLock的性能要更好一些,具体的使用看情况进行。
jdk1.6以前synchronized的性能是很差的,jdk1.6以后对synchronized的性能优化了不少,和ReentrantLock性能差不了多少。官方也表示更支持synchronized,以后还有优化的余地,所以在都能符合需求的情况下,推荐使用synchronized。
cpu调度线程,通过将时间片分配给不同的线程进行调度。时间片的切换也就是线程的切换,需要清除寄存器、缓存数据,切换后加载线程需要的数据,需要耗费一定的时间。线程阻塞后,通过notify、notifyAll唤醒。假如线程1在尝试获取锁,获取失败,挂起。这时锁被释放,线程1被唤醒,尝试获取锁,结果又被其他线程抢占锁,线程1继续挂起,获取锁的线程只占用锁很短的时间,释放锁,线程1又被唤醒。。。就这样,线程1反复的挂起、唤醒,线程1认为其他线程获取锁就一定会对锁内的资源进行更新等操作,所以不断等待,这就是悲观锁。synchronized这种独占锁就是悲观锁。
乐观锁并不加锁,首先会认为在自己修改资源之前其他线程不会对资源进行更新等操作,它会尝试用锁内资源进行自己的操作,如果修改后的数据发生冲突,就会放弃之前的操作。就这样一直循环,知道操作成功。
CAS就是一种乐观锁的概念,内有三个操作数---内存原值(C)、预期旧值(A)、新值(B),当且只当内存原值与预期旧值的结果一样时,才更新新值。不然就是不断地循环尝试。Java中java.util.concurrent.atomic包相关类就是 CAS的实现.
类名 | 说明 |
---|---|
AtomicBoolean | 可以用原子方式更新的 boolean 值。 |
AtomicInteger | 可以用原子方式更新的 int 值。 |
AtomicIntegerArray | 可以用原子方式更新其元素的 int 数组。 |
AtomicIntegerFieldUpdater | 基于反射的实用工具,可以对指定类的指定 volatile int 字段进行原子更新。 |
AtomicLong | 可以用原子方式更新的 long 值。 |
AtomicLongArray | 可以用原子方式更新其元素的 long 数组。 |
AtomicLongFieldUpdater | 基于反射的实用工具,可以对指定类的指定 volatile long 字段进行原子更新。 |
AtomicMarkableReference | AtomicMarkableReference 维护带有标记位的对象引用,可以原子方式对其进行更新。 |
AtomicReference | 可以用原子方式更新的对象引用。 |
AtomicReferenceArray | 可以用原子方式更新其元素的对象引用数组。 |
AtomicReferenceFieldUpdater | 基于反射的实用工具,可以对指定类的指定 volatile 字段进行原子更新。 |
AtomicStampedReference AtomicStampedReference | 维护带有整数“标志”的对象引用,可以用原子方式对其进行更新。 |
这种不需要锁的非阻塞算法,在性能上是要优于阻塞算法。一般使用如下,实现自增 i++
的同步操作
public class Test { public AtomicInteger i; public void add() { i.getAndIncrement(); } } 复制代码