一、同步问题的提出
public class Shared implements Runnable{
public static void main(String[] args) {
Shared s1 = new Shared();
new Thread(s1, "person1").start();
new Thread(s1, "person2").start();
new Thread(s1, "person3").start();
}
private int ticket = 5;
public void run() {
for(int i=0;i<40;i++){
if(ticket>0){
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + this.ticket--);
}
}
}
}
输出结果:
person3 5
person2 4
person1 3
person3 2
person1 1
person2 0
person3 -1
-1位错误输出。 如果想解决这种问题,就需要使用同步。所谓同步就是在统一时间段中只有有一个线程运行,
其他的线程必须等到这个线程结束之后才能继续执行。
二、synchronized关键字
synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程A每次运行到这个方法时,都要检查有没有其它正在用这个方法的线程B(或者C D等),有的话要等正在使用这个方法的线程B(或者C D)运行完这个方法后再运行此线程A,没有的话,直接运行 它包括两种用法:synchronized 方法和 synchronized 块。
1. synchronized 方法:
通过在方法声明中加入 synchronized关键字来声明 synchronized 方法。如:
public synchronized 返回值 方法名(成员函数列表){
......
}
synchronized 方法控制对类成员变量的访问:每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态(因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方法均被声明为 synchronized)。
synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率,典型地,若将线程类的方法 run() 声明为 synchronized ,由于在线程的整个生命期内它一直在运行,因此将导致它对本类任何 synchronized 方法的调用都永远不会成功。当然我们可以通过将访问类成员变量的代码放到专门的方法中,将其声明为 synchronized ,并在主方法中调用来解决这一问题,但是 Java 为我们提供了更好的解决办法,那就是 synchronized 块。一个实例
public class HelloSynchronized implements Runnable{
public static void main(String[] args) {
HelloSynchronized s1 = new HelloSynchronized();
new Thread(s1, "person1").start();
new Thread(s1, "person2").start();
new Thread(s1, "person3").start();
}
private int ticket = 5;
public void run() {
for(int i=0;i<40;i++){
sale();
}
}
public synchronized void sale(){
if(ticket>0){
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + this.ticket--);
}
}
}
输出结果person1 5
person3 4
person2 3
person3 2
person1 1
2. synchronized 块:
通过 synchronized关键字来声明synchronized 块。语法如下:
synchronized(syncObject) {
//允许访问控制的代码
}
synchronized 块是这样一个代码块,其中的代码必须获得对象 syncObject (如前所述,可以是类实例或类)的锁方能执行,具体机制同前所述。由于可以针对任意代码块,且可任意指定上锁的对象,故灵活性较高。
public void run() {
for(int i=0;i<40;i++){
synchronized (this) {
if(ticket>0){
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + this.ticket--);
}
}
}
}
1)是某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。这时,不同的对象实例的synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;
2)是某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。
使用方法
1. 把synchronized当作函数修饰符时,示例代码如下:
Public synchronized void methodAAA()
{
//….
}
这也就是同步方法,那这时synchronized锁定的是哪个对象呢?它锁定的是调用这个同步方法对象。
也就是说,当一个对象P1在不同的线程中执行这个同步方法时,它们之间会形成互斥,达到同步的效果。
但是这个对象所属的Class所产生的另一对象P2却可以任意调用这个被加了synchronized关键字的方法。
上边的示例代码等同于如下代码:
public void methodAAA()
{
synchronized (this) // (1)
{
//…..
}
}
(1)处的this指的是什么呢?它指的就是调用这个方法的对象,如P1。可见同步方法实质是将synchronized作用于object reference。――那个拿到了P1对象锁的线程,才可以调用P1的同步方法,而对P2而言,P1这个锁与它毫不相干,程序也可能在这种情形下摆脱同步机制的控制,造成数据混乱
2.同步块,示例代码如下:
public void method3(SomeObject so)
{
synchronized(so)
{
//…..
}
}
这时,锁就是so这个对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以这样写程序,但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的instance变量(它得是一个对象)来充当锁:
class Foo implements Runnable
{
private byte[] lock = new byte[0]; // 特殊的instance变量
Public void methodA()
{
synchronized(lock) { //… }
}
//…..
}
注:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而
Object lock= new Object()则需要7行操作码。
3.将synchronized作用于static 函数,示例代码如下:
Class Foo
{
public synchronized static void methodAAA() // 同步的static 函数
{
//….
}
<span style="white-space:pre"> </span>//等价于
public void methodBBB()
{
synchronized(Foo.class) // class literal(类名称字面常量)
}
}
代码中的methodBBB()方法是把class literal作为锁的情况,它和同步的static函数产生的效果是一样的,取得的锁很特别,是
当前调用这个方法的对象所属的类(Class,而不再是由这个Class产生的某个具体对象了)。
三、Lock关键字
1、使用方法
Lock
实现提供了比使用synchronized
方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition
对象。
锁是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问。一次只能有一个线程获得锁,对共享资源的所有访问都需要首先获得锁。不过,某些锁可能允许对共享资源并发访问,如ReadWriteLock
的读取锁。
synchronized
方法或语句的使用提供了对与每个对象相关的隐式监视器锁的访问,但却强制所有锁获取和释放均要出现在一个块结构中:当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的词法范围内释放所有锁。
虽然 synchronized
方法和语句的范围机制使得使用监视器锁编程方便了很多,而且还帮助避免了很多涉及到锁的常见编程错误,但有时也需要以更为灵活的方式使用锁。例如,某些遍历并发访问的数据结果的算法要求使用 "hand-over-hand" 或 "chain locking":获取节点 A 的锁,然后再获取节点 B 的锁,然后释放 A 并获取 C,然后释放 B 并获取 D,依此类推。Lock
接口的实现允许锁在不同的作用范围内获取和释放,并允许以任何顺序获取和释放多个锁,从而支持使用这种技术。
package synch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Bank {
private Lock bankLock = new ReentrantLock();
private final double[] accounts;
public Bank(int n ,double initialBalance) {
accounts = new double[n];
for (int i = 0; i < accounts.length; i++) {
accounts[i] = initialBalance;
}
}
public void transfer(int from , int to ,double amount) {
bankLock.lock();
try{
if( accounts[from]<amount ) return;
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());
}finally{
bankLock.unlock();
}
}
private double getTotalBalance() {
double sum =0;
for(double a:accounts)
sum += a;
return sum;
}
public int size() {
return accounts.length;
}
}
package synch;
public class TransferRunnable implements Runnable{
private Bank bank;
private int fromAccount;
private double maxAccount;
private int DELAY = 10;
public TransferRunnable(Bank b, int from, double max){
bank = b;
fromAccount = from;
maxAccount = max;
}
public void run() {
try{
while(true){
int toAccount = (int) (bank.size()*Math.random());
double amount = maxAccount*Math.random();
bank.transfer(fromAccount, toAccount, amount);
Thread.sleep((int) (DELAY*Math.random()));
}
}catch(InterruptedException e){
}
}
}
package synch;
public class BankTest {
public static final int NACCOUNTS = 100;
public static final double INITIAL_BALANCE = 1000;
public static void main(String[] args) {
Bank bank = new Bank(NACCOUNTS, INITIAL_BALANCE);
int i;
for (i = 0; i < NACCOUNTS; i++) {
TransferRunnable r = new TransferRunnable(bank, i, INITIAL_BALANCE);
Thread t = new Thread(r);
t.start();
}
}
}
lock(), in java.util.concurrent.locks.Lock 获取锁;如果锁同时被另一个线程拥有则发生阻塞。
unlock() 释放锁
ReentrantLock() 构建一个可以被用来保护临界区的可重入锁
2、条件对象
锁可以有一个或多个条件对象。每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。
当余额不足时,线程应该阻塞,放弃锁。我们可以加入条件对象来表达“余额充足”。
public class Bank {
<span style="white-space:pre"> </span>private Lock bankLock ;
<span style="white-space:pre"> </span>
<span style="white-space:pre"> </span>private final double[] accounts;
<span style="white-space:pre"> </span>
<span style="white-space:pre"> </span>private Condition sufficientFunds;
<span style="white-space:pre"> </span>
<span style="white-space:pre"> </span>public Bank(int n ,double initialBalance) {
<span style="white-space:pre"> </span>accounts = new double[n];
<span style="white-space:pre"> </span>for (int i = 0; i < accounts.length; i++) {
<span style="white-space:pre"> </span>accounts[i] = initialBalance;
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>bankLock = new ReentrantLock();
<span style="white-space:pre"> </span>sufficientFunds = bankLock.newCondition();
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>
<span style="white-space:pre"> </span>public void transfer(int from , int to ,double amount) throws InterruptedException {
<span style="white-space:pre"> </span>bankLock.lock();
<span style="white-space:pre"> </span>try{
<span style="white-space:pre"> </span>while( accounts[from]<amount )
<span style="white-space:pre"> </span>sufficientFunds.await();
<span style="white-space:pre"> </span>System.out.print(Thread.currentThread());
<span style="white-space:pre"> </span>accounts[from] -= amount;
<span style="white-space:pre"> </span>System.out.printf(" %10.2f from %d to %d", amount, from, to);
<span style="white-space:pre"> </span>accounts[to] += amount;
<span style="white-space:pre"> </span>System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
<span style="white-space:pre"> </span>sufficientFunds.signalAll();
<span style="white-space:pre"> </span>}finally{
<span style="white-space:pre"> </span>bankLock.unlock();
<span style="white-space:pre"> </span>}<span style="white-space:pre"> </span>
<span style="white-space:pre"> </span>
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>private double getTotalBalance() {
<span style="white-space:pre"> </span>double sum =0;
<span style="white-space:pre"> </span>
<span style="white-space:pre"> </span>for(double a:accounts)
<span style="white-space:pre"> </span>sum += a;
<span style="white-space:pre"> </span>
<span style="white-space:pre"> </span>return sum;
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>
<span style="white-space:pre"> </span>public int size() {
<span style="white-space:pre"> </span>return accounts.length;
<span style="white-space:pre"> </span>}
}
void await() throws InterruptedException
-
造成当前线程在接到信号或被
中断之前一直处于等待状态。
与此
Condition
相关的锁以原子方式释放,并且出于线程调度的目的,将禁用当前线程,且在发生以下四种情况之一 以前,当前线程将一直处于休眠状态:- 其他某个线程调用此
Condition
的signal()
方法,并且碰巧将当前线程选为被唤醒的线程;或者 - 其他某个线程调用此
Condition
的signalAll()
方法;或者 - 其他某个线程中断当前线程,且支持中断线程的挂起;或者
- 发生“虚假唤醒”
在所有情况下,在此方法可以返回当前线程之前,都必须重新获取与此条件有关的锁。在线程返回时,可以保证 它保持此锁。
- 其他某个线程调用此
void signalAll()
- 唤醒所有等待线程。 它 不会立即激活一个等待线程,而是仅仅解除等待线程的阻塞,以便这些线程可以在当前线程退出同步方法之后,通过竞争实现对对象的访问。一旦锁成为可用的,它们中的某个将从await调用返回,获得该锁并从被阻塞的地方继续执行。此时线程应该再次测试该条件。
-