表现为三个方面:原子性,可见性和有序性。
原子性:对于共享变量,当前线程一旦操作,在其他线程看来是不可分割的。当前线程要么操作完毕,要么没有操作,操作过程中的中间结果,其他线程是不可见的。
可见性:一个线程对共享变量进行修改以后,另外一个线程没办法立即看到。由于每一个线程都从主内存复制了一份共享变量到自己的内存中,所以就导致了可见性问题。
有序性:编译器为了优化性能,有时候会改变程序中语句的先后顺序,这种优化不会影响程序的执行结果,但是有时候也可能导致线程安全问题。
synchronized是一个内部锁,它可以修饰方法和代码块。在多线程环境下,同步方法或者代码块在同一个时刻只能有一个线程执行,其余线程只能阻塞或者等待获取锁,也就是乐观锁。
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,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的锁对象的对象头Mark Down中关联了一个monitor,JVM的线程执行到同步代码块的时候,如果发现锁对象中没有monitor就会创建一个,monitor有两个比较重要的变量:owner(拥有锁的线程)、recursions(计数器)。一个线程拥有了monitor之后,其他的线程只能进入阻塞或者等待状态。monitor是重量级锁。monitorenter:获取锁;monitorexit:释放锁。
<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,然后执行同步代码块。
JDK1.6引入以后默认开启自旋锁。
重量级锁:monitor会阻塞和等待线程,当线程没有竞争到锁的时候,线程就会进入阻塞或者等待状态,只有持有锁的线程执行完同步代码块以后,monitor才回去唤醒其他的线程去竞争锁。
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探测到一连串细小的操作都是用同一个锁对象, 将同步代码块的范围放大,放到这串操作的外面,这样只需要加依次锁即可。 */
同步代码块中尽量短,减少同步代码块的执行时间,减少锁的竞争,执行时间变短,那么等待的线程就会变得少一些,这样一来可能轻量级锁和自旋锁就能过解决。