当多个线程都需要修改同一个资源时,就可能出现错误,不同步的状态。需要使用同步机制使它们正确工作。使用关键词synchronized来进行同步。
同步代码块的语法格式为:
synchronized(obj){
同步代码块
}
obj叫做同步监视器,线程要执行同步代码块之前必须要获得对同步监视器的锁定。即可以理解为对obj上锁。任何时刻,只能有一个线程拥有obj的锁。
应该使用两个或多个线程共享的资源来作为同步监视器。
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| public class Main {
public static void main(String[] args) throws InterruptedException {
Accout acc = new Accout("123456w", 200);
Thread thread1 = new TakeMoney("甲", acc, 200); Thread thread2 = new TakeMoney("乙", acc, 200);
thread1.start(); thread2.start();
}
}
class TakeMoney extends Thread {
private Accout accout; private double money;
public TakeMoney(String name, Accout accout, double money) { super(name); this.accout = accout; this.money = money; }
@Override public void run() {
synchronized (accout) { if (accout.getBalance() >= money) {
System.out.println(this.getName() + "取钱成功,取出" + money); accout.setBalance(accout.getBalance() - money); System.out.println("当前余额为:" + accout.getBalance()); } else { System.out.println("余额不足,取钱失败"); } }
} }
class Accout { }
|
与同步代码块相对应的,Java提供了同步方法,同步方法就是使用synchronized关键词修饰某个方法,对于synchronized修饰的实例方法,线程在执行该方法时,取得的是调用该方法的对象的锁,即相当于synchronized(this)。
使用同步方法可以方便实现线程安全的类,线程安全类具体以下特征:
- 该类对象可以被多个线程安全访问
- 每个线程调用该对象任意方法后都将得到正确的结果
- 每个线程调用该对象的任意方法后,该对象状态保持合理状态
上个例子可以使用同步方法来做:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| public class Main {
public static void main(String[] args) throws InterruptedException {
Accout acc = new Accout("123456w", 200);
Thread thread1 = new TakeMoney("甲", acc, 200); Thread thread2 = new TakeMoney("乙", acc, 200);
thread1.start(); thread2.start();
}
}
class TakeMoney extends Thread {
private Accout accout; private double money;
public TakeMoney(String name, Accout accout, double money) { super(name); this.accout = accout; this.money = money; }
@Override public void run() {
accout.takeMoney(this, accout, money); } }
class Accout { public synchronized void takeMoney(Thread thread, Accout accout, double money) {
if (accout.getBalance() >= money) { System.out.println(thread.getName() + "取钱成功,取出" + money); accout.setBalance(accout.getBalance() - money); System.out.println("当前余额为:" + accout.getBalance()); } else { System.out.println("余额不足,取钱失败"); }
} }
|
释放同步监视器的锁定
任何线程进入同步代码块,同步方法之前,要获得同步监视器的锁定,然后线程会在几种情况释放该锁
- 执行到方法、同步代码块结束,线程释放监视器
- 在执行时遇到break、return终止该代码块
- 在执行时遇到了未处理的Error或者Exception,导致异常结束
- 当前线程执行时,程序执行了同步监视器对象的wait方法,当前线程暂停,并且释放监视器
同步锁
Java提供了一种更强大的线程同步机制------通过显式定义同步锁对象来实现同步。这种机制下,同步锁使用Lock对象充当。
Lock是控制多个线程对共享资源进行访问的工具,线程开始访问共享资源时,应先获得Lock对象。
在实现线程安全控制中,比较常用的是ReentrantLock(可重入锁),使用该对象可以显式的加锁、释放锁,通常使用该锁的代码格式为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class X { private final ReentrantLock lock = new ReentrantLock(); public void m() { lock.lock(); try { } finally { lock.unlock(); } } }
|
将之前例子中需要线程安全的方法改写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public void takeMoney(Thread thread, Accout accout, double money) { lock.lock(); try { if (accout.getBalance() >= money) { System.out.println(thread.getName() + "取钱成功,取出" + money); accout.setBalance(accout.getBalance() - money); System.out.println("当前余额为:" + accout.getBalance()); } else { System.out.println("余额不足,取钱失败"); } } finally { lock.unlock(); }
}
|
同样可以实现线程安全
使用Lock对象和使用synchronized方法类似,只不过一个是显式使用Lock对象,另一个是隐式使用当前对象作为同步监视器。
ReentrantLock具有可重入性,意思是多个线程可以在已被加锁的ReentrantLock对象再次加锁,即可执行多次lock方法,ReentrantLock对象会维持一个计数器来追踪对lock对象的嵌套调用。线程在每次调用lock加锁后,必须调用unlock释放锁。
避免死锁的策略
当两个或多个线程在互相等待对方释放同步监视器时就会发生死锁。Java没有检测,也不会解决死锁。需要人为干预避免死锁出现。
可以使用几个方法来避免
- 避免多次锁定,尽量避免同一个线程对多个同步监视器进行锁定
- 具有相同加锁顺序:如果多个线程需要对多个同步监视器进行锁定,则应该保证它们以相同顺序请求加锁
- 使用定时锁,程序调用Lock对象的tryLock方法可以指定time和unit参数,当超过指定时间就会自动释放对Lock的锁定
- 死锁检测:依靠算法来实现的死锁预防机制