java多线程4:synchronized关键字

简介:

java多线程4:synchronized关键字

概述
java有各种各样的锁,并且每种锁的特性不同,合理场景下利用锁可以展现出非常高的效率。synchronized内置锁就是Java的一种重量级锁,它能够解决并发编程中出现多个线程同时访问一个共享,可变的临界资源时出现的线程安全问题。让多个线程序列化访问临界资源,同一时刻,只能有一个线程访问临界资源,同步互斥,这样就保证了操作的原子性。

synchronized使用
同步方法块

public class ThreadDemo5 implements Runnable{

private int count = 0;

@Override
public void run() {
    synchronized (this){
        for (int i = 0; i < 10; ++i){
            count++;
            System.out.println("执行的线程是=>" + Thread.currentThread().getName() + "执行结果为->" + count);
        }
    }

}

public static void main(String[] args) {
    ThreadDemo5 threadDemo5 = new ThreadDemo5();
    Thread thread1 = new Thread(threadDemo5,"thread1");
    Thread thread2 = new Thread(threadDemo5,"thread2");
    thread1.start();
    thread2.start();
}

}

执行结果

执行的线程是=>thread1执行结果为->1
执行的线程是=>thread1执行结果为->2
执行的线程是=>thread1执行结果为->3
执行的线程是=>thread1执行结果为->4
执行的线程是=>thread1执行结果为->5
执行的线程是=>thread1执行结果为->6
执行的线程是=>thread1执行结果为->7
执行的线程是=>thread1执行结果为->8
执行的线程是=>thread1执行结果为->9
执行的线程是=>thread1执行结果为->10
执行的线程是=>thread2执行结果为->11
执行的线程是=>thread2执行结果为->12
执行的线程是=>thread2执行结果为->13
执行的线程是=>thread2执行结果为->14
执行的线程是=>thread2执行结果为->15
执行的线程是=>thread2执行结果为->16
执行的线程是=>thread2执行结果为->17
执行的线程是=>thread2执行结果为->18
执行的线程是=>thread2执行结果为->19
执行的线程是=>thread2执行结果为->20

同步方法块,synchronized锁的是括号里的对象,每个线程要进入代码块前必须先获取对象的的锁,才可执行。synchronized是一个隐式锁,也是jvm内置的锁,它会自动加锁和解锁,同时java的每个对象都可以作为锁。

普通同步方法

public class ThreadDemo6 implements Runnable {

private int count = 0;

@Override
public void run() {
    say();
}

private synchronized void say(){
    for (int i = 0; i < 10; ++i){
        count++;
        System.out.println("现在执行的线程执行=>" + Thread.currentThread().getName() + "结果为->" + count);
    }
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

public static void main(String[] args) {
    ThreadDemo6 threadDemo6 = new ThreadDemo6();
    Thread thread1 = new Thread(threadDemo6,"Thread-1");
    Thread thread2 = new Thread(threadDemo6,"Thread-2");
    thread1.start();
    thread2.start();
}

}

执行结果

现在执行的线程执行=>Thread-1结果为->1
现在执行的线程执行=>Thread-1结果为->2
现在执行的线程执行=>Thread-1结果为->3
现在执行的线程执行=>Thread-1结果为->4
现在执行的线程执行=>Thread-1结果为->5
现在执行的线程执行=>Thread-1结果为->6
现在执行的线程执行=>Thread-1结果为->7
现在执行的线程执行=>Thread-1结果为->8
现在执行的线程执行=>Thread-1结果为->9
现在执行的线程执行=>Thread-1结果为->10
/停顿5秒/
现在执行的线程执行=>Thread-2结果为->11
现在执行的线程执行=>Thread-2结果为->12
现在执行的线程执行=>Thread-2结果为->13
现在执行的线程执行=>Thread-2结果为->14
现在执行的线程执行=>Thread-2结果为->15
现在执行的线程执行=>Thread-2结果为->16
现在执行的线程执行=>Thread-2结果为->17
现在执行的线程执行=>Thread-2结果为->18
现在执行的线程执行=>Thread-2结果为->19
现在执行的线程执行=>Thread-2结果为->20

普通同步方法,通过例子可以知道他是一个对象锁,线程1未释放锁,线程2只能被动等待,改下代码

public static void main(String[] args) {

    Thread thread1 = new Thread(new ThreadDemo6(),"Thread-1");
    Thread thread2 = new Thread(new ThreadDemo6(),"Thread-2");
    thread1.start();
    thread2.start();
}

执行结果

现在执行的线程执行=>Thread-2结果为->1
现在执行的线程执行=>Thread-2结果为->2
现在执行的线程执行=>Thread-2结果为->3
现在执行的线程执行=>Thread-2结果为->4
现在执行的线程执行=>Thread-2结果为->5
现在执行的线程执行=>Thread-2结果为->6
现在执行的线程执行=>Thread-2结果为->7
现在执行的线程执行=>Thread-2结果为->8
现在执行的线程执行=>Thread-2结果为->9
现在执行的线程执行=>Thread-2结果为->10
现在执行的线程执行=>Thread-1结果为->1
现在执行的线程执行=>Thread-1结果为->2
现在执行的线程执行=>Thread-1结果为->3
现在执行的线程执行=>Thread-1结果为->4
现在执行的线程执行=>Thread-1结果为->5
现在执行的线程执行=>Thread-1结果为->6
现在执行的线程执行=>Thread-1结果为->7
现在执行的线程执行=>Thread-1结果为->8
现在执行的线程执行=>Thread-1结果为->9
现在执行的线程执行=>Thread-1结果为->10
停顿。。

不是同一个对象锁,所以线程1和线程2不存在锁的互斥,并且不存在共享资源count变量,所以多个线程访问的必须是同一个对象,锁才会变得有意义。

静态同步方法

public class ThreadDemo6 implements Runnable {

private static int count = 0;

@Override
public void run() {
    say();
}

private static synchronized void say(){
    for (int i = 0; i < 10; ++i){
        count++;
        System.out.println("现在执行的线程执行=>" + Thread.currentThread().getName() + "结果为->" + count);
    }
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
public static void main(String[] args) {
    Thread thread1 = new Thread(new ThreadDemo6(),"Thread-1");
    Thread thread2 = new Thread(new ThreadDemo6(),"Thread-2");
    thread1.start();
    thread2.start();
}

}

执行结果

现在执行的线程执行=>Thread-1结果为->1
现在执行的线程执行=>Thread-1结果为->2
现在执行的线程执行=>Thread-1结果为->3
现在执行的线程执行=>Thread-1结果为->4
现在执行的线程执行=>Thread-1结果为->5
现在执行的线程执行=>Thread-1结果为->6
现在执行的线程执行=>Thread-1结果为->7
现在执行的线程执行=>Thread-1结果为->8
现在执行的线程执行=>Thread-1结果为->9
现在执行的线程执行=>Thread-1结果为->10
/停顿/
现在执行的线程执行=>Thread-2结果为->11
现在执行的线程执行=>Thread-2结果为->12
现在执行的线程执行=>Thread-2结果为->13
现在执行的线程执行=>Thread-2结果为->14
现在执行的线程执行=>Thread-2结果为->15
现在执行的线程执行=>Thread-2结果为->16
现在执行的线程执行=>Thread-2结果为->17
现在执行的线程执行=>Thread-2结果为->18
现在执行的线程执行=>Thread-2结果为->19
现在执行的线程执行=>Thread-2结果为->20

即使他们是不同的对象,但执行的都是一个类的方法,在执行同步静态方法时,争抢的是类锁,这也是和非静态同步方法所区别开来。因为他们是两个不同的锁,一个是对象锁,一个是类锁。所以,在代码中,一个线程可以同时抢有对象锁,类锁。

