前言
回顾前面:
多线程三分钟就可以入个门了!
Thread源码剖析
多线程基础必要知识点!看了学习多线程事半功倍
本文章主要讲的是Java多线程
加锁机制,有两种:- Synchronized
- 显式Lock
不得不唠叨几句:
- 在《Java核心技术卷 一》是先讲比较难的显式Lock,而再讲的是比较简单的Synchronized
- 而《Java并发编程实战》在前4章零散地讲解了Synchronized,将显式Lock放到了13章
其实都比较坑,如果能先系统讲了Synchronized锁机制,接着讲显式Lock锁机制,那就很容易理解了。也不需要跨那么多章节。
那么接下来我们就开始吧~
一、synchronized锁
1.1synchronized锁是什么?
synchronized是Java的一个关键字,它能够将代码块(方法)锁起来
- 它使用起来是非常简单的,只要在代码块(方法)添加关键字synchronized,即可以实现同步的功能~
public synchronized void test() { // 关注公众号Java3y // doSomething }
synchronized是一种互斥锁
- 一次只能允许一个线程进入被锁住的代码块
synchronized是一种内置锁/监视器锁
- Java中每个对象都有一个内置锁(监视器,也可以理解成锁标记),而synchronized就是使用对象的内置锁(监视器)来将代码块(方法)锁定的!
1.2synchronized用处是什么?
- synchronized保证了线程的原子性。(被保护的代码块是一次被执行的,没有任何线程会同时访问)
- synchronized还保证了可见性。(当执行完synchronized之后,修改后的变量对其他的线程是可见的)
Java中的synchronized,通过使用内置锁,来实现对变量的同步操作,进而实现了对变量操作的原子性和其他线程对变量的可见性,从而确保了并发情况下的线程安全。
1.3synchronized的原理
我们首先来看一段synchronized修饰方法和代码块的代码:
public class Main { //修饰方法 public synchronized void test1(){ } public void test2(){ // 修饰代码块 synchronized (this){ } } }
来反编译看一下:
同步代码块:
- monitorenter和monitorexit指令实现的
同步方法(在这看不出来需要看JVM底层实现)
- 方法修饰符上的ACC_SYNCHRONIZED实现。
synchronized底层是是通过monitor对象,对象有自己的对象头,存储了很多信息,其中一个信息标示是被哪个线程持有。
具体可参考:
- https://blog.csdn.net/chenssy/article/details/54883355
- https://blog.csdn.net/u012465296/article/details/53022317
1.4synchronized如何使用
synchronized一般我们用来修饰三种东西:
- 修饰普通方法
- 修饰代码块
- 修饰静态方法
1.4.1修饰普通方法:
用的锁是Java3y对象(内置锁)
public class Java3y { // 修饰普通方法,此时用的锁是Java3y对象(内置锁) public synchronized void test() { // 关注公众号Java3y // doSomething } }
1.4.2修饰代码块:
用的锁是Java3y对象(内置锁)--->this
public class Java3y { public void test() { // 修饰代码块,此时用的锁是Java3y对象(内置锁)--->this synchronized (this){ // 关注公众号Java3y // doSomething } } }
当然了,我们使用synchronized修饰代码块时未必使用this,还可以使用其他的对象(随便一个对象都有一个内置锁)
所以,我们可以这样干:
public class Java3y { // 使用object作为锁(任何对象都有对应的锁标记,object也不例外) private Object object = new Object(); public void test() { // 修饰代码块,此时用的锁是自己创建的锁Object synchronized (object){ // 关注公众号Java3y // doSomething } } }
上面那种方式(随便使用一个对象作为锁)在书上称之为-->客户端锁,这是不建议使用的。
书上想要实现的功能是:给ArrayList添加一个putIfAbsent()
,这需要是线程安全的。
假定直接添加synchronized是不可行的
使用客户端锁,会将当前的实现与原本的list耦合了:
书上给出的办法是使用组合的方式(也就是装饰器模式)
1.4.3修饰静态方法
获取到的是类锁(类的字节码文件对象):Java3y.class
public class Java3y { // 修饰静态方法代码块,静态方法属于类方法,它属于这个类,获取到的锁是属于类的锁(类的字节码文件对象)-->Java3y.class public synchronized void test() { // 关注公众号Java3y // doSomething } }
1.4.4类锁与对象锁
synchronized修饰静态方法获取的是类锁(类的字节码文件对象),synchronized修饰普通方法或代码块获取的是对象锁。
- 它俩是不冲突的,也就是说:获取了类锁的线程和获取了对象锁的线程是不冲突的!
public class SynchoronizedDemo { //synchronized修饰非静态方法 public synchronized void function() throws InterruptedException { for (int i = 0; i <3; i++) { Thread.sleep(1000); System.out.println("function running..."); } } //synchronized修饰静态方法 public static synchronized void staticFunction() throws InterruptedException { for (int i = 0; i < 3; i++) { Thread.sleep(1000); System.out.println("Static function running..."); } } public static void main(String[] args) { final SynchoronizedDemo demo = new SynchoronizedDemo(); // 创建线程执行静态方法 Thread t1 = new Thread(() -> { try { staticFunction(); } catch (InterruptedException e) { e.printStackTrace(); } }); // 创建线程执行实例方法 Thread t2 = new Thread(() -> { try { demo.function(); } catch (InterruptedException e) { e.printStackTrace(); } }); // 启动 t1.start(); t2.start(); } }
结果证明:类锁和对象锁是不会冲突的!
1.5重入锁
我们来看下面的代码:
public class Widget { // 锁住了 public synchronized void doSomething() { ... } } public class LoggingWidget extends Widget { // 锁住了 public synchronized void doSomething() { System.out.println(toString() + ": calling doSomething"); super.doSomething(); } }
- 当线程A进入到LoggingWidget的
doSomething()
方法时,此时拿到了LoggingWidget实例对象的锁。 - 随后在方法上又调用了父类Widget的
doSomething()
方法,它又是被synchronized修饰。 - 那现在我们LoggingWidget实例对象的锁还没有释放,进入父类Widget的
doSomething()
方法还需要一把锁吗?
不需要的!
因为锁的持有者是“线程”,而不是“调用”。线程A已经是有了LoggingWidget实例对象的锁了,当再需要的时候可以继续“开锁”进去的!
这就是内置锁的可重入性。
1.6释放锁的时机
- 当方法(代码块)执行完毕后会自动释放锁,不需要做任何的操作。
- 当一个线程执行的代码出现异常时,其所持有的锁会自动释放。
- 不会由于异常导致出现死锁现象~