Java对于synchronized的初步认识-创新互联

目录

10余年的遵化网站建设经验,针对设计、前端、开发、售后、文案、推广等六对一服务,响应快,48小时及时工作处理。营销型网站建设的优势是能够根据用户设备显示端的尺寸不同,自动调整遵化建站的显示方式,使网站能够适用不同显示终端,在浏览器中调整网站的宽度,无论在任何一种浏览器上浏览网站,都能展现优雅布局与设计,从而大程度地提升浏览体验。创新互联从事“遵化网站设计”,“遵化网站推广”以来,每个客户项目都认真落实执行。

一、什么是线程安全问题?

二、体验使用synchronized关键字来处理线程安全问题

三、synchronized的使用

①synchronized修饰成员方法

总结一下以上三个场景:

②针对静态方法加锁

③修饰代码块

四、可重入锁,死锁

①什么是可重入锁:

②死锁产生的条件


一、什么是线程安全问题?

(1)根本原因是在于各个线程是由操作系统随机进行调度,并且各个线程是抢占式执行的。

在多线程的环境当中,存在线程共享的数据。

①当其中多个线程尝试修改共享变量的值,就有可能引发线程安全问题。

②如果读个线程仅仅读但是不修改共享的变量就不会存在线程安全问题。

③如果多个线程串行化,”排好队",挨个修改变量的值就不会引发线程安全问题。

(2)代码当中的体现是:多个线程同时修改一个变量的值。如图所示:需求是吧变量a自增5;如果其中一个线程执行a+=2,另外一个线程执行a+=3。那么两个线程会出现什么样的结果呢?

可以看到,得到的结果一个为5,另外一个为6,并没有得到预期的结果a=8;这样就出现了线程安全的问题;  

(3)如果修改操作是原子的,那也不存在线程安全问题,但是大部分的修改都是非原子性的。

static class Counter1{
        int count;
        public void add(){
            count++;
        }
    }

如图所示,此时方法add就是尝试对成员变量"count"进行修改,修改为count+1。这个看似“原子”的操作,实际上是非原子的,分为以下三个步骤:

①把count的值从内存当中读取出来;(load)

②执行count++;(add)

③把自增操作之后的count返回到内存当中。(save)

如果把这三个原子操作,像事物一样封装到一起,那么就可以有效解决线程安全的问题了。

把这非原子的操作变成原子的,那么这个操作就是加锁

二、体验使用synchronized关键字来处理线程安全问题

给出一个业务场景:一个程序当中有三个线程,分别为main线程,线程1,线程2.让线程1调用上面代码块的add方法,让线程2也并发调用上面代码块的add方法。验证运行的结果:代码如下:

public class ThreadHomework2 {
    static class Counter1{
        int count;
        public void add(){
            count++;
        }
    }
    public static void main(String[] args) {
        System.out.println("....................................."+func());
    }

    public static int func(){
        Counter1 counter1= new Counter1();
        long start=System.currentTimeMillis();
        Thread thread1=new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i=0;i<500000;i++){
                    System.out.println("thread1:"+counter1.count);
                    counter1.add();
                }
            }
        });
        thread1.start();
        Thread thread2=new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i=0;i<500000;i++){
                    System.out.println("thread2::::::::"+counter1.count);
                    counter1.add();
                }
            }
        });
        thread2.start();
        try {
            //此处的join方法是,方便thread1,thread2
            //一起调用结束之后,在main线程当中统计时间
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end=System.currentTimeMillis();
        System.out.println(end-start);
        return counter1.count;
    }
}

当线程1与线程2并发修改count变量的时候,就会出现"bug“观察运行的结果:

50W+50W,按照期待,应当输出100W。但是此处为什么输出了99W9862,比预期少了呢?这就是两个线程thread1,thread2同时修改变量count的值所造成的影响。图解一下:

以上图示当中:哪个线程最后save,count的值为最终哪个线程save()的值

这就好像两个事物并发执行的道理一样,事物2(thread2)的load操作读取到了 事物1(thread1)还没有保存,即:还没有save()的数据,这也就出现了类似于"脏读”的场景。

