为什么需要并发程序?
线程是java语言中不可或缺的重要功能,它们能使复杂的异步代码变得更简单,从而极大地简化了复杂系统的开发。另外,在开发当数据量大的时候,往往需要使用多线程来提高程序的运行速度,尤其是如今处在一个大数据的时代。在并发编程中,就是需要解决实现线程安全问题,而这个问题的核心就在于要对状态访问操作进行管理,说简单点,就是要管理好对共享数据的访问。“共享”意味着多个线程可以同时访问,如果多个线程同时操作一个共享的变量,就会出现错误的数据,所以就需要采取相应的方法来解决它。
在java中有多种方式可以防止代码块受并发访问的干扰,这里说说synchronized同步和Lock显示锁。
synchronized同步
为了解决线程安全问题,java从早期的版本就开始提供synchronized关键字来实现同步。从1.0版本开始,java的每一个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁将保护这个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。另外,同步synchronized提供两种锁的方式,一个是同步方法,另一个是同步代码块,同步方法用的是该方法的所属类的对象,而同步代码块的锁由你指定,可以是任意对象。可以看看这里。synchronized关键字能自动提供一个锁以及相关的“条件”,对于大多数情况,用它是很方便的。
synchronized(对象) { // 任意对象都可以。这个对象就是锁。 需要被同步的代码;}
public synchronized void transfer() {}
Lock显示锁
java从1.5版本之后,提供了Lock接口。这时候,直接将锁封装成了对象。线程进入同步就是具备了锁,执行完,离开同步,就是释放了锁。在后期对锁的分析过程中,发现,获取锁,或者释放锁的动作应该是锁这个事物更清楚。所以将这些动作定义在了锁当中,并把锁定义成对象。所以,同步是隐式的锁操作,而Lock对象是显示的锁操作。
bankLock.lock(); //a ReentrantLock object try { //临界区 } finally { bankLock.unlock(); //如果在临界区抛出异常,必须保证锁被释放 }另外,有一个需要注意的地方是,锁的唤醒机制的不同。
在用同步synchronized来实现加锁时,由于这时的锁用的是任意对象,所以如wait,notify,notifyAll等操作锁的等待唤醒的方法都定义在Object中。
而现在用Lock时,用的锁是Lock对象。所以查找等待唤醒机制方式需要通过Lock接口来完成。而Lock接口中并没有直接操作等待唤醒的方法,而是将这些方式又单独封装到了一个对象中。这个对象就是Condition,将Object中的三个方法进行单独的封装。并提供了功能一致的方法 await()、signal()、signalAll()体现新版本对象的好处。
下面是java核心技术卷1中关于显示锁核心代码:
/** * Transfers money from one account to another. * @param from the account to transfer from * @param to the account to transfer to * @param amount the amount to transfer */ public void transfer(int from, int to, double amount) throws InterruptedException { bankLock.lock(); try { while (accounts[from] < amount) sufficientFunds.await(); System.out.print(Thread.currentThread()); accounts[from] -= amount; System.out.printf(" %10.2f from %d to %d", amount, from, to); accounts[to] += amount; System.out.printf(" Total Balance: %10.2f%n", getTotalBalance()); sufficientFunds.signalAll(); } finally { bankLock.unlock(); } }这里使用一个锁来保护Bank中的transfer方法,模拟现实中转账操作。
假定一个线程调用transfer时,在执行结束前被剥夺了运行权。假定第二个线程也调用transfer,由于第二个线程不能获得锁,将在调用lock方法时被阻塞。它必须等待第一个线程完成transfer方法的执行之后才能再度激活。当第一个线程释放锁时,那么第二个线程才可以运行。 锁是可重入的,因为线程可以重复地获得已经持有的锁。锁保持一个持有计数来跟踪对lock方法的嵌套调用。线程在每一次调用lock都要使用unlock来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法。例如,当transfer方法调用getTotalBalance方法,这也会封锁bankLock对象,此时bankLock持有的计数为2.当getTotalBalance方法退出是,持有计数变为1.当transfer方法退出的时候,持有的计数变为0.线程释放锁。上面实例代码中调用的方法getTotalBalance与transfer方法使用的是同一个锁。
/** * Gets the sum of all account balances. * @return the total balance */ public double getTotalBalance() { bankLock.lock(); try { double sum = 0; for (double a : accounts) sum += a; return sum; } finally { bankLock.unlock(); } }另外,上面的案列使用显示锁Lock,同样可以用同步的方法来实现,将transfer方法用synchronized修饰,同样在transfer中调用的getTotalBalance也需要用synchronized来修饰。
/** * Transfers money from one account to another. * @param from the account to transfer from * @param to the account to transfer to * @param amount the amount to transfer */ public synchronized void transfer(int from, int to, double amount) throws InterruptedException { while (accounts[from] < amount) wait(); System.out.print(Thread.currentThread()); accounts[from] -= amount; System.out.printf(" %10.2f from %d to %d", amount, from, to); accounts[to] += amount; System.out.printf(" Total Balance: %10.2f%n", getTotalBalance()); notifyAll(); }
/** * Gets the sum of all account balances. * @return the total balance */ public synchronized double getTotalBalance() { double sum = 0; for (double a : accounts) sum += a; return sum; }以上代码案列均来自于java核心技术卷。