Java基础知识陷阱(九)

本文发表于本人博客

今天我来说说关于JAVA多线程知识,有错误请指出。大家都知道JAVA在服务端上处理也有很大优势,很多公司也有在服务器跑JAVA进程,这说明JAVA在处理这个多线程以及并发下也有一定有优点的(这说法有点坑了)。下面来看看

那java中,不具备直接操作像操作系统一样的PV信号,然而它提供了synchronized来实现同步机制,可能这样说不够严谨。JAVA的基类Object中有以下几个方法:

public final native void notify();
public final native void notifyAll();
public final native void wait(long timeout) throws InterruptedException;

凡是继承实现Object对象的都有这样的方法,都有一个内存锁:当有线程获取改内存锁之后其它线程就无法访问改内存转入等待,知道占用改内存锁的线程释放内存锁才可以进入,这里网上好多地方也只是简单的说了下,我也是现在才比较了解原来:第一次当有线程获得了改对象的内存锁时此线程就进入锁池,其它需要使用该对象的线程进入等待池(即时调用了wait()).知道占用改对象内存锁的线程调用notify()或者调用了notifyAll()释放了,这里好像还有一个超时条件现在不知道回头再看看!!那么系统就会从等待池中随意挑选一个线程占用该对象的内存锁进入锁池,这里还要注意的是:Class也是一种对象,不是我们普通认为的new出来的才是对象,在这里它也属于对象所以就出现了对整个对象加锁、对整个class加锁的情况了!

上面的3个方法必须与synchronized关键字一起使用,因为wait()以及notify()都是在已经获得对象、类锁的时候才可以操作,才能够进入等待池以及释放锁。

春节将至,下面来个抢火车票的例子简单说明一下,先看Train类:

class Train{
/**
* 头等高级坐席
*/
private int Tickets = 10;
/**
* 购买火车票
* @throws Exception
*/
public void Purchase(String name) throws Exception{
// 模拟获取Tickets数据操作
Thread.sleep(50);
if(Tickets > 0){
// 模拟获取订单数据等操作
Thread.sleep(10);
int temp = Tickets--;
System.out.println(name + "抢到:" + temp);
// 模拟生成订单等操作
Thread.sleep(10);
}
else{
System.out.println(name + "抢票失败!");
}
}
}

TrainThread类:

