黑马程序员_日记13_Java多线程(三)

本文深入探讨了多线程同步问题及其解决方案,包括使用同步代码块和同步函数来确保多线程环境下数据的一致性和安全性。通过具体代码示例展示了如何在Java中实现同步,以避免数据竞争和冲突。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 ——- android培训java培训、期待与您交流! ———-

一、多线程的安全问题

1 概述

在前面的例子中我把ticket–和打印语句分开的时候,
出现了一张票被卖4词的情况。
当我把这两条语句合并的时候,再把票额调高到30000时候,
多次测试,也会偶尔出现一张票被卖2次以上。
这就说明多线程共享数据存在安全问题。

……票额到30000,CPU利用率都飙升到97了!(⊙﹏⊙)b

下面我们通过查阅API,分析原因,解决问题。

通过查阅API,我们在Thread类中找到了sleep(long millis)方法:
这个方法让线程睡眠一定的时间,参数为long类型,时间为毫米级.
因为这个方法是静态方法,所以可以直接用类名调用。
即:Thread.sleep(10);//让线程睡眠10毫米。

编译后,得到了如下提示:
ThreadDemo4.java:50: 错误: 未报告的异常错误InterruptedException; 必须对其进行捕
获或声明以便抛出
Thread.sleep(10);
^
1 个错误

再次查阅API,发现sleep方法是有这个异常InterruptedException。
这个异常我们不采取抛出的方式,而是采取处理掉。
即:try{}catch(){};

编译运行后的结果出现了票额为负数的。
Ticket 3 is saled by Thread-0
Ticket 2 is saled by Thread-0
Ticket 0 is saled by Thread-2
Ticket 1 is saled by Thread-3
Ticket -1 is saled by Thread-1

这直观的说明了多线程有安全问题!

2 出现安全问题的原因

出现安全问题的原因是什么呢???
因为run方法中有多条操作语句,特别是共享数据ticket被两条语句操作。
这就会产生严重的安全问题:当线程2执行到第一条判断ticket语句的时候,
线程1可能正在执行ticket–操作,这样就让共享数据ticket出现问题!

3 解决问题的方法

那么解决问题的办法是什么呢???
直观的想法是,让操作共享数据ticket语句的代码变成一条,
但是上一个例子中,已经证明在多次运行的时候,还是谁出现问题。
在Java,系统给出了专业的解决办法:同步代码块
synchronized(对象){共享数据被操作的那部分代码}。

在使用同步代码块的时候要注意:
一定是共享数据被操作的那部分代码块,而不是其他。
对象其实就可以类比为锁。

4 同步的前提:

1、一定是>=2个线程的情况下使用
2、一定是多个线程使用同一个锁

同步的时候一定是只有一个线程在运行同步代码块

5 同步的好处与局限

同步的好处是:保证了多线程的安全性!
同步的局限性是:每次线程都得先判断锁是开还是关,消耗掉了更多的资源。

System.exit(0);//注意在使用java ThreadDemo> .\1.txt命令的时候,
java就一直在调用CPU,会消耗非常多的CPU资源,CPU利用率到达97%,甚至让电脑死机。
但是因为是多线程,主线程执行到这一句的时候,程序就结束了。
这一句还是不能用。

6 代码1

这个是上面分析所用的代码


class Ticket implements Runnable
{
    //私有化票额,因为接口是共享的,所以不需要static修饰,提高了效率
    private  int ticket =100 ;
    //创建一个对象,供synchrnozied传递
    Object obj = new Object();

    //覆盖Runanble接口的run方法,把线程执行的代码存入
    public void run()
    {
            //建立一个循环售票,当票售完即停止
            while(true)
            {
                synchronized(obj)
                {
                        if(ticket > 0)
                        {
                            //因为sleep方法抛出了中断异常,所以必须处理掉,这里采取不声明的方式
                            try{Thread.sleep(10);}catch(Exception e){}
                            //打印当前线程对象售出的票号,并立即减少1张票额
                            System.out.println("Ticket "+(ticket--)+" is saled by "+Thread.currentThread().getName());
                        }
                }
            }
    }
}

class ThreadDemo4 
{
    public static void main(String[] args) 
    {
        //创建一个Ticket对象,注意这里和第一种创建线程方法的不同。
        //这里创建的是票的对象,第一种方法,创建的是售票线程的对象。
        //在这里就共享了票额。
        Ticket t =new Ticket();

        //利用构造函数Thread(Runnable target)创建4个售票线程
        //这里利用了多态的形式,建立了接口的引用
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        Thread t3 = new Thread(t);
        Thread t4 = new Thread(t);

        //调用start方法,启动线程并调用run方法,开始售票
        t1.start();
        t2.start();
        t3.start();
        t4.start();

    }
}

7 代码2

下面再用一个例子来说明多线程的安全问题

