多线程共享内存引发的问题:
问题引起的原因:线程并不持有资源,资源归进程所持有,多个线程并发执行时候,
- 线程访问资源的先后顺序无法保证
- 两个线程同时访问一个资源
这回导致结果的不可预知性。一个简单的例子,有一个银行(Bank),需要转账(TransferRunnable),最后对结果进行分析
package com.test.thread;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Bank {
private double[] accounts;
public Bank(int size,double initMoney) {
this.accounts = new double[size];
for (int i = 0; i < accounts.length; i++) {
accounts[i] = initMoney;
}
}
public void transfer(int from,int to,double amount){
if (accounts[from] < amount) {
return;
}
System.out.println(Thread.currentThread());
accounts[from] -= amount;
System.out.printf("%10.2f from %d to %d ",amount,from,to);
accounts[to] += amount;
System.out.printf("总余额 %10.2f%n ",getTotalBalance());
}
public synchronized void transferEasy(int from,int to,double amount){
if (accounts[from] < amount) {
return;
}
System.out.println(Thread.currentThread());
accounts[from] -= amount;
System.out.printf("%10.2f from %d to %d ",amount,from,to);
accounts[to] += amount;
System.out.printf("总余额 %10.2f%n ",getTotalBalance());
}
public int size(){
return accounts.length;
}
public double getTotalBalance(){
double sum = 0;
for (int i = 0; i < accounts.length; i++) {
sum += accounts[i];
}
return sum;
}
}
package com.test.thread;
public class TransferRunnable implements Runnable {
private Bank mBank;
private double maxAmount;
private int fromAccount;
private int DELAY = 100;
public TransferRunnable(Bank mBank, double maxAmount, int fromAccount) {
this.mBank = mBank;
this.maxAmount = maxAmount;
this.fromAccount = fromAccount;
}
@Override
public void run() {
try {
while(true){
int toAccount = (int) (mBank.size()*Math.random());
double amount = maxAmount*Math.random();
mBank.transferEasy(fromAccount, toAccount, amount);
Thread.sleep(DELAY);
}
} catch (InterruptedException e) {
}
}
}
package com.test.thread;
public class UnsynchBank {
public static void main(String[] args) {
Bank bank = new Bank(100, 1000d);//100个1000元的账户
for (int i = 0; i < 100; i++) {
TransferRunnable mRunnable = new TransferRunnable(bank, 1000, i);
Thread thread = new Thread(mRunnable,"线程" + i);
thread.start();
}
}
}
可以看到银行的金额初始时100*1000,而在程序运行过程中,银行余额不在是这个数字,这就是多线程引起的同步问题。因此要在线程之间建立同步机制。
多线程同步的基本原理:
Java虚拟机为每一个对象配备以把锁(Lock),和一个等候集(wait set),对象内部锁锁住的是一些同步方法或者同步代码块,一个方法要成为一个同步方法只需在方法前加修饰符synchronized,同步代码块格式为:
synchronized(引用类型的表达式){
//代码块
}
后面我将为大家详细讲解synchronized的原理及实现过程。
Java虚拟机通过对象的锁确保在任何同一个时刻内最多只有一个线程方法该方法或者代码块。当没有代码执行的时候该锁处于打开状态。一旦有线程执行该方法或代码块就会锁定状态,其余项访问该内容的线程就会进入到该对象的等候集,线程自身进入阻塞状态。线程执行完毕,释放该锁,JVM会根据优先级选择线程进入该代码执行,优先级相同的情况下是随机选择的。
如果线程在执行同步方法或者同步代码块的时候调用了类的wait方法,该线程就会进入该对象的等候集。对象锁自动打开。
通过调用类的notify()或者notifyAll()方法激活线程。激活后线程进入就绪状态,就绪状态的线程会等待操作系统的调度。notify()之后代码会从调用wait方法的下一条语句开始执行。
对于附属于对象的非同步方法和语句块,各个线程可以同时并发地运行,而不受对象锁控制,通常只有将可能存在的共享资源冲突或对线程有时序要求的方法和语句块有必要设置为同步代码块或者同步方法。
对象锁
从JAVA SE 5.0开始,有两种机制防止代码块受到并发访问的干扰,Java语言提供了一个关键字synchronized关键字来达到这个目的,并且引入了ReentrantLock类,synchronized关键字自动提供了一个锁以及相关的“条件”,这里先理解下ReentrantLock的使用
mLock.lock;
try{
//逻辑代码
}catch(Exception e){
}finally{
mLock.unlock();//一定要释放锁,不然会进入死锁
}
这一结构确保任何时刻只有一个线程进入临界区,一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。其他线程调用lock的时候线程就会阻塞
锁是可重入的,因为线程可以自己重复地获得已经持有的锁,锁保持有计数,来跟踪对lock方法的嵌套调用,线程在每一次调用lock的时候都要调用unlock来释放锁,当计数器最后变为0的时候,线程释放该锁
公平锁:公平锁在释放该锁后,会在等待集中选择等待时间最长的线程获得该锁。这样子将大大降低性能。
条件对象:
通常线程进入临界区,却发现在某一个条件满足之后才能执行,要使用一个条件对象来管理那些已经获取了锁但却不能做有用工作的线程,Java库中,条件对象的实现。
一个锁对象可以拥有一个或者多个相关的条件对象。可以调用newCondition(),方法获取一个条件对象。
Condition mCondition = lock.newConditiom();
当我们调用mCondition.await()时,当前线程被阻塞,并放弃了该锁。一旦一个线程调用await()方法,它进入条件等待集。当锁可用时,进入阻塞状态。直到另一个方法调用signalAll()方法的时候。
mCondition.signalAll();
Important:最终需要其他线程调用mCondition.signalAll()方法。当一个线程调用await()方法的时候,线程是没有办法重新激活自己的。如果没有线程调用signallAll(),该线程将永远无法执行,进入死锁。
锁和条件的关键之处:
锁是用来保护代码片段的,任何时刻只有一个线程能够执行被保护的代码
锁可以管理试图进入保护的代码的线程
锁可以拥有一个或者多个条件
每个条件对象管理那些已经进入被保护的代码块但还不能运行的线程
synchronized
到这里我们可以看出,synchronized实际上是lock,和condition的一种分装,java设计者为我们提供了非常优雅的方式实现代码同步,synchronized是lock和condition的一个组合,通过代码可以看到效果是一至的,但是代码中使用synchronized,更优美,更简洁。
volatile
多处理的计算机能够暂时在寄存器或本地内存的缓冲区中保存内存中的值,结果是不同线程读取到的值可能不同
编译器可以改变指令的执行顺序,这种顺序的改变不会改变代码语义,但是编译器假定内存的值仅仅在代码中有显示的修改指令才会改变,然而内存的值可以被另外一个线程改变。
volatile关键字为实例域的同步访问提供了一种免锁机制,如果一个变量被申明为volatile,那么编译器和虚拟机就知道该域是可能被另一个线程并发执行的。
private boolean isFinish;
private synchronized boolean isFinish(){
return isFinish;
}
private synchronized setFinish(boolean isFinish){
this.isFinish = isFinish;
}
等价于
private volatile isFinish;
域在以下情况下并发访问是安全的:
- 域是final类型的
- 对域所有的访问由公共锁保护
- 域是volatile的
经过同步后整个银行转账应该是这样子的
package com.test.thread;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Bank {
private Lock mLock;
private Condition mCondition;
private double[] accounts;
public Bank(int size,double initMoney) {
this.accounts = new double[size];
for (int i = 0; i < accounts.length; i++) {
accounts[i] = initMoney;
}
mLock = new ReentrantLock();
mCondition = mLock.newCondition();
}
public void transfer(int from,int to,double amount){
mLock.lock();
try {
if (accounts[from] < amount) {
System.out.println("余额不足,线程" + Thread.currentThread() + "进入等待");
mCondition.await();//线程进入等待状态
}
System.out.println(Thread.currentThread());
accounts[from] -= amount;
System.out.printf("%10.2f from %d to %d ",amount,from,to);
accounts[to] += amount;
System.out.printf("总余额 %10.2f%n ",getTotalBalance());
mCondition.signalAll();
} catch (Exception e) {
e.printStackTrace();
}finally{
mLock.unlock();
}
}
public synchronized void transferEasy(int from,int to,double amount){
if (accounts[from] < amount) {
System.out.println(amount + "都没有,余额不足,线程" + Thread.currentThread() + "进入等待");
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(Thread.currentThread());
accounts[from] -= amount;
System.out.printf("%10.2f from %d to %d ",amount,from,to);
accounts[to] += amount;
System.out.printf("总余额 %10.2f%n ",getTotalBalance());
notifyAll();
}
public int size(){
return accounts.length;
}
public double getTotalBalance(){
mLock.lock();
double sum = 0;
for (int i = 0; i < accounts.length; i++) {
sum += accounts[i];
}
mLock.unlock();
return sum;
}
}