zoukankan      html  css  js  c++  java
  • 线程的同步之Synchronized的使用

       一、介绍       

     线程的同步:一般的并发指的就是多个线程访问同一份资源。多个线程同时访问(修改)同一份资源的话,就会有可能造成资源数据有误。

     如果多个线程访问多个不同资源,就不会造成线程同步。

     如果要解决这个问题,就需要对线程使用同步存取。java中提供了一个synchronized关键字来对方法或者某个块加锁。从而达到锁定某个区域,不可

     同时修改以免数据有误的情况。

      synchronized关键字可以锁定的部分:

      1、锁定方法:在方法上加入synchronized关键字就表明在使用该方法的时候需要获取相应的锁。

      2、锁定块:锁定块的参数需要是对象,不可是基本类型数据

            synchronized(引用类型变量 | this | 对象.class){

              //逻辑代码

            }

      

       上图表示非同步线程和同步线程的比较,可以看出非同步的时候,线程1和线程2都是在同一个时间段访问同一个transter方法,而使用了同步之后,线程2如果想调用transter方法就必须等待线程1调用完成后才可执行。


     二、实例       

    这里以12306抢票代码为例来说明线程同步的synchronized关键字的使用。

    1、未使用synchronized锁的情况

    首先来看未使用synchronized的情况会是什么样?

    抢票的线程代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    class Web12306 implements Runnable{
        private int num=10;//总共10张票
        private boolean flag = true;
        @Override
        public void run() {
            while(flag){
                //黄牛抢到了3    农民工抢到了1 黄牛抢到了0  程序员抢到了-1
                test1();//线程不安全,数据不准确:结果有-1值
            }
        }
        //1、线程不安全
        public void test1(){
            if (num<=0) {
                flag = false;
                return;//跳出循环,结束
            }
            try {
                Thread.sleep(500);//模拟延时
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"抢到了"+num--);
        }
    }

    测试线程代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class SynDemo1 {
        public static void main(String[] args) {
            //真实角色
            Web12306 web = new Web12306();
            //代理角色
            Thread proxy1 = new Thread(web,"黄牛");
            Thread proxy2 = new Thread(web,"程序员");
            Thread proxy3 = new Thread(web,"农民工");
            proxy1.start();
            proxy2.start();
            proxy3.start();
        }
    }

    测试结果如下:可以看出最后的结果会出现0和-1这样错误的数据。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    黄牛抢到了10
    农民工抢到了8
    程序员抢到了9
    黄牛抢到了7
    农民工抢到了6
    程序员抢到了5
    黄牛抢到了4
    农民工抢到了3
    程序员抢到了2
    黄牛抢到了1
    农民工抢到了0
    程序员抢到了-1

    为什么会出现这样的数据呢?

    因为现在三个线程都启动了,都是在运行状态中访问test1方法,修改其中的num值。因为他们三个会同时都会进入该方法的情况,所以修改的数据也会出现当:黄牛抢走了1,这时候农民工和程序员还在test1方法里,他俩也会对num进行--操作。所以,最后的结果就是0和-1


    2、使用synchronized关键字锁定方法:

    线程修改抢票代码,在test1方法上加入synchronized关键字,使该方法锁定。调用时需要先获取锁(线程安全)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class Web12306 implements Runnable{
        private int num=10;//总共10张票
        private boolean flag = true;
        @Override
        public void run() {
            while(flag){
                test2();//线程安全,数据准确
            }
        }
        //2、方法锁:加上synchronized表示线程安全的
        public synchronized void test2(){
            if (num<=0) {
                flag = false;
                return;//跳出循环,结束
            }
            try {
                Thread.sleep(500);//模拟延时
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"抢到了"+num--);
        }
    }

    继续使用上面的main方法测试,测试结果如下:抢票结果正确没问题。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    黄牛抢到了10
    黄牛抢到了9
    黄牛抢到了8
    黄牛抢到了7
    黄牛抢到了6
    黄牛抢到了5
    黄牛抢到了4
    黄牛抢到了3
    黄牛抢到了2
    农民工抢到了1


    3、使用synchronized锁定代码块:锁定当前对象

    继续修改抢票代码,在方法内部使用synchronized锁定块

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    class Web12306 implements Runnable{
        private int num=10;//总共10张票
        private boolean flag = true;
        @Override
        public void run() {
            while(flag){
                test3();//线程安全,数据准确
            }
        }
        //3、锁定块:当前对象也就是Web12306
        public void test3(){
            synchronized(this){//锁定当前对象
                if (num<=0) {
                    flag = false;
                    return;//跳出循环,结束
                }
                try {
                    Thread.sleep(500);//模拟延时
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"抢到了"+num--);
            }
        }
    }

    继续使用上面的main方法测试,测试结果如下:抢票结果正确没问题。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    黄牛抢到了10
    黄牛抢到了9
    黄牛抢到了8
    黄牛抢到了7
    黄牛抢到了6
    黄牛抢到了5
    黄牛抢到了4
    黄牛抢到了3
    黄牛抢到了2
    农民工抢到了1


    4、使用synchronized锁定代码块:锁定部分代码块

    可以看出test3方法是使用synchronized关键字锁定了整个方法区域。那如果就只锁定一部分呢?这里假如只锁定if(num<=0)这个判断部分

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    class Web12306 implements Runnable{
        private int num=10;//总共10张票
        private boolean flag = true;
        @Override
        public void run() {
            while(flag){
                test4();//线程不安全,数据不准确:出现-1 【锁定范围不正确】
            }
        }
        //4、使用synchronized锁定部分资源
        public void test4(){
            synchronized(this){
                if (num<=0) {
                    flag = false;
                    return;//跳出循环,结束
                }
            }//只锁定到此
            try {
                Thread.sleep(500);//模拟延时
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"抢到了"+num--);
        }
    }

    使用main方法测试结果如下:可以看出最后的结果同样也会出现0和-1这样错误的数据。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    黄牛抢到了10
    农民工抢到了8
    程序员抢到了9
    黄牛抢到了7
    农民工抢到了6
    程序员抢到了5
    黄牛抢到了4
    农民工抢到了3
    程序员抢到了2
    黄牛抢到了1
    农民工抢到了0
    程序员抢到了-1

    分析下为什么会出现这样的结果?我们知道test4中只锁定了if这部分。假设现在程序num现在等于1

    1.此时线程A,B,C三个线程都会进入到12行,if判断的部分。A先进来拿到了锁,判断此时num=1 。然后释放锁走到18行,try的部分

    2.线程A在18行try部分并没有对num--操作。此时线程B也进入到了12行拿到了锁。也到了18行。现在18行是A,B两个线程。A往下执行拿走了num

     等线程B再去拿num的时候,num已经等于0了。

    3.同理,C再去拿num的时候num已经是0-1 = -1了。


    5、使用synchronized锁定部分资源:只锁定num变量

    由于synchronized的参数需要是对象,所以把基本类型包装成引用类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    class Web12306 implements Runnable{
        private int num=10;//总共10张票
        private boolean flag = true;
        @Override
        public void run() {
            while(flag){
                test5();//线程不安全,数据不准确:出现重复数据【锁定范围不正确】
            }
        }
        //5、使用synchronized锁定部分资源:锁定num变量
        public void test5(){
            synchronized((Integer)num){
                if (num<=0) {
                    flag = false;
                    return;//跳出循环,结束
                }
            }
            try {
                Thread.sleep(500);//模拟延时
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"抢到了"+num--);
        }
    }

    使用main方法测试结果如下:可以看出最后的结果会出现重复数据(两个6)锁定资源不正确也是线程不安全的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    黄牛抢到了10
    农民工抢到了9
    程序员抢到了8
    农民工抢到了7
    黄牛抢到了6
    程序员抢到了6
    黄牛抢到了5
    农民工抢到了4
    程序员抢到了3
    黄牛抢到了2
    程序员抢到了1
    农民工抢到了0
    黄牛抢到了-1


     三、总结       

    1、synchronized关键字表示锁,可以加在方法上或者一个代码块中

       synchronized(引用类型变量 | this | 对象.class){ 

       //需要锁的区域 

       }

    2、不加synchronized关键字的方法是线程不安全的

      加了synchronized表示线程安全,线程安全的话会降低效率。因为共享的资源被加了锁,会有锁等待时间

    3、在加synchronized代码块的时候需要注意,注意锁的范围。

      范围太大----->会降低效率。范围太小------>线程不安全







  • 相关阅读:
    Linux系统开发笔记
    软件测试 | Chrome 浏览器+Postman还能这样做接口测试 ?
    yum 在线安装 nginx
    画图3D Paint 3D工作区黑屏
    InfluxDB 存储结构、读、写
    纯前端保存下载文件到本地
    umijs 配置的一些用法和解释 记录
    mongodb在双活(主备)机房的部署方案和切换方案设计
    mongodb oplog详解和格式分析
    麒麟操作系统上安装docker并加载镜像
  • 原文地址:https://www.cnblogs.com/meet/p/5290946.html
Copyright © 2011-2022 走看看