回顾一下,在事物当中,是如何解决脏读的问题的?就是给“写”的操作加锁,只有“写”提交之后,才可以“读”,对应的数据。这也是读已提交的事物隔离级别。


那么在代码当中,是如何实现让线程1的三个操作"load,add,save"变成原子呢?那就是加锁:此处,就是给add方法加锁;锁住”load,add,save"这三个操作。

让这三个非原子的操作变为原子的

static class Counter1{
        int count;
        synchronized public void add(){
            count++;
        }
    }

如何理解synchronized的原子性呢?那就是,针对synchronized所修饰的代码块,当一个线程竞争到锁之后,该线程对于该代码块的一切操作都是要求全部执行完成,不存在执行一半的情况。


synchronized的阻塞作用:

加上锁->synchronized之后,执行的效果就变成:当两个线程同时调用add方法的时候,其中一个线程假如是(thread1)会竞争到"锁",另外一个线程(thread2)会进入阻塞状态,进入阻塞队列。也就是Thread类的状态当中的BLOCKED状态。等待竞争到锁的线程执行完add方法之后,Thread2会从阻塞队列当中离开,重新回到就绪队列当中。

三、synchronized的使用  ①synchronized修饰成员方法

 场景1:两个线程针对同一个对象调用相同的add()方法

 可以看到,两个线程thread1,thread2同时并发调用的是同一个对象(counter1)的add方法。这样,就会造成其中一个线程竞争到锁,另外一个线程没有竞争到锁的情况。

 假如thread1优先比thread2调度到CPU内核上面执行,那么当thread1执行到add()方法的时候,此时thread1就会对counter1这个对象加锁。

 如果没有竞争到锁,那么没有竞争到锁的线程会进入阻塞状态,等待竞争到锁的线程执行完add方法之后,自己再从阻塞状态回到就绪状态。

场景二:如果两个线程分别处理的是两个不同的对象呢?

如下代码:

可以看到,即使没有对add方法采用synchronized关键字修饰,count1最后的值为50W,count2最后的值也为50W。没有出现"bug“。也就是说,如果针对成员方法加锁,那么锁住的是(this),也就是调用这个方法的对象。可以理解为,线程针对对象加锁。此时线程0与线程1,2,3想要同时针对count1对象进行加锁,那么就产生了锁竞争。需要注意的是,如果两个线程,其中一个针对对象加锁,另外一个线程没有针对对象加锁,那么也不会产生锁冲突。产生锁冲突的前提条件一定是两个不同的线程同时竞争一个对象


 场景3:两个线程,一个调用add()方法来完成count1对象的count值自增。另外一个调用add2()方法,也完成count的自增呢?其中add2()为没有加锁的方法

代码实现:

运行结果,也是出现bug的:

此时这种写法,和没有加锁,是一个道理。因为,其中一个线程加了锁,另外一个线程没有加锁,此时就不存在锁竞争的情况;


总结一下以上三个场景:

(1)多个线程同时针对一个对象进行加锁操作,这个时候会出现锁竞争的情况。一旦出现锁竞争,就会出现线程阻塞等待的情况;

(2) 如果两个线程针对不同的对象加锁,不会出现锁竞争的情况;

(3)两个线程,如果一个加锁,另外一个不加锁,那么也不会出现锁竞争的情况。

②针对静态方法加锁

 如果synchronized针对的是一个类的静态代码块,或者静态方法,那么锁住的就是这个方法所在类的类对象:即:类.class。

③修饰代码块
  public void add(){
             //进入代码块,就"加锁”,离开代码块,就“解锁”
             //可以指定任意需要加锁
             synchronized (this) {
                 count++;
             }
             //下面的代码块没有被加锁
             System.out.println("12334444e");
        }

  含义就是,当线程执行到add()当中加锁的代码块的时候,线程会针对指定的对象加锁,当执行完count++操作之后,自动解锁。在上图的代码当中,count++所在的代码块是被加了锁的,但是下面那句输出没有被加锁。

