线程同步分析
线程同步是什么?
多个线程访问同一份资源(共享资源)的时候,线程之间相同协调的过程成为线程同步。
在一般情况下,创建一个线程是不能提高程序的执行效率的,所以要创建多个线程。但是多个线程同时运行的时候可能调用线程函数,在多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入数据会被多次的覆盖,所以就要使线程同步。
synchronized关键字
synchronized可以保证方法或者代码快在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。
每个Java对象都有用作一个实现同步的锁,这些锁成为java内置锁,可以理解为,在object对象中有个lock对象。
当你使用synchronized(this)相当于synchronized(this.lock)
当你使用synchronized(obj)相当于synchronized(obj.lock),因此我们说是持有某个对象的锁。
sleep()方法,睡眠过程中,并不释放当前持有的锁。
三种应用方式:
1、普通同步方法(实例方法),锁是当前实例对象,进入同步方法前要获得当前实例的锁
2、静态同步方法,锁是当前类的class对象,进入同步代码前要获得当前类对象的锁
3、同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁
保护共享资源
要保护好需要同步的对象,需要对访问共享资源的所有方法或代码块都要考虑是否需要加入锁。因为别的线程可以自由访问非同步(即:未加锁)的方法,这样可能会对同步的方法产生影响。
生产者和消费者
下面使用最经典的生产者消费者的例子,讲解线程同步。
生产者->做馒头
消费者->吃馒头
/**
* 馒头封装类
*/
class ManTou {
// 给馒头一个id
int id;
public ManTou(int id) {
this.id = id;
}
@Override
public String toString() {
return "ManTou{" +
"id=" + id +
'}';
}
}
使用一个数组(篮子)来存放馒头,然后维护一个栈顶指针
image.png
/**
* 篮子
*/
class Basket {
// 栈顶指针, 该装第几个了
int index = 0;
// 容量
ManTou[] arrayManTou = new ManTou[6];
}
提供一个向篮子里扔馒头(push)和从篮子里取馒头(pop)的方法。
public void push(ManTou manTou) {
arrayManTou[index] = woTou;
index++;
}
public synchronized ManTou pop() {
index--;
ManTou mt = arrayManTou[index];
arrayManTou[index] = null;
return mt;
}
但是我们这样会不会有问题呢?
对于每一个做馒头的人和吃馒头吃的人,都相当于是一个线程。
每个做馒头的人都会调用push方法,向篮子里扔馒头。每个吃馒头的人,都会调用pop方法,从篮子里取出馒头。
当做馒头的人小A(A线程)调用push方法向筐里扔馒头的过程中,在 arrayManTou[index] = woTou;这一句之后,很不幸,此时CPU时间片分给了做馒头的小B(B线程执行)。 那问题就来了:index 还没来得及++。小B做好了馒头也往篮子里面扔,就把刚才小A丢进去的馒头给覆盖了。这个问题的关键就在于这两条语句之间不能被打断,因此要在push方法上加synchronized。
同样的,pop方法也有这样的问题,因此也要在pop方法上添加synchronized关键字。
那接下来还有其他的问题:
对于做馒头的人而言,篮子满了怎么办?因为我们篮子里面数组的容量只有6。
既然篮子只有这么大,那就等会在做馒头吧,等篮子里的馒头被吃掉了,再往篮子里面扔馒头。
public synchronized void push(ManTou manTou) {
while (index == arrayManTou.length) { // 满了
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
arrayManTou[index] = manTou;
index++;
notify();// 唤醒一个正在wait在当前对象上的线程 notify无法唤醒自己
// notifyAll();
}
注意:这个wait是Object的的wait方法
this.wait()是啥意思? 是指当前执行这个代码块的线程wait, 也就是已经持有了synchronized(this)语句中的this对象的锁的线程等待。等待在哪里?等待在this对象上。等待其他线程调用这个(this)对象的notify方法的时候唤醒自己。
一个线程进入push方法的时候, 已经拿到了锁了。在它执行的过程中,遇到一个事件,必须阻塞。也就是说做馒头的人,在往篮子里扔的时候,先检查了一下篮子满了,他就只能等着了,不能再往里扔了,再扔就冒出来了。要等到有人吃了,才能再继续往里扔。
调用wait()或者notify()之前,必须使用synchronized关键字持有被wait/notify的对象的锁。只有持有了锁,才有资格wait。如果你压根拿不到锁,就根本无法wait。
也就是你 synchronized(XX) 和 XX.wait 必须是同一个对象, 否则抛出 java.lang.IllegalMonitorStateException
那何时醒来?等篮子里的馒头被别人吃了,让吃馒头的人把他叫醒就行啦, 即:等待别的线程调用同一个Basket对象的notify/nofityAll 方法的时候, 就会醒来啦。
对于吃馒头的人而言,篮子空了咋整?很简单,等着呗。等人家做好了咱再吃。
public synchronized ManTou pop() {
while (index == 0) { // 空了
try {
this.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
index--;
ManTou mt = arrayManTou[index];
notify();
return mt;
}
细心的小伙伴可能已经发现:我们在push方法和pop方法的最后都调用了notify方法。
notify() 唤醒一个正在wait在当前对象上的线程 notify无法唤醒自己
notifyAll() 唤醒所有正在wait在当前对象上的线程
显然,刚才已经有线程wait在了Basket对象上。
在push方法中:如果篮子里没有满的话,我们还是向往常一样往篮子里扔馒头,但是扔完了,记得叫醒等着吃馒头的人。因为可能有人在等着吃。
在pop方法中:如果篮子不是空的,取出了一个就赶紧通知做馒头的人, 说:“现在篮子已经不是满的了,有空间了,你们可以做起来啦。”
这个篮子,我们终于是封装好了,现在我们把做馒头的人和吃馒头的人也封装起来。
/**
* 做馒头的人
*/
class Producer implements Runnable {
Basket basket;
public Producer(Basket basket) {
this.basket = basket;
}
@Override
public void run() {
for (int i=0; i<20; i++) {
ManTou manTou = new ManTou(i);
basket.push(manTou);
System.out.println("生产了:" + manTou);
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
生产者(做馒头的人)需要知道往哪个篮子里扔馒头,所以要持有篮子的引用。
当创建生产者的时候,就告诉他要往哪个篮子里扔。因此我们提供一个构造方法,为Basket赋值
生产的过程,也就是我们的 run()方法啦。在run()方法中,不断做馒头,不断往篮子里扔。
/**
* 消费者(吃馒头的人)
*/
class Consumer implements Runnable {
Basket basket;
public Consumer(Basket basket) {
this.basket = basket;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
ManTou manTou = basket.pop();
System.out.println("消费了:" + manTou);
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
消费者( 吃馒头的人),需要知道从哪个篮子里拿馒头吃。所以也要持有筐的引用。我们在构造方法中,为Basket赋值。
消费的过程, 也就是run()方法。不断从篮子里取出馒头。
有个细节,我们要注意。在push方法判断篮子满的时候, 以及在pop方法判断篮子空的时候,我们都用了while,为什么用while 而不用if呢?
考虑下面这样一种情况:在push方法中,如果在wait的时候被打断,将进入catch 代码块去处理异常,异常处理之后,就跳出了if, 继续下面的执行。如果此时篮子还是满的呢? 就有问题了。
所以要用while。即便是发生了Exception, 仍要要回头先检查是否已经满了, 如果满了, 还要继续wait。如果不满了,才能继续向下执行。
总结:
存放馒头的篮子,就是所谓的共享资源,对于共享资源的保护,就是需要对访问共享资源的所有方法和代码块都要考虑加入锁,也就是Baseket类中的push和pop方法。
wait()和sleep()的区别
1、wait是Object的方法,sleep是Thread的方法。
2、wait的时候不再持有那个锁,等notify醒来才重新获取锁,sleep是睡着,还是拥有锁,并不释放
3、调用wait的时候必须锁定对象,如果没有进入synchronized代码块,则没有wait的资格