Java教程

【Java并发编程】synchronized关键字

本文主要是介绍【Java并发编程】synchronized关键字,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

线程安全问题

    表现为三个方面:原子性,可见性和有序性。

    原子性:对于共享变量,当前线程一旦操作,在其他线程看来是不可分割的。当前线程要么操作完毕,要么没有操作,操作过程中的中间结果,其他线程是不可见的。

    可见性:一个线程对共享变量进行修改以后,另外一个线程没办法立即看到。由于每一个线程都从主内存复制了一份共享变量到自己的内存中,所以就导致了可见性问题。

    有序性:编译器为了优化性能,有时候会改变程序中语句的先后顺序,这种优化不会影响程序的执行结果,但是有时候也可能导致线程安全问题。

synchronized关键字

      synchronized是一个内部锁,它可以修饰方法和代码块。在多线程环境下,同步方法或者代码块在同一个时刻只能有一个线程执行,其余线程只能阻塞或者等待获取锁,也就是乐观锁。

  • synchronized在上锁的过程中有一个锁升级的过程

    • 无锁:刚把对象new出来
    • 偏向锁:第一次上锁的时候加偏向锁
    • 轻量级锁(无锁,自旋锁,自适应锁):一旦多个线程争用锁对象,升级为轻量级锁
    • 重量级锁:多个线程竞争锁比较激烈的时候,升级为重量级锁
  • 锁升级底层过程
    • Java代码中使用synchronized
    • 编译生成字节码文件,monitor监视锁状态
    • monitorenter:获取锁对象
      • 无锁、偏向锁、轻量级锁、重量级锁
        • lock_comxchg来控制锁 
    • monitorexit:释放锁对象

synchronized是如何解决线程安全问题

原子性

synchronized锁住共享资源,能够保证一次只允许一个线程操作共享资源。

public class Main {
    static int num = 0;
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; ++ i) { // 不加synchronized 27375
                    synchronized (Main.class) { // 加synchronized 30000
                        num ++;
                    }
                }
            }
        };
        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        Thread t3 = new Thread(runnable);
        t1.start(); t2.start(); t3.start();
        t1.join(); t2.join(); t3.join();
        System.out.println(num);
    }
}

可见性

    使用synchronized以后,Lock原子操作首先会从主存中复制一份共享资源到自己的内存中,然后修改共享资源以后将更新后的值刷新到主内中中,这样一来主内中的值对于其他的线程就可见了。

public class Main {
    static boolean flag = true;
    static int num = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (flag) {
                // 没有synchronized的时候,程序在打印"线程2:flag = false"之后会进入死循环
                // 因为t1会将flag复制一份到自己的内存中,因为没有synchronized
                // 所以flag一直使用的是自己内存中的值,因此当主内存中的值flag=false的时候
                // 当前线程依然没有停下,当使用synchronized的时候,当前线程就回去主内存中更新flag的值
                // 程序自然也就停下来了
                System.out.println("--------"); // 相当于使用了synchronized
                //public void println(String x) {
                //    synchronized (this) { 有synchronized就会刷新t1工作内存中的flag值
                //        print(x);
                //        newLine();
                //    }
                //}
            }
        });
        Thread.sleep(2000);
        t1.start();
        Thread t2 = new Thread(() -> {
            flag = false;
            System.out.println("线程2:flag = false"); //源代码中有synchronized,会执行lock执行原子操作
        });
        t2.start();
    }
}

有序性

  • 指令重排序是为了保证程序的执行效率,加了synchronized以后仍然可能出现指令重排序,但是synchronized可以保证指令重排序以后,单线程情况下最终的结果是不会改变的。

    • synchronized遵循as-if-serial:不管编译器和CPU如何重排序,保证单线程情况下程序的运行结果依然是正确的。
    • volatile遵循happened-befores:不管编译器和CPU如何重排序,保证多线程情况下程序的运行结果是正确的。
    • 这两个规则都是为了保证在不改变程序结果的情况下,尽量的提高程序的并行度。

synchronized特性

可重入性

    一个线程可以重复使用synchronized,synchronized的锁对象中有一个计数器recursions记录当前线程获取了多少次锁,计数器为0的时候就释放锁,这样可以更好的封装代码,避免死锁。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                synchronized (Main.class) {
                    System.out.println("---------------");
                    test();

                }
            }
            private void test() {
                synchronized (Main.class) {
                    System.out.println("=============");
                }
            }
        };
        new Thread(runnable).start();
        new Thread(runnable).start();
    }
}

