zoukankan      html  css  js  c++  java
  • 线程同步(JAVA笔记-线程基础篇)

    在多线程应用程序中经常会遇到线程同步的问题。比如:两个线程A线程B可能会 “同时” 执行同一段代码,或修改同一个变量。而很多时候我们是不希望这样的。
    这时候,就需要用到线程同步。

    多线程引发的问题

    为了演示多线程引发问题,我们模仿买票,写一个简单的小程序。

    • 实现Runnable模拟买票
      public class SellTicket implements Runnable {
            //有30张票
            private int tickets=30;
            public void run() {
                //写一个死循环,模拟在不断的卖票。
                while (true){
                    //票数大于零,代表还有票,继续卖。
                    //如果票数小于等于零,也就是没票了。跳出循环,停止卖票
                    if(tickets > 0){
                        sell();
                    }else{
                        break;
                    }
                }
            }
            //买票方法,模拟买票的动作
            public void sell(){
                //记录下现在的票数
                int oldNumber = tickets;
                //卖掉一张后的票数,--tickets代表票数减一,模拟卖掉了一张票
                int nowNumber = --tickets;
                System.out.println(Thread.currentThread().getName()+",卖出了第("+ oldNumber +")张票,还剩("+ nowNumber +")张票。");
            }
        }
      
    • 是线程模拟窗口买票
      public class MyTest {
            public static void main(String[] args) {
                //使用同一个免票实例对象
                //所以,由于下面所有的窗口都用的是这一个对象。所以他们的票也都是sellTicket的tickets属性。
                SellTicket sellTicket=new SellTicket();
      
                //模拟多个窗口同时买票,每个线程代表一个买票窗口
                //Tread()的第二个参数,代表线程名。用线程名,模拟窗口名。
                Thread window1=new Thread(sellTicket,"窗口-1");
                Thread window2=new Thread(sellTicket,"窗口-2");
                Thread window3=new Thread(sellTicket,"窗口-3");
                Thread window4=new Thread(sellTicket,"窗口-4");
                Thread window5=new Thread(sellTicket,"窗口-5");
                
                //各个窗口开始工作
                window1.start();
                window2.start();
                window3.start();
                window4.start();
                window5.start();
            }
        }
      
      > 输出:
        窗口-3,卖出了第(28)张票,还剩(27)张票。
        窗口-5,卖出了第(30)张票,还剩(29)张票。
        窗口-1,卖出了第(26)张票,还剩(25)张票。
        窗口-2,卖出了第(27)张票,还剩(26)张票。
        ...
        ...
        窗口-2,卖出了第(6)张票,还剩(5)张票。
        窗口-5,卖出了第(4)张票,还剩(3)张票。
        窗口-3,卖出了第(2)张票,还剩(1)张票。
        窗口-4,卖出了第(0)张票,还剩(-1)张票。
        窗口-2,卖出了第(1)张票,还剩(0)张票。
      

    上面的例子模拟了多窗口买票,但是看,输出结果是不是又问题?怎么还有第(-1)张票。难道还有站票不成?当然不存在的,这是我们程序出现了问题。
    这就是多线程同时操作统一参数的问题。也就是上面例子中SellTicket对象的private int tickets=30;属性。

    p.s.我运行了好多遍都没出现错误的情况,后面给每个方法休眠了0.5秒才出现上面的错误结果。说明多线程不同步代码发生的错误不是百分之百的,只是有一定的概率。

    为什么会出现上面这种情况?

    众所周知,多线程的同步不是真的同步执行的。只是CPU切换运行线程所以看上去是几个线程同步执行。理解了这个概念往下看。(实在不懂的可以百度一下其他大佬的文章,我之后可能还会写一个笔记来记录-如果有必要的话)

    • 我们一下最后三个输出结果,我们发现出错的是窗口4,也就是第四个线程。
        ...以上的可以省略,当然上面的也可能出现问题。比如两个不同的窗口卖出了同一张票。但是我没运行出来这个结果。。。。
        窗口-3,卖出了第(2)张票,还剩(1)张票。
        窗口-4,卖出了第(0)张票,还剩(-1)张票。
        窗口-2,卖出了第(1)张票,还剩(0)张票。
      
    • 出错的代码在下面。
        //在这里检测当前票数
        if(tickets > 0){
            sell();
        }else{
            break;
        }
      
      代码运行步骤如下:
      1. 到最后还剩一张票的时候。这时候 “窗口-4的线程” 运行了。它查看了一下当前票数,发现还有一张。然后进入到if()方法准备运行下面的代码。

        sell();
        
      2. 但是,“窗口-4的线程” 还没来得及运行sell()方法把票减一,或者运行到sell()里面,还没来得及减去仅剩的一张票,这时候CPU把运行的权力送给了 “窗口-2的线程” 。此时票数还是1。

      3. “窗口-2的线程” 动作比较快,它很快的检测当前票数是“1”,并进入if()方法,然后运行sell();将票数减一。

      4. 这时候再到 “窗口-4的线程” 此时票数就只剩下0了,但是他还是得运行sell()。也就只能卖出了第(0)张票,还剩(-1)张票了。

    Java线程同步

    为了避免上面的情况,可以使用以下三个方法来解决。

    1. 同步代码块
    2. 同步方法
    3. 同步锁

    同步代码块

    直接上代码

    synchronized (this){
        //这里的代码,只允许一个线程运行。
        //等一个线程运行结束,把锁交给下一个等待的线程运行。
    }
    

    说明
    1.this: 这里的this是同步锁也叫同步监听对象。
    2.this可以是任何对象。
    3.但是一般把当前多线程,并发访问的共同资源当作同步锁。在例子中也就是sellTicket对象。所以在对象里面可以写程this

    修改上面的例子

    public void run() {
        //写一个死循环,模拟在不断的卖票。
        while (true){
            //票数大于零,代表还有票,继续卖。
            //如果票数小于等于零,也就是没票了。跳出循环,停止卖票
            synchronized (this.getClass()){
                if(tickets > 0){
                    sell();
                }else{
                    break;
                }
            }
        }
    }
    

    同步方法

    同步方法,其实跟同步代码块差不多,不过这个是在方法上添加synchronized关键字。

    public synchronized void sell(){
        //这里的代码,只允许一个线程运行。
        //等一个线程运行结束,把锁交给下一个等待的线程运行。
    }
    

    要在上面的例子中使用同步方法,需要改一下。把if()判断放到sell()方法里面。

    public synchronized void sell(){
        //在sell()方法在判断下当前票数
        if(tickets > 0){
            //记录下现在的票数
            int oldNumber = tickets;
            //卖掉一张后的票数,--tickets代表票数减一,模拟卖掉了一张票
            int nowNumber = --tickets;
            System.out.println(Thread.currentThread().getName()+",卖出了第("+ oldNumber +")张票,还剩("+ nowNumber +")张票。");
        }
    }
    

    同步锁

    以上两种方法,都需要一个关键字synchronized。同步锁需要用到一个接口。

    public interface Lock {
        //生源n多代码,详细的去看源码。
    }
    

    接口里面有两个比较重要的方法。

    /**
    * 请求一个锁
    * Acquires the lock. 
    */
    void lock();
    
    /**
    * 释放锁
    * Releases the lock.
    */
    void unlock();
    

    在这两个方法之间的代码都是同步的。
    但是Lock只是个接口,没法使用。
    JUC包给我们一个常用的实现类ReentrantLock。部分源码如下:

    public class ReentrantLock implements Lock, java.io.Serializable {
        //感兴趣的可以去看看源码
    }
    

    修改我们的案例代码

    public class SellTicket implements Runnable {
        //有30张票
        private int tickets=30;
    
        Lock lock=new ReentrantLock();
    
        public void run() {
            //写一个死循环,模拟在不断的卖票。
            while (true){
                //票数大于零,代表还有票,继续卖。
                //如果票数小于等于零,也就是没票了。跳出循环,停止卖票
                lock.lock();
                //synchronized (this.getClass()){
                if(tickets > 0){
                    sell();
                }else{
                    break;
                }
                //}
                lock.unlock();
    
            }
        }
        //买票方法,模拟买票的动作
        public synchronized void sell(){
                //记录下现在的票数
                int oldNumber = tickets;
                //卖掉一张后的票数,--tickets代表票数减一,模拟卖掉了一张票
                int nowNumber = --tickets;
                System.out.println(Thread.currentThread().getName()+",卖出了第("+ oldNumber +")张票,还剩("+ nowNumber +")张票。");
        }
    }
    

    瞎总结

    • 需要同步就要加上锁。代码锁了,其他线程就只能在门外等着。
    • 无论是同步块、同步方法、锁机制。都有一个范围。前两个是在大括号中间{},后者在两个方法中间lock();unlock();
    • 但是加上同步的时候,里面的代码就只能由一个线程一次执行完。这样就违反我们使用多线程的目的。(俗称效率低下)
    • 所以为了提高效率,我们要尽量想办法,把加锁的代码范围缩小,缩小,再缩小。(前提是程序不会因为多线程出问题,毕竟安全比效率重要)

    作者:BobC

    文章原创。如你发现错误,欢迎指正,在这里先谢过了。博主的所有的文章、笔记都会在优化并整理后发布在个人公众号上,如果我的笔记对你有一定的用处的话,欢迎关注一下,我会提供更多优质的笔记的。
  • 相关阅读:
    Hibernate 配置双向多对多关联
    转 方法区(method) )、栈区(stack)和堆区(heap)之JVM 内存初学
    java web 实战经典(二)
    flex 生成多边形时内、外环计算
    java web 开发实战经典(一)
    sql语句联表更新(从一个数据库中的一张表更新到另一个数据库的另一张表)
    javascript DOM编程艺术(检测与性能优化)
    java数据库基本操作(sqlserver 2000为例)
    同一台电脑上配置多个解压版tomcat方法(本例安装两个)
    css那些事(一)
  • 原文地址:https://www.cnblogs.com/Eastry/p/13081157.html
Copyright © 2011-2022 走看看