目的:该程序是否有安全问题??

需求:
银行有一个金库。
有两个储户分别存300元,每次存100,存3次。

分析:
名词提炼法
银行,金库,储户

类:银行,储户

银行的属性:金库
银行的方法:存钱

明确实现方法的结果,打印金库金额。返回值为void
明确未知变量:存入的金额。double

储户的属性:无
储户的方法:无。

问题概述:
两个储户分别调用银行的存钱方法往银行存钱,
这就要求用多线程解决。
每次存100元,存3词。
这就规定了存钱方法所传递的参数money是double,
用循环解决。

//建立一个银行类
class Banke
{
    //私有化金库
    private double moneySum = 0;

    //定义存钱方法
    public void add(double money)
    {
        //用户把钱存到银行,银行金库存款增加
        moneySum = moneySum + money;

        //打印金库存款
        System.out.println("金库有存款:"+moneySum+"元");
    }
}

//定义储户类,用Runnable实现
class Customer implements Runnable
{
    //建立银行对象
    Banke b = new Banke();

    //覆盖run方法,添加线程要执行的代码
    public void run()
    {
        //用户存入3词,每次存100元
        for(int i = 0;i < 3;i++)
        {
            //银行调用银行的存钱方法
            b.add(100);
        }
    }
}

//定义主类
class BankDemo
{
    public static void main(String[] args)
    {
        //建立储户的对象
        Customer c = new Customer();

        //创建储户线程1,
        Thread t1 = new Thread(c);//这里是利用了多态,Thread(Runnable target)

        //创建储户线程2,
        Thread t2 = new Thread(c);//这里是利用了多态,Thread(Runnable target)

        //启动储户线程1
        t1.start();

        //启动储户线程2
        t2.start();

    }
}

运行结果
金库有存款:200.0元
金库有存款:300.0元
金库有存款:400.0元
金库有存款:200.0元
金库有存款:500.0元
金库有存款:600.0元

出现问题的原因是:
线程执行完但是没有打印结果。

8 代码3

和上次一样,利用sleep()让线程睡眠

//建立一个银行类
class Banke
{
    //私有化金库
    private double moneySum = 0;

    //定义存钱方法
    public void add(double money)
    {
        //用户把钱存到银行,银行金库存款增加
        moneySum = moneySum + money;

        //利用sleep让线程在这里停一会儿,观察运行结果
        try{Thread.sleep(10);}catch(Exception e){}
        //打印金库存款
        System.out.println("金库有存款:"+moneySum+"元");
    }
}

//定义储户类,用Runnable实现
class Customer implements Runnable
{
    //建立银行对象
    Banke b = new Banke();

    //覆盖run方法,添加线程要执行的代码
    public void run()
    {
        //用户存入3词,每次存100元
        for(int i = 0;i < 3;i++)
        {
            //银行调用银行的存钱方法
            b.add(100);
        }
    }
}

//定义主类
class BankDemo
{
    public static void main(String[] args)
    {
        //建立储户的对象
        Customer c = new Customer();

        //创建储户线程1,
        Thread t1 = new Thread(c);//这里是利用了多态,Thread(Runnable target)

        //创建储户线程2,
        Thread t2 = new Thread(c);//这里是利用了多态,Thread(Runnable target)

        //启动储户线程1
        t1.start();

        //启动储户线程2
        t2.start();

    }
}

运行结果
金库有存款:200.0元
金库有存款:200.0元
金库有存款:400.0元
金库有存款:400.0元
金库有存款:600.0元
金库有存款:600.0元
结果显示确实存在安全问题

二、多线程的同步代码块

1 理论分析

那么这个安全问题如何解决呢??
复习上节课的内容:
利用同步代码块来解决

那么如何同步呢??
步骤:
1、找到多线程运行的那部分代码
2、找到多线程运行的共享数据
3、多线程运行的代码中哪些代码是操作共享数据的

对应本题:
步骤:
1、多线程运行的那部分代码

public void run()
    {
        //用户存入3词,每次存100元
        for(int i = 0;i < 3;i++)
        {
            //银行调用银行的存钱方法
            b.add(100);
        }
    }

以及add方法的那部分代码

public void add(double money)
    {
        //用户把钱存到银行,银行金库存款增加
        moneySum = moneySum + money;

        //利用sleep让线程在这里停一会儿,观察运行结果
        try{Thread.sleep(10);}catch(Exception e){}
        //打印金库存款
        System.out.println("金库有存款:"+moneySum+"元");
    }

2、多线程共享的数据很明显就是金库存款moneySum

3、多线程中哪些代码是操作moneySum的呢???

//用户把钱存到银行,银行金库存款增加
        moneySum = moneySum + money;

        //利用sleep让线程在这里停一会儿,观察运行结果
        try{Thread.sleep(10);}catch(Exception e){}
        //打印金库存款
        System.out.println("金库有存款:"+moneySum+"元");

