先简单描述一个问题,现在停车场还有一个空位,但是停车场的东西两个入口都正在有车要进入,这两个入口显示剩余车辆只有一个,所以是允许车进入的,所以这两辆车都进了停车场,最后的结果就是有一辆车没有车位。
这个例子可能后果不是很严重,没车位而已,那如果换成银行呢,账户余额是10000块钱,你拿着卡去atm取钱,同时在手机银行给别人转账,同时进行,atm上查询到你的余额是10000,你要取10000,同时你在手机银行给别人发起了转账,由于你的余额是10000,所以钱是够得,可以转账。
ATM上你取完10000之后你的余额还剩10000-10000=0元。但是这个结果还没有上传到服务器之前,服务器的余额还是10000,所以手机端转账也发起了,剩余余额还是10000,可以转账,转账之后结果是10000-10000也是0元,这是ATM将余额修改为0,并成功上传,这时手机银行也对余额进行了修改,要修改为0.所以你10000的余额成功的让银行亏损10000。这个例子的后果是不是很严重了。
线程安全问题
经过上面的例子我们可以知道,多线程同时对同一数据进行操作就有可能产生问题。既然出现问题的原因是多线程对同一数据同时进行操作,所以我们解决这个问题的思路也很简单,就是多线程不能对同一数据同时操作,要让他们在对同一数据进行操作时必须保持一定的顺序先后进行。
解决思路
在多线程并发时,我们对它们要修改的数据上一把锁,在其中一个线程对这个数据进行修改时,这把锁锁上,其他线程将不能对这个数据进行操作。
这里要提出一个概念:锁对象。锁对象就是我们在操作数据时上的那一把锁。有几个注意事项:
1.锁对象必须是针对这个数据进行操作的所有线程所共有的对象。
2.锁对象可以是任何对象。
总结:锁对象可以是这些线程共有的任何对象,就类似于大铁锁可以锁门,密码锁可以锁门,指纹锁也可以锁门,是什么对象不重要,只要是这些线程共有的就可以。
解决方案
同步代码块
语法:
synchronized(锁对象){
代码区
}
代码区中的代码就是要被锁住的代码,在这一处的代码将只能被一个线程执行,其他线程想要执行这一处的代码,需要等正在执行此处代码的线程执行完毕之后才可以执行。
同步方法
语法:
访问权限修饰符 synchronized 返回值类型 方法名(形参列表){
方法体
}
在同步方法中的代码就是被锁住的,谁调用同步方法,锁对象就代表谁。所以同步方法的锁对象就是this。
同步静态方法
语法:
访问权限修饰符 static synchronized 返回值类型 方法名(形参列表){
方法体
}
同步静态方法中的方法体就是要被锁住的代码。 同步静态方法的锁对象就是这个类的类对象。
类对象,是一个对象在加载的时候jvm自动生成的对象,而不是通过new关键字加构造函数创建的对象。一个类只有一个类的对象,想要获取一个类的类对象,可以使用类名.class或者对象名.getClass()获取。
死锁
死锁是指多个线程互相持有对方资源,使线程无法结束也无法从线程锁中出来的问题。
举例:
public class Test{
public static void main(String[] args){
Object obj1 = new Object();
Object obj2 = new Object();
Thread t01 = new Thread(){
public void run(){
synchronized(obj1){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程1的外锁,可以打印出来");
synchronized(obj2){
System.out.println("线程1的内锁,肯定打印不出来");
}
}
}
};
Thread t02 = new Thread(){
public void run(){
synchronized(obj2){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程2的外锁,可以打印出来");
synchronized(obj1){
System.out.println("线程2的内锁,肯定打印不出来");
}
}
}
};
t01.start();
t02.start();
}
}
这里简单解释一下上面的代码以及运行的结果。
线程1的外锁是用obj1锁住的,内锁是obj2锁住的,线程2恰好相反,外锁是用obj2锁住的,内锁是obj1锁住的.
如果线程1抢到了时间片,开始执行,进入到了外锁之中,所以obj1这把锁就锁上了,然后线程1开始了休眠,线程2开始执行,线程2进入外层锁之后obj2就锁住了,然后线程2开始休眠。
当线程1结束休眠之后开始尝试进入内锁,但是内锁是obj2,已经被线程2锁住了。线程2结束休眠之后开始尝试进入内锁,内锁是obj1,已经被线程1锁上了,所以无法进入内锁。
因此,线程1 和 线程2 就一直在它们的内锁外面等待这个锁打开,所以这个程序一直无法结束,也无法继续运行下去。这就是死锁。
要避免死锁只能是我们尽量不手动的在同步中套同步,如果同步嵌套,一定要分析代码逻辑是否会形成死锁,最后,锁对象一定要自定义一个对象充当锁对象,不要随便找个str或者其他对象。
线程间通讯
线程间通讯的作用就是线程之间相互传递简单的消息。
线程间通讯的所有方法都是Object提供的,而且线程间通讯的方法只能在同步代码块、同步方法或同步静态方法中调用。
常用方法
public final void wait();
让当前线程开始无限期休眠。
public final native void wait(long timeout);
让当前线程开始休眠一定的时间,传入的参数就是要休眠的时间,单位是毫秒
public final void wait(long timeout, int nanos)
让当前线程开始休眠一定的时间,传入的参数就是要休眠的时间,一参是毫秒,二参是纳秒。最终要休眠的时间就是一参和二参经过单位换算之后的总时间。
public final native void notifyAll();
唤醒以调用这个方法的对象为锁对象的所有线程。
public final native void notify();
随机唤醒一个以调用这个方法的对象为锁对象的线程。
在这里我们看到wait()的作用是休眠,上一篇文章也介绍了一个休眠的方法sleep();这里简单介绍一下它们的区别。
- wait方法让线程休眠之后会释放当前线程所占有的线程锁,sleep休眠之后不会释放当前线程占有的线程锁。就类似于在卫生间上厕所,wait是上厕所的人在睡着之前把锁打开,然后在卫生间睡着了。sleep就相当于上厕所的人在卫生间睡着了,锁还没打开,外面的人不管有多着急也只能等。
- wait只能在同步中调用,sleep没有要求
- wait是Object提供的,只能由锁对象调用,sleep是Thread提供的静态方法,没有这个要求。
另外notify和notifyAll的唤醒机制也简单解释一下:notify在唤醒时只能唤醒由调用notify这个方法的锁对象中的wait方法睡眠的对象。例如:
上面的死锁代码中线程1和线程2都有obj1和obj2这两个锁对象,如果线程1的休眠是obj1.wait()导致的休眠,那线程2只能用obj1.notify()或者obj1.notifyAll()唤醒。
如果线程2调用obj2.notify()或者obj2.notifyAll(),由于线程1调用睡眠方法的锁对象和线程2调用唤醒方法的锁对象不是一个对象,所以线程2将不能唤醒线程1.
生产者与消费者模式
在实际的软件开发中,我们将生产数据的模块称为生产者,将处理数据的模块称为消费者。生产者消费者模式就是在二者之间加入一个缓冲区,降低二者的耦合度。
生产者不需要考虑消费者消耗数据的速度,只需要关注缓存区剩余的空间大小,如果生产的数据塞满了缓冲区,就释放一定的资源,提高消费者消费数据的速度。
消费者不需要考虑生产者生产数据的速度,只需要关注缓存区剩余的空间大小,如果缓冲区中的数据空了,就释放一定的资源让生产者生产数据。
用现实生活中工厂为例子,我们在工厂中设定一个仓库,用来存放工人生产的产品,工人负责生产,只需要考虑一下仓库是否被塞满了,当仓库塞满之后就可以放假,等仓库有空间可以存放货物之后再进行生产。
市场部的销售人员只负责卖掉仓库中的产品,如果仓库中的产品全部空了之后就可以放假,等待工人生产出新的产品。
用代码模拟一下这个过程。
在这个过程中有四个类
工人 销售 工厂 环境类
工厂
仓库中剩余的产品数量
仓库中最大能容纳的产品数量
生产产品的方法
出售商品的方法
工人
去上班的工厂
可以去工厂生产产品
销售
去上班的工厂
卖工厂的东西
环境类
用代码实现
class Factory{
private int num = 0;
private final int MAX = 100;
public synchronized void produce(){
//如果产品的数量等于仓库的最大容量,就不生产了,开始放假
if(num >= MAX){
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else{
//仓库没满,就赶快干活
num++;
//仓库中又生产出了产品,叫销售回来卖东西。
this.notifyAll();
}
}
public synchronized void sell(){
//如果仓库中没有产品,销售人员可以放假。
if(num == 0){
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else{
//仓库中还有产品,销售抓紧速度
num--;
//仓库中又卖出东西了,可以让工人开始生产了
this.notifyAll();
}
}
}
class ProduceRunnable implements Runnable(){
//工人上班的工厂,只有在一个工厂上班的员工才能对同一个仓库进行操作。
private Factory factory;
public ProduceRunnable(Factory factory){
this.factory = factory;
}
public void run(){
while(true){
factory.produce();
}
}
}
class SellRunnable implements Runnable(){
//销售上班的工厂,只有在一个工厂上班的员工才能对同一个仓库进行操作。
private Factory factory;
public ProduceRunnable(Factory factory){
this.factory = factory;
}
public void run(){
while(true){
factory.sell();
}
}
}
在环境类中创建几个工人对象和销售对象,创建一个工厂对象,并指定这些员工上班的工厂是同一个工厂。然后开始运行,就可以起到我们预期的效果,我们要做的就根据程序运行的情况,调整工人和销售的数量即可。