线程同步的作用
在多线程中,当两个及以上线程并发访问同个资源时,尽管有控制线程的方法,但由于线程调度具有不确定性,所以极容易导致错误,这时就需要线程同步机制来解决此问题。
实现线程同步——同步监视器
Java中,任何对象都能作为同步监视器。同步监视器是为了确保一个线程在对对象操作时,别的线程无权访问,即任何时刻只能有一个线程对同步监视器锁定。所以一般把并发访问的共享资源作为同步监视器。
在Java中,使用关键字synchronized或者Lock类来对同步监视器锁定。
synchronized的用法
- 同步代码块:synchronized(obj){},传入的obj对象就是同步监视器,在执行同步代码块中的代码之前,必须先获得对同步监视器的锁定。
- synchronized修饰方法:同步方法的监视器是this,所以调用该方法的对象就是同步监视器,则无需显式指定。
Lock的用法
- 有ReentrantLock(可重入锁)、ReentrantReadWriteLock(可重入读写锁)、以及Java8新增的StampedLock类。
- Lock对象相当于同步监视器对象,并发访问时,每次只能允许一个线程对Lock对象加锁,执行完之后必须手动释放锁,释放锁方法一定要放在finally语句中。
- 另外ReentrantReadWriteLock(提供了三种读写操作模式,但不常用。
线程同步实例
先给上一段多线程实现的取钱代码,没有采用线程同步,看看会出现什么情况。
public class Account {
private int balance;//余额
public Account(int balance) {
this.balance = balance;
System.out.println("账户当前余额:" + balance);
}
//取钱方法
public void quqian(int money) {
if(money <= balance) {
System.out.println(Thread.currentThread().getName() + ",取出" + money);
//睡眠线程2s,用于测试另一个线程
try {
Thread.sleep(2000);
} catch (InterruptedException e) {e.printStackTrace();}
//修改余额的代码
this.balance -= money;
System.out.println(Thread.currentThread().getName()+",余额:" + this.balance);
}else {
System.out.println(Thread.currentThread().getName() + "取钱失败!");
}
}
}
//执行存钱操作的Runnable线程
class QqPerson implements Runnable{
private int money;
private Account ac;
public QqPerson(Account ac,int money) {
this.ac = ac;
this.money = money;
}
@Override
public void run() {
ac.quqian(money);
}
}
public class Test {
public static void main(String[] args) {
Account ac = new Account(1000);
//传入Account对象、取钱金额
QqPerson q1 = new QqPerson(ac,600);
QqPerson q2 = new QqPerson(ac,600);
//线程类传入Runnable对象
Thread t1 = new Thread(q1,"取钱1号");
Thread t2 = new Thread(q2,"取钱2号");
//启动线程
t1.start();
t2.start();
}
}
- 实例解析:在执行修改余额代码之前,调用sleep(2000)强制让线程睡眠2s,这样能更好地体现多个线程访问共享资源时,由于线程调度的不确定性导致的线程安全问题。事实上,如果不加sleep(),就会出现一会正确一会错误的情况。遇到这种问题,就需要使用线程同步来解决。
使用同步代码块实例
//执行存钱操作的线程
class QqPerson implements Runnable{
private int money;
private Account ac;
public QqPerson(Account ac,int money) {
this.ac = ac;
this.money = money;
}
@Override
public void run() {
//加入同步代码块
synchronized(ac){
ac.quqian(money);
}
}
}
- 实例解析:其余的代码都是相同的,只是把并发访问共享资源的执行代码放入同步代码块中,在代码中,依然使用sleep()来睡眠线程,但是会发现别的线程无法执行,这是因为同步锁还未被释放,执行完后释放锁,别的线程才能获取同步锁。
使用同步方法实例
//取钱方法
public synchronized void quqian(int money) {
if(money <= balance) {
System.out.println(Thread.currentThread().getName() + ",取出" + money);
//睡眠线程1s,用于测试另一个线程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {e.printStackTrace();}
//修改余额的代码
this.balance -= money;
System.out.println(Thread.currentThread().getName()+",余额:" + this.balance);
}else {
System.out.println(Thread.currentThread().getName() + "取钱失败!");
}
}
- 实例解析:因为并发访问共享资源的执行代码是由一个方法来完成,所以可以用synchronized修饰该方法。加锁原理和同步代码块一样,只是无需显式加入同步锁对象。
- 加锁流程:第一个线程执行取钱方法,先获得同步锁对象,然后执行方法内部代码,执行到sleep()会睡眠当前线程,但因为睡眠线程获得了锁对象,所以别的线程此时无权访问,睡眠时间到了之后,又开始继续执行,直到代码执行结束,然后释放锁。下一个线程开始执行,又继续前面的步骤。
使用Lock加锁实例
public class Account {
private int balance;//余额
ReentrantLock lock = new ReentrantLock();
StampedLock slock = new StampedLock();
public Account(int balance) {
this.balance = balance;
System.out.println("账户当前余额:" + balance);
}
//取钱方法
public void quqian(int money) {
try {
lock.lock();//加锁
if(money <= balance) {
System.out.println(Thread.currentThread().getName() + ",取出" + money);
//修改余额的代码
this.balance -= money;
System.out.println(Thread.currentThread().getName()+",余额:" + this.balance);
}else {
System.out.println(Thread.currentThread().getName() + "取钱失败!");
}
}finally {
lock.unlock();//手动释放锁
}
}
}
- 实例解析:上述代码使用了ReentrantLock(可重入锁)来实现线程同步。Lock对象代表了并发访问的同步监视器的领域,以Lock对象作为显式的同步锁对象。使用Lock加锁,一定要手动释放锁。
线程同步注意点:
- 线程持有同步锁对象期间,使用sleep()不会释放锁。但如果使用wait()会导致释放同步锁。
- sleep()的调用者是当前线程,而wait()的调用者是对象,所以wait()会导致锁被释放。
- 同步代码块需要显式传入同步锁对象,同步方法无需同步锁对象,因为隐式传入。
- Lock对象相当于隐式传入的同步锁对象,所以使用时要保证该同步锁对象是访问共享资源的调用对象。
- Lock可以嵌套加锁,即在Lock加锁的代码中能执行另一段加锁的代码。
线程死锁
线程同步时,两个及以上线程相互持有同步锁对象,这就导致无法对同步监视器对象进行锁定,造成所有线程都进入阻塞状态,这就是死锁。出现死锁不会出现异常,不会有任何提示。如果通过eclipse的控制台来查看,看起来程序是结束了,但红正方形的出现说明了程序是运行的,只是一直在等待锁的释放。
关键字volatile——用于修饰变量
volatile:易变、不稳定的。
volatile使用场景:
如果只是读写一两个实例变量就使用线程同步,则开销会很大,volatile关键字能为实例变量的同步访问提供了一种免锁机制。用于在多线程中同步公共变量,保证用户在操作一个变量,但不保证多线程的原子性。
原子性解释:
假设对共享变量除了赋值之外并不完成其他操作,那么可以将这些共享变量声明为volatile。变量使用同步的条件:
如果向一个变量写入值,而这个变量接下来可能会被另一个线程读取,或者从一个变量读值,而这个变量可能是之前被另一个线程写入的,此时必须使用同步。