并发编程-synchronized

x33g5p2x  于2022-03-09 转载在 其他  
字(5.7k)|赞(0)|评价(0)|浏览(511)

并发编程-synchronized

说在前面的话

正如我开篇所说,我要整理一些java并发编程的学习文档,这一篇就是第三篇:synchronized关键字。

主要说synchronized关键字的使用,锁原理,锁重入,脏读,锁升级等问题。

欢迎关注和点赞。

Lat‘s go

脏读

先来看一个概念:脏读。

所谓脏读就是读取到了脏数据。

哪什么是脏数据呢?

所谓脏数据就是没有意义的数据或者说不确定的数据。

脏读产生的情况基本就是一个线程读取到了其他线程在修改但是并没有修改完成的数据。

就好像:如花正和男朋友闹别扭,这时小明乘虚而入,和如花眉来眼去,小明就以为自己就是如花的男朋友了,殊不知如花晚上回去和男朋友认真的讨论一些问题之后,立刻就和好了。那么小明心里的自以为的想法就是毫无意义的。 这个想法就是脏想法。

好的,我们来看一个例子:

package com.st.sync;

import java.util.concurrent.TimeUnit;

/**
 * @author 戴着假发的程序员
 * @company 江苏极刻知学-起点编程
 */
public class SyncDemo0 {
    // 这里存储如花的想法
    private static String info = "如花深爱这自己的男朋友";
    public static void main(String[] args) {
        // 线程1,表示如花自己的想法变化
        new  Thread(()->{
            System.out.println("如花刚开始的想法:"+info);
            // 开始闹别扭
            info = "这个男朋友真的不太行......";
            // 开始思考问题
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 如花和男朋友讨论某些问题之后
            info = "这个男朋友该行的时候还是挺行的......";
        }).start();
        // 线程2,表示小明去读取如花的想法
        new Thread(()->{
            // 小明读取到的如花的想法
            System.out.println("小明看到的:"+info);
            // 小明和如花的暧昧中
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("小明最后看到的:"+info);
        }).start();
    }
}

结果:

在这个例子中小明第一次看到的想法就是脏想法,就是脏数据。因为如花那个线程还在不断的修改想法,所以修改过程小明读取的想法数据就是不确定的。是没有意义的。

这就是所谓脏读,在上面的例子中防止脏读可以使用synchronized关键字。下一小节就来说说synchronized关键字。

synchronized是啥?咋用?

synchronized关键字可以给代码(资源)上锁。

你说上锁是啥意思?

上锁就是上锁呀,你去进厕所之后,为了安全或者为了避免尴尬就会给厕所门上锁,这时这个厕所就是你一个人独享的。所以呀,上锁就是这个上锁。

程序中的上锁,就是给某些资源或者代码上锁,上锁之后,这个资源职能被当前的线程使用,其他的线程职能得带。这就上锁。

**首先要知道synchronized上锁,其实锁的是对象,具体锁什么对象,要看情况了。**具体如何锁对象,后面的章节再来说。

你要说怎么用,就来看看下面的案例吧:

**情况1:**同步静态方法。

如果一个同步的静态方法被修饰synchronized,那么在执行这个方法的时候,这个方法所在类的Class对象就会被上锁。其他的线程就无法执行当前类中的任何synchronized的静态方法了。当然实例方法是可以执行的。

上菜:

package com.st.sync;

import java.util.concurrent.TimeUnit;

/**
 * @author 戴着假发的程序员
 */
public class SyncDemo {
    public static void main(String[] args) {
        // 线程1,执行同步静态方法1
        new Thread(()->{
            try {
                method1();
            } catch (InterruptedException e) {
            }
        }).start();
        // 线程2,执行同步静态方法2
        new Thread(()->{method2();}).start();
        // 线程3 ,执行同步非静态方法
        new Thread(()->{new SyncDemo().method3();}).start();
    }
    // 同步静态方法1
    public synchronized  static void method1() throws InterruptedException {
        System.out.println("同步静态方法1开始");
        // 一开始就稍微睡一会
        TimeUnit.SECONDS.sleep(1);
        System.out.println("同步静态方法1结束");
    }
    // 同步静态方法2
    public synchronized  static void method2() {
        System.out.println("同步静态方法1开始");
        System.out.println("同步静态方法1结束");
    }
    // 同步实例方法
    public synchronized void method3(){
        System.out.println("同步实例方法执行");
    }
}

执行结果:

很明显同步静态方法1开始之后,即使进入的阻塞状态,同步实例方法不受影响的执行,但是同步静态方法2就必须等待同步静态方法1完全结束,释放了Class对象的锁之后才能执行。

啥?你想问非同步方法会不会受影响???这个我拒绝回答,因为这个不在并发编程的范畴…

**情况2:**同步实例方法

如果使用synchronized修饰实例方法,那么当某个线程执行同步实例方法的时候这个实例方法调用者(this)就会被锁。其他线程就无法调用这个对象的其他同步实例方法了。

上菜:

/**
 * @author 戴着假发的程序员
 */
