zoukankan      html  css  js  c++  java
  • 多线程的同步-sychronized

    线程同步场景


       假设盖伦有10000基础血量,这个时候他在基地被别人虐泉水。这时候就会出现这种场景,有多个线程在打击盖伦,减少他的血量。于此同时,基地又有多个线程在给盖伦恢复血量。假设增加血量的线程数和攻击减少血量的线程数是一样的,并且每次改变的值都是1,那么最终盖伦血量应该为基数10000才对。但是,结果不是这个样子的!

      示例代码 :Hero

    package com.thread;
    
    public class Hero {
        public String name;
        public float hp;
        public int damage;
        //回血
        public void revoer() {
            hp = hp +1;
        }
        //掉血
        public void hurt() {
            hp = hp -1;
        }
    
        public void attackHero(Hero h) {
            try {
                //为了表示攻击需要时间   每次攻击暂停1秒
                Thread.sleep(1000);
            }catch(InterruptedException e) {
                e.printStackTrace();
            }
            h.hp -= damage;
            System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n", name, h.name, h.name, h.hp);
            if(h.isDead()) {
                System.out.println(h.name + "死了!");
            }
        }
        //判断英雄死了没
        public boolean isDead() {
            return 0 >= hp?true:false;   //血量大于0  没死   isDead=false
        }
    }

      线程同步代码

    package com.thread.thread9;
    
    import com.thread.Hero;
    
    public class TestThread {
        public static void main(String[] args) {
            final Hero gareen = new Hero();
            gareen.name = "盖伦";
            gareen.hp = 10000;
    
            System.out.printf("盖伦初始血量是 %.0f%n", gareen.hp);
    
            //多线成同步问题指的是多个线程同时修改一个数据的时候 导致的问题
            //假设盖伦有10000滴血   并且在基地里  同时又被多个英雄攻击
            //用java代码来表示  就是多个线程在减少盖伦的hp
            //n个线程增加盖伦的hp
            int n = 10000;
    
            Thread[] addThreads = new Thread[n];   //增加血量
            Thread[] reduceThreads = new Thread[n];   //减少血量
    
            for(int i=0; i<n; i++) {
                Thread t = new Thread() {
                    public void run() {
                        gareen.revoer();  //加血
                        try {
                            Thread.sleep(100);
                        }catch(InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                };
                t.start();  //启动线程
                addThreads[i] = t;  //将单个线程放入线程数组中
            }
    
            //n个线程减少盖伦的hp    for循环内部都是用的局部变量
            for(int i=0; i<n; i++) {
                Thread t = new Thread() {
                    public void run() {
                        gareen.hurt();
                        try {
                            Thread.sleep(100);
                        }catch(InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                };
                t.start();   //启动每一个线程
                reduceThreads[i] = t; //将当个线程放入减少的线程组中
            }
    
            //等待所有增加线程结束
            for (Thread t: addThreads) {
                try{
                    t.join();
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    
            //等待所有减少线程结束
            for(Thread t: reduceThreads) {
                try{
                    t.join();
                }catch(InterruptedException e) {
                    e.printStackTrace();
                }
            }
    
            //代码执行到这里  所有增加减少线程都结束了
            //增加和减少线程的数量是一样的  每次都是增加 减少1
            //那么所有线程都结束后  盖伦的hp应该还是初始值
            //但是事实观察到的是
    
            //%d 代表整数参数
            //%n 换行
            //%.0f  float精度
            System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量变成了%.0f%n", n, n, gareen.hp);
        }
    }

    为什么?


      理论上我们想要:数据10000+1=10001,然后10001-1=10000

      增加线程的操作步骤有三步,取数据,加法,放回数据。减少线程的操作步骤也有三步,取数据,减法,放回数据。

      由于线程的启动是很迅速的,而取数据,加法,放回数据这三步需要很长的时间,所以,可能会出现这种情况。

      当增加线程刚刚在修改数据的时候,还没有提交数据。减少线程就启动线程了,并且拿到还没增加的10000,进行减一操作,然后再把改好的9999放回去,这样我们最终读到的数据就是9999。

      最终读到的9999和理论10000数据不一致,根本原因是两个线程的争抢,一个线程读到了另一个线程还未提交的数据-脏数据。

     

    解决方案-上锁


       给每个线程sychronized上锁,当t1线程执行的过程中就把t2线程挡在外面。在每次一个线程执行完成后,重新抢夺cpu资源进行单个线程执行。

    线程内部上锁


      记录下每行代码执行的时间。给每个线程上锁,看线程占用对象资源后的执行情况。

    package com.thread.thread10;
    
    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    public class TestThread {
        public static String now() {   //显示当前时间
            return new SimpleDateFormat("HH:mm:ss").format(new Date());  //格式化时间
        }
    
        public static void main(String[] args) {
            final Object someObject = new Object();   //创建一个所有对象的老祖宗
            Thread t1 = new Thread() {   //实例一个对象
                public void run() {  //重写run方法
                    try {
                        //执行一行代码 都打印时间
                        System.out.println(now() + "t1线程已经运行");
                        System.out.println(now() + this.getName() + "试图占有对象: someobject");
                        synchronized (someObject) {   //当前这个线程  占领这个对象  然后给这个对象上锁
                            System.out.println(now() + this.getName() + "占有对象:someObject");
                            Thread.sleep(5000);  //模拟对象在干事情
                            System.out.println(now() + this.getName() + "释放对象:someObject");
                        }
                        System.out.println(now() + "t1 线程结束");
                    }catch(InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            t1.setName("t1");
            t1.start();
            Thread t2 = new Thread() {
                public void run() {
                    try{
                        System.out.println(now() + "t2线程已经运行");
                        System.out.println(now() + this.getName() + "试图占有对象:someObject");
                        synchronized (someObject) {   //线程t2占领了someobject对象   上了锁  然后其他对象都访问不到了
                            System.out.println(now() + this.getName() + "占有对象:someobject");
                            Thread.sleep(5000);
                            System.out.println(now() + this.getName() + "释放对象:someObject");
                        }
                        System.out.println(now() + "t2线程结束");
                    }catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            t2.setName(" t2");
            t2.start();
        }
    }

      t1和t2,由于线程启动的时间很短,如果运行程序多次,你会看到t1和t2是没有顺序的。最开始的时候,实际上是他们在抢占资源,谁抢到谁就去占坑位。由于加上了sychronized关键字,所以必须等锁内的最后一行代码,释放了对象,t2才能继续抢占。

     

    synchronized使用方式


       分为三种。一是,在线程内部,run方法内的业务逻辑加上锁。二是,在业务方法内部进行加锁,如果有线程使用该方法,就会触发上锁机制。三是,在业务方法上增加锁,如果有线程使用该方法,就会触发上锁机制。

      注意: 如果要保证当前线程不会被抢占资源,那么当前线程最好都加上锁,这样才能保证每个线程执行了自己内部的所有任务。

      一是,在线程内部上锁,在线程同步代码基础上进行更改。实例线程的时候,内部重写run方法,方法内部业务逻辑加锁。

    package com.thread.thread11;
    
    import com.thread.Hero;
    
    /**
     * 线程内部每个线程需要执行的方法 给它加上synchronized关键字  使用全局
     * object对象   保持全局唯一
     */
    
    public class TestThread {
        public static void main(String[] args) {
            final Object someObject = new Object();   //声明一个公共的object对象
            final Hero gareen = new Hero();
            gareen.name = "盖伦";
            gareen.hp = 10000;
    
            System.out.printf("盖伦初始血量是 %.0f%n", gareen.hp);
    
            //多线成同步问题指的是多个线程同时修改一个数据的时候 导致的问题
            //假设盖伦有10000滴血   并且在基地里  同时又被多个英雄攻击
            //用java代码来表示  就是多个线程在减少盖伦的hp
            //n个线程增加盖伦的hp
            int n = 10000;
    
            Thread[] addThreads = new Thread[n];   //增加血量
            Thread[] reduceThreads = new Thread[n];   //减少血量
    
            for(int i=0; i<n; i++) {
                Thread t = new Thread() {
                    public void run() {
                        //在线程内部加血方法加锁
                        synchronized (someObject) {
                            gareen.revoer();  //加血
                        }
                        try {
                            Thread.sleep(100);
                        }catch(InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                };
                t.start();  //启动线程
                addThreads[i] = t;  //将单个线程放入线程数组中
            }
    
            //n个线程减少盖伦的hp    for循环内部都是用的局部变量
            for(int i=0; i<n; i++) {
                Thread t = new Thread() {
                    public void run() {
                        synchronized (someObject) {
                            gareen.hurt();
                        }
    //                    gareen.hurt();
                        try {
                            Thread.sleep(100);
                        }catch(InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                };
                t.start();   //启动每一个线程
                reduceThreads[i] = t; //将当个线程放入减少的线程组中
            }
    
            //等待所有增加线程结束
            for (Thread t: addThreads) {
                try{
                    t.join();
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    
            //等待所有减少线程结束
            for(Thread t: reduceThreads) {
                try{
                    t.join();
                }catch(InterruptedException e) {
                    e.printStackTrace();
                }
            }
    
            //代码执行到这里  所有增加减少线程都结束了
            //增加和减少线程的数量是一样的  每次都是增加 减少1
            //那么所有线程都结束后  盖伦的hp应该还是初始值
            //但是事实观察到的是
    
            //%d 代表整数参数
            //%n 换行
            //%.0f  float精度
            System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量变成了%.0f%n", n, n, gareen.hp);
    
        }
    }

      二是,在业务方法内部添加锁。

    package com.thread.thread12;
    
    public class Hero {
        public String name;
        public float hp;
    
        public int damage;
    
        //回血
        public void revoer() {
            hp = hp +1;
        }
        //掉血
        public void hurt() {
            //使用this作为同步对象
            //哪一个调用这个方法 this就是指代的那一个对象
            synchronized (this) {
                hp = hp -1;
            }
        }
    
        public void attackHero(Hero h) {
            try {
                //为了表示攻击需要时间   每次攻击暂停1秒
                Thread.sleep(1000);
            }catch(InterruptedException e) {
                e.printStackTrace();
            }
            h.hp -= damage; //血量在被攻击力一点点的减少
            //%s 字符串 %.0f 双精度数 可以带小数
            System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n", name, h.name, h.name, h.hp);
            if(h.isDead()) {
                System.out.println(h.name + "死了!");
            }
        }
    
        //判断英雄死了没
        public boolean isDead() {
            return 0 >= hp?true:false;   //血量大于0  没死   isDead=false
        }
    }

      三是业务方法上添加锁。

    package com.thread.thread13;
    
    public class Hero {
        public String name;
        public float hp;
    
        public int damage;
    
        //回血
        public synchronized void revoer() {
            hp = hp +1;
        }
        //掉血
        public synchronized void hurt() {
            //使用this作为同步对象
            //哪一个调用这个方法 this就是指代的那一个对象
            hp = hp -1;
        }
    
        public void attackHero(Hero h) {
            try {
                //为了表示攻击需要时间   每次攻击暂停1秒
                Thread.sleep(1000);
            }catch(InterruptedException e) {
                e.printStackTrace();
            }
            h.hp -= damage; //血量在被攻击力一点点的减少
            //%s 字符串 %.0f 双精度数 可以带小数
            System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n", name, h.name, h.name, h.hp);
            if(h.isDead()) {
                System.out.println(h.name + "死了!");
            }
        }
    
        //判断英雄死了没
        public boolean isDead() {
            return 0 >= hp?true:false;   //血量大于0  没死   isDead=false
        }
    }

    线程安全类


      如果一个类,其方法都是有synchronized修饰的,那么该类就叫做线程安全的类。

      synchronized意义:同一时间,只有一个线程能够进入这种类的一个实例去修改数据,进而保证这个实例中的数据的安全(不会同时被多线程抢占修改而变成脏数据)

    StringBuffer和StringBuilder对比


       StringBuffer源码,这个类在多个线程同时修改一个字符串的时候,不会发生字符串拼接异常,它会等待一个拼接成功后,基于上一个线程执行下一段拼接。线程安全

      正因为线程安全需要同步,所以效率上比线程不安全的StringBuilder耗时要久,效率要慢。

     HashMap和Hashtable对比


       Hashtable方法具有synchronized修饰,线程安全。但是不可以放null值。

      HashMap线程不安全,可以放null值

    ArrayList和Vector对比 


       vector是线程安全的类,方法上加了synchronized关键字。

     

     线程不安全转成线程安全


       这里ArrayList集合创建的list1线程不安全,使用synchronizedList转换成了list2线程安全的对象

    List<String> list1 = new ArrayList<>();
    List<String> list2 = Collections.synchronizedList(list1);

      synchronizedList在Collections中声明了方法,从图中我们可以看到方法内new了两个类SynchronizedRandomAccessList和SynchronizedList

      根据源码可以看到,两个类最终都会到一个方法

      下图是SynchronizedCollection的声明方法。所以说,list经过synchronizedList方法的转化,相当于在原来list的基础上,封装了一层,每个方法重写并且加了一把锁在上面。 

  • 相关阅读:
    Java 学习使用常见的开源连接池
    Java 数据库操作
    Java 集合的简单理解
    windows中在vs code终端使用bash
    敏捷开发、DevOps相关书籍——书单
    使用Dockerfile来构建镜像
    Redis集群搭建
    使用redis限制ip访问次数
    NFS服务器搭建
    ssh 中 远程文件传输
  • 原文地址:https://www.cnblogs.com/HelloM/p/14404257.html
Copyright © 2011-2022 走看看