Java多线程总结(二)

Java多线程并发问题之线程锁

Posted by Anriku on March 13, 2018

Java多线程并发修改问题的分析

我们先来看一个银行转款的问题:

//这是一个银行类
public class Bank {
    private final double[] accounts;

    public Bank(int n, double initialBalance) {
        accounts = new double[n];
        Arrays.fill(accounts, initialBalance);
    }

    public void transfer(int from, int to, double account) {
        if (accounts[from] < account) return;
        System.out.print(Thread.currentThread());
        accounts[from] -= account;
        System.out.printf("%10.2f from %d to %d", account, from, to);
=========================================================================================
		//这是我们要进行分析的地方
        accounts[to] += account;      =========================================================================================
        System.out.printf("Total balance: %10.2f%n", getTotalBalance());
    }

    public double getTotalBalance() {
        double sum = 0;
        for (double a :
                accounts) {
            sum += a;
        }
        return sum;
    }

    public int size() {
        return accounts.length;
    }
}


//这是一个测试类
public class UnsynchBankTest {
    public static final int NACCOUNTS = 100;
    public static final double INITIAL_BALANCE = 1000;
    public static final double MAX_ACCOUNT = 1000;
    public static final int DELAY = 10;

    public static void main(String[] args) {
        Bank bank = new Bank(NACCOUNTS,INITIAL_BALANCE);
        for (int i = 0;i < NACCOUNTS;i++){
            int fromAccount = i;
            Runnable runnable = ()->{
                try{
                    while (true){
                        int toAccount = (int) (bank.size()*Math.random());
                        double amount = MAX_ACCOUNT*Math.random();
                        bank.transfer(fromAccount,toAccount,amount);
                        Thread.sleep((int) (DELAY*Math.random()));
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            };
            Thread t = new Thread(runnable);
            t.start();
        }
    }
}

accounts[to] += account不是一个原子操作(也就是这行代码由许多指令组成)。可能有如下的指令:

  • 将accounts[to]加载到寄存器。
  • 增加amount。
  • 将结果写回accounts[to]。

多线程问题由就像下图一样会出现:

  • 由上图可以看到,由于Thread1的store还有执行就被Thread2线程抢占了,然后Thread2的store是无效的。这就是多线程共同改变变量所带来的问题。

多线程并发修改问题的解决方案———锁机制

Java有两种机制可以防止代码块并发访问的问题。

  • 通过synchronized关键词来解决。
  • 通过Java SE 5.0引入的ReentrantLock类来进行解决。

通过ReentrantLock保护代码块

ReentrantLock(锁对象)
//这是用ReentrantLock的基本结构
myLock.lock();
try{
    critical section
}finally{
    myLock.unlock();
}

这个结构确保了在任何时刻只有一个线程进入临界区。一旦一个线程封锁了锁对象,其它任何线程都无法通过lock语句。当其它线程调用lock的时候,它们都会被阻塞

像上面一样把锁对象的释放放在finally代码块中非常的重要。避免了在临界区的代码抛出异常后,锁没有释放而造成其它线程永远的阻塞。

还有一个需要注意的是,使用锁就不能使用带资源的try语句

  • 首先是解锁的方法名不是close。
  • 然后,就是带资源的try语句希望在它的首部声明一个新变量。但是如果使用一个锁,你是需要多个变量共享那个锁变量的(而不是一个新变量)。

还有需要注意的是:不同线程访问同一个ReentrantLock对象,会产生阻塞。但是,如果是访问的不同的ReentrantLock对象,它们之间没有关系。

在一个被锁保护的代码中可以调用另一个使用相同的锁的方法,这叫做锁是可重入的。

条件对象

条件对象(条件变量)是用来对一个获得锁却又不能做有用工作的线程进行处理的类

一个锁对象可以有一个或多个相关的条件对象。你可以使用newCondition方法获得一个条件对象。

我们先来看下上面我们要改成条件对象的部分:

//没有使用条件对象:当账号的余额小于要转的数量的时候就进行return返回。
if (accounts[from] < account) return;


//使用条件对象
while (accounts[from] < amount){
    条件对象.await();
}
......(经过一系列操作)
//当账号余额发生变化的时候
条件对象.signalAll();

这里我们得理解的是当一个条件对象调用await方法的时候,当然线程现在被阻塞了,并放弃了锁。这样其它线程就能获取锁。

还得注意一个问题,等待获得锁的线程和调用await方法的线程存在着本质上的区别的。当一个线程调用了await方法后,它进入了该条件的等待集。当锁可用的时候,调用await方法的线程不能立即就解除了阻塞状态。只有当另一个线程调用同一个条件的signalAll方法的时候,该线程才能解除阻塞状态。

当调用await方法的线程重新获取了锁后,它会从被阻塞的地方继续执行

由条件对象导致死锁的两种情况:

  • 如果所有的线程都调用了await方法处于阻塞状态,没有其它的线程调用signalAll方法来解除它们的阻塞,将会造成死锁现象。此时怎个程序挂着。
  • 如果一个线程通过signal方法来解除一个等待线程的阻塞状态,但是,解除后发现还是不满足执行条件,又被阻塞了。这时如果全部线程都处于阻塞状态,就产生死锁。

这里说明一下两个激活线程的方法:

  • ignalAll方法:解除等待线程的阻塞状态,让这些线程在当前线程退出同步方法后,进行竞争执行。
  • signal方法:随机解除一个等待线程的阻塞状态

通过Synchronized关键字解决并发问题

使用Synchronized方法解决并发问题

其实,使用Synchronized的关键字其本质是获取一个线程的内部的对象锁。简单的来说就是,每个对象都有一个内部锁,并且该锁有一个内部条件;然后我们通过调用同步方法来获取这个锁。就像下面的等价代码一样。

public synchronized void method(){
    method body;
}

//等价于下面

public void method(){
	this.intrinsicLock.lock();
    try{
    	method body;
    }finally{
        this.intrinsicLock.unlock();
    }
}

使用同步代码块来解决并发问题

上面已经说过每一个对象都有一个内部的对象了。我们除了可以通过同步方法来获取这个锁以外,还可以通过通过代码块来获取这个锁。具体的模版如下:

synchronized(object){
    critical section;
}

object如果是对实例方法进行同步就直接this就行;如果是对静态方法进行同步那么object就是类.class

synchronized常见方法的使用(对于上面两种方式)

在使用synchronized关键字的时候,Object类的三个final方法waitnotifyAll以及notify分别对应Condition(条件对象)awaitsignalAllsignal方法。

一个类的静态方法也能够使用synchronized关键字来进行同步。当该方法被调用时,该类的class对象(类.class)的锁被锁住。因此,没有其它线程可以调用这个类的这个或者任何其它的同步静态方法

PS:上面的几种方式来解决并发问题的代码我就不列出来了,作为一个思考自己去写吧==

volatile域

volatilte关键字有下面两个作用:

  • JMM模型(Java内存模型)可以知道所有线程都共享一个主内存区(线程共享的),每一个线程都有自己的一个工作区(线程私有的,存储主内存区中的变量副本)。然后,volatile的第一个作用就是让你读取的变量都是主内存区中最新的变量;当一个线程修改了volatile变量,这个变量会立即写入到主内存区。
  • 第二个作用就是禁止进行指令重排序优化。

具体的理解我就不说了,要说的话还有很多的东西。详细理解请参考全面理解Java内存模型(JMM)及volatile关键字

总结

今天我们主要是引出了多线程的并发问题。并给出了解决方案:这里我们细分为三种方式(其实都是通过锁对象来进行解决的):

  • 通过ReentrantLock锁来进行解决
  • 通过synchronized方法来进行解决
  • 通过同步代码块来进行解决

然后我们在最后又介绍了一下volatile关键字。

参考

转载请注明地址