Java中锁的概念
- 自旋锁:为了不放弃CPU执行事件,循环的使用CAS技术对数据尝试进行更新,直至成功。
- 悲观锁:假定会发生并发冲突,同步所有对数据的相关操作,从读数据就开始上锁。
- 乐观锁:假定没有冲突,在修改数据时如果发现数据和之前获取的不一致,则读最新数据,修改后重试修改。
- 独享锁(写):给资源加上写锁,线程可以修改资源,其他线程不能再加锁。(单写)
- 共享锁(读):给资源加上读锁后只能读不能改,其他线程也只能加读锁,不能加写锁。(多读)
- 可重入锁、不可重入锁:线程拿到一把锁之后,可以自由进入同一把锁所同步的其他代码。
- 公平锁、非公平锁:争抢锁的顺序,如果是按先来后到,则为公平。
几种重要的锁实现方式:Synchronized
、ReentrantLock
、ReentrantReadWriteLock
。
同步关键字synchronized
属于最基本的线程通信机制,基于对象监视器实现的。
Java中的每个对象都与一个监视器相关联,一个线程可以锁定或解锁。
一次只有一个线程可以锁定监视器。
试图锁定该监视器的任何其他线程都会被阻塞,直到它们可以获得该监视器上的锁定为止。
特性: 可重入、独享、悲观锁
锁的范围: 类锁、对象锁、锁消除、锁粗化。
提示:同步关键字,不仅是实现同步,根据JMM规定还能保证可见性(读取最新主内存数据,结束后写入主内存)。
同步关键字加锁原理
同步关键字加锁原理 - 轻量级锁
偏向锁到轻量级锁
重量级锁 - 监视器(monitor)
过程解析
第一次取得锁时会在对象的Markword
标志为偏向锁,此时同一个用户来使用,不用进行取锁操作,直到有其他的用户也想取这个锁,对象的Markword
标志就会升级为轻量级锁,若上一个用户正在使用,这个用户自旋重锁,自旋CAS,若没有在使用,则直接开始竞争,若在并发高时,上个用户使用时间过长,这个用户不断的自旋重锁达到一定的次数(可以自己设置),则会升级为重量级锁,在每次自旋的时候会等待一段时间,以节约CPU资源。
锁消除、锁粗化
- 锁消除:在热点代码,运行时Jit编译优化,将两个锁变成一个更大的锁包含两个内容。
- 锁粗化:在热点代码,运行时Jit编译优化,把锁消除,进入时不需要监视器。
类锁和对象锁
我们都知道类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。
静态方法锁和方法锁的区别就在于,类的每个对象静态方法是一样的,所以静态方法锁相当于类锁,而类的每个对象方法是不一样的,所以方法锁就相当于对象锁。
//对象锁
public class ObjectSyncDemo1 {
public void test1() {
synchronized (this) {
try {
System.out.println(Thread.currentThread() + " 我开始执行");
Thread.sleep(3000L);
System.out.println(Thread.currentThread() + " 我执行结束");
} catch (InterruptedException e) {
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
new ObjectSyncDemo1().test1();
}).start();
Thread.sleep(1000L); // 等1秒钟,让前一个线程启动起来
new Thread(() -> {
new ObjectSyncDemo1().test1();
}).start();
}
}
输出结果:
Thread[Thread-0,5,main] 我开始执行
Thread[Thread-1,5,main] 我开始执行
Thread[Thread-0,5,main] 我执行结束
Thread[Thread-1,5,main] 我执行结束
//类锁
public class ObjectSyncDemo1 {
public void test1() {
synchronized (ObjectSyncDemo1.class) {
try {
System.out.println(Thread.currentThread() + " 我开始执行");
Thread.sleep(3000L);
System.out.println(Thread.currentThread() + " 我执行结束");
} catch (InterruptedException e) {
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
new ObjectSyncDemo1().test1();
}).start();
Thread.sleep(1000L); // 等1秒钟,让前一个线程启动起来
new Thread(() -> {
new ObjectSyncDemo1().test1();
}).start();
}
}
输出结果:
Thread[Thread-0,5,main] 我开始执行
Thread[Thread-0,5,main] 我执行结束
Thread[Thread-1,5,main] 我开始执行
Thread[Thread-1,5,main] 我执行结束
//方法锁
public class ObjectSyncDemo1 {
public synchronized void test1() {
try {
System.out.println(Thread.currentThread() + " 我开始执行");
Thread.sleep(3000L);
System.out.println(Thread.currentThread() + " 我执行结束");
} catch (InterruptedException e) {
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
new ObjectSyncDemo1().test1();
}).start();
Thread.sleep(1000L); // 等1秒钟,让前一个线程启动起来
new Thread(() -> {
new ObjectSyncDemo1().test1();
}).start();
}
}
输出结果:
Thread[Thread-0,5,main] 我开始执行
Thread[Thread-1,5,main] 我开始执行
Thread[Thread-0,5,main] 我执行结束
Thread[Thread-1,5,main] 我执行结束
//静态方法锁
public class ObjectSyncDemo1 {
public static synchronized void test1() {
try {
System.out.println(Thread.currentThread() + " 我开始执行");
Thread.sleep(3000L);
System.out.println(Thread.currentThread() + " 我执行结束");
} catch (InterruptedException e) {
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
new ObjectSyncDemo1().test1();
}).start();
Thread.sleep(1000L); // 等1秒钟,让前一个线程启动起来
new Thread(() -> {
new ObjectSyncDemo1().test1();
}).start();
}
}
输出结果:
Thread[Thread-0,5,main] 我开始执行
Thread[Thread-0,5,main] 我执行结束
Thread[Thread-1,5,main] 我开始执行
Thread[Thread-1,5,main] 我执行结束
synchronized的缺陷:当某个线程进入同步方法获得对象锁,那么其他线程访问这里对象的同步方法时,必须等待或者阻塞,这对高并发的系统是致命的,这很容易导致系统的崩溃。如果某个线程在同步方法里面发生了死循环,那么它就永远不会释放这个对象锁,那么其他线程就要永远的等待。这是一个致命的问题。
当然同步方法和同步代码块都会有这样的缺陷,只要用了synchronized关键字就会有这样的风险和缺陷。既然避免不了这种缺陷,那么就应该将风险降到最低。这也是同步代码块在某种情况下要优于同步方法的方面。例如在某个类的方法里面:这个类里面声明了一个对象实例,SynObject so=new SynObject();在某个方法里面调用了这个实例的方法so.testsy();但是调用这个方法需要进行同步,不能同时有多个线程同时执行调用这个方法。
这时如果直接用synchronized修饰调用了so.testsy();代码的方法,那么当某个线程进入了这个方法之后,这个对象其他同步方法都不能给其他线程访问了。假如这个方法需要执行的时间很长,那么其他线程会一直阻塞,影响到系统的性能。
如果这时用synchronized来修饰代码块:synchronized(so){so.testsy();},那么这个方法加锁的对象是so这个对象,跟执行这行代码的对象没有关系,当一个线程执行这个方法时,这对其他同步方法时没有影响的,因为他们持有的锁都完全不一样。
可重入锁
public class ObjectSyncDemo2 {
public synchronized void test1(Object arg) {
System.out.println(Thread.currentThread() + " 我开始执行 " + arg);
if (arg == null) {
test1(new Object());
}
System.out.println(Thread.currentThread() + " 我执行结束" + arg);
}
public static void main(String[] args) throws InterruptedException {
new ObjectSyncDemo2().test1(null);
}
}
输出结果:
Thread[main,5,main] 我开始执行 null
Thread[main,5,main] 我开始执行 java.lang.Object@31b7dea0
Thread[main,5,main] 我执行结束java.lang.Object@31b7dea0
Thread[main,5,main] 我执行结束null
我们可以看出synchronized是个可重入锁,在某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。