 monitor和monitorexit
Java的互斥锁是如何的实现的,javap -verbose ThreadDemo3.class 看下字节码子令。

同步代码块

    

非静态方法同步

静态方法同步

同步块中monitor被占用就处于锁定状态,其他本次抢锁失败的线程将会放入Wait Set等待同步队列中进行等待,占用锁的线程执行完同步块并且释放锁后将会通知放入同步队列的的其他线程,通知他们,我释放锁了赶紧来抢吧!而相对于普通的静态同步方法和非静态同步方法,常量池汇中多了ACC_SYNCHRONIZED标记,方法调用就会去检查是不是有这个标记如果有,jvm就会要求线程在调用前先请求锁,但无论哪种实现,在实质上还是通过对象相关联的的monitor获取的。

而monitor是什么哪?它是每个对象创建之后都会在jvm内部维护一个与之对应Monitor(监视器锁)也有人叫管程反正都是一个东西,可以理解为每个对象天生都有一把看不见的锁我们叫他monitor锁,而每个线程会有一个可用的MR(Monitor Record)列表,还有一个全局可用列表,每一个被锁住的对象都会和一个MR相关联,并且对象monitor中会有一个owner字段存放占用该锁线程唯一标识,表示这个锁已经被哪个线程占用,synchronized就是基于进入与退出Monitor对象实现方法与代码块同步,而监视器锁的实现哪是依赖底层操作系统Mutex lock(互斥锁),它是一个重量级锁,每次从用户态切换到内的态的资源消耗是比较大的,也因此从jdk1.6后,java对synchronized进行了优化,从一开始的无锁状态->偏向锁状态->轻量级锁状态->重量级锁状态,并且这个状态是不可逆的。

jvm加锁过程

