同步与异步,如何解决线程安全问题—synchronized详解,对象锁与类锁,静态与非静态同步方法详解

同步异步与synchronized关键字的使用(对象锁与类型,静态与非静态同步方法)

  • 同步与异步
  • 解决线程安全问题
    • synchronized
    • 对象锁
    • 对象锁和类锁(静态与非静态同步方法)

同步与异步

原意
同步:是所有的操作都做完,才返回给用户结果(某个人做完一件事情之后再去做另一件事情)。例如,你去楼下超市给你女朋友买姨妈巾,但是超市店里没货了,需要去仓库调取,中间需要2个小时,而你就在超市里面等待。那么这两个小时都属于你买姨妈巾所需要的时间,你是不能做别的事情的,这就是同步。
异步:不用等所有操作等做完,就相应用户请求(某个人在做一件事情的时候能得到一种结果,然后趁机去做别的事情)。例如,你去楼下超市给你女朋友买姨妈巾,但是超市店里没货了,需要去仓库调取,中间需要2个小时,但是你跟老板说了姨妈巾来的时候给你送上楼或者打电话叫你下来拿,然后你只花了两分钟去做完这个事情,当然后台还是需要2个小时去完成整个过程,但是这个时间你可以去做其他的一些事情,比如你买了红糖红枣给你女朋友熬汤。这就是异步!

基于同步和异步的本意,在java的线程中为了解决多个线程访问同一资源的问题,还可以这么理解:
同步:我做完,你再做。当一个线程访问该资源时,不允许其他线程访问。例如,公司就一个饮水机,二狗同学在接水时其他同学都不能使用饮水机。
异步:我做我的,你做你的。多个线程可以同时访问一个资源,可能会发生线程安全问题。例如,公司就一个饮水机,二狗同学在接水时刚好二蛋同学在给饮水机换水,这就是安全问题。


解决线程并发安全问题

刚才已经介绍了线程安全问题是由异步操作引起的,所以我们要解决线程并发安全问题就要把异步操作变成同步操作--同步锁。

synchronized同步锁

synchronized关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。

在Java语言中,每一个对象有一把锁。线程可以使用synchronized关键字来获取对象上的锁。synchronized关键字可应用在方法级别(粗粒度锁)或者是代码块级别(细粒度锁)。
同步方法和同步块:

同步方法就是在方法前加关键字synchronized,然后被同步的方法一次只能有一个线程进入,其他线程等待。

1)修饰代码块——同步代码块

同步块是在方法内部使用大括号使得一个代码块得到同步。同步块会有一个同步的”目标“,使得同步块更加灵活一些(同步块可以通过”目标“决定需要锁定的对象)。一般情况下,如果此”目标“为this,那么同步方法和同步块没有太大的区别。意思是当两个并发线程访问该对象(同一个对象)中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块例如,上面的安全性问题案例我们修改一下:

public class DemoSyn {
    static Integer num=10;
    public static void main(String[] args) {
        final DemoSyn syn=new DemoSyn();
        Thread t1=new Thread(){
            public void run(){
                syn.printNum(0);
            }
        };
        Thread t2=new Thread(){
            public void run(){
                syn.printNum(1);
            }
        };
        t1.start();
        t2.start();
    }
    public  void printNum(int i){
        synchronized(this){
            if(i==0){
                num=100;
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }else{
                num=200;
            }
            System.out.println(num);
        }
    }
}

运行修改之后的代码你会发现,执行之后的代码已经解决了线程安全问题,不会再出现100,100或者200,200的情况。

2)修饰方法

同步方法就是在方法前加关键字synchronized,然后被同步的方法一次只能有一个线程进入,其他线程等待。

