天天看点

synchronized介绍,及相关面试题

作者:程序员的秃头之路

synchronized 简介

synchronized 是Java中的一个关键字,用于控制多线程并发访问同步代码的机制。它可以用来避免由于多线程同时访问同一代码块而导致的数据不一致问题,确保了代码块在任一时间点只能被一个线程访问。

作用:

1、 确保线程安全:synchronized关键字可以保证被它修饰的代码块或者方法在任一时间点只能被一个线程访问,从而避免了线程安全问题。 2、 锁的释放与获取:当一个线程获取到一个对象或者类的synchronized锁后,其他试图访问这个对象或者类的线程将会阻塞,直到锁被释放。当一个线程结束或者发生异常时,它持有的所有synchronized锁都会被自动释放。 3、 可见性:synchronized关键字可以保证线程之间的可见性。当一个线程修改了一个共享变量的值,新值对所有其他线程都是可见的。

地位:

synchronized在Java中是重要的同步机制,它是基于入口控制的一种方式,可以在方法和代码块级别应用。在Java的早期版本中,它是唯一的线程同步机制。然而,在Java 5以后,java.util.concurrent包引入了更多的并发工具类,如ReentrantLock,Semaphore等。虽然这些新的工具提供了更多的灵活性,但是synchronized仍然是一个简单而强大的同步工具,尤其是在对性能要求不是特别高的情况下。

不控制并发的影响:

如果我们不适当地控制并发,可能会导致各种问题,如数据不一致、脏读、丢失更新等。这些问题可能会导致应用程序的行为出现错误或者不可预期的情况。对于关键部分的代码,如修改共享数据的代码,我们需要使用synchronized或者其他同步机制来避免这些问题。如果不这么做,可能会导致严重的问题,甚至可能导致系统崩溃。

用法

在Java中,我们通常使用synchronized关键字来创建对象锁(实例锁)和类锁。以下是一些例子:

对象锁(实例锁):

对象锁是针对对象实例的,如果一个对象实例采用了对象锁,那么在一个时间点只能有一个线程能访问这个对象的synchronized方法或者synchronized代码块。

public class SyncExample {
    public synchronized void syncMethod() {
        // 一些需要同步的代码
    }
}

SyncExample example = new SyncExample();
example.syncMethod();           

在上述示例中,syncMethod方法是synchronized的,当一个线程调用这个方法时,它会获取example对象的锁,然后其他线程如果想要调用这个方法,就必须等待这个线程释放这个锁。

类锁:

类锁是针对类的,也就是说在一个时间点只能有一个线程能访问这个类的synchronized静态方法或者synchronized代码块。

public class SyncExample {
    public static synchronized void syncMethod() {
        // 一些需要同步的代码
    }
}

SyncExample.syncMethod();           

在上述示例中,syncMethod方法是静态的synchronized方法,当一个线程调用这个方法时,它会获取SyncExample类的锁,然后其他线程如果想要调用这个方法,就必须等待这个线程释放这个锁。

注意,对象锁和类锁是两种不同的锁,他们不会互相影响。也就是说,一个线程获取了对象锁并不会阻止其他线程获取类锁,反之亦然。

多线程访问同步方法的7种情况

1、 两个线程同时访问一个对象的相同的synchronized方法:

public class SyncExample {
    public synchronized void syncMethod() {
        // 一些需要同步的代码
    }
}

class MyRunnable1 implements Runnable {
    private SyncExample example;

    public MyRunnable1(SyncExample example) {
        this.example = example;
    }

    @Override
    public void run() {
        example.syncMethod();
    }
}

SyncExample example = new SyncExample();
Thread thread1 = new Thread(new MyRunnable1(example));
Thread thread2 = new Thread(new MyRunnable1(example));
thread1.start();
thread2.start();           

在这个例子中,两个线程尝试访问同一个对象的同一个synchronized方法,所以只有一个线程能够在同一时间访问这个方法。

2、 两个线程同时访问两个对象的相同的synchronized方法:

SyncExample example1 = new SyncExample();
SyncExample example2 = new SyncExample();
Thread thread1 = new Thread(new MyRunnable1(example1));
Thread thread2 = new Thread(new MyRunnable1(example2));
thread1.start();
thread2.start();           

在这个例子中,两个线程尝试访问两个不同对象的同一个synchronized方法,这两个线程可以同时访问这个方法,因为他们访问的是两个不同对象的锁。

3、 两个线程同时访问两个对象的相同的static的synchronized方法:

class MyRunnable2 implements Runnable {
    @Override
    public void run() {
        SyncExample.syncStaticMethod();
    }
}

Thread thread1 = new Thread(new MyRunnable2());
Thread thread2 = new Thread(new MyRunnable2());
thread1.start();
thread2.start();           