 对象内存结构
上文说过每个Java对象都是天生的锁,存放在Java的对象头中,对象头包含三个区域,对象头,实例数据,补齐填充

第一部分是存储对象自身运行时的数据,哈希码,GC,偏向时间戳,保存对象的分代年龄,锁状态标志,偏向锁线程id,线程持有的锁,如果是数组还需要一块区域存放数组大小,class的对象指针是虚拟机通过他确定这个对象是哪个类的实例,我们平时getClass直接获取类就跟这个有关,官方称这部分为Mark Word,第二部分略过,第三部分规定对象的大小必须是8字节的整数倍,至于为什么,lz没去深究暂时不知道。我们重点关注是Mark Word的锁标志位,所以锁的状态是保存在对象头中的,至于偏向状态,篇幅有限,下节在谈。

锁的粗化和消除
锁的粗化

锁带来性能开销是很大的,为了保证多线程的并发操作,通常会要求每个线程持有锁的时间越短越好,但如果遇到一连串对同一把锁进行请求和释放的操作,jvm会进行优化智能的把锁操作的整合成一个较大同步块,从而减少了对锁的频繁申请和释放提高性能。

public class ThreadDemo7 implements Runnable {

public void test(){
    synchronized (this){
        System.out.println(1111);
    }
    synchronized (this){
        System.out.println(222);
    }
    synchronized (this){
        System.out.println(333);
    }
}
public static void main(String[] args) {
    ThreadDemo7 threadDemo7 = new ThreadDemo7();
    Thread thread = new Thread(threadDemo7);
    thread.start();
}

@Override
public void run() {
    test();
}

}

锁的消除

我们设置了同步块,在字节码中也发现了monitorenter和monitorexit,至少看上去有锁的获取和释放过程,但执行的结果与我们预测的风马牛不相及。

public class ThreadDemo8 implements Runnable {

private static int count = 0;
@Override
public void run() {
    synchronized (new Object()){
        count++;
        System.out.println("锁的消除...=>"  + Thread.currentThread().getName() + "值=>" + count);
    }
}

public static void main(String[] args) {
    ThreadDemo8 threadDemo8 = new ThreadDemo8();
    for (int i = 0; i < 10; ++i){
        Thread thread = new Thread(threadDemo8);
        thread.start();
    }
}

}

执行结果

锁的消除...=>Thread-6值=>4
锁的消除...=>Thread-4值=>2
锁的消除...=>Thread-5值=>4
锁的消除...=>Thread-0值=>4
锁的消除...=>Thread-1值=>6
锁的消除...=>Thread-2值=>6
锁的消除...=>Thread-3值=>7
锁的消除...=>Thread-9值=>8
锁的消除...=>Thread-7值=>9
锁的消除...=>Thread-8值=>10

这是因为jit在编译代码时,使用了逃逸分析的技术,判断程序中的使用锁的对象是否被其他线程使用,如果只被一个线程使用,这个同步代码就不会生成synchronized锁标识的锁申请和释放的机器码,消除了锁的使用流程。所以,并不是所有的实例对象都存放在堆区,如果发生线程逃逸行为,将会存储在线程栈上。

总结
锁的重入和锁膨胀升级,在后期在慢慢整理。

参考

https://blog.csdn.net/axiaoboge/article/details/84335452

https://www.cnblogs.com/xrq730/p/4853578.html

原文地址https://www.cnblogs.com/dslx/p/12787683.html

相关文章
|
1天前
|
Java 开发者
Java基础知识整理,注释、关键字、运算符
在日常的工作中,总会遇到很多大段的代码,逻辑复杂,看得人云山雾绕,这时候若能言简意赅的加上注释,会让阅读者豁然开朗,这就是注释的魅力!
34 11
|
1天前
|
安全 Java 调度
Java线程:深入理解与实战应用
Java线程:深入理解与实战应用
13 0
|
1天前
|
Java
Java中的并发编程:理解和应用线程池
【4月更文挑战第23天】在现代的Java应用程序中,性能和资源的有效利用已经成为了一个重要的考量因素。并发编程是提高应用程序性能的关键手段之一,而线程池则是实现高效并发的重要工具。本文将深入探讨Java中的线程池,包括其基本原理、优势、以及如何在实际开发中有效地使用线程池。我们将通过实例和代码片段,帮助读者理解线程池的概念,并学习如何在Java应用中合理地使用线程池。
|
5天前
|
安全 Java
深入理解 Java 多线程和并发工具类
【4月更文挑战第19天】本文探讨了Java多线程和并发工具类在实现高性能应用程序中的关键作用。通过继承`Thread`或实现`Runnable`创建线程,利用`Executors`管理线程池,以及使用`Semaphore`、`CountDownLatch`和`CyclicBarrier`进行线程同步。保证线程安全、实现线程协作和性能调优(如设置线程池大小、避免不必要同步)是重要环节。理解并恰当运用这些工具能提升程序效率和可靠性。
|
5天前
|
安全 Java 开发者
Java并发编程:深入理解Synchronized关键字
【4月更文挑战第19天】 在Java多线程编程中,为了确保数据的一致性和线程安全,我们经常需要使用到同步机制。其中,`synchronized`关键字是最为常见的一种方式,它能够保证在同一时刻只有一个线程可以访问某个对象的特定代码段。本文将深入探讨`synchronized`关键字的原理、用法以及性能影响,并通过具体示例来展示如何在Java程序中有效地应用这一技术。
|
6天前
|
安全 Java
java多线程(一)(火车售票)
java多线程(一)(火车售票)
|
安全 Java
Java多线程学习(二)synchronized关键字(1)
ava并发编程这个领域中synchronized关键字一直都是元老级的角色,很久之前很多人都会称它为“重量级锁”。但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后变得在某些情况下并不是那么重了。
4658 0