public class DemoSyn {
    static Integer num=10;
    public static void main(String[] args) {
        final DemoSyn syn=new DemoSyn();
        Thread t1=new Thread(){
            public void run(){
                syn.printNum(0);
            }
        };
        Thread t2=new Thread(){
            public void run(){
                syn.printNum(1);
            }
        };
        t1.start();
        t2.start();
    }
    public synchronized void printNum(int i){

        if(i==0){
            num=100;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }else{
            num=200;
        }
        System.out.println(num);

    }
}
同步锁使用情况
(转自https://blog.csdn.net/u012221046/article/details/52396474)
一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

 二、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。

 三、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞,阻塞的意思就是不仅这个方法不能执行,这之后的方法也不能执行。

 四、这三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。

 五、以上规则对其它对象锁同样适用.

注意:在这里要强调一下对象锁的使用,我们所有的同步锁都是针对同一个对象的锁。所以仔细看下列案例的区别:

//新建一个实现了Runnable的A 用来创建任务
class A implements Runnable{
        public void run(){
            synchronized (this) {
                for(int i=1;i<=10;i++){
                    System.out.print(i+" ");
                }
            }
        }
}

第一种情况,添加一个同步锁,创建一个A类对象(任务),但是创建了两个线程去分别执行这个任务:

    public class Main {
    public static void main(String[] args) {
        A a=new A();
        //创建了t1线程
        Thread t1=new Thread(a);
        //创建t2线程
        Thread t2=new Thread(a);
        //启动两条线程针对同一个对象 
        t1.start();
        t2.start();
    }
}

这时候因为加了线程锁,所以肯定是分别输出1~10,不会出现线程安全问题
第二种情况,创建两个A类对象(两个任务),分别用t1线程处理a1任务和t2线程处理a2任务,这时候你会发现输出结果的差异:

    public class Main {
    public static void main(String[] args) {
        A a1=new A();
        A a2=new A();
        //创建了t1线程
        Thread t1=new Thread(a1);
        //创建t2线程
        Thread t2=new Thread(a2);
        //启动两条线程针对同一个对象 
        t1.start();
        t2.start();
    }
}
对象锁与类锁

synchronized可以同时修饰静态方法和实例方法,但类锁和对象锁是两个不一样的锁,控制着不同的区域,它们是互不干扰的。同样,线程获得对象锁的同时,也可以获得该类锁,即同时获得两个锁,这是允许的。
如何获取一个类锁,很简单,我们之前所有的锁都是锁普通方法和变量,当我们锁的是一个static静态方法或者变量时则就代表了获取了类锁。

非静态和静态的区别主要在于(以同步方法为例):非静态的同步方法是锁定类的实例的,而静态的同步方法是锁定类的。
也就是说,对于非静态的同步方法,在同一时刻,一个类的一个实例中,只有一个线程能进入同步的方法。但是对于多个实例,每一个实例的一个线程都可以进入同一同步的方法。
而对于静态的同步方法,在同一时刻,一个类的多个实例都会只有一个线程进入该同步方法! 

同上面的对象锁一样,当我们使用两个对象去执行两个任务的时候,是没有锁机制的。例如:

    public class DemoSyn {
    static Integer num=10;
    public static void main(String[] args) {
        final DemoSyn syn1=new DemoSyn();
        final DemoSyn syn2=new DemoSyn();
        Thread t1=new Thread(){
            public void run(){
                syn1.printNum();
            }
        };
        Thread t2=new Thread(){
            public void run(){
                syn2.printNum();
            }
        };
        t1.start();
        t2.start();
    }
    public synchronized void printNum(){
        for(int i=1;i<=150;i++){
            System.out.print(i+" ");
        }
    }
}

运行结果你会发现多个线程执行多个对象的任务时并没有同步执行,还是异步执行的。因为他们都是各自的对象锁,那要如何完成多个对象执行某一个方法时实现同步执行呢?就把该方法变成一个static静态方法——类锁。例如:

public class DemoSyn {
    static Integer num=10;
    public static void main(String[] args) {
        final DemoSyn syn1=new DemoSyn();
        final DemoSyn syn2=new DemoSyn();
        Thread t1=new Thread(){
            public void run(){
                syn1.printNum();
            }
        };
        Thread t2=new Thread(){
            public void run(){
                syn2.printNum();
            }
        };
        t1.start();
        t2.start();
    }
    public static synchronized void printNum(){
        for(int i=1;i<=150;i++){
            System.out.print(i+" ");
        }
    }
}

运行结果你会发现该锁实现了所有该类对象对该静态同步方法的执行是同步的。

猜你喜欢

转载自blog.csdn.net/qq_34598667/article/details/81065023