在这个例子中,两个线程尝试访问同一个类的静态synchronized方法,只有一个线程能够在同一时间访问这个方法,因为这个方法的锁是类锁,而不是对象锁。

4、 两个线程同时访问同一对象的synchronized方法与非synchronized方法:

class MyRunnable3 implements Runnable {
    private SyncExample example;

    public MyRunnable3(SyncExample example) {
        this.example = example;
    }

    @Override
    public void run() {
        example.nonSyncMethod();
    }
}

Thread thread1 = new Thread(new MyRunnable1(example));
Thread thread2 = new Thread(new MyRunnable3(example));
thread1.start();
thread2.start();           

在这个例子中,一个线程访问synchronized方法,另一个线程访问非synchronized方法,他们可以同时访问,因为非synchronized方法不需要获取对象的锁。

5、 两个线程访问同一对象的不同的synchronized方法:

class MyRunnable4 implements Runnable {
    private SyncExample example;

    public MyRunnable4(SyncExample example) {
        this.example = example;
    }

    @Override
    public void run() {
        example.anotherSyncMethod();
    }
}

Thread thread1 = new Thread(new MyRunnable1(example));
Thread thread2 = new Thread(new MyRunnable4(example));
thread1.start();
thread2.start();           

在上述的例子中,两个线程访问同一对象的不同synchronized方法,它们不能同时访问,因为这两个方法都需要获取对象的锁。

6、 两个线程同时访问同一对象的static的synchronized方法与非static的synchronized方法:

class MyRunnable5 implements Runnable {
    @Override
    public void run() {
        SyncExample.syncStaticMethod();
    }
}

class MyRunnable6 implements Runnable {
    private SyncExample example;

    public MyRunnable6(SyncExample example) {
        this.example = example;
    }

    @Override
    public void run() {
        example.syncMethod();
    }
}

SyncExample example = new SyncExample();
Thread thread1 = new Thread(new MyRunnable5());
Thread thread2 = new Thread(new MyRunnable6(example));
thread1.start();
thread2.start();           

在这个例子中,一个线程访问静态的synchronized方法,另一个线程访问非静态的synchronized方法,他们可以同时访问,因为静态的synchronized方法获取的是类锁,而非静态的synchronized方法获取的是对象锁。

7、 方法抛出异常后,会释放锁吗:

当一个synchronized方法抛出异常时,当前的线程自动释放锁。以下是一个例子:

public class SyncExample {
    public synchronized void syncMethod() {
        // 一些代码
        throw new RuntimeException();
        // 更多代码
    }
}           

在上述例子中,当syncMethod方法抛出异常时,任何线程试图访问这个方法的其余部分的尝试都会失败,因为锁已经被释放。其他线程可以开始执行这个方法或者任何其他synchronized方法或者代码块。

原理

1、 加解锁原理:

在Java中,当一个线程要访问某个对象的synchronized方法或者synchronized代码块时,它需要首先获得该对象的监视器锁。如果该锁没有被其他线程持有,那么该线程就可以获取到这个锁并继续执行;否则,该线程就会被阻塞,直到锁被释放。

在JVM层面,加锁和解锁主要通过monitorenter和monitorexit两条字节码指令来实现。当JVM执行到monitorenter指令时,首先会检查这个锁的计数器,如果为0则表示锁可以被获取,然后将锁的计数器设为1;如果锁已经被其他线程获取,那么当前线程就会被阻塞。当JVM执行到monitorexit指令时,锁的计数器会被减1,当计数器为0时,锁就被释放。

2、 可重入原理:

可重入意味着一个线程可以多次获取同一个锁。在Java中,每个锁都关联了一个获取该锁的线程和获取次数的计数器。当一个线程获取锁时,JVM会记录下获取该锁的线程,并且将获取次数设为1;如果同一个线程再次获取该锁,那么获取次数就会增加1;每次释放锁,获取次数就会减1,当获取次数为0时,锁就被释放。这就是可重入的基本原理。

3、 可见性原理:

Java内存模型通过在synchronized代码块前后添加内存屏障来确保可见性。当一个线程退出synchronized代码块时,JVM会插入一个Store/Store屏障,这个屏障会强制刷新线程的工作内存,使得在该synchronized代码块中的所有写入操作都同步到主内存中。当另一个线程进入synchronized代码块时,JVM会插入一个Load/Load屏障,这个屏障会使得线程的工作内存无效,使得线程必须从主内存中读取共享变量。这样就保证了线程之间对共享变量操作的可见性。

synchronized缺陷

1、 效率低:

synchronized是Java中最基本的同步机制,但其使用阻塞式的锁,会让等待锁的线程进入BLOCKED状态,而后续线程调度和唤醒的过程都是需要消耗系统资源的,当并发竞争激烈的时候,这种开销就变得非常大,从而导致程序执行效率降低。

