JAVA面试题 请谈谈你对Sychronized关键字的理解?

Java基础

浏览数:169

2019-7-25

AD:资源代下载服务

面试官:sychronized关键字有哪些特性?

应聘者:

  • 可以用来修饰方法;

  • 可以用来修饰代码块;

  • 可以用来修饰静态方法;

  • 可以保证线程安全;

  • 支持锁的重入;

  • sychronized使用不当导致死锁;

了解sychronized之前,我们先来看一下几个常见的概念:内置锁、互斥锁、对象锁和类锁。

内置锁

在Java中每一个对象都可以作为同步的锁,那么这些锁就被称为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。

互斥锁

内置锁同时也是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B抛出异常或者正常执行完毕释放这个锁;如果B线程不释放这个锁,那么A线程将永远等待下去。

对象锁和类锁

对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,两个锁实际是有很大的区别的。

  • 对象锁是用于对象实例方法;

  • 类锁是用于类的静态方法或者一个类的class对象上的

一个对象无论有多少个同步方法区,它们共用一把锁,某一时刻某个线程已经进入到某个synchronzed方法,那么在该方法没有执行完毕前,其他线程无法访问该对象的任何synchronzied 方法的,但可以访问非synchronzied方法。

如果synchronized方法是static的,那么当线程访问该方法时,它锁的并不是synchronized方法所在的对象,而是synchronized方法所在对象的对应的Class对象,

因为java中无论一个类有多少个对象,这些对象会对应唯一一个Class对象,因此当线程分别访问同一个类的两个对象的static,synchronized方法时,他们的执行也是按顺序来的,也就是说一个线程先执行,一个线程后执行。

 synchronized的用法:修饰方法和修饰代码块,下面分别分析这两种用法在对象锁和类锁上的效果。

对象锁的synchronized修饰方法和代码块

public class TestSynchronized {
    public void test1() {
        synchronized (this) {
            int i = 5;
            while (i-- > 0) {
                System.out.println(Thread.currentThread().getName() + " : " + i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException ie) {
                }
            }
        }
    }

    public synchronized void test2() {
        int i = 5;
        while (i-- > 0) {
            System.out.println(Thread.currentThread().getName() + " : " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException ie) {
            }
        }
    }

    public static void main(String[] args) {
        final TestSynchronized myt2 = new TestSynchronized();
        Thread test1 = new Thread(new Runnable() {
            public void run() {
                myt2.test1();
            }
        }, "test1");
        Thread test2 = new Thread(new Runnable() {
            public void run() {
                myt2.test2();
            }
        }, "test2");
        test1.start();
        test2.start();
    }
}

打印结果如下:

test2 : 4
test2 : 3
test2 : 2
test2 : 1
test2 : 0
test1 : 4
test1 : 3
test1 : 2
test1 : 1
test1 : 0

上述的代码,第一个方法用了同步代码块的方式进行同步,传入的对象实例是this,表明是当前对象;第二个方法是修饰方法的方式进行同步。因为第一个同步代码块传入的this,所以两个同步代码所需要获得的对象锁都是同一个对象锁,下面main方法时分别开启两个线程,分别调用test1和test2方法,那么两个线程都需要获得该对象锁,另一个线程必须等待。上面也给出了运行的结果可以看到:直到test2线程执行完毕,释放掉锁,test1线程才开始执行。这里test2方法先抢到CPU资源,故它先执行,它获得了锁,它执行完毕后,test1才开始执行。

如果我们把test2方法的synchronized关键字去掉,执行结果会如何呢? 

test1 : 4
test2 : 4
test2 : 3
test2 : 2
test2 : 1
test2 : 0
test1 : 3
test1 : 2
test1 : 1
test1 : 0

我们可以看到,结果输出是交替着进行输出的,这是因为,某个线程得到了对象锁,但是另一个线程还是可以访问没有进行同步的方法或者代码。进行了同步的方法(加锁方法)和没有进行同步的方法(普通方法)是互不影响的,一个线程进入了同步方法,得到了对象锁,其他线程还是可以访问那些没有同步的方法(普通方法)。

类锁的修饰(静态)方法和代码块  

public class TestSynchronized {
    public void test1() {
        synchronized (TestSynchronized.class) {
            int i = 5;
            while (i-- > 0) {
                System.out.println(Thread.currentThread().getName() + " : " + i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException ie) {
                }
            }
        }
    }

    public static synchronized void test2() {
        int i = 5;
        while (i-- > 0) {
            System.out.println(Thread.currentThread().getName() + " : " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException ie) {
            }
        }
    }

