多线程、线程锁的一些我们会忽略的特性(一)

多线程是我们学习java的一个重点,但总是看着别人的案例就会做,一离开案例敲多线程就各有各的奇葩结果。解决问题的关键,还是要更深入理解多线程和线程锁的特性。

什么时候应该用多线程呢?

以下两种情况下建议使用多线程处理:

  • 当需要同时做多件事情:一边播放背景音乐,一边显示画面,或者一边上传文件,一边响应用户的操作,这种需要多线程。
  • 当某个操作耗时很久:比如与服务器通信,从数据库获取数据,即使很快,我们都默认他是耗时很久的任务,需要用到多线程的。

因为线程池的引入,现在已经很少有人用继承Thread的方式来开启多线程,我也推荐使用实现Runnable的方式来开启多线程,这有3个好处:

  • 可以让线程多一个继承的机会:如果通过继承thread来继承任务,就无法再继承其他类了。
  • 可以让线程间多了一种共享数据的方式:一个Runnable对象可以生成多个线程,这多个线程之间可以共享Runnable对象的实例成员。如果用继承thread方式实现,则只能把共享资源变成静态,才能实现共享。
  • 可以使用线程池:线程池只接受实现了RunnableCallable的对象,不接受thread对象作为参数。

使用多线程时应该思考些什么

定义Runnable对象的时候,我们应该把Runnable当成一份任务清单,一个线程是一个任务的执行人。

  • new了不同的Runnable对象,就是生成了不同的任务,不同的任务之间基本没什么联系了,最多就共用同一个静态成员变量,因为实例成员变量已经不在共享了,各有各的任务信息。
  • 而用同一个Runnable对象生成不同的线程时,就说明是要执行同一个任务,会共享Runnable对象的成员变量的。

举一个售票员卖票的例子,两位售票员要卖出一共5张电影票:

public class SellingTicket implements Runnable {
    private int count = 5;

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        while (true) {
            //synchronized (this) {
            if (count > 0) {
                try {
                    Thread.sleep(((int) (Math.random() * 10 + 10)));  //睡10~20毫秒模拟售票耗时
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count--;
                System.out.println(name + "卖出了一张票," +
                        "目前还有票" + count + "张");

            } else {
                System.out.println(name + "发现票卖完了!下班!");
                break;
            }
            //}
        }
    }
}

定义好Runnable以后,就在main中调用,我分两种调用的方式:

//方式一:
public class TestDemo {
    public static void main(String[] args) {
        SellingTicket sellingTicket = new SellingTicket();
        new Thread(sellingTicket, "售票员B").start();
        new Thread(sellingTicket, "售票员A").start();
    }
}

运行结果:
售票员A卖出了一张票,目前还有票3张
售票员B卖出了一张票,目前还有票4张
售票员A卖出了一张票,目前还有票2张
售票员B卖出了一张票,目前还有票1张
售票员A卖出了一张票,目前还有票0张
售票员B卖出了一张票,目前还有票-1张
售票员B发现票卖完了!下班!
售票员A发现票卖完了!下班!
//方式二
public class TestDemo {
    public static void main(String[] args) {
        new Thread(new SellingTicket(), "售票员B").start();
        new Thread(new SellingTicket(), "售票员A").start();
    }
}

运行结果:
售票员A卖出了一张票,目前还有票4张
售票员B卖出了一张票,目前还有票4张
售票员B卖出了一张票,目前还有票3张
售票员A卖出了一张票,目前还有票3张
售票员B卖出了一张票,目前还有票2张
售票员A卖出了一张票,目前还有票2张
售票员B卖出了一张票,目前还有票1张
售票员A卖出了一张票,目前还有票1张
售票员B卖出了一张票,目前还有票0张
售票员B发现票卖完了!下班!
售票员A卖出了一张票,目前还有票0张
售票员A发现票卖完了!下班!

出现了两个问题:

  • 第二个调用方法中,一共卖出了10张票的问题。
  • 第一种调用方法出现了还剩下-1张票

我们一个一个来处理:

问题一:第二个调用方法中,一共卖出了10张票。
第一种调用方式,用了一个Runnable对象,生成两个线程,第二种调用方式,new了两个Runnable对象,生成两个线程,从他们的运行结果我们可以看出,第二种调用方式一共卖出了10张票,每个线程(售票员)各自卖出了5张票
这是因为new了两个Runnable对象,等于是告诉线程(售票员B)进行一共全新的任务:卖出5张票。
而两个线程共用同一个Runnable对象,则是同时告诉两个线程(售票员A和售票员B)要卖出5张票

从内存图上看,我们也能看出区别来:

栈2
栈1
使用
使用
new Runnable对象
int count=5
线程2-售票员B
run(){
...
}
线程1-售票员A
run(){
...
}

调用方式1:两个线程共用一个Runnable对象

栈2
栈1
使用
使用
new Runnable对象
int count=5
new Runnable对象
int count=5
线程2-售票员B
run(){
...
}
线程1-售票员A
run(){
...
}

另外,还有因为一个线程一个栈,所以被放在run方法中的变量都是不能共享的,是线程自己私有的变量。如果Runnable对象的成员变量:count 被定义在run方法中,同样也会造成共卖出10张票的问题。
这是很多新接触多线程的朋友可能出错的地方,一定要注意的。

多线程会遇到的问题

那么接下来解决第二个问题,为什么会出现剩下-1张票的情况呢?这就是所谓的线程同步问题。让我们想象一下下面的场景:、

  • 售票员A售票员B共同接收到一个任务:卖出5张票。
  • 这5张票被放在一个箱子里,两个售票员在卖票之前先要来看看箱子还有没有票,确认过有票才卖(票数-1),没有就停止售票了
  • 卖啊卖啊卖剩一张了,这时候售票员A的窗口来客人了,他先到箱子里看有没有票:还剩一张。卖!
  • 就在售票员A在给客人做卖票手续的时候,售票员B的窗口也来了客人,售票员B先到箱子里看还有没有票:还剩一张!
  • 售票员A做完卖票手续,把票数-1,票数变成0。这时售票员B也做完售票手续,把票数-1。于是票数变成-1了

就是因为在售票员A做售票手续的时候,售票员B来看有没有票剩余,才出现这个问题,那解决的思路就很简单了:在售票员A从看有没有票剩余开始,一直到售票员A把票数-1结束,都禁止售票员B来碰这个箱子。问题就解决了。
这也是同步锁的思想,我将再开一篇文章来讲这个问题。

本章所有源码已经上传github:https://github.com/huheman/Blog,
本人水平有限,如您发下有错漏请在评论不吝指教。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值