不可中断性

    当前线程一旦获取了锁,其他线程要想获取锁对象,必须等到当前线程释放锁以后,其他线程才能获取锁,如果当前线程持有锁的时候,其他的线程必须处于堵塞或者等待状态,当前线程不能中断。

synchronized底层原理

      synchronized的锁对象的对象头Mark Down中关联了一个monitor,JVM的线程执行到同步代码块的时候,如果发现锁对象中没有monitor就会创建一个,monitor有两个比较重要的变量:owner(拥有锁的线程)、recursions(计数器)。一个线程拥有了monitor之后,其他的线程只能进入阻塞或者等待状态。monitor是重量级锁。monitorenter:获取锁;monitorexit:释放锁。

Java对象布局

  • 一个对象在内存中分为三个区域:对象头、实例数据、对齐数据
  • 在64位系统中,对象头占16字节,但是JVM默认开启了指针压缩,将对象头压缩为12字节
    • Mark Down(8字节):关于synchronized的所有信息(锁信息)都存储在这里
      • 001:无锁
      • 101:偏向锁
      • 00:轻量级锁
      • 10:重量级锁
    • 类型指针(8字节):class pointer,对象是属于哪一个class类的
    • 实例数据:成员变量占用的字节
    • 对齐数据:整体的字节数不是8的倍数的时候需要补齐到8的倍数个字节
  • 查看对象布局:在项目pom文件中,引入依赖。在Java代码中使用API。
    <dependencies>
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.9</version>
        </dependency>
    </dependencies>
public class Main {
    public static void main(String[] args) {
        Obj obj = new Obj();
        obj.hashCode();
        String s = ClassLayout.parseInstance(obj).toPrintable();
        System.out.println(s);
    }
}
class Obj {
    int val;
    boolean flag;
}
/*
JVM默认开启了指针压缩,将对象头压缩为12个字节(如果不压缩则为16字节)
1265094477
4b67cf4d //对应下面的对象头看一看
syn.Obj object internals:
 OFFSET  SIZE      TYPE DESCRIPTION      VALUE
      0     4           (object header)  01 4d cf 67 (00000001 01001101 11001111 01100111) (1741638913)
      4     4           (object header)  4b 00 00 00 (01001011 00000000 00000000 00000000) (75)
      8     4           (object header)  43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
     12     4       int Obj.val          0 // int占用4个字节
     16     1   boolean Obj.flag         false // boolean占用1个字节
     17     7           (loss due to the next object alignment) // 字节不够8的倍数,补充7个字节
Instance size: 24 bytes //一共24个字节
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
*/

偏向锁

      JDK1.6引入,当锁总是在被同一个线程获取的时候,为了让获取锁的代价更低引入了偏向锁,JDK1.6之后偏向锁是默认开启的,锁会偏向第一个获取到它的线程,然后在对象头中存储这个线程的ID,线程进入同步代码块之前检查当前锁对象是否是偏向锁、锁标志位和ThreadID即可,如果是则直接进入代码块。

import org.openjdk.jol.info.ClassLayout;

public class Main {
    static Main main = new Main();
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < 3; ++ i) { // 反复使用同一个线程获取锁
                    synchronized (main) {
                        System.out.println(ClassLayout.parseInstance(main).toPrintable());
                    }
                }
            }
        });
        thread.start();
    }
}
/*
偏向锁在 Java 6之后是默认启用的,但在应用程序启动几秒钟之后才激活,可以使用 -
XX:BiasedLockingStartupDelay=0 参数关闭延迟,如果确定应用程序中所有锁通常情况下处于竞争
状态,可以通过 XX: -UseBiasedLocking=false 参数关闭偏向锁。
*/

/*
00000101 最低8位,结尾是101,偏向锁
syn.Main object internals:
 OFFSET  SIZE   TYPE DESCRIPTION           VALUE
      0     4        (object header)       05 c8 23 17 (00000101 11001000 00100011 00010111) (388220933)
      4     4        (object header)       00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)       05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

syn.Main object internals:
 OFFSET  SIZE   TYPE DESCRIPTION            VALUE
      0     4        (object header)        05 c8 23 17 (00000101 11001000 00100011 00010111) (388220933)
      4     4        (object header)        00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)        05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

syn.Main object internals:
 OFFSET  SIZE   TYPE DESCRIPTION             VALUE
      0     4        (object header)         05 c8 23 17 (00000101 11001000 00100011 00010111) (388220933)
      4     4        (object header)         00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)         05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


Process finished with exit code 0
*/

    原理:刚开始是无锁状态,线程第一次获取锁对象的时候,JVM会把对象头中偏向锁和锁标志位修改为1和01,表示当前是偏向锁,同时使用CAS操作将这个线程的ID存储到对象头的Mark Down中,如果存储成功,那么线程在反复进入这个同步代码块的时候,JVM就不需要进行任何的操作了,就提高了执行效率。