    public static void main(String[] args) {
        final TestSynchronized myt2 = new TestSynchronized();
        Thread test1 = new Thread(new Runnable() {
            public void run() {
                myt2.test1();
            }
        }, "test1");
        Thread test2 = new Thread(new Runnable() {
            public void run() {
                TestSynchronized.test2();
            }
        }, "test2");
        test1.start();
        test2.start();
    }
}

输出结果如下:

test1 : 4
test1 : 3
test1 : 2
test1 : 1
test1 : 0
test2 : 4
test2 : 3
test2 : 2
test2 : 1
test2 : 0

类锁修饰方法和代码块的效果和对象锁是一样的,因为类锁只是一个抽象出来的概念,只是为了区别静态方法的特点,因为静态方法是所有对象实例共用的,所以对应着synchronized修饰的静态方法的锁也是唯一的,所以抽象出来个类锁。其实这里的重点在下面这块代码,synchronized同时修饰静态和非静态方法

public class TestSynchronized {
    public synchronized void test1() {
        int i = 5;
        while (i-- > 0) {
            System.out.println(Thread.currentThread().getName() + " : " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException ie) {
            }
        }
    }

    public static synchronized void test2() {
        int i = 5;
        while (i-- > 0) {
            System.out.println(Thread.currentThread().getName() + " : " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException ie) {
            }
        }
    }

    public static void main(String[] args) {
        final TestSynchronized myt2 = new TestSynchronized();
        Thread test1 = new Thread(new Runnable() {
            public void run() {
                myt2.test1();
            }
        }, "test1");
        Thread test2 = new Thread(new Runnable() {
            public void run() {
                TestSynchronized.test2();
            }
        }, "test2");
        test1.start();
        test2.start();
    }
}

输出结果如下:

test1 : 4
test2 : 4
test1 : 3
test2 : 3
test2 : 2
test1 : 2
test2 : 1
test1 : 1
test1 : 0
test2 : 0

上面代码synchronized同时修饰静态方法和实例方法,但是运行结果是交替进行的,这证明了类锁和对象锁是两个不一样的锁,控制着不同的区域,它们是互不干扰的。同样,线程获得对象锁的同时,也可以获得该类锁,即同时获得两个锁,这是允许的。

synchronized是如何保证线程安全的  

如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

我们通过一个案例,演示线程的安全问题:

我们来模拟一下火车站卖票过程,总共有100张票,总共有三个窗口卖票。

