线程同步的需求由来
在我们实现Runnable接口创建线程类时,通常会涉及到资源的共享问题,例如在SecondThread类中:
public class SecondThread implements Runnable{
private int i=0;
@Override
public void run() {
for( ;i<10;i++) {
//实现Runnable接口的线程类必须使用Thread类的静态方法获取当前线程
System.out.println(Thread.currentThread().getName()+"线程中的run方法执行"+" " +"i的值为:"+i);
}
}
public static void main(String[] args) {
SecondThread st=new SecondThread();
for(int i=0;i<10;i++) {
System.out.print("当前线程名字为:"+" ");
System.out.println(Thread.currentThread().getName()+" " +"循环变量i的值为:"+i);
if (i==5) {
new Thread(st,"子线程1").start();
new Thread(st,"子线程2").start();
}
}
}
}
在该程序的多次运行中,可能会出现两个子线程输出同一个i值的情况(某次运行结果中同时出现两个0,如下所示),这是我们不希望看到的结果,我们希望看到的是两个子线程输出不重复的0~9,10个数; 出现这中情况的原因是:在子线程1执行了run()方法中的`System.out.println(Thread.currentThread().getName()+”线程中的run方法执行”+” ” +”i的值为:”+i);`语句后,还没有执行`i++`语句,JVM的线程调度器就将线程的运行机会给了子线程2,这个时候线程2再去执行run()`方法中的System.out.println(Thread.currentThread().getName()+”线程中的run方法执行”+” ” +”i的值为:”+i)`语句,此时i的值并没有改变,所以会出现与线程1相同的输出结果。
当前线程名字为: main 循环变量i的值为:0 当前线程名字为: main 循环变量i的值为:1 当前线程名字为: main 循环变量i的值为:2 当前线程名字为: main 循环变量i的值为:3 当前线程名字为: main 循环变量i的值为:4 当前线程名字为: main 循环变量i的值为:5 当前线程名字为: main 循环变量i的值为:6 子线程1线程中的run方法执行 i的值为:0 子线程2线程中的run方法执行 i的值为:0 当前线程名字为: 子线程2线程中的run方法执行 i的值为:2 子线程1线程中的run方法执行 i的值为:1 子线程2线程中的run方法执行 i的值为:3 main 循环变量i的值为:7 当前线程名字为: 子线程2线程中的run方法执行 i的值为:5 子线程1线程中的run方法执行 i的值为:4 子线程2线程中的run方法执行 i的值为:6 main 循环变量i的值为:8 当前线程名字为: 子线程2线程中的run方法执行 i的值为:8 子线程1线程中的run方法执行 i的值为:7 子线程2线程中的run方法执行 i的值为:9 main 循环变量i的值为:9另举一个比较常用的例子:取票;假设共有余票2张,6个线程同时去执行取票操作,理论上只有2人能够取到票,取不到票的灰输出余票不足,但是观察输出结果可以看到输出并非我们乐意看到的,一个子线程内语句this.setTicketAmount(ticketAmount-drawAmount)还未执行时就被其他线程抢占CPU;
public class TakeTicketThread implements Runnable{
private int ticketAmount=2;
private int drawAmount;
public TakeTicketThread(int inputTicket) {
this.drawAmount=inputTicket;
}
@Override
public void run() {
if (drawAmount<=this.getTicketAmount()) {
System.out.println(Thread.currentThread().getName()+"取票成功!"+" "+" 取票张数为"+drawAmount);
this.setTicketAmount(ticketAmount-drawAmount);
System.out.println("余票张数为"+this.getTicketAmount());
}else {
System.out.println("余票不足!");
}
}
public int getTicketAmount() {
return ticketAmount;
}
public void setTicketAmount(int ticketAmount) {
this.ticketAmount = ticketAmount;
}}
public class TakeTicketTest {
public static void main(String[] args) {
//每次取票1张的线程
TakeTicketThread th=new TakeTicketThread(1);
new Thread(th,"甲取票").start();
new Thread(th,"乙取票").start();
new Thread(th,"丙取票").start();
new Thread(th,"丁取票").start();
new Thread(th,"戊取票").start();
new Thread(th,"戌取票").start();
}
}
某次运行输出结果为:
甲取票取票成功! 取票张数为1
丁取票取票成功! 取票张数为1
丙取票取票成功! 取票张数为1
余票张数为-1
乙取票取票成功! 取票张数为1
余票张数为-2
余票张数为0
戊取票取票成功! 取票张数为1
余票张数为1
余票张数为-3
余票不足!
那么如何实现当其中一个子线程进入到run()执行体后保证该线程将该方法体中的语句执行完后,再执行其它线程呢?就取票例子来说,一旦有用户开始了取票的操作,能否等该操作完成之后其它人才可以进行取票呢?
- 同步代码块
- 同步方法
- 同步锁
同步代码块
由于run()方法的方法体不具有同步性造成了上面的问题,Java引入了同步监视器来解决这个问题,在任何时候只能有一个线程可以获得对同步监视器的锁定,只有在同步代码块执行完了以后,才会释放同步监视器的锁定,使用了同步监视器的代码块称为同步代码块,其应用形式为:
synchronized(obj){
.......
//同步代码块
}
再来考虑下我们希望的取票操作是怎样的:我们希望当有用户取票也就是开始执行了run()方法体之后,完成该执行体类的所有操作,包括显示取票成功,计算出余票,或者是显示余票不足,修改代码如下:
public class TakeTicketThread implements Runnable{
private int ticketAmount=2;
private int drawAmount;
public TakeTicketThread(int inputTicket) {
this.drawAmount=inputTicket;
}
@Override
public void run() {
synchronized(this) {//锁定当前对象
if (drawAmount<=this.getTicketAmount()) {
System.out.println(Thread.currentThread().getName()+"取票成功!"+" "+" 取票张数为"+drawAmount);
this.setTicketAmount(ticketAmount-drawAmount);
System.out.println("余票张数为"+this.getTicketAmount());
}else {
System.out.println("余票不足!");
}
}
}
public int getTicketAmount() {
return ticketAmount;
}
public void setTicketAmount(int ticketAmount) {
this.ticketAmount = ticketAmount;
}
}
输出结果如下所示:
甲取票取票成功! 取票张数为1
余票张数为1
戊取票取票成功! 取票张数为1
余票张数为0
余票不足!
余票不足!
余票不足!
余票不足!
可以看到正是我们希望的结果。在修改后的程序中使用synchronized(this)锁定当前线程对象TakeTicketThread对象,因此也就锁定了总的票数这一实例变量,其它线程将无法获取到该实例变量,只有当该锁被释放后,该实例变量才能被其它线程修改;
这样的做法实际就是“加锁+修改+释放锁”的逻辑,任何线程在修改公共资源时,都先对其进行了加锁,在加锁期间其它线程无法修改该资源,只有当线程修改完之后释放资源锁定。这种方式保证了并发线程在任何时刻只有一个线程可以进入修改共享资源的代码区,保证了线程的安全。
同步方法
使用synchronized修饰方法,将方法设置为同步方法,默认锁定其调用对象。
public class TakeTicketThread implements Runnable{
private int ticketAmount=2;
private int drawAmount;
public TakeTicketThread(int inputTicket) {
this.drawAmount=inputTicket;
}
//定义同步方法
public synchronized void takeTicket() {
if (drawAmount<=this.getTicketAmount()) {
System.out.println(Thread.currentThread().getName()+"取票成功!"+" "+" 取票张数为"+drawAmount);
this.setTicketAmount(ticketAmount-drawAmount);
System.out.println("余票张数为"+this.getTicketAmount());
}else {
System.out.println("余票不足!");
}
}
@Override
public void run() {
takeTicket();
}
public int getTicketAmount() {
return ticketAmount;
}
public void setTicketAmount(int ticketAmount) {
this.ticketAmount = ticketAmount;
}
}
程序输出结果为:
甲取票取票成功! 取票张数为1
余票张数为1
丙取票取票成功! 取票张数为1
余票张数为0
余票不足!
余票不足!
余票不足!
余票不足!
以上线程类中将取票的具体逻辑抽取出来,定义了takeTicket()方法,该方法使用了synchronized修饰符修饰,这就是同步方法,同步方法的同步监视器就是this,也就是调用该方法的对象。从上面的结果中可以看到使用同步方法达到了同样的效果,与此同时,分离出了业务逻辑。
同步锁
同步锁lock是一种更强大的线程同步机制,这种机制通过显示定义同步锁对象来实现同步;Lock与ReadWriteLock是java提供的两个接口,实现类有ReentrantLock, ReentrantReadWriteLock,ReadLock, ReentrantReadWriteLock,WriteLock,这些锁的实现类提供了比synchronized代码块和方法更强大的功能。 锁是用于控制多线程访问共享资源的工具。 通常,锁提供对共享资源的独占访问权限:一次只有一个线程可以获取该锁,并且对共享资源的所有访问都要求首先获取该锁。 但是,某些锁可能允许并发访问共享资源,例如ReadWriteLock的读取锁定。
ReentrantLock是可重入的互斥锁,它由成功锁定并且没有释放锁的线程拥有;如果锁没有被其它线程拥有,那么调用锁的线程将返回并且获得该锁。使用该锁将程序作以下修改,可以看到使用同步锁实现了同样的输出效果,个人理解来看,同步锁就是强行将程序与一个锁对象绑定,通过这个对象来实现同步代码块和同步方法的作用,并且这种方式会更加灵活,锁对象有自己的一套方法,可以更智能的控制锁和返回锁的一些状态,但是这种方式必须显示使用unlock()释放锁。
import java.util.concurrent.locks.ReentrantLock;
/**
* @author XueXin
* 取票线程类
*/
public class TakeTicketThread implements Runnable{
private int ticketAmount=2;
private int drawAmount;
//新建一个锁对象,可重入的互斥锁
private final ReentrantLock lock = new ReentrantLock();
public TakeTicketThread(int inputTicket) {
this.drawAmount=inputTicket;
}
public void takeTicket() {
//使用同步锁的lock()方法锁定对象
lock.lock();
if (drawAmount<=this.getTicketAmount()) {
System.out.println(Thread.currentThread().getName()+"取票成功!"+" "+" 取票张数为"+drawAmount);
this.setTicketAmount(ticketAmount-drawAmount);
System.out.println("余票张数为"+this.getTicketAmount());
}else {
System.out.println("余票不足!");
}
lock.unlock();
}
@Override
public void run() {
takeTicket();
}
public int getTicketAmount() {
return ticketAmount;
}
public void setTicketAmount(int ticketAmount) {
this.ticketAmount = ticketAmount;
}
}
输出结果:
甲取票取票成功! 取票张数为1
余票张数为1
丙取票取票成功! 取票张数为1
余票张数为0
余票不足!
余票不足!
余票不足!
余票不足!
以下是ReentrantLock的部分方法截取,可以看到使用同步锁,功能上更加强大,控制更加灵活。