1. 临界资源问题演示
我们来模拟一个场景:5个售票员同时售卖100张票,卖完为止!
代码演示:
public class ThreadTest {
public static void main(String[] args) {
Runnable r = new Runnable() {
int i = 100;
@Override
public void run() {
while(i>0)
{
i--;
System.out.println(Thread.currentThread().getName()+"卖出了一张票,剩余票数:" + i);
}
}
};
Thread t1 = new Thread(r,"t1");
Thread t2 = new Thread(r,"t2");
Thread t3 = new Thread(r,"t3");
Thread t4 = new Thread(r,"t4");
Thread t5 = new Thread(r,"t5");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
执行结果:
可以看到,每个售票员正常买票,并输出了剩余票数,但是在这其中时不时就会出现票数显示重复,并且剩余票的数量并不是逐一递减,这就是因为多个线程同时操作一个变量导致的临界资源问题!
2. 临界资源问题分析
临界资源定义:
多个线程共享的资源叫临界资源
问题分析:
我们在上一篇文章中介绍过,多线程执行本质上是线程抢夺CPU时间片的结果,所以就可能出现t1线程抢夺到CPU时间片,对票数做了自减操作,此时CPU时间片被t2线程抢走,正常输入完成,因为每个线程的虚拟机栈独立,所以t2线程记录的剩余票数为97,所以仍然会输出剩余97!同样,此问题也会导致剩余数目不是顺序递减的情况,比如图二,t3线程抢夺到了CPU时间片,但是随即时间片又被其他线程抢夺,此时t3线程的剩余票数始终为71,此时CPU时间片被其他线程抢夺,票数减少,t3线程重新抢夺回CPU时间片后,可能剩余票数已经很少,但是t3线程的记录仍为71,所以导致了输出票数不是逐级递减的问题!
解决方法:
既然是由于抢夺时间片问题导致的问题,那就需要保证在变量使用的过程中,每次同时只能有一个线程操作该变量,使用同步逻辑同步线程即可!
3. 同步代码块
synchronized (锁) {
}
使用方法:
在大括号内的代码可以保证线程同步,小括号内为锁对象(可以为任何对象),代码执行时,系统会给代码块加上相应对象的锁,此时如果有其他线程访问,如果锁对象相同,就必须等待其它线程执行完成释放锁对象!
同步原理:
此时,如果已经有线程执行到同步代码处,并持有相同的锁,其他线程就会在执行到此处时进入锁池等待,待到当前线程执行完毕,释放锁标记后,锁池中等待的线程就会挣钱锁标记,正抢到锁标记的线程继续执行,其他线程继续等待!
public class ThreadTest {
public static void main(String[] args) {
Runnable r = new Runnable() {
int i = 100;
@Override
public void run() {
while(i>0)
{
synchronized ("锁") {
i--;
System.out.println(Thread.currentThread().getName() + "卖出了一张票,剩余票数:" + i);
}
}
}
};
Thread t1 = new Thread(r,"t1");
Thread t2 = new Thread(r,"t2");
Thread t3 = new Thread(r,"t3");
Thread t4 = new Thread(r,"t4");
Thread t5 = new Thread(r,"t5");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
此时输出就会发现票数已经是依次递减:
注意:如果此时另一个线程持有不同的锁对象访问,是可以访问的!
synchronized ("锁"+i) {
i--; System.out.println(Thread.currentThread().getName() + "卖出了一张票,剩余票数:" + i);
}
此时可以发现,虽然代码被上锁,但是由于持有的锁并不相同,其他线程的代码依旧可以访问,同步代码块无效!
回到上方,我们对代码添加了同步代码块,线程正常执行,票数逐一递减,但是翻到最后我们就会发现这样的问题:
这是因为:
当t4线程执行到0时,其它线程已经通过了while循环的循环条件,进入锁池等待,当t4执行完后,其它线程并不知道剩余票数已经为0,只知道逐一递减,所以造成了负数的情况,所以,多线程并发时一定要注意临界资源问题,并加以额外的判断!
解决方法:
synchronized ("锁") {
if(i==0)//在同步代码块中额外判断
return;
i--;
System.out.println(Thread.currentThread().getName() + "卖出了一张票,剩余票数:" + i);
}
问题成功解决!
4. 同步方法
如果我们的同步逻辑比较简单,整个方法中的逻辑都需要同步,就可以直接使用Synchronized关键字修饰整个方法,此时整个方法就会被线程同步。
注意:我们知道同步代码需要有一个锁对象
- 静态方法:锁对象为当前类的类锁,即:类名.class
- 非静态方法:锁对象为当前对象,即this
public class ThreadTest {
public static int i =100;
public synchronized static void soild()
{
if(i==0)
return;
i--;
System.out.println(Thread.currentThread().getName() + "卖出了一张票,剩余票数:" + i);
}
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
Runnable r = new Runnable() {
@Override
public void run() {
while(i>0) {
soild();
}
}
};
Thread t1 = new Thread(r,"t1");
Thread t2 = new Thread(r,"t2");
Thread t3 = new Thread(r,"t3");
Thread t4 = new Thread(r,"t4");
Thread t5 = new Thread(r,"t5");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
5. 显式锁ReentrantLock
ReentrantLock显式锁与synchronize同步代码块使用方法大致相同,不过可以将上锁的细节交给程序员控制。
public class ThreadTest {
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
Runnable r = new Runnable() {
int i = 100;
@Override
public void run() {
while(i>0)
{
reentrantLock.lock();
if(i==0)
return;
i--;
System.out.println(Thread.currentThread().getName() + "卖出了一张票,剩余票数:" + i);
reentrantLock.unlock();
}
}
};
Thread t1 = new Thread(r,"t1");
Thread t2 = new Thread(r,"t2");
Thread t3 = new Thread(r,"t3");
Thread t4 = new Thread(r,"t4");
Thread t5 = new Thread(r,"t5");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
测试发现,与上方代码执行完全相同,临界资源问题解决完毕!