2、 不够灵活:

synchronized只有在获取锁和释放锁时有两种选择:阻塞或者成功获取锁。并没有提供尝试获取锁或者在指定时间内尝试获取锁等更灵活的操作,这在一些特定的场景下可能会造成问题。例如,如果一个线程无限期地等待获取一个锁,那么这可能会导致系统的整体性能下降,甚至导致死锁。

3、 无法预判是否成功获取到锁:

在使用synchronized时,没有办法知道在请求获取锁时是否能够成功。如果多个线程都在等待获取同一个锁,那么只有一个线程能够成功,其他线程都会被阻塞。并且,我们无法预判哪个线程会成功获取到锁。

4、 不具备公平性:

synchronized并没有公平性选择,它并不关注哪个线程等待的时间最长,所有等待的线程都有可能获取到锁,这就有可能导致某些线程等待时间过长,出现"饥饿"现象。

5、 无法中断一个正在等待获取锁的线程:

如果一个线程在等待获取一个synchronized锁,那么它就处于BLOCKED状态,这个时候是无法被中断的。如果这个线程需要被中断,比如为了取消操作或者处理一个紧急事件,那么我们就无法做到。

6、 锁释放问题:

synchronized在出现异常时会自动释放锁,但是如果同步块或方法中有return、continue或者是goto等导致控制流程跳出的语句,那么可能会导致锁没有被正确释放。而且,如果在同步块或方法中,没有对异常进行处理,那么在发生异常时,其它线程会立即进入同步块或方法,可能会访问到不正确的数据。

Synchronized的优化

Java的Synchronized关键字在JDK 1.5以后进行了很多优化,这些优化在许多情况下都可以显著提高性能。

1、 锁粗化:

锁粗化是JVM针对连续操作进行的优化。如果一个线程对一个对象进行了连续的加锁和解锁操作,JVM可能会将这些操作合并为一个对该对象的加锁操作和一个解锁操作。这种优化可以减少获得锁和释放锁的次数,从而提高性能。

2、 锁消除:

锁消除是JVM的另一个优化技术,它主要用于消除不必要的同步。如果JVM可以确定某个同步操作是不必要的,那么它可能会消除这个同步操作。例如,如果一个对象只在一个线程中使用,那么对这个对象的同步操作就是不必要的,JVM可以消除这些同步操作。

3、 锁升级:

在JDK 1.6中引入了偏向锁,轻量级锁和重量级锁三种锁状态,这就是所谓的锁升级。当一个线程首次访问一个对象时,JVM可能会将对象的锁设置为偏向锁,这是一种对线程友好的锁,它假设没有其他线程会竞争该锁,因此可以通过简单的改变对象头中的线程ID来获取和释放锁,避免了CAS操作。

当有其他线程尝试竞争偏向锁时,偏向锁就会升级为轻量级锁。轻量级锁使用CAS操作来获取和释放锁,但仍然避免了线程阻塞。如果有多个线程竞争轻量级锁,那么轻量级锁就会升级为重量级锁,这时需要线程阻塞来获取和释放锁。

以上优化策略的目标都是为了减少锁操作的开销,提高并发性能。

当一个线程进入某个对象的一个synchronized的实例方法后,其它线程是否可进入此对象的其它方法?

在Java中,如果一个线程进入了一个对象的synchronized实例方法,那么其他线程在没有得到锁的情况下,是无法访问该对象的其他synchronized实例方法的。因为对象锁是在整个对象上的,一旦一个线程获得了对象的锁,那么其他线程就无法访问这个对象的任何其他synchronized实例方法。

然而,如果这个对象的其他方法没有被声明为synchronized,那么其他线程是可以访问这些非synchronized方法的。因为这些方法并没有在访问时要求对象锁。

以下是一个示例代码:

class MyObject {
    public synchronized void method1() {
        System.out.println(Thread.currentThread().getName() + " entered method1");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " leaving method1");
    }

    public synchronized void method2() {
        System.out.println(Thread.currentThread().getName() + " entered method2");
        System.out.println(Thread.currentThread().getName() + " leaving method2");
    }

    public void method3() {
        System.out.println(Thread.currentThread().getName() + " entered method3");
        System.out.println(Thread.currentThread().getName() + " leaving method3");
    }
}

public class SynchronizedTest {
    public static void main(String[] args) {
        MyObject myObject = new MyObject();

        new Thread(() -> myObject.method1()).start();
        new Thread(() -> myObject.method2()).start();
        new Thread(() -> myObject.method3()).start();
    }
}           

在这个示例中,method1和method2都被声明为synchronized,method3没有。如果、运行上面的代码,、会发现method1和method2不能同时被两个线程访问。但是method3可以同时被其他线程访问,因为它不需要对象锁。

synchronized介绍,及相关面试题

继续阅读