public class SyncDemo1 {
    public static void main(String[] args) {
        // 实例对象
        SyncDemo1 sd = new SyncDemo1();
        // 线程1,执行同步实例方法1
        new Thread(()->{
            try {
                sd.method1();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        // 线程2,执行同步实例方法2
        new Thread(()->{sd.method2();}).start();
    }

    // 同步实例方法1
    public synchronized void method1() throws InterruptedException {
        System.out.println("同步实例方法1开始");
        // 稍微睡一会
        TimeUnit.SECONDS.sleep(2);
        System.out.println("同步实例方法1结束");
    }
    // 同步实例方法2
    public synchronized void method2(){
        System.out.println("同步实例方法2执行");
    }
}

结果:

很明显,即使同步实例方法1进入阻塞,同步实例方法2也必须等待实例方法1结束之后才能执行。因为sd对象被锁了。

**情况3:**同步代码块

同步代码块很好理解。synchronized(对象){} 锁的就是()里面的对象。

上菜:

package com.st.sync;

import java.util.concurrent.TimeUnit;

/**
 * @author 戴着假发的程序员
 * @company 江苏极刻知学-起点编程
 */
public class SyncDemo2 {
    public static void main(String[] args) {
        // 准备上锁的家伙
        Object obj = new Object();
        // 线程1,锁obj
        new Thread(()->{
            synchronized (obj){
                System.out.println("线程1,锁了obj");
                // 小睡一会
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程1,准备释放obj");
            }
        }).start();
        // 线程2,也要锁obj
        new Thread(()->{
            synchronized (obj){
                System.out.println("现场2,锁了obj");
            }
        }).start();
    }
}

线程2必须等线程1释放了obj之后才能继续执行。

好的,稍微总结一下:

当synchronized修饰静态方法的时候,锁的是Class对象。

当synchronized修饰实例方法的时候,锁的是this对象。

当synchronized修饰某个代码块的时候,锁的是()里面的对象。

锁对象到低是啥意思呢?看下一小节。

synchronized到低是咋锁对象的?

synchronized到低是咋锁对象。说的是锁对象头的。来看看我的解释:

java对象在内存中就是一串二进制,也就是一串”01“。这一串”01“的开头的部分结构都是一样的。在开头的这部分结构中,最后的3个位置的bit位就是这个对象的锁标志位。所谓锁标志位就是说,当这三个bit位的值发生改变的时候这个对象的被锁状态就在发生改变。 至于具体如何变换,下一小节”锁升级中“有详细说明。

所以要给这个对象上锁,就是要尝试改变这个对象的锁标志位的值,改变成功就是获取锁成功。

比如:

上锁状态:

解锁状态:

所谓锁对象大概就是这样,应该不难理解吧。

synchronized锁升级

synchronized关键在上锁,在JDK1.6之前都是重量级锁。在JDK1.6之后就进行了优化,会根据情况自动升级。

啥是重量级锁?

所谓重量级锁,就是你上厕所的时候把门锁了,你室友过来一看,门锁了就直接走了,等你上完厕所之后再通知他去上厕所。

对应的有个轻量级锁,也是乐观锁。就是你上厕所的时候把门锁了,你是有也很着急,不敢单个一份一秒,于是乎他就守在厕所门口,不断问题:”好了没?好了没?…“。你只要一好。他就第一时间冲入厕所。(这个就是重入是CAS实现的)

好的,接下来我们来聊聊锁升级的事情。

在JDK1.6开始,对象分为:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。

上一小节说过了。在对象头的最后三个bit位就是锁标识位。大致情况是这样。

锁标志位锁状态
0 01无锁状态
1 01偏向锁状态
1 00轻量级锁状态
1 11重量级锁状态

当然了你可以在控制台输出对象头观察对象头的情况。(这个使用第三方的工具很容易做到)。但是你要看到锁升级的数据变化可能有点麻烦。 今天这个文档我的确没有准备写锁升级的测试程序。 大家如果有需要可以留言,我来补充一篇。

接下来我们来说说,synchronized锁是如何升级的。

专业描述:刚创建的对象是无锁状态,当有一个线程使用的时候,这个对象会成为偏向锁状态。当有产生少量竞争的时候会自动调为轻量级锁。当有较多的并发和竞争时会调整为重量级锁。

我的描述是这样:

  • 无锁状态: 小明和如花只是普通朋友,小明可以不受限制的和很多姑娘暧昧。
  • 偏向锁状态:小明和如花确定了男女朋友关系,小明只和如花暧昧,而且也没有其他的姑娘找小明。
  • 轻量级锁:小明和如花明确了关系,但是小明又遇到了诗诗,姗姗,美美,倩倩。。。。各个都让小明心动。于是如花一怒之下收缴了小明的作案工具“手机”。但是即便如此,这些诗诗呀,姗姗呀,美美呀,还是时不时的来找小明。
  • 重量级锁:由于小明的魅力,来找小明的各种 诗诗,各种倩倩太多了,如花为了安全直接带小明领取了结婚证。并且全世界宣告小明目前是她的私有资源。响应追求小明,必须等她和小明离婚再说。

sychronized锁升级是自动处理的,大致就是上面的情况。如果还有不明白的,欢迎提问。

锁的重入

所谓锁的重入就是某一个线程已经获取了一把锁,由于各种原因又一次尝试获取同一把锁,这就锁重入。

sychronized是支持锁的重入的。例子非常简单。就是在一个同步方法里面尝试调用另外一个同步的方法。

看一个案例:

package com.st.sync;

/**
 * @author 戴着假发的程序员
 * @company 江苏极刻知学-起点编程
 */
public class SyncDemo3 {
    public static void main(String[] args) {
        method1();
    }
    // 同步静态方法1
    public synchronized static void method1(){
        System.out.println("同步静态方法1");
        // 调用静态同步方法2
        method2();
    }
    // 同步静态方法2
    public synchronized static void method2(){
        System.out.println("静态同步方法2");
    }
}

我们在同步静态方法1中调用静态同步方法2,肯定是行得通的。因为两个都是同步静态方法,所以两个方法锁的是同一个对象Class对象。所以当调用同步方法2的时候就是锁的重入。

关于synchronized我就说到这里。。。还有什么需要补充的欢迎大家留言…

相关文章