8锁现象其实就是关于锁的8个问题。
8锁现象一直都是JavaJUC面试的高频考点。
简单来说,我们需要永远判断以下三个问题,即
如何判断锁的是谁!永远知道什么锁,锁到底锁的是谁!
下面通过一个简单的场景来彻底玩转8锁现象!
定义一个资源类People,里面有两个经过synchronized上锁后的方法,一个exercise(),方法里面执行输出跑步,一个study(),方法里面输出考研。
1.第一个问题
1.定义一个people对象,两个线程执行 先输出跑步还是考研?
具体代码如下所示:
package com.lock8;
import java.util.concurrent.TimeUnit;
/*
* 1.定义一个people对象,两个线程执行 先执行跑步还是考研?
* */
public class Test1 {
public static void main(String[] args) {
People people = new People();
new Thread(()->{
people.exercise();
},"A").start();
//这里睡眠1s 当然也可以不睡眠
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
people.study();
},"B").start();
}
}
class People{
public synchronized void exercise(){
System.out.println("跑步");
}
public synchronized void study(){
System.out.println("考研");
}
}
结果展示:
原因分析:
原因不仅仅是应为线程A的people.exercise()先执行,究其根本是因为在People资源类的方法里面有synchronized的存在,而synchronized的对象是方法的调用者,我们这里只有一个People实例,即只有一个调用者,所以此时两个方法用的是同一个锁,即谁先拿到该锁便谁先执行!
2.第二个问题
2.还是两个线程执行 但是exercise方法内部首先延迟4秒 此种情况下先输出跑步还是考研?
package com.lock8;
import java.util.concurrent.TimeUnit;
public class Test1 {
public static void main(String[] args) {
People people = new People();
new Thread(()->{
people.exercise();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
people.study();
},"B").start();
}
}
class People{
public synchronized void exercise(){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("跑步");
}
public synchronized void study(){
System.out.println("考研");
}
}
结果展示:
原因分析:
这里虽然在exercise方法内部延迟4s,但是我们在study和exercise方法上面加上了synchronized锁,且只定义了一个people对象,即两个线程的上锁的是同一个对象,即exercise在延迟的同时没有释放锁,所以study方法会在exercise方法执行之后再执行!
3.第三个问题
3.增加了一个普通方法exam(),在exam里面输出考试,同样是两个线程执行,一个线程执行exercise方法,一个线程执行非同步方法exam,是先输出跑步还是考试?
package com.lock8;
import java.util.concurrent.TimeUnit;
public class Test2 {
public static void main(String[] args) {
People1 people = new People1();
new Thread(()->{
people.exercise();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
people.exam();
},"B").start();
}
}
class People1{
public synchronized void exercise(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("跑步");
}
public synchronized void study(){
System.out.println("考研");
}
public void exam(){
System.out.println("考试");
}
}
结果展示:
原因分析:
由于非同步方法exam上面没有上synchronized锁,不受锁的影响,所以在exercise内部延迟的同时,程序就会先输出考试,接着等延迟过后在输出跑步!
4.第四个问题
4.定义两个资源对象,两个线程执行两个同步方法,先输出考研还是跑步?
package com.lock8;
import java.util.concurrent.TimeUnit;
public class Test2 {
public static void main(String[] args) {
People1 people1= new People1();
People1 people2= new People1();
new Thread(()->{
people1.exercise();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
people2.study();
},"B").start();
}
}
class People1{
public synchronized void exercise(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("跑步");
}
public synchronized void study(){
System.out.println("考研");
}
public void exam(){
System.out.println("考试");
}
}
结果展示:
原因分析:
由于synchronized锁的是方法的调用者,但是我们这里定义了两个对象,即每个对象都有一把锁,就有两把锁,所以当第一个对象的exercise方法被延迟后,程序会自动调用第二个对象的study方法,因此锁的不是同一个对象,因此会先输出考研后输出跑步。
5.第五个问题
5.增加两个静态的同步方法,即在exercise方法和study方法前面加一个static关键字,将方法都变为静态方法
package com.lock8;
import java.util.concurrent.TimeUnit;
public class Test3 {
public static void main(String[] args) {
People2 people= new People2();
new Thread(()->{
people.exercise();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
people.study();
},"B").start();
}
}
class People2{
public static synchronized void exercise(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("跑步");
}
public static synchronized void study(){
System.out.println("考研");
}
public void exam(){
System.out.println("考试");
}
}
原因分析:
这里的方法用static关键字修饰,用static修饰的锁,锁的不是方法调用者本身,而锁的是类的模板,即Class模板,而众多的类实例只有一个类模板,因此锁的类模板也是全局唯一。所以只要exercise方法不释放锁,就不会执行study方法。
6.第六个问题
6.这里定义两个类对象,同样增加两个静态同步方法,先输出跑步还是考研?
package com.lock8;
import java.util.concurrent.TimeUnit;
public class Test3 {
public static void main(String[] args) {
People2 people1= new People2();
People2 people2= new People2();
new Thread(()->{
people1.exercise();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
people2.study();
},"B").start();
}
}
class People2{
public static synchronized void exercise(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("跑步");
}
public static synchronized void study(){
System.out.println("考研");
}
public void exam(){
System.out.println("考试");
}
}
结果展示:
原因分析:
由于这次采用的static同步锁,因此锁的是类模板对象,而众多类实例的类模板全局唯一。(当类的.class文件经过类加载器后,就会将类模板初始化加载到JVM的方法区中,而众多的类实例都是通过类模板映射加载,可以输出众多实例的类模板的hashCode来证明!)同样的,由于上锁的是同一个对象,即Class类模板,因此主要exercise方法不释放锁,无论延迟多久,都不会执行study方法,因此会先输出跑步!
7.第七个问题
7.一个静态同步方法,一个普通的同步方法,只定义一个对象,先输出跑步还是考研?
package com.lock8;
import java.util.concurrent.TimeUnit;
public class Test4 {
public static void main(String[] args) {
People3 people= new People3();
new Thread(()->{
people.exercise();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
people.study();
},"B").start();
}
}
class People3{
//静态同步方法
public static synchronized void exercise(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("跑步");
}
//同步方法
public synchronized void study(){
System.out.println("考研");
}
public void exam(){
System.out.println("考试");
}
}
结果展示:
原因分析:
此种情况,普通的同步方法(synchronized)锁的是方法的调用者,而静态同步方法(static synchronized )锁的是Class类模板,即两个方法锁的不是同一个对象,所以后面调用的方法不需要去等待前一个延迟额锁,及时exercise方法不释放锁,我也可以去执行study方法,因此会先输出考研!
简单来说:如果两个锁上锁的是同一个对象,则必须要等到一个方法执行结束才能执行下一个方法,而当两个锁上锁的不是同一个对象,则当一个方法发生执行延迟时,就可以先执行下一个方法!
8.第八个问题
8.一个静态同步方法,一个普通的同步方法,定义两个对象,先输出跑步还是考研?
package com.lock8;
import java.util.concurrent.TimeUnit;
public class Test4 {
public static void main(String[] args) {
People3 people1= new People3();
People3 people2= new People3();
new Thread(()->{
people1.exercise();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
people2.study();
},"B").start();
}
}
class People3{
//静态同步方法
public static synchronized void exercise(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("跑步");
}
//普通同步方法
public synchronized void study(){
System.out.println("考研");
}
public void exam(){
System.out.println("考试");
}
}
结果展示:
原因分析:
此时一个普通同步方法,锁的是方法的调用者一个静态同步方法,锁的是Class类模板,即表示二者上锁的不是同一个对象,所以不用等待exercise方法执行结束再去执行study方法,因此也会先输出考研!
小结
我们可以采用synchronized 给方法上锁,即表示方法是普通同步锁,使用synchronized 上锁的是方法的调用者;
我们也可以采用static synchronized 给方法上锁,表示方法是静态的同步方法锁,使用static synchronized 上锁锁的是资源类的Class类模板,而Class类模板是全局唯一;
规律:如果上锁的是同一个对象,则执行会按照先后顺序依次执行,只有前面的方法执行结束,才能执行后面的方法;如果上锁的不是同一个对象,首先也会按照先后顺序执行,一旦执行的过程中发现有sleep方法,即延迟,有CPU的空闲,就会执行下面与它不是同一个锁的方法,提高CPU执行的效率!