所以,同步代码块要同步的就是这部分代码了,同步代码块为:
synchronized(对象)
{
        //用户把钱存到银行,银行金库存款增加
        moneySum = moneySum + money;

        //利用sleep让线程在这里停一会儿,观察运行结果
        try{Thread.sleep(10);}catch(Exception e){}
        //打印金库存款
        System.out.println("金库有存款:"+moneySum+"元");
}

2 代码

//同步代码块解决安全问题

//建立一个银行类
class Banke
{
    //私有化金库
    private double moneySum = 0;
    //创建一个对象,供synchronized使用
    Object obj = new Object();

    //定义存钱方法
    public void add(double money)
    {
            synchronized(obj)
            {
                    //用户把钱存到银行,银行金库存款增加
                    moneySum = moneySum + money;

                    //利用sleep让线程在这里停一会儿,观察运行结果
                    try{Thread.sleep(10);}catch(Exception e){}
                    //打印金库存款
                    System.out.println("金库有存款:"+moneySum+"元");
            }
    }
}

//定义储户类,用Runnable实现
class Customer implements Runnable
{
    //建立银行对象
    Banke b = new Banke();

    //覆盖run方法,添加线程要执行的代码
    public void run()
    {
        //用户存入3词,每次存100元
        for(int i = 0;i < 3;i++)
        {
            //银行调用银行的存钱方法
            b.add(100);
        }
    }
}

//定义主类
class BankDemo
{
    public static void main(String[] args)
    {
        //建立储户的对象
        Customer c = new Customer();

        //创建储户线程1,
        Thread t1 = new Thread(c);//这里是利用了多态,Thread(Runnable target)

        //创建储户线程2,
        Thread t2 = new Thread(c);//这里是利用了多态,Thread(Runnable target)

        //启动储户线程1
        t1.start();

        //启动储户线程2
        t2.start();

    }
}

运行结果为:
金库有存款:100.0元
金库有存款:200.0元
金库有存款:300.0元
金库有存款:400.0元
金库有存款:500.0元
金库有存款:600.0元

多次运行,结果依旧不变,说明多线程安全问题已经解决。

三、多线程的同步函数

1 分析

既然代码块可以有同步,那么函数能同步吗??
答案是肯定的。
被synchronized修饰的函数就称之为同步函数。
所以上例的另一种解决安全问题的方法是:

//定义存钱方法
    public stnchronized void add(double money)
    {

                    //用户把钱存到银行,银行金库存款增加
                    moneySum = moneySum + money;

                    //利用sleep让线程在这里停一会儿,观察运行结果
                    try{Thread.sleep(10);}catch(Exception e){}
                    //打印金库存款
                    System.out.println("金库有存款:"+moneySum+"元");

    }

2 代码

//同步函数解决安全问题
//建立一个银行类
class Banke
{
    //私有化金库
    private double moneySum = 0;

    //定义存钱方法
    public synchronized void add(double money)
    {
        //用户把钱存到银行,银行金库存款增加
        moneySum = moneySum + money;

        //利用sleep让线程在这里停一会儿,观察运行结果
        try{Thread.sleep(10);}catch(Exception e){}

        //打印金库存款
        System.out.println("金库有存款:"+moneySum+"元");
    }
}

//定义储户类,用Runnable实现
class Customer implements Runnable
{
    //建立银行对象
    Banke b = new Banke();

    //覆盖run方法,添加线程要执行的代码
    public void run()
    {
        //用户存入3词,每次存100元
        for(int i = 0;i < 3;i++)
        {
            //银行调用银行的存钱方法
            b.add(100);
        }
    }
}

//定义主类
class BankDemo
{
    public static void main(String[] args)
    {
        //建立储户的对象
        Customer c = new Customer();

        //创建储户线程1,
        Thread t1 = new Thread(c);//这里是利用了多态,Thread(Runnable target)

        //创建储户线程2,
        Thread t2 = new Thread(c);//这里是利用了多态,Thread(Runnable target)

        //启动储户线程1
        t1.start();

        //启动储户线程2
        t2.start();

    }
}

运行结果
金库有存款:100.0元
金库有存款:200.0元
金库有存款:300.0元
金库有存款:400.0元
金库有存款:500.0元
金库有存款:600.0元

结果显示,安全问题已经被解决

四、小结

1 同步代码块的写法

synchronized(对象)
{
    共享数据被操作的代码
}

2 同步的前提

1、大于等于2个线程在运行
2、多个线程使用的是同一个锁

3 同步利弊

同步:只有一个线程运行
好处:多线程–安全
局限:判断锁—–消耗了更多的资源

4 如何同步,如何找问题

1、找多线程运行的代码
2、找多线程共享的数据
3、找1中的哪些代码操作了2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值