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