2.多线程锁
多线程锁
乐观锁与悲观锁
悲观锁:认为 在使用数据的时候一定会有线程来修改数据,因此在获取数据的时候会先加索,确保数据不会被别的线程修改
synchronized和Lock的实现类都是悲观锁
使用场景:适合操作写多的场景,先加锁可以保证写操作时数据正确,显示锁定后再操作同步资源
乐观锁:认为自己在使用数据时不会有别的线程来修改数据或者资源,所以不会添加锁。在java中时通过使用无锁变成来实现,只是在更新的时候去判断之前有没有别的线程更新了这个数据
没有更新:将数据写入
更新了:放弃修改、重新抢锁等
实现方式:1、版本号机制 version 2、CAS比较交换
使用场景:读操作比较多的场景,不加锁能大幅提升读的效率,乐观锁是一种无锁算法。
synchronized 八锁
synchronized 使用场景:
- 作用于实例方法,当前实例加锁,进入同步代码块需要先获取当前实例锁
- 作用于代码块,对代码块加锁synchronized (this) {}
- 作用于静态代码,当前类加锁,进入同步代码块需要获取当前类的Class对象
案例:两个线程去操作同一个资源类
- 锁对象
- 标准访问,两个线程访问同一个对象的不同同步方法
class Phone{
public synchronized void sendSMS(){
System.out.println("发短信");
}
public synchronized void sendEmail(){
System.out.println("发邮件");
}
public void call(){
System.out.println("打电话");
}
}
public class EightLock {
public static void main(String[] args) {
Phone phone1 = new Phone();
new Thread(phone1::sendEmail, "a").start();
new Thread(phone1::sendSMS, "b").start();
}
}
输出结果:
发邮件
发短信
- 在某一个同步方法中加入暂停三秒,是先执行发邮件还是还是发短信
class Phone {
public synchronized void sendSMS() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("发短信");
}
public synchronized void sendEmail() {
System.out.println("发邮件");
}
public void call() {
System.out.println("打电话");
}
}
public class EightLock {
public static void main(String[] args) {
Phone phone1 = new Phone();
new Thread(phone1::sendEmail, "a").start();
new Thread(phone1::sendSMS, "b").start();
}
}
结果:
发邮件
发短信
说明:一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
- 调用普通方法和同步方法
class Phone {
public synchronized void sendSMS() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("发短信");
}
public synchronized void sendEmail() {
System.out.println("发邮件");
}
public void call() {
System.out.println("打电话");
}
}
public class EightLock {
public static void main(String[] args) {
Phone phone1 = new Phone();
// 同步方法 执行时间3秒
new Thread(phone1::sendSMS, "a").start();
// 普通方法
new Thread(phone1::call, "b").start();
}
}
结果:
打电话
发短信
说明:普通方法与锁无关,直接访问
- 有两个对象分别调用同步方法
class Phone {
public synchronized void sendSMS() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("发短信");
}
public synchronized void sendEmail() {
System.out.println("发邮件");
}
public void call() {
System.out.println("打电话");
}
}
public class EightLock {
public static void main(String[] args) {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(phone1::sendSMS, "a").start();
new Thread(phone2::sendEmail, "b").start();
}
}
结果:
发邮件
发短信
说明:不同对象调用,锁对象不同
- 锁模板(类|类.Class)
- 两个静态同步方法,两个线程调用一个对象
class Phone {
public static synchronized void sendSMS() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("发短信");
}
public static synchronized void sendEmail() {
System.out.println("发邮件");
}
public void call() {
System.out.println("打电话");
}
}
public class EightLock {
public static void main(String[] args) {
Phone phone1 = new Phone();
new Thread(() -> phone1.sendSMS(), "a").start();
new Thread(() -> phone1.sendEmail(), "b").start();
}
}
结果:
发短信
发邮件
- 两个静态同步方法,两个线程调用两个对象
class Phone {
public static synchronized void sendSMS() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("发短信");
}
public static synchronized void sendEmail() {
System.out.println("发邮件");
}
public void call() {
System.out.println("打电话");
}
}
public class EightLock {
public static void main(String[] args) {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(() -> phone1.sendSMS(), "a").start();
new Thread(() -> phone2.sendEmail(), "b").start();
}
}
结果:
发短信
发邮件
说明:三种synchronized锁的内容有一些差别:对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁—>实例对象本身,对于静态同步方法,锁的是当前类的CLass对象,如iPhone.class唯一的一个模板对于同步方法块,锁的是synchronized括号内的对象
- 一个静态方法,一个普通方法,两个线程操作一个对象
class Phone {
public static synchronized void sendSMS() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("发短信");
}
public static synchronized void sendEmail() {
System.out.println("发邮件");
}
public void call() {
System.out.println("打电话");
}
}
public class EightLock {
public static void main(String[] args) {
Phone phone1 = new Phone();
new Thread(() -> phone1.sendSMS(), "a").start();
new Thread(phone1::call, "b").start();
}
}
结果:
打电话
发短信
- 一个静态方法,一个普通方法,两个线程操作两个对象
public static synchronized void sendSMS() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("发短信");
}
public static synchronized void sendEmail() {
System.out.println("发邮件");
}
public void call() {
System.out.println("打电话");
}
}
public class EightLock {
public static void main(String[] args) {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(() -> phone1.sendSMS(), "a").start();
new Thread(() -> phone2.call(), "b").start();
}
}
结果
打电话
发短信
说明:
当一个线程试图访问同步代码时它首先必须得到锁,正常退出或抛出异常时必须释放锁。
所有的普通同步方法用的都是同一把锁—>实例对象本身,就是new出来的具体实例对象本身,本类this 也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
所有的静态同步方法用的也是同一把锁—>类对象本身,就是我们说过的唯一模板Class
具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的
但是一旦一个静态同步方法获得锁后,其他的静态同步方法都必须等待该方法释放锁才能获得锁
- 类锁与对象锁说明
jvm将类加载到方法区创建.Class有且只有一份,jvm通过Class模板创建不同的实例对象存放在堆空间
所以对象锁和类锁是不一样的
synchronized 底层字节码
- 修饰代码块
public void lockTest();
Code:
0: aload_0
1: getfield #3 // Field object:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter
7: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
10: ldc #5 // String hello world!
12: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
15: aload_1
16: monitorexit
17: goto 25
20: astore_2
21: aload_1
22: monitorexit
23: aload_2
24: athrow
25: return
底层实现:使用 monitorenter monitorexit 指令
一般一个monitorenter对应两个 monitorexit 指令
- 修饰实例方法
public synchronized void lockTest1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #5 // String hello world!
5: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 16: 0
line 17: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this LLockUse;
方法有 ACC_SYNCHRONIZED 标识
调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置。
如果设置了,执行线程会将先持有monitor锁,然后再执行方法,
最后在方法完成(无论是正常完成还是非正常完成)时释放monitor
- 静态方法
public synchronized void lockTest1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #5 // String 1 hello world!
5: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 16: 0
line 17: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this LLockUse;
public static synchronized void lockTest2();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=0, args_size=0
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #7 // String 2 hello world!
5: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 21: 0
line 22: 8
ACC_STATIC, ACC_SYNCHRONIZED 访问标志区分该方法是否静态同步方法
公平锁和非公平锁
### 公平锁
多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的Lock lock = new ReentrantLock(true);//true表示公平锁,先来先得
非公平锁
多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或者饥饿的状态(某个线程一直得不到锁)Lock lock = new ReentrantLock(false);/lfalse表示非公平锁,后来的也可能先获得锁Lock lock = new ReentrantLock();//默认非公平锁
为什么会有公平锁和非公平锁?为什么默认是非公平锁
- 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲状态时间。
- 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。
什么时候使用公平锁/非公平锁?
如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;否则那就用公平锁。
可重入锁(递归锁)
是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
如果是1个有synchronized修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
可重入锁种类
-
隐式锁(synchronized关键字使用的锁)
指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
简单的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的同步块,同步方法都支持
-
显示锁(Lock中的ReentrantLock)
synchronized实现可重入锁原理
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenter时,如果目标锁对象(资源)的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。死锁及相关排查
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
排查:命令 jps jstack 图形化 jconsole