线程同步
我们知道,线程存在的意义是为了同时独立的做事,实现并发。
考虑下面的问题:
如果两个人同时(假设可以做到真正的同时)用一个账户去不同的柜台或不同的银行点取钱,是不是最多可以取出双倍的钱?三个人是不是就最多可以取出三倍余额的钱……这事越想越刺激。
问题是,真的可以吗?先用代码简单模拟下这个过程。
先定义一个账户类:
//银行账户类
class account
{
private int balance; //余额
public account(int Balance)
{
balance=Balance;
}
//取钱
public void withdraw()
{
if(balance>0)
{
System.out.println("取出"+balance);
balance=0;
}
else
{
System.out.println("取钱失败");
}
}
public int getBanlance()
{
return balance;
}
}
再定义一个柜台线程类:
class bankCounter extends Thread
{
private account myAc;
public bankCounter(account MyAc)
{
myAc=MyAc;
}
@Override
public void run()
{
System.out.println(getName()+":");
myAc.withdraw();
}
}
接下来,测试一下:我们模拟用五个柜台(五个线程)同时取一个账户的钱:
public class SyncThread2
{
public static void main(String []args)
{
account myAc=new account(1000);
final int num=5;
bankCounter st[]=new bankCounter[num];
for(int i=0;i<num;i++)
{
st[i]=new bankCounter(myAc);
st[i].setName("Sync"+i);
st[i].start();
}
}
}
运行结果:
原来真的可以取出多于余额的钱 ?(nfp!!!)
事实上,银行如果是这样,早就不会有银行了,可是问题出在哪,怎么解决?
分析:
通过上面的5个线程的输出语句很容易看出他们的run函数缠绵交错(还真是同步啊),我们有理由认为,不同线程在调用账户类中的取钱函数时,函数中的每条语句的执行也可能会如此缠绵。这样,应该会出现,在一个线程已经取钱但还没有减账户余额时,另一个账线程去探查账户余额的情况。这种情况很明显后面的线程会判定可以取钱,这样,我们就能取出多于余额的钱了。
解决:
不难想出,我们只要让每个线程在判断余额时,所判断的余额的确是一个实时正确的余额就能解决这个问题。那么某个线程在从拿到余额到修改完余额的过程中别的线程不能访问余额是不是就可以达到上面的目的?也就是说,拿到它,把它暂时锁起来,用完了,再放出去?这,就是所谓的线程同步问题,也就是人为去限制不同线程对公共数据的使用问题。
synchronized
这个关键字,所能实现的功能其实就是上面所说的“锁”,单词翻译是“同步的”,所以也叫同步锁。
用法
1.修饰代码块
synchronized(object o)
{
//…
}
修饰代码块时,先调用到该代码块的线程会获得o对象的拥有权直至该代码块执行结束,在此期间如果别的线程想要用o对象,则会处于阻塞状态。
如果o是一个类,那么这个类的所有对象的使用权在代码块执行期间都只属于调用这个代码块的线程。
2.修饰方法:
public synchronized void method()
{
//…
}
*修饰方法时,synchronized 可以放在函数返回类型前的任意位置
类比上面修饰代码块不难理解,修饰方法时,(某个线程)在外部通过对象调用这个方法,方法执行期间,调用该对象此方法(其他方法不受影响)的其他线程就会处于阻塞状态。
如果是个static方法呢?我们知道静态方法是属于类的方法,所以当synchronized 修饰静态方法,那么方法执行期间,通过该类以及该类的所有对象调用此方法的其他线程就会处于阻塞状态。
(上面可能词不达意,小伙伴们自己再理解。不过最好自己动手敲代码试试。)
样例
(只需改变下账户的取钱函数这部分)
通过修饰代码块
(我把balance属性改成了Interger)
//取钱
public void withdraw()
{
synchronized(balance)
{
if(balance>0)
{
System.out.println("取出"+balance);
balance=0;
}
else
{
System.out.println("取钱失败");
}
}
}
这样写,“锁住的”只是一个对象的balance这一属性
运行结果:
**如果把synchronized后面括号里换成this会是什么样?
对于本样例,效果无变化,但实际上区别很大。如果是this,那么先调用该代码块的线程会将这个对象整体都“锁住”。实际上,我觉得,能锁的少就不要锁得多。
通过修饰方法
//取钱
public synchronized void withdraw()
{
if(balance>0)
{
System.out.println("取出"+balance);
balance=0;
}
else
{
System.out.println("取钱失败");
}
}
怎么样?是不是很简单=》?其实,只要搞明白锁的是什么,基本就清楚了。划重点:搞明白锁的是什么!!!
当然,这也是我们用的时候需要注意的问题,那就是到底要锁什么。
说白了,共享的数据是什么,就要同步(“锁”)什么。
需要注意
1.synchronized 方法不会继承,子类重写后有需要要申明调用父类的方法。抽象方法,接口方法都不能申明为synchronized 。
2.构造方法不能声明为synchronized方法 ,但可以用上面的同步代来代替。
3.synchronized同步会增加系统开销,有额外的性能消耗,甚至会造成死锁,所以尽量避免无用的同步控制。
死锁
上面提到,当一个线程获得一个对象的锁后,其他要用到该对象的线程会等待这个线程释放该对象锁。如果有线程互相等待对象锁释放就会造成死锁。看下下面的例子:
假设员工需要找其他一个(只一个)员工进行合作。也就是这样:
class Worker
{
public int id;
public Worker(int Id){id=Id;}
//一个人只能同时和另一个人合作,那么合作时就不能和另一个人合作,也就是该函数应该被同步
public synchronized void workWith(Worker another)
{
//synchronized(this)
{
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (another)
{
System.out.println(id+"与"+another.id+"在一起工作");
}
}
}
}
那么如果两个人互相寻求合作,会怎么样呢?
public void test()
{
Worker w1=new Worker(1);
Worker w2=new Worker(2);
new Thread(()->{w1.workWith(w2);}).start();
new Thread(()->{w2.workWith(w1);}).start();
}
我们用1个线程让w1找w2合作,另一个线程让w2找w1合作。
程序运行没有输出。
分析:2个线程分别拥有一个对象的锁后等待200ms需获得另一个对象的锁,但两个对象此时都是被锁状态,就进入了死锁状态。
可能这个例子很生硬,但不生硬的死锁往往就不容易被发现了》*《
为了使同步控制更加灵活,还可以利用wait-notify机制。
wait-notify/notifyAll
在一个同步代码块中,可以在满足某些条件时,调用对象的wait()方法暂时释放对象锁,使其他线程有机会使用该对象,同时本线程进入等待状态。与之对应,当其他线程调用该对象的notify()方法时,就会唤醒在等待的其他任意一个线程,notifyAll()方法会唤醒所有在等待的其他线程。
先通过经典的生产–消费模型运用下:
生产者-消费者模型
设有一定数量的生产者生产产品供一定数量的消费者消费, 但仓库可储存数量有上限,当仓库满时,生产者停止生产直到有产品被消耗 当仓库容量为0时,消费者等待有新的产品生成。
不防假设:
生产和消费一件产品都需要一定时间,生产者生产占用公共时间,消费者的消费不占用公共的时间。
(当一个生产者发现仓库不满,他开始生产,到他生产完成放入仓库的这段时间,别的生产者和消费者不能查看仓库;消费者只是取走产品,产品的消耗和仓库无关)
在这样的设定下,仓库的实时产品数量就是公共的数据,就是需要同步(“锁”)的东西。
测试代码:
public class Produce_Consume {
final int storeCapacity=10;
Stack<Integer> store=new Stack<>();
//生产者类,在产品数量不足时生产
class Producer extends Thread
{
@Override
public void run()
{
while(true)
{
synchronized(store)
{
while(store.size()>=storeCapacity) //如果产品数量足够,本生产者进入等待状态
{
System.out.println(getName()+" find store is full");
try {
store.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//否则,生产一个产品
store.add(store.size()+1);
System.out.println(getName()+" produced product"+store.size());
//生产消耗公共时间,那么应该放在同步代码块中
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
store.notifyAll(); //唤醒其他所有线程
}
}
}
}
//消费者
class Consumer extends Thread
{
@Override
public void run()
{
while(true)
{
synchronized(store)
{
while(store.size()==0) //如果没有产品,本消费者者进入等待状态
{
System.out.println(getName()+" find store is empty");
try {
store.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//否则,消费一个产品
System.out.println(getName()+" consumed product"+store.size());
store.pop();
store.notifyAll(); //唤醒所有其他线程
}
//消费不占用公共时间,所以放在同步代码块外。
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public void test()
{
int pNum=3,cNum=5; //生产者,消费者数量;
Producer pro;
for(int i=0;i<pNum;i++)
{
pro=new Producer();
pro.setName("producer"+i);
pro.start();
}
Consumer con;
for(int i=0;i<cNum;i++)
{
con=new Consumer();
con.setName("consumer"+i);
con.start();
}
}
public static void main(String args[])
{
new Produce_Consume().test();
}
}
运行结果:
学习了其他博客和资料,我觉得用到wait-notify需要注意的几点问题:
1.wait-notify(notifyAll)语句放在同步代码块中。
2.由于调用某个对象的notify后该对象锁是有CPU调度随机分配(?好像和线程进入等待状态的顺序有关)的,而且只有一个线程被唤醒,这样很容易造成死锁。所以在没有绝对把握不会造成死锁的情况下,最好使用notifyAll来减小造成死锁的可能。
3.线程调用wait后,本线程将会停在wait语句。
4.!!!wait所需要满足的条件应该用while循环来判断而不是if。
关于这一点,拿上面的生产消费模型来说,假如用if来判断,也就是写成:if(store.size()>=maxCapacity){//…},1号生产者拿到store的锁后,判断了store已满,释放锁进入等待状态,唤醒其他所有在等待store锁的线程,注意,这里1号的运行将停在wait语句。假设锁被分配给了2号生产者,2号生产者判断仓库为满后,释放锁。如果此时store的锁被分配给了1号生产者,那么一号生产者就直接往下进行语句,也就是会再去生产产品,显然这不符合设定。但如果用的是while循环来判断,即使store的锁被分配给了1号生产者,他也不能继续生产。
(以上所有仅为个人此时认知,如有错误,请不吝指正)