public class SellTicket {
    public static void main(String[] args) {
        // 创建票对象
        Ticket ticket = new Ticket();
        // 创建3个窗口
        Thread t1 = new Thread(ticket, "窗口1");
        Thread t2 = new Thread(ticket, "窗口2");
        Thread t3 = new Thread(ticket, "窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

// 模拟票
class Ticket implements Runnable {
    // 共100票
    int ticket = 100;

    @Override
    public void run() {
        // 模拟卖票
        while (true) {
            if (ticket > 0) {
                // 模拟选坐的操作
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在卖票:"
                        + ticket--);
            }
        }
    }
}

运行结果发现:上面程序出现了问题

  • 票出现了重复的票

  • 错误的票 0、-1

其实,线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

那么出现了上述问题,我们应该如何解决呢?

线程同步(线程安全处理Synchronized)

java中提供了线程同步机制,它能够解决上述的线程安全问题。

线程同步的方式有两种:

  • 方式1:同步代码块

  • 方式2:同步方法

同步代码块

同步代码块: 在代码块声明上 加上synchronized

synchronized (锁对象) {
    可能会产生线程安全问题的代码
}

同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能够保证线程安全。

使用同步代码块,对火车站卖票案例中Ticket类进行如下代码修改:

public class SellTicket {
    public static void main(String[] args) {
        // 创建票对象
        Ticket ticket = new Ticket();
        // 创建3个窗口
        Thread t1 = new Thread(ticket, "窗口1");
        Thread t2 = new Thread(ticket, "窗口2");
        Thread t3 = new Thread(ticket, "窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

// 模拟票
class Ticket implements Runnable {
    // 共100票
    int ticket = 100;

    Object lock = new Object();

    @Override
    public void run() {
        // 模拟卖票
        while (true) {
            // 同步代码块
            synchronized (lock) {
                if (ticket > 0) {
                    // 模拟选坐的操作
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()
                            + "正在卖票:" + ticket--);
                }
            }
        }
    }
}

当使用了同步代码块后,上述的线程的安全问题,解决了。

同步方法

同步方法:在方法声明上加上synchronized

public synchronized void method(){
       可能会产生线程安全问题的代码
}

同步方法中的锁对象是 this

使用同步方法,对火车站卖票案例中Ticket类进行如下代码修改:

public class SellTicket {
    public static void main(String[] args) {
        // 创建票对象
        Ticket ticket = new Ticket();
        // 创建3个窗口
        Thread t1 = new Thread(ticket, "窗口1");
        Thread t2 = new Thread(ticket, "窗口2");
        Thread t3 = new Thread(ticket, "窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

// 模拟票
class Ticket implements Runnable {
    // 共100票
    int ticket = 100;

    Object lock = new Object();

    @Override
    public void run() {
        // 模拟卖票
        while (true) {
            // 同步方法
            method();
        }
    }

    // 同步方法,锁对象this
    public synchronized void method() {
        if (ticket > 0) {
            // 模拟选坐的操作
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在卖票:"
                    + ticket--);
        }
    }
}

  

synchronized支持锁的重入吗?  

我们先来看下面一段代码:

public class ReentrantLockDemo {
    public synchronized void a() {
        System.out.println("a");
        b();
    }

    private synchronized void b() {
        System.out.println("b");
    }

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                ReentrantLockDemo d = new ReentrantLockDemo();
                d.a();
            }
        }).start();
    }
}

上述的代码,我们分析一下,两个方法,方法a和方法b都被synchronized关键字修饰,锁对象是当前对象实例,按照上文我们对synchronized的了解,如果调用方法a,在方法a还没有执行完之前,我们是不能执行方法b的,方法a必须先释放锁,方法b才能执行,方法b处于等待状态,那样不就形成死锁了吗?那么事实真的如分析一致吗?

运行结果发现:

a
b

代码很快就执行完了,实验结果与分析不一致,这就引入了另外一个概念:重入锁。在 java 内部,同一线程在调用自己类中其他 synchronized 方法/块或调用父类的 synchronized 方法/块都不会阻碍该线程的执行。就是说同一线程对同一个对象锁是可重入的,而且同一个线程可以获取同一把锁多次,也就是可以多次重入。在JDK1.5后对synchronized关键字做了相关优化。

synchronized死锁问题

同步锁使用的弊端:当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉。

synchronzied(A锁){
    synchronized(B锁){
    }
}

我们进行下死锁情况的代码演示:

public class DeadLock {
    Object obj1 = new Object();
    Object obj2 = new Object();

    public void a() {
        synchronized (obj1) {
            synchronized (obj2) {
                System.out.println("a");
            }
        }
    }

    public void b() {
        synchronized (obj2) {
            synchronized (obj1) {
                System.out.println("b");
            }
        }
    }

    public static void main(String[] args) {
        DeadLock d = new DeadLock();
        new Thread(new Runnable() {
            @Override
            public void run() {
                d.a();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                d.b();
            }
        }).start();
    }
}

上述的代码,我们分析一下,两个方法,我们假设两个线程T1,T2,T1运行到方法a了,拿到了obj1这把锁,此时T2运行到方法b了,拿到了obj2这把锁,T1要往下执行,就必须等待T2释放了obj2这把锁,线程T2要往下面执行,就必须等待T1释放了持有的obj1这把锁,他们两个互相等待,就形成了死锁。

为了演示的更明白,需要让两个方法执行过程中睡眠10ms,要不然很难看到现象,因为计算机执行速度贼快

public class DeadLock {
    Object obj1 = new Object();
    Object obj2 = new Object();

    public void a() {
        synchronized (obj1) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (obj2) {
                System.out.println("a");
            }
        }
    }

    public void b() {
        synchronized (obj2) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (obj1) {
                System.out.println("b");
            }
        }
    }

    public static void main(String[] args) {
        DeadLock d = new DeadLock();
        new Thread(new Runnable() {
            @Override
            public void run() {
                d.a();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                d.b();
            }
        }).start();
    }

}

感兴趣的童鞋,下去可以试一下,程序执行不完,永远处于等待状态。

总结

  • sychronized是隐式锁,是JVM底层支持的关键字,由JVM来维护;

  • 单体应用下,多线程并发操作时,使用sychronized关键字可以保证线程安全;

  • sychronized可以用来修饰方法和代码块,此时锁是当前对象实例,修饰静态方法时,锁是对象的class字节码文件;

  • 一个线程进入了sychronized修饰的同步方法,得到了对象锁,其他线程还是可以访问那些没有同步的方法(普通方法);

  • sychronized支持锁的重入;

  

作者:Java蚂蚁

出处:https://www.cnblogs.com/marsitman/p/11235552.html

版权:转载请在文章明显位置注明作者及出处。

作者:Java蚂蚁