轻量级锁

    JDK1.6引入,如果有多个线程来竞争锁的时候,要尽量避免重量级锁引起的消耗,偏向锁就可以升级成轻量级锁,偏向锁会被撤销,可以撤销为无锁状态和轻量级锁状态,特定情况下轻量级锁的开销会比较小。但是如果多个线程在同一适合进入临界区,那么轻量级锁就会升级为重量级锁。

  • 等待的线程不多
  • 执行线程消耗的时间比较少

原理:将对象的Mark Down复制到栈帧的Lock Record中,Mark Down更新为指向Lock Record的指针

  • 判断当前是对象是不是无锁状态(hashcode,0,01),如果是JVM在当前线程栈帧中创建出一个Lock Record空间,其中dispalaced hdr存储了Mark Down的信息(hashcode,分代年龄,锁标志),owner指针指向当前对象头。

  • JVM利用CAS操作将Mark Down中的信息更新为指向Lock Record的指针,如果成功竞争到锁以后就将锁标志位修改为00,然后执行同步代码块。

  • 如果不是无锁状态,那么就判断Mark Down是否指向当前线程的Lock Record,如果是则表示当前线程已经拥有了当前对象的锁,直接执行同步代码块,否则就说明锁被其他线程抢占了,此时轻量级锁升级为重量级锁,锁标志位修改为10,其他的线程进入阻塞或者等待状态。

自旋锁&自适应锁

      JDK1.6引入以后默认开启自旋锁。

  • 重量级锁:monitor会阻塞和等待线程,当线程没有竞争到锁的时候,线程就会进入阻塞或者等待状态,只有持有锁的线程执行完同步代码块以后,monitor才回去唤醒其他的线程去竞争锁。

  • 频繁的阻塞和唤醒线程对于CPU来说,消耗很大,这个过程中CPU需要从用户态转换为核心态。在执行同步代码块的时候,由于花费的时间比较短,那么就有可能出现一种情况:同步代码块执行结束,阻塞的线程可能还在进入到阻塞状态的过程中,这段时间内阻塞线程,然后再唤醒线程CPU的消耗就会变大。如果同步代码块中的执行时间比较短,那么可以尝试让竞争锁的线程在外面多循环几次,这样就可以不让其进入阻塞状态,循环一定次数的时候就可以获取到锁,这就是自旋锁。自旋锁默认自旋次数为10次。在自旋的过程中当前线程也是需要占用CPU的资源的,如果同步代码块的执行时间比较短,那么这个线程占用CPU的时间就会比较少,自旋的就可以降低开销。如果同步代码块的执行时间比较长,那么自旋的时间就会比较长,这样一来还不如让线程阻塞。
  • 自适应锁:自旋次数和时间不固定,有JVM决定。

锁消除

    JDK1.6有优化,当没有锁竞争的情况下,即便是又synchronized,那么JIT也会帮助我们自动消除synchronized,也就是取消了获取锁的过程。

锁粗化

public class Main {
    static Main main = new Main();
    public static void main(String[] args) throws Exception {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < 100; i++) {
            sb.append("aa");
        }
    }
}
/*
上面的代码中可能要频繁的获取锁和释放锁,
那么在优化的时候,直接消除append中的synchronized,
将synchronized加入到for循环的外面,把小锁变成大锁。

锁粗化:JVM探测到一连串细小的操作都是用同一个锁对象,
将同步代码块的范围放大,放到这串操作的外面,这样只需要加依次锁即可。
*/

synchronized优化

  • 同步代码块中尽量短,减少同步代码块的执行时间,减少锁的竞争,执行时间变短,那么等待的线程就会变得少一些,这样一来可能轻量级锁和自旋锁就能过解决。

  • 将一个锁拆分为多个锁提高并发度,降低锁的粒度。
    • ConcurrentHashMap:效率高,线程可以并行
    • Hashtable:效率低,只能串行,可以读写分离
  • 读写分离:读不加锁,写入和删除加锁
    • ConCurrentHashMap
    • CopyOnWriteArrayList
    • CopyOnWirteSet  
    • LinkedBlockedQueue:入队和出队使用的是不同的锁
这篇关于【Java并发编程】synchronized关键字的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!