四、可重入锁,死锁 ①什么是可重入锁:

 一个线程连续针对同一把锁,连续加锁两次,是否会产生死锁。如果不会阻塞自己,那么就不是可重入锁。如果是,那就会阻塞自己。如果阻塞了自己,那么就相当于产生了”死锁“。

代码:

class Counter{
    public int count;
    synchronized public void add(){
        synchronized (this) {
            count++;
        }
    }

}

  分析一下代码的执行流程:

  当多个线程并发调用add()方法的时候,其中一个线程(thread1)可以获取到锁,其中,该线程针对调用的对象,count1加锁。此时该线程获得了锁,其余线程进入阻塞状态。被加锁的对象就是(count1)

 当进入add()方法内部的时候,遇到了synchronized修饰的代码块。此时线程1继续尝试获取锁。前面我们也提到,加锁是线程针对对象加锁。可是当调用add()方法的时候,已经针对(this,也就是该方法的调用者count1)加锁了,当再次尝试获取锁的时候,是否会阻塞呢?


 我们回顾一下什么情况下会出现因为加锁而产生的线程阻塞:当多个线程尝试竞争同一个未被加锁的对象的时候,没有竞争到锁的线程会进入阻塞状态。

 换而言之:如果一个对象被加锁了,那么其他线程想继续获取到这个对象的锁,那么其他线程都无法获取到,都会进入阻塞状态。


那么此时,针对count1,也就是下一个synchronized代码块当中的this,不允许其他线程继续对this加锁,但是,如果thread1此时也不允许对synchronized代码块中的代码加锁的话,thread1也会继续进入阻塞状态。这样的话,所有的线程都进入了阻塞状态。


总结一下:如果一个线程针对同一把锁连续两次加锁,在第二次尝试加锁的时候,不会让自身进入阻塞等待的状态,那么称这个锁是可重入锁。根据代码的执行情况,synchronized就是可重入锁。

其中,java当中还有ReentrantLock也是可重入锁

②死锁产生的条件

  先举一个形象一点的例子:小明和小红一起饺子,其中小明先拿到了酱油,小明对小红说,你把醋给我,我就把酱油给你。小红一听很不乐意,他反过来说,你如果把酱油先给我,我才会把醋给你。这个时候,两个人就僵持住了,谁也给不出。也就产生了“死锁”。

 A.死锁准确的定义:

 死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。

 B.分析一下死锁产生的四个必要条件:

 (1)互斥使用:在上面的场景当中,线程小明拿到了锁(酱油);线程小红也拿到了锁(醋)。

这个时候,如果线程小明尝试获取被小红加锁的对象”醋“,那么小明就会获取锁失败。进而进入阻塞的状态。小红如果想获取小明的锁(酱油)也同理。

所以,互斥使用的含义就是:其中一个线程拿到了锁,其他与当前线程竞争锁的线程就必须进入阻塞状态,等待锁的释放。

(2)不可抢占线程1拿到锁之后,线程1一定要主动释放锁,其他线程才可以获取到

  (3)请求和保持:  

 在上述的场景当中,线程小明获取到锁"酱油"之后,当他想再次获取到小红的醋的时候,不会因为去尝试获取小红的醋而丢失了本来属于自己的锁:酱油。、

所以,请求和保持就是:

 线程1获取到对应的一把锁之后,如果想尝试再次获取其他的锁,当前拥有的锁仍然是保持的,不会丢失。

       (4)循环等待:

 如上面的场景,小红和小明都在等待对方先释放锁,但是都没有自行先释放锁。这个就是循环等待。在线程当中,循环等待的场景就是:

两个线程都在等待获取到对应的锁之后,都在等待对方释放锁。这就是循环等待

  注意:以上四个条件,缺一不可,是出现死锁的充分必要条件,但是。归根结底,还是因为synchronized是必须要等到获取到锁的线程执行完加锁的代码块之后,其他线程才能继续获取到当前的锁。但是,其他的锁不一定跟synchronized一样。

  

 

你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧


分享文章:Java对于synchronized的初步认识-创新互联
浏览路径:http://azwzsj.com/article/dspcdj.html