class TrainThread extends Thread{
private Train train;
public TrainThread(Train train){
this.train = train;
}
@Override
public void run() {
try {
this.train.Purchase(Thread.currentThread().getName());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

main函数:

    public static void main(String[] args) {
//春节抢购火车票例子,模拟100人抢购10张票
Train train = new Train();
TrainThread[] threads = new TrainThread[100];
for (int i = 0; i < 100; i++) {
TrainThread th1 = new TrainThread(train);
threads[i] = th1;
}
for (int i = 100 - 1; i >= 0; i--) {
threads[i].start();
}
}

执行结果输出:

Thread-99抢到:10
Thread-97抢到:9
Thread-81抢到:1
Thread-79抢到:0
Thread-25抢票失败!
Thread-27抢票失败!
Thread-38抢票失败!
Thread-98抢到:-1
Thread-96抢到:-2
Thread-40抢票失败!
Thread-84抢到:-3

不行啊,连-1的票都出来了完全没实现要求,那分析下:应该是在if(Tickets > 0)里面模拟睡眠了10毫秒引起的,因为在睡眠10毫秒的时候很多线程也进来已经判断了if(Tickets > 0),那既然分析了原因有什么方法解决啊,当然有,我们可以把这个代码块锁起来,当一个线程正在判断操作Tickets变量的时候不让其它线程去访问,那Java里面有个关键字synchronized,我们再来看看代码:

    public void Purchase(String name) throws Exception{
// 模拟获取Tickets数据操作
Thread.sleep(50);
synchronized(this){
if(Tickets > 0){
// 模拟获取订单数据等操作
Thread.sleep(10);
int temp = Tickets--;
System.out.println(name + "抢到:" + temp);
// 模拟生成订单等操作
Thread.sleep(10);
}
else{
// System.out.println(name + "抢票失败!");
}
}
}
}

现在再来看输出:

Thread-93抢到:10
Thread-5抢到:9
Thread-3抢到:8
Thread-1抢到:7
Thread-4抢到:6
Thread-2抢到:5
Thread-0抢到:4
Thread-13抢到:3
Thread-18抢到:2
Thread-16抢到:1

多运行几次发现没有重复也不会出现负数的情况了,正常!再来分析下,代码用到synchronized(this):synchronized语句块结束后会自动释放锁让等待池的其中一个线程获得该锁。这还需要注意一下sleep以及notify方法的区别:它们2个最重要的区别是sleep释放CPU控制权但是并为释放内存锁,而notify还释放内存锁。其实还有一个用法synchronized锁住对象的方法,可以把代码修改成如下:

    public synchronized void Purchase(String name) throws Exception{
// 模拟获取Tickets数据操作
Thread.sleep(50);
if(Tickets > 0){
// 模拟获取订单数据等操作
Thread.sleep(10);
int temp = Tickets--;
System.out.println(name + "抢到:" + temp);
// 模拟生成订单等操作
Thread.sleep(10);
}
else{
// System.out.println(name + "抢票失败!");
}
}

看到这里大家都会觉得这样使用更简单啊,是这样是简单了但是可能要考虑到的是性能问题,如果这个对象还是static的话那么久相当于synchronized(class)了,大大的降低系统运行速度,这个上面也说了,不单是对象有内存锁,连类也是存在内存锁的!

不是说实现多线程有2种方法,一是继承Thread;二是实现接口Runnable。是的下面我们把Train改成TrainRun并实现接口Runnable,代码如下:

class TrainRun implements Runnable{
/**
* 头等高级坐席
*/
private int Tickets = 10;
private Object obj = new Object();
/**
* 购买火车票
* @throws Exception
*/
public synchronized void Purchase(String name) throws Exception{
Thread.sleep(50);
if(Tickets > 0){
Thread.sleep(10);
int temp = Tickets--;
System.out.println(name + "抢到:" + temp);
Thread.sleep(10);
}
else{}
} @Override
public void run() {
try {
this.Purchase(Thread.currentThread().getName());
} catch (Exception e) {e.printStackTrace();}
}
}

main函数:

        TrainRun train = new TrainRun();
Thread[] threads = new Thread[100];
for (int i = 0; i < 100; i++) {
Thread th1 = new Thread(train);
threads[i] = th1;
}
for (int i = 100 - 1; i >= 0; i--) {
threads[i].start();
}

输出结果也是一样达到要求:

Thread-99抢到:10
Thread-0抢到:9
Thread-2抢到:8
Thread-4抢到:7
Thread-6抢到:6
Thread-8抢到:5
Thread-10抢到:4
Thread-12抢到:3
Thread-14抢到:2
Thread-16抢到:1

上面不是说了使用wait以及notify方法吗,不错说了,这3个主要用于线程间互相唤醒的功能,上面的例子中没有这个需求互相唤醒共同协调完成一件事所以也就没用,不过看下面的例子:

一个阿姨负责传递碗筷,每次传递一个,一个阿姨负责洗碗,每次洗一个,盆能最多容纳5个碗,用代码描述这2个阿姨的工作过程,先分析下,这里一个阿姨负责传递一个阿姨负责洗碗,要用到2个线程,而且是盆里的碗是共享使用的,传递一个增加一个,洗一个就减一个,洗碗的时候如果盆里没碗则等待传递完成,如果传递的时候盆里的碗数量超过5则等待洗碗完成。看代码:

Conf配置类:

class Conf{
public static int passCount = 2;//阿姨传碗筷次数
public static int washCount = 2;//阿姨洗碗筷次数
public static int couter = 5; //盆做大容量5个
}

工作Work类:

class Work{
public int count = 0;
/**
* 洗碗筷
*/
public synchronized void Wash(){
if (count < 1){
try { wait(); } catch (InterruptedException e) {e.printStackTrace();}
}
System.out.println("\t已经有碗筷:" + count + " 个");
System.out.println("\t\t正在洗碗筷:第" + count + "个");
count--;
System.out.println("\t\t还剩下碗筷:" + count + "个");
notify();
}
/**
* 传碗筷
*/
public synchronized void Pass(){
if(count >= Conf.couter){
try { wait(); } catch (InterruptedException e) {e.printStackTrace();}
}
System.out.println("\t已经有碗筷:" + count + " 个");
System.out.println("\t\t再传一个碗筷");
count++;
System.out.println("\t\t传送完毕");
notify();
}
}

传送阿姨PassAunt类:

class PassAunt extends Thread{
private Work work ;
public PassAunt(Work work){
this.work = work;
}
@Override
public void run() {
for (int i = 0; i < Conf.passCount; i++) {
this.work.Pass();
try {Thread.sleep(50); } catch (InterruptedException e) {e.printStackTrace();}
}
}
}

洗碗阿姨WashAunt类:

class WashAunt extends Thread{
private Work work ;
public WashAunt(Work work){
this.work = work;
}
@Override
public void run() {
for (int i = 0; i < Conf.washCount; i++) {
this.work.Wash();
try {Thread.sleep(50); } catch (InterruptedException e) {e.printStackTrace();}
}
}
}

main函数:

    public static void main(String[] args) {
Work work = new Work();
PassAunt passThread = new PassAunt(work);
WashAunt washThread = new WashAunt(work);
passThread.start();
washThread.start();
}

结果如下:

    已经有碗筷:0 个
再传一个碗筷
传送完毕
已经有碗筷:1 个
正在洗碗筷:第1个
还剩下碗筷:0个
已经有碗筷:0 个
再传一个碗筷
传送完毕
已经有碗筷:1 个
正在洗碗筷:第1个
还剩下碗筷:0个

可以看到输出传递2个洗2个,满足条件。如果现在客人突然较多,老板娘也加入传递碗筷队伍来了,那现在看调用:

    public static void main(String[] args) {
Work work = new Work();
PassAunt passThread = new PassAunt(work);
PassAunt passThread01 = new PassAunt(work);
WashAunt washThread = new WashAunt(work);
passThread.start();
passThread01.start();
washThread.start();
} class Conf{
public static int passCount = 2;//阿姨传碗筷次数
public static int washCount = 4;//阿姨洗碗筷次数
public static int couter = 5; //盆做大容量5个
}

运行多次输出:

    已经有碗筷:2 个
正在洗碗筷:第2个
还剩下碗筷:1个
已经有碗筷:1 个
正在洗碗筷:第1个
还剩下碗筷:0个

从这里看看到,我们增加了一个传递的阿姨并修改洗碗的次数washCount结果是正常。那么我们再来修改下passCount以为4及washCount为2来看看多一个阿姨洗碗结果又是怎么样呢!

class Conf{
public static int passCount = 4;//阿姨传送碗筷次数
public static int washCount = 2;//阿姨传送碗筷次数
public static int couter = 5; //盆做大容量5个
}
    public static void main(String[] args) {
Work work = new Work();
PassAunt passThread = new PassAunt(work);
WashAunt washThread = new WashAunt(work);
WashAunt washThread01 = new WashAunt(work);
passThread.start();
washThread01.start();
washThread.start();
}

结果输出可能是:

    已经有碗筷:0 个
正在洗碗筷:第0个
还剩下碗筷:-1个
已经有碗筷:-1 个
再传一个碗筷
传送完毕

奇怪竟然出现-1个碗,显然是错误的,按道理1个传递碗筷的阿姨对应2个洗碗的阿姨数量是正确的。那现在看看这个洗碗的方法哪里有问题呢,当前线程调用了wait方法,再次醒来继续往下执行,这个时候没去判断这个盆里还有多少碗,修改下使用while:

    public synchronized void Wash(){
while (count < 1){
try { wait(); } catch (InterruptedException e) {e.printStackTrace();}
}
System.out.println("\t已经有碗筷:" + count + " 个");
System.out.println("\t\t正在洗碗筷:第" + count + "个");
count--;
System.out.println("\t\t还剩下碗筷:" + count + "个");
notify();
}

并把洗碗的次数随便设置:

public static int washCount = 2;//阿姨传送碗筷次数

再次运行看结果:

    已经有碗筷:0 个
再传一个碗筷
传送完毕
已经有碗筷:1 个
正在洗碗筷:第1个
还剩下碗筷:0个

运行多次没错,最后洗碗全部洗完了!那洗碗是搞定了,传递碗筷会不会出现错误的情况呢,接下来看看同时增加一个传递碗筷的阿姨以及洗碗的阿姨,看代码:

class Conf{
public static int passCount = 2;//阿姨传送碗筷次数
public static int washCount = 2;//阿姨传送碗筷次数
public static int couter = 5; //盆做大容量5个
}
    public static void main(String[] args) {
Work work = new Work();
PassAunt passThread = new PassAunt(work);
PassAunt passThread01 = new PassAunt(work);
WashAunt washThread = new WashAunt(work);
WashAunt washThread01 = new WashAunt(work);
passThread.start();
washThread01.start();
passThread01.start();
washThread.start();
}

输出结果:

    已经有碗筷:0 个
再传一个碗筷
传送完毕
已经有碗筷:1 个
正在洗碗筷:第1个
还剩下碗筷:0个

再次把passCount为4以及washCount为2,看结果:

    已经有碗筷:2 个
再传一个碗筷
传送完毕
已经有碗筷:3 个
再传一个碗筷
传送完毕

到最后还有碗筷并为洗完,分析一下这个是由于2个传递碗筷的阿姨总共传送了8个碗筷,而2个阿姨洗碗的的总次数总共才是4,所以留下4个碗筷未洗,分析验证结果留下4个是没错!那修改下其变量值:

    public static int passCount = 4;//阿姨传送碗筷次数
public static int washCount = 4;//阿姨传送碗筷次数
public static int couter = 5; //盆做大容量5个

再看结果可能是:

    已经有碗筷:2 个
正在洗碗筷:第2个
还剩下碗筷:1个
已经有碗筷:1 个
正在洗碗筷:第1个
还剩下碗筷:0个

多次运行最后洗碗,说明正确。到这里可能大家都发现了这个运行受到passCount以及washCount的影响,可以去掉适合洗碗阿姨有就洗,传送的阿姨有万就传递这样的需求不,当然可以,看下面代码:

修改配置Conf类

class Conf{
public static int couter = 5; //盆做大容量5个
}

再修改Work类:

class Work{
//上面跟之前一样省略
/**
* 传碗筷
*/
public synchronized void Pass(){
while(count >= Conf.couter){
try { wait(); } catch (InterruptedException e) {e.printStackTrace();}
}
System.out.println("\t已经有碗筷:" + count + " 个");
System.out.println("\t\t再传一个碗筷");
count++;
System.out.println("\t\t传送完毕");
notify();
}
}

再修改PassAunt类:

class PassAunt extends Thread{
//上面跟之前一样省略
@Override
public void run() {
while(true){
this.work.Pass();
try {Thread.sleep(50); } catch (InterruptedException e) {e.printStackTrace();}
}
}
}

再修改WashAunt类:

class WashAunt extends Thread{
//上面跟之前一样省略
@Override
public void run() {
while (true) {
this.work.Wash();
try {Thread.sleep(50); } catch (InterruptedException e) {e.printStackTrace();}
}
}
}

再修改下mian函数:

    public static void main(String[] args) {
Work work = new Work();
PassAunt passThread = new PassAunt(work);
PassAunt passThread01 = new PassAunt(work);
PassAunt passThread02 = new PassAunt(work);
PassAunt passThread03 = new PassAunt(work);
WashAunt washThread = new WashAunt(work);
WashAunt washThread01 = new WashAunt(work);
passThread.start();
washThread01.start();
passThread01.start();
passThread02.start();
passThread03.start();
washThread.start();
}

其实就是把if修改成while,把for循环修改成while,多次运行看结果,显示中不会出现负数以及超过5个碗筷的数量,完全正确。再次测试下,new多几个传送碗筷的阿姨或者洗碗的阿姨,查看结果也正常。

这次先到这里。坚持记录点点滴滴!

上一篇:Scala学习(四)---映射和元组


下一篇:hiboCoder 1041 国庆出游 dfs+思维