标签:
多个线程同时对共享的同一数据存取 ,在这种竞争条件下如果不进行同步很可能会造成数据的讹误。
例如:有一个共享变量int sum=0, 一个线程正调用 sum+=10,另一个线程正好也在调用sum+=20,期望的结果应该是sum=30。 但是+=操作并不是原子的,虚拟机需要用多条指令才能来完成这个操作(load,add, store),每个指令执行完都有可能被剥夺执行权,同时让另一个线程继续运行。(可以使用javap -c -v CLASS命令将class文件反编译为可阅读的虚拟机字节码,能够看到虚拟机实际执行的指令)。
{
register=sum; //load
register=sum+N; //add
sum=register; //store
}
很有可能出现类似于下面的情况,实际的结果将是错误的。
线程1 | 线程1寄存器 | 线程2 | 线程2寄存器 | 变量sum |
load指令:将sum变量值存入线程1的寄存器 | 0 | x | 0 | |
add指令:寄存器中值+10 | 10 | x | 0 | |
线程1中断,线程2开始执行 | 10 | x | 0 | |
10 | load指令:将sum变量值存入线程2的寄存器 | 0 | 0 | |
10 | add指令:寄存器中值+20 | 20 | 0 | |
10 | store指令:将寄存器中值写回sum变量 | 20 | 20 | |
10 | 线程2执行完成,线程1继续执行 | 20 | 20 | |
store指令:将寄存器值写回sum变量 | 10 | 20 | 10 | |
线程1执行完成 | 10 | 20 | 10 |
示例代码:(为了演示,把+=操作拆成了3条语句,并且调用Thread.sleep(1)模拟执行3条语句中间的中断 )
class MyTask implements Runnable { private int add=0; private int[] suma; public MyTask(int[] suma,int add) { this.add=add; this.suma=suma; } @Override public void run() { try { int register=suma[0]; register+=add; Thread.sleep(1); suma[0]=register; } catch (Exception e) { e.printStackTrace(); } } }; int[] sum=new int[1]; sum[0]=0; Thread t1=new Thread(new MyTask(sum,10)); Thread t2=new Thread(new MyTask(sum,20)); t1.start(); t2.start(); while(t1.isAlive() || t2.isAlive()) Thread.sleep(100); System.out.println("expect:"+30+" real:"+sum[0]);
出现错误的原因就是线程存取共享数据的过程中,另一个线程也可以存取共享数据,数据自然就紊乱了。解决的思路也很简单:存取共享数据的代码,一次只允许一个线程执行,互补干扰,就永远不会出现错误的情况了。
Java中的锁机制就是这个思路的具体实现,锁可以保证线程一个一个互斥的访问临界区。一个线程获得了锁才能进入临界区,同时其他线程都不再能获得锁而被锁挡在临界区外,只有当获得锁的线程离开临界区代码并且解开了锁,剩余的线程里才会再有一个幸运儿再次获得锁进入临界区,锁继续挡住其他的线程。
首先创建一个共享的锁对象(Lock,ReentrantLock),线程1执行存取共享变量的代码之前(进入临界区)先调用锁对象的lock方法进行锁定,然后才开始执行load,add指令。add指令执行完线程1被中断了,线程2开始执行,同样的也要先调用lock方法,但是该锁对象已经处于锁定的状态了,线程2调用lock方法会一直被阻塞,直到持有这个锁的线程(线程1)把锁解开后线程2的lock调用才能成功返回。也就是说只要线程1不解锁,线程2就别想再走一步了。线程2因为要等线程1释放锁所以阻塞了,线程1又得到执行的机会,继续执行store指令,然后线程1访问共享变量的部分就执行完了,它还必须要调用锁对象的unlock方法解锁,让其他线程能够再次获得锁。线程2现在还被lock方法阻塞着,忽然等待的事件发生了(线程1解锁),线程2才能又有机会继续执行,这时lock方法终于成功返回了,锁被线程2持有了(如果还有其他线程调用lock方法来想要获得这个锁,更刚才线程2的情况一样,会被阻塞晾到一边等着)。线程2依次执行load,add,store指令完成功能,然后解锁。整个过程就结束了。
线程1 | 线程1寄存器 | 线程2 | 线程2寄存器 | 变量sum |
线程1调用lock() | 0 | x | 0 | |
load指令:将sum变量值存入线程1的寄存器 | 0 | x | 0 | |
add指令:寄存器值+10 | 10 | x | 0 | |
线程1中断,线程2开始执行 | 10 | 0 | 0 | |
10 | 线程2调用lock()阻塞.... | 0 | 0 | |
10 | 线程2中断,线程1继续执行 | 0 | 0 | |
store指令:将寄存器中值写回sum变量 | 10 | 0 | 10 | |
线程1执行完成,调用unlock() | x | 0 | 10 | |
x | 线程2继续执行,阻塞的lock()方法终于成功返回 | 0 | 10 | |
x | load指令:将sum变量值存入线程2的寄存器 | 10 | 10 | |
x | add指令:寄存器值+20 | 30 | 10 | |
x | store指令:将寄存器中值写回sum变量 | 30 | 30 | |
x | 线程2执行完成,线程1继续执行 | 30 | 30 |
示例代码:(线程数扩大为100个)
class MyTask implements Runnable { private int add=0; private int[] suma; private Lock lock; public MyTask(int[] suma,int add, Lock lock) { this.add=add; this.suma=suma; this.lock=lock; } @Override public void run() { try { lock.lock(); int register=suma[0]; register+=add; Thread.sleep(1); suma[0]=register; } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }; int[] sum=new int[1]; sum[0]=0; int expect=0; ReentrantLock lock=new ReentrantLock(); Thread[] ts=new Thread[100]; for(int i=0; i<100; i++) { int v=i+1; ts[i]=new Thread(new MyTask(sum,v,lock)); ts[i].start(); expect+=v; } for(int i=0; i<100; i++) if(ts[i].isAlive()) Thread.sleep(100); System.out.println("expect:"+expect+" real:"+sum[0]);
死锁
所有线程都在阻塞等待特定的条件,没有一个能继续执行的情况就是死锁。
在代码层面依靠Java中各种同步机制都不能避免和打破死锁。应该在设计的层面排除死锁产生的条件。
可重入锁。一个线程如果已经获得了锁,还可以再次获得该锁。被一个锁保护的代码里可以调用另一个被该锁包含的方法。锁中有一个持有计数用来追踪lock方法的嵌套调用,有多少次lock调用就要有多少次unlock调用才能释放锁。
只用共享使用同一个锁对象的线程才会被该锁同步。如果不同线程用的是不同的锁对象,线程之间不会有影响。
线程结束时,持有的所有锁都会被释放。
通常线程进入临界区后,却发现还要等某一条件满足了才能执行(因为该线程一直等到条件满足,而其他线程被锁挡在无法修改条件,就出现死锁了)。条件对象就是用来管理这些已经获得了锁但确不能做有用工作的线程。
一个锁可以由一个或多个相关的条件对象。
下面是一个使用了锁和条件的示例,模拟银行类:
public class Bank { private Lock bankLock; private Condition condition; private double[] accounts; public Bank(int n, double initBalance) { bankLock=new ReentrantLock(); condition=bankLock.newCondition(); accounts=new double[n]; for(int i=0; i<n; i++) accounts[i]=initBalance; } public int getSize() { return accounts.length; } public void transfer(int from, int to, double amount) throws InterruptedException { bankLock.lock(); try { while(accounts[from]<amount) condition.await(); accounts[from]-=amount; accounts[to]+=amount; System.out.println(from+"=="+amount+"==>"+to+", \t total:"+getTotalBalance()); condition.signalAll(); } finally { bankLock.unlock(); } } public double getTotalBalance() { bankLock.lock(); try { double sum=0; for(double a: accounts) sum+=a; return sum; } finally { bankLock.unlock(); } } }
前两个小节的锁和条件时java中最基本的同步机制,开发者可以以此为基础设计复杂的锁控制机制。此外,Java还提供了一些比较通用的便捷方式。在每个Java对象中, 都隐含了一个内部锁和该锁的一个条件对象。该对象的方法可以使用synchronized关键字声明,那么一个线程要调用该方法时会自动调用该对象内部锁的lock,对象方法调用完成后会自动unlock。方法中调用this.wait()方法和this.notifyAll()/this.notify()方法实际上使用了内部的条件对象的对应方法,(这三个方法都是Object类的final方法,与条件对象的方法名不同以避免冲突)。静态方法上加synchronized关键字,使用的是类对象(XXXX.class)的内部锁和条件对象。
内部锁和条件有一些限制:不能中断正在试图获取锁的线程。试图获取锁时不能设置超时。每个所只有一个条件,可能不够用。
同步实现方式推荐:
这几个方法实际上都是调用了对象内部隐含Condition对象的对应方法,只能在synchronized方法或块内调用。如果当前线程不是对象锁的持有线程则抛出IllegalMonitorStateException异常。
Object
使用对象内置锁改写前面的Bank类(使用隐含在对象内部的锁和条件,synchronzied关键字自动实现内置锁的lock,unlock调用)
class Bank1 { private double[] accounts; public Bank1(int n, double initBalance) { accounts=new double[n]; for(int i=0; i<n; i++) accounts[i]=initBalance; } public int getSize() { return accounts.length; } public synchronized void transfer(int from, int to, double amount) throws InterruptedException { while(accounts[from]<amount) this.wait(); accounts[from]-=amount; accounts[to]+=amount; System.out.println(from+"=="+amount+"==>"+to+", \t total:"+getTotalBalance()); this.notifyAll(); } public synchronized double getTotalBalance() { double sum=0; for(double a: accounts) sum+=a; return sum; } }
同步块和客户端锁定
既然每个对象内部都有一个隐含的锁和条件。也可以用一个其他对象代替Lock和Condition的对象,这也是synchronized关键字的另一种用法,这种方式也叫客户端锁定(对象的内部锁可以被外部客户使用)。
如果一个对象仅仅作为锁的替代被使用,是不会有什么问题。但是如果希望使用该锁的方法和用做锁的对象自身的同步方法进行协同就需要注意了(例如用一个Verctor对象作为锁,希望用锁的这个方法和vector对象自身的add,remove等方法进行同步,不能同时执行),因为不知道对象内部那些方法是同步的,所有这样使用锁会很脆弱。所有也不推荐使用。
示例如下:
class Bank2 { private Vector<Double> accounts; public Bank2(int n, double initBalance) { accounts=new Vector<Double>(); for(int i=0; i<n; i++) accounts.add(initBalance); } public int getSize() { return accounts.size(); } public void transfer(int from, int to, double amount) throws InterruptedException { synchronized(accounts) { while(accounts.get(from)<amount) accounts.wait(); accounts.set(from, accounts.get(from)-amount); accounts.set(to, accounts.get(to)+amount); System.out.println(from+"=="+amount+"==>"+to+", \t total:"+getTotalBalance()); accounts.notifyAll(); } } public double getTotalBalance() { synchronized(accounts) { double sum=0; for(double a: accounts) sum+=a; return sum; } } }
Lock和Condition功能虽强,但不是面向对象的。监视器不是具体的类,而是一种保证多线程安全的类设计方式,满足其条件的类就可以认为是监视器。
特点:
Java部分实现了监视器,每个对象都有内部的锁和条件,方法可以用synchnorized关键字同步。但是也有区别:并不要求所有域私有;方法也不要求都同步;内部锁对客户可用。
不同的线程如果仅仅是读写内存中共享的实例域,按道理说应该是不会出错的。但因为两个原因,实际会出现很大的问题。一是线程会把内存中的值复制到寄存器或是缓冲区中,代码中的同一个变量在不同的线程中实际已经不在一个地方了。二是编译器会进行优化,改变指令顺序增加吞吐量,优化后读取的值很可能存在差异。
使用锁进行同步可以保证正确性。锁会要求在必要的时候刷新本地缓存来保持锁的效应,并且不能不正当的重新排序指令。
仅仅为读写一两个实例域就使用同步,开销较大。而且有的时候使用锁也比较麻烦。例如某个实例的getter和setter方法加了synchnorized关键字,如果一个线程获得了该对象锁并调用了一个耗时较长的方法,那么另一个线程调用get,set方法都会阻塞。解决方法是为这个实例单独使用一个锁,但是就更麻烦了。
volatile关键字为实例域的同步读取提供了一种免锁机制。编译器和虚拟机知道该域可能被另一个线程并发更新。但是volatile并不能保证域的原子性修改,仅适用于除了赋值之外并不完成其他操作的域。
final变量赋值后不能再修改,所有线程看到的都是同一个值,读取是安全的。如果变量是一个对象,调用对象的方法就不一定安全了,需要考虑同步。
java.util.concurrent.atomic包中提供了很多类使用高效机器级指令来而不是锁来保证操纵的原子性。例如AtomicInteger提供了incrementAndGet,decrementAndGet方法实现原子性的自增自减操作,用作共享计数器时无需同步。其他类有AtomicBoolean, AtomicLong, AtomicReference以及Boolean, int, long, reference的原子数组。这些类主要用于开发并发工具,不推荐使用。
有时候需要避免共享变量,例如共享SimpleDataFormat对象,如果不同步,多个线程同时使用这个对象时会造成内部数据紊乱;如果使用同步,开销很大;如果每个线程中创建局部的对象,也很浪费。这种情况可以考虑使用TreadLocal。
ThreadLocal类对象被多个线程共享,每个线程中第一次调用get时会调用initialValue方法为该线程初始化一个新实例,以后再调用get方法都会返回这个实例(ThreadLocal类内部维持着一个以Thread为key的映射)。
public static final ThreadLocal<SimpleDateFormat> dateformat=new ThreadLocal<SimpleDateFormat>() { protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); } }; //Runnable对象的run方法中 String dateStamp=dateFormat.get().format(new Date());
ThreadLocal<T>
生成随机数也有类似的问题,Random类是线程安全的,但是通过同步共享效率很低,也可以使用ThreadLocal的方式。或者直接使用ThreadLocalRandom类。
//Runnable对象的run方法中 int rand=ThreadLocalRandom.current().nextInt(10);
ThreadLocalRandom
有些数据可以同时被多个线程读取,但是同时只能有一个线程修改。使用ReentrantReadWriteLock类跟适合, ReentrantReadWriteLock类对象可以抽取出两个锁,一个是同步读取线程的读锁,一个是用于同步修改线程的写锁。
ReentrantReadWriteLock 没有实现Lock接口,而是实现了ReadWriteLock接口,不能赋值给Lock变量
使用读写锁的示例:
class Bank3 { private ReadWriteLock bankLock; private Lock readLock; private Lock writeLock; private Condition condition; private double[] accounts; public Bank3(int n, double initBalance) { bankLock=new ReentrantReadWriteLock(); readLock=bankLock.readLock(); writeLock=bankLock.writeLock(); condition=writeLock.newCondition(); accounts=new double[n]; for(int i=0; i<n; i++) accounts[i]=initBalance; } public int getSize() { return accounts.length; } public void transfer(int from, int to, double amount) throws InterruptedException { writeLock.lock(); try { while(accounts[from]<amount) condition.await(); accounts[from]-=amount; accounts[to]+=amount; System.out.println(from+"=="+amount+"==>"+to+", \t total:"+getTotalBalance()); condition.signalAll(); } finally { writeLock.unlock(); } } public double getTotalBalance() { readLock.lock(); try { double sum=0; for(double a: accounts) sum+=a; return sum; } finally { readLock.unlock(); } } }
直接调用Thread类的suspend()和resume()方法挂起的位置是是不确定的,很可能造成死锁,风险太大。但是有时确实需要挂起的功能,用锁和条件对象可以实现相对安全的申请挂起操作,可以保证在固定的安全位置才挂起,但是代码结构设计不当仍然可能死锁。
挂起造成死锁的示例(子线程有可能在获得list对象锁后在临界区中间被挂起,主线程调用getCount()获取list对象锁失败被阻塞,形成死锁):
public void testSuspendDeadthLock() throws InterruptedException { class UnSafeSuspendTask implements Runnable { private List<Integer> list=new ArrayList<Integer>(); @Override public void run() { for(int i=0; i<1000; i++) { synchronized(list) { list.add(i); System.out.println("input "+i); } try { Thread.sleep(1); } catch (InterruptedException e) { return; } } } public int getCount() { synchronized(list){ return list.size(); } } } UnSafeSuspendTask task=new UnSafeSuspendTask(); Thread t=new Thread(task); t.start(); while(t.isAlive()) { t.suspend(); System.out.println("count: "+task.getCount()); t.resume(); Thread.sleep(1); } System.out.println("--done--"); }
安全的申请挂起方式,保证在安全的位置才挂起:
public void testSuspendRequest() throws InterruptedException { class SafeSuspendTask implements Runnable { private volatile boolean suspendRequested=false; private Lock suspendLock=new ReentrantLock(); private Condition suspendCondition=suspendLock.newCondition(); private List<Integer> list=new ArrayList<Integer>(); @Override public void run() { for(int i=0; i<1000; i++) { //do work... synchronized(list) { list.add(i); System.out.println("input "+i); } try { Thread.sleep(1); } catch (InterruptedException e) { return; } //suspend check if(suspendRequested) { suspendLock.lock(); try{ while(suspendRequested) suspendCondition.await(); } catch(InterruptedException e) { return; } finally{ suspendLock.unlock(); } } } } public int getCount() { synchronized(list){ return list.size(); } } public void requestSuspend() { suspendRequested=true; } public void requestResume() { suspendRequested=false; suspendLock.lock(); try{ suspendCondition.signalAll(); } finally{ suspendLock.unlock(); } } } SafeSuspendTask task=new SafeSuspendTask(); Thread t=new Thread(task); t.start(); while(t.isAlive()) { task.requestSuspend(); System.out.println("count: "+task.getCount()); task.requestResume(); Thread.sleep(1); } System.out.println("--done--"); }
标签:
原文地址:http://www.cnblogs.com/pixy/p/4796638.html