synchronized关键字理解

引入

需求

先看一个简单的需求
我们现在模拟一下银行的叫号机生产号码(号码范围为1~100),假设我们现在有四个取号机,要求每个人取得到号码不重复,并且不能有遗漏,很多人就很快的可以写出下面的代码

代码

package cn.kevinlu98;

/**
 * @Author: Kevin·Lu
 * @Date: 7:29 PM 2019/8/20
 * @Description: 多线程模拟银行叫号
 */
public class TicketDemo extends Thread {

    private static int index = 0;

    private static final int MAX = 100;

    @Override
    public void run() {
        try {
            while (index < MAX) {
                index++;
                System.out.println(Thread.currentThread().getName() + "叫到的号码是:" + index);
                Thread.sleep(10);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        TicketDemo t1 = new TicketDemo();
        TicketDemo t2 = new TicketDemo();
        TicketDemo t3 = new TicketDemo();
        TicketDemo t4 = new TicketDemo();

        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

结果

这段代码看上去没有任何问题,按道理是会产生不重复不遗漏的号码,但我们运行一下看看

[外链图片转存失败(img-n1exNj8v-1566318152510)(https://t1.picb.cc/uploads/2019/08/20/gQdhS0.png)]

分析

看我们的运行结果,大家可能运行一次出不了错,建议多运行几次,当然这种错误的发生是存在可能性的。
现在我们来分析一下出现这种情况的原因

[外链图片转存失败(img-iJ8RF91X-1566318105742)(https://t1.picb.cc/uploads/2019/08/20/gQoUj1.png)]

这张图就很清楚的解释了为什么会出现这种原因,其实就是因为每个线程内部做的事情不是一个原子操作导致的
有了这个思路之后name怎么解决这个问题就很简单了,我们可以给我们每个线程做的操作加一把锁就可以了,如果这段代码没有执行完,其他线程不允许做动作就可以了

优化

public synchronized void run() {
        try {
            while (index < MAX) {
                synchronized (TicketDemo.class) {
                    index++;
                    System.out.println(Thread.currentThread().getName() + "叫到的号码是:" + index);
                    Thread.sleep(10);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

分析

我们现在再来分析一下代码,首先在while循环内部加上了synchronized关键字(在这里先理解为是一把锁),内部的代码块在执行的时候就不会被打断,现在先运行看看

[外链图片转存失败(img-1n5n0wJ3-1566318105743)(https://t1.picb.cc/uploads/2019/08/20/gQoKLD.png)]

此时的代码执行结果的确没有遗漏,也没有重复,但是注意最后,这个原因相信大家很快也能想到,我们画张图分析一下

[外链图片转存失败(img-MlU85aoC-1566318105744)(https://t1.picb.cc/uploads/2019/08/20/gQodfv.png)]

所以此时我们应该在加一句判断,最终代码

最终代码

public synchronized void run() {
    try {
        while (index < MAX) {
            synchronized (TicketDemo.class) {
                if (index < MAX) {
                    index++;
                    System.out.println(Thread.currentThread().getName() + "叫到的号码是:" + index);
                    Thread.sleep(10);
                }
            }
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

}

最终结果

[外链图片转存失败(img-wfl7B5lz-1566318105745)(https://t1.picb.cc/uploads/2019/08/20/gQooMi.png)]

synchronized(同步) 具体用法

锁机制

何为同步?我们先了解一下锁机制
锁机制有两个重要的特性

  • 互斥性: 即在同一时间只允许一个线程持有某个锁,通过这种特性来实现多线程中的协调机制,这样在同一时间只有一个线程对需同步的代码块(复合操作)进行访问。互斥性我们也往往称为操作的原子性。
  • 可见性: 必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作从而引起不一致。

synchronized 的分类

按同步方法分类

  • 修饰非静态方法
public synchronized void methodName();
  • 修饰静态方法
public synchronized static void methodName();

按同步代码块分类

  • 修饰对象
synchronized(this || object) {
    // ... 代码块
}
  • 修饰类
synchronized(ClassName.class) {
    // ... 代码块
}

获取锁

  • 修饰非静态方法或修饰对象时,此时的锁对应的是对象级别的锁
    在 Java 中,每个对象都会有一个 monitor 对象,这个对象其实就是 Java 对象的锁,通常会被称为“内置锁”或“对象锁”。类的对象可以有多个,所以每个对象有其独立的对象锁,互不干扰。
    具体实现
    • 某一线程占有这个对象的时候,先monitor 的计数器是不是0,如果是0还没有线程占有,这个时候线程占有这个对象,并且对这个对象的monitor+1;如果不为0,表示这个线程已经被其他线程占有,这个线程等待。当线程释放占有权的时候,monitor-1
    • 同一线程可以对同一对象进行多次加锁,+1,+1,重入性
  • 修饰静态方法或修饰类时,此时的锁对应的是类级别的锁
    在 Java 中,针对每个类也有一个锁,可以称为“类锁”,类锁实际上是通过对象锁实现的,即类的Class对象锁(这个Class对象是指类在被类加载器加载到java虚拟机后再堆中由java.long.Class这个类为其创建的一个入口,简单的理解其实就是java.long.Class的一个实例对象)。每个类只有一个 Class 对象,所以每个类只有一个类锁。

原理分析

先看下我们的测试代码

测试代码

package cn.kevinlu98;

import java.util.concurrent.TimeUnit;

/**
 * @Author: Kevin·Lu
 * @Date: 8:01 PM 2019/8/20
 * @Description:
 */
public class SyncroDemo01 {

    public void accessResource3() {
        synchronized (this) {
            try {
                TimeUnit.MINUTES.sleep(2);
                System.out.println(Thread.currentThread().getName() + " is running");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

    public static void main(String[] args) {
        SyncroDemo01 demo01 = new SyncroDemo01();
        for (int i = 0; i < 5; i++) {
            new Thread(demo01::accessResource3).start();
        }

    }

}

分析

先用jconsole看下
终端直接输入jconsole,然后找到你的进程,然后双击进入,点击不安全的连接

thread-0

[外链图片转存失败(img-LA6E3c2P-1566318105746)(https://t1.picb.cc/uploads/2019/08/20/gaywvX.png)]

注意看状态,timed_waiting
至于每种状态具体的含义可以 传送门

thread-其他

[外链图片转存失败(img-zDQlpMTe-1566318105746)(https://t1.picb.cc/uploads/2019/08/20/gayfUG.png)]

我就不一一列了,有兴趣可以自己用jconsole测一下,状态都是blocked,因为此时线程的拥有者为thread-0
我们等两分钟过去,因为我在代码里睡眠了2分钟

thread-4

[外链图片转存失败(img-Ea2vEFSZ-1566318105747)(https://t1.picb.cc/uploads/2019/08/20/gayiMj.png)]

此时thread-4挣得到了锁,状态timed_waiting,其他都在blocked

jstack

输入命令

jstack [进程pid]

我们在用jstack看一下,更加直观,此时thread-0已经执行完成,thread-4正在执行

[外链图片转存失败(img-Kb4oERWt-1566318105753)(https://t1.picb.cc/uploads/2019/08/20/gaylVK.png)]

反编译一下看看

在idea点击终端,因为这里直接打开就是项目根目录,切换比较方便,然后切换到out/production/包名下

[外链图片转存失败(img-GAd5qNcf-1566318105754)(https://t1.picb.cc/uploads/2019/08/20/gayCpe.png)]

执行反编译命令

javap -v SyncroDemo01

[外链图片转存失败(img-uKXgI1JC-1566318105755)(https://t1.picb.cc/uploads/2019/08/20/gaycYs.png)]

可以看到我们反编译的代码中出现了monitor这个词,前面我们也有提到java的synchronized是通过monitor进行加锁的
这是我们对代码块进行加锁,我们现在看看对方法加锁会不会有所不同呢?

public synchronized void accessResource2() {
    try {
        TimeUnit.SECONDS.sleep(2);
        System.out.println(Thread.currentThread().getName() + " is running");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

我们找到这个方法的反编译代码,确实不同,这里是通过阿紫flags中加上ACC_SYNCHRONIZED关键字进行标识

[外链图片转存失败(img-IqCT1tgw-1566318105756)(https://t1.picb.cc/uploads/2019/08/20/gay1u7.png)]

结束

到现在大家应该对java中synchronized有了初步的了解,感兴趣可以自己在查查其他资料

发布了31 篇原创文章 · 获赞 3 · 访问量 4526

猜你喜欢

转载自blog.csdn.net/Lengwenin/article/details/99899887