多线程的同步-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的基础上,封装了一层,每个方法重写并且加了一把锁在上面。 

 

posted @ 2021-02-15 19:08  上天安排的最大嘛!  阅读(56)  评论(0编辑  收藏  举报