一、线程同步介绍
现实生活中,我们会遇到“同一资源,多个人都想使用”的问题,比如排队打饭,抢票等场景;
在多线程场景下,多个线程访问同一对象,并且部分线程设计修改该对象,这时候我们就需要线程同步机制。
线程同步其实就是一个等待机制,多个线程同时访问一个对象,此时需要将线程排队,抢夺锁监视器,获得锁监视器的线程先执行,其他线程阻塞,等待;执行完成后释放锁监视器,对列中的线程再去争抢锁监视器,即获得锁监视器的线程才可以执行;仿佛排队上厕所蹲坑,进去一个人把门关上了,其他想上厕所的只能排队等待。
说白了线程同步:就是通过 对列 + 锁的方式实现的。
二、线程同步的实现方式
2.1、下面介绍单体项目(单个jvm的情况),线程同步的几种方式:
2.1.1、同步方法
语法格式:public synchronized void method(int args){}
synchronized方法控制对“对象”的访问,必须获取调用该方法的对象锁,该方法才能执行,否则线程阻塞,方法一旦执行,就独占该锁,直到这个方法返回,锁才释放,后边的线程才能获得锁,继续执行;
synchronized方法弊端:
1)、若将一个方法声明为synchronized方法,将会影响效率;
2) 、若给一个方法加synchronized,锁的范围太大,本应该只锁一下改操作,对于读操作不应该加锁。
2.1.2、 抢车票案例
class ButTick implements Runnable{
//票
private int titck = 10;
private boolean flag = true;
@Override
public void run() {
//买票
while(flag){ //1.创建一个循环
try {
buy(); //2、执行线程方法
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
/**
* synchronized:同步方法 控制对对象的访问 buy这个方法的对象是ButTick
* 即为this ,this可以省略
*/
private synchronized void buy() throws InterruptedException {
//判断是否有票
if(titck <= 0){ //3.判断共享数据是否到达了末尾(到了末尾)
flag = false;
return;
}
Thread.sleep(100);
//4.判断共享数据是否到达了末尾(没有到末尾,执行核心逻辑)
System.out.println(Thread.currentThread().getName()+"拿到了第"+titck--+"张票");
}
2.2、同步代码块
语法格式:synchronized(Obj){}
2.2.1、Obj 锁监视器
Obj可以是任何对象,但是推荐使用共享资源作为锁监视器;(一般指被修改的对象)
执行流程:对多线程并发访问同一资源时;第一个线程,获取锁监视器成功了,执行其中的代码,第二个线程访问资源时,先检查锁监视器是否被占用,被占用则阻塞等待;直到第一个线程执行完任务,释放锁监视器;
2.2.2 银行取款案例(夫妻二人从同一账户取钱)
模拟银行取钱
class Drawing extends Thread{
Account account;//账户
int drawingMoeny;//取了多少钱
int nowMoney;//手里的钱
public Drawing(Account account,int drawingMoeny, String name){
super(name);
this.account = account;
this.drawingMoeny = drawingMoeny;
}
//取钱
@Override
public void run() {
//同步代码块
synchronized (account){
if(account.money - drawingMoeny < 0 ){
System.out.println(this.getName()+"钱不够取不了==》》》");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
account.money = account.money - drawingMoeny; //卡内余额减去 你取得钱
//手里的钱
nowMoney = nowMoney + drawingMoeny;
System.out.println(account.name+"余额为:"+account.money);
//Thread.currentThread().getName() == this,getName
System.out.println(this.getName()+"手里的钱" + nowMoney);
}
}
}
模拟账户
class Account{
int money;//余额
String name;//卡名
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
2.3、Lock锁方式
synchronized同步方法和同步代码块,加锁是隐式的,我们看不到具体在哪加的锁;
为了能更清晰的表达如何加锁、释放锁 jdk1.5以后引入的Lock锁;
Lock是一个接口,没法实现需要使用它的实现类ReentrantLock()叫做可重入锁
void lock():获得锁
void unlock():释放锁
2.3.1、将synchronize修改为lock锁方式代码如下
public class MyLock extends Thread{
static int ticket = 0;
static Lock lock = new ReentrantLock();
@Override
public void run() {
while(true){
//添加同步代码块
//synchronized (MyLock.class){
lock.lock();
try{
if(ticket < 100){
Thread.sleep(10);
ticket++;
System.out.println(getName() + "正在卖第" + ticket +"票!!!");
}else{
break;
}
}catch(Exception e){
e.printStackTrace();
}finally{//释放锁
lock.unlock();
}
// }
}
}
}
总结:以上是单体应用下最常见的三种实现线程同步的方式;后边会介绍阻塞对列方式,以及分布式锁实现线程同步。