一文了解synchronized高并发关键字

声明:尊重他人劳动成果,转载请附带原文链接!学习交流,仅供参考!

前置知识:

1、什么是线程安全性?

要对线程安全性给出一个确切的定义是非常复杂的。
首先,我们在互联网上可以搜索到许多的"定义",例如:
…可以在多个线程中调用,并且线程之间不会出现错误的交互。
…可以同时被多个线程调用,而调用者无须执行额外的动作。
如果看看这些定义,很难不对线程安全性产生困惑。大概意思就是"如果某个类可以在多线程中安全地使用,那么它就是一个线程安全的类。"对于这种说法,虽然没有太多的争议,但对我们来说也不会有太大的帮助。
我们可以这样认为:
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

2、什么是原子性?

简单的来说,原子性就是说一个操作不可以被中途cpu暂停然后调度, 即不能被中断, 要不就执行完, 要不就不执行. 如果一个操作是原子性的, 那么在多线程环境下, 就不会出现变量被修改等奇怪的问题。
例如:递增操作 ++count,虽然它是一种紧凑的语法,使其看上去只是一个操作,但是这个操作并非原子的,因而它并不会作为一个不可分割的操作来执行,实际上,它包含了三个独立的操作,读取count的值,将值加1,然后将计算结果写入count,这是一个"读取-修改-写入"的操作序列,如果出现在两个线程在没有同步的情况下,同时对一个计数器执行递增操作,就会出现数值偏差。

3、什么内置锁?

java提供了一种内置的锁机制来支持原子性,同步代码块(下面在用法中讲解到),每个java对象都可以用作一个实现同步的锁,这些锁就称为内置锁或者叫做监视器锁。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,而无论是通过正常的控制路径退出,还是通过从代码块中抛出异常退出。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。

4、什么是可见性?

可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉。在当单线程中,如果某个变量先写入值,然后在没有其他写入操作的情况下读取这个变量,那么总能得到相同的值,然而,当读操作和写操作在不同的线程中执行时,情况却并非如此,这听起来或许有些难受。通常我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。

1、Synchronized的介绍

在java编程中,经常需要用到同步机制,而用的最多 Synchronized,它是java中的关键字,可以用来保障原子性、可见性和有序性。

2、不使用Synchronized会出现什么情况

例子:两个线程我们对一个数进行递减操作,直到这个数不满足大于0的时候结束!

package synchronize;
/**
 * @author delingw
 * @version 1.0
 * 不使用synchronized关键字会出现的情况
 * 例子:两个线程我们对一个数进行递减操作,直到这个数不满足大于0的时候结束!
 */
public class SynchronizedNoUse implements Runnable {
    
    
    int num = 10;
    @Override
    public void run() {
    
    
        System.out.println(Thread.currentThread().getName() + " 进入线程!");
        while (num > 0) {
    
    
            System.out.println(Thread.currentThread().getName() + " num=" + num);
            num--;
        }
        System.out.println(Thread.currentThread().getName() + " 退出线程!");
    }
    public static void main(String[] args) throws InterruptedException {
    
    
        SynchronizedNoUse synchronizedNoUse = new SynchronizedNoUse();
        // 线程一
        Thread thread1 = new Thread(synchronizedNoUse);
        // 线程二
        Thread thread2 = new Thread(synchronizedNoUse);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
    }
}

结果:

Thread-0 进入线程!
Thread-1 进入线程!
Thread-0 num=10
Thread-1 num=10
Thread-1 num=8
Thread-1 num=7
Thread-1 num=6
Thread-1 num=5
Thread-1 num=4
Thread-1 num=3
Thread-1 num=2
Thread-1 num=1
Thread-1 退出线程!
Thread-0 num=9
Thread-0 退出线程!

通过结果我们会知道了数据出现了错误。
原因是:num–虽然是一行代码,但实际上至少包含了以下三个动作:
1、读取num的值
2、计算num-1
3、把num-1的计算结果写回到内存中,赋值num
由于这三步并非原子性,也就是说并没有一气呵成,也就导致了线程的不安全!
假如我们加了锁,就会解决这个问题,原因是在同一个时刻,一把锁只能有一个线程使用,其他线程使用则必须等待。

package synchronize;

/**
 * @author delingw
 * @version 1.0
 * 不使用synchronized关键字会出现的情况
 * 例子:两个线程我们对一个数进行递减操作,直到这个数不满足大于0的时候结束!
 */
public class SynchronizedNoUse implements Runnable {
    
    
    int num = 10;

    @Override
    public synchronized void run() {
    
    
        System.out.println(Thread.currentThread().getName() + " 进入线程!");
//        while (num > 0) {  // 取消while防止其他线程得不到
            System.out.println(Thread.currentThread().getName() + " num=" + num);
            num--;
//        }
        System.out.println(Thread.currentThread().getName() + " 退出线程!");
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        SynchronizedNoUse synchronizedNoUse = new SynchronizedNoUse();
        // 线程一
        Thread thread1 = new Thread(synchronizedNoUse);
        // 线程二
        Thread thread2 = new Thread(synchronizedNoUse);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
    }
}

结果:

Thread-0 进入线程!
Thread-0 num=10
Thread-0 退出线程!
Thread-1 进入线程!
Thread-1 num=9
Thread-1 退出线程!

3、Synchronized的两种用法

对象锁

什么是对象锁?

同个对象在多个线程中去调用非静态synchronized作用的代码块、方法时,需要获取对象锁,一个对象的对象锁是唯一的,所有只有一个线程能拿到,因此当有线程在执行synchronized的方法时,其他线程需要等待或者阻塞

对象锁实现的两种方式
1、代码块

注意:synchronized默认的锁是this

package synchronize;

/**
 * @author delingw
 * @version 1.0
 * 对象锁代码块
 */
public class SynchronizedNoUse implements Runnable {
    
    

    @Override
    public void run() {
    
    
        synchronized (this) {
    
    
            System.out.println("我是对象锁" + Thread.currentThread().getName());
            try {
    
    
                Thread.sleep(1);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "运行结束!");
        }
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        SynchronizedNoUse synchronizedNoUse = new SynchronizedNoUse();
        Thread t1 = new Thread(synchronizedNoUse);
        Thread t2 = new Thread(synchronizedNoUse);
        t1.start();
        t2.start();
        // 执行结果
        while (t1.isAlive() || t2.isAlive()) {
    
    
        }
        System.out.println("执行结束!");
    }
}

结果:

我是对象锁Thread-0
Thread-0运行结束!
我是对象锁Thread-1
Thread-1运行结束!
执行结束!
2、方法上
package synchronize;

/**
 * @author delingw
 * @version 1.0
 * 对象锁方法上
 */
public class SynchronizedNoUse implements Runnable {
    
    

    @Override
    public synchronized void run() {
    
    
        System.out.println("我是对象锁" + Thread.currentThread().getName());
        try {
    
    
            Thread.sleep(1);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束!");

    }

    public static void main(String[] args) throws InterruptedException {
    
    
        SynchronizedNoUse synchronizedNoUse = new SynchronizedNoUse();
        Thread t1 = new Thread(synchronizedNoUse);
        Thread t2 = new Thread(synchronizedNoUse);
        t1.start();
        t2.start();
        // 执行结果
        while (t1.isAlive() || t2.isAlive()) {
    
    
        }
        System.out.println("执行结束!");
    }
}

结果:

我是对象锁Thread-0
Thread-0运行结束!
我是对象锁Thread-1
Thread-1运行结束!
执行结束!

类锁

什么是类锁?

类的不同对象在多个多线程中去调用静态synchronized作用的代码块,方法时需要获取类锁,一个类的类锁是唯一的,
只有一个线程能拿到,因此当有对象在线程执行synchronized的方法时,其他线程的对象需要进入阻塞队列等待

类锁实现的两种方式
1、代码块
package synchronize;

/**
 * @author delingw
 * @version 1.0
 * 类锁代码块
 */
public class SynchronizedNoUse implements Runnable {
    
    

    @Override
    public void run() {
    
    
        synchronized (SynchronizedNoUse.class) {
    
    
            classBlock();
        }
    }

    private static void classBlock() {
    
    
        System.out.println("我是类锁" + Thread.currentThread().getName());
        try {
    
    
            Thread.sleep(1);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束!");
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        SynchronizedNoUse synchronizedNoUse1 = new SynchronizedNoUse();
        SynchronizedNoUse synchronizedNoUse2 = new SynchronizedNoUse();
        Thread t1 = new Thread(synchronizedNoUse1);
        Thread t2 = new Thread(synchronizedNoUse2);
        t1.start();
        t2.start();
        // 执行结果
        while (t1.isAlive() || t2.isAlive()) {
    
    
        }
        System.out.println("执行结束!");
    }
}

结果:

我是类锁Thread-0
Thread-0运行结束!
我是类锁Thread-1
Thread-1运行结束!
执行结束!
2、方法上
package synchronize;
/**
 * @author delingw
 * @version 1.0
 * 类锁方法上
 */
public class SynchronizedNoUse implements Runnable {
    
    
    @Override
    public void run() {
    
    
        classBlock();
    }
    private synchronized static void classBlock() {
    
    
        System.out.println("我是类锁" + Thread.currentThread().getName());
        try {
    
    
            Thread.sleep(1);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束!");
    }
    public static void main(String[] args) throws InterruptedException {
    
    
        SynchronizedNoUse synchronizedNoUse1 = new SynchronizedNoUse();
        SynchronizedNoUse synchronizedNoUse2 = new SynchronizedNoUse();
        Thread t1 = new Thread(synchronizedNoUse1);
        Thread t2 = new Thread(synchronizedNoUse2);
        t1.start();
        t2.start();
        // 执行结果
        while (t1.isAlive() || t2.isAlive()) {
    
    
        }
        System.out.println("执行结束!");
    }
}

结果:

我是类锁Thread-0
Thread-0运行结束!
我是类锁Thread-1
Thread-1运行结束!
执行结束!

对象锁和类锁两者区别

:一个是对象锁(只有一个实例化对象),一个是类锁(可以有多个实例化对象,但是只有一个"class对象")。

4、Synchronized的性质

可重入性

当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。“重入"的一种实现方法是为每个锁关联一个获取计数值或者一个所有者线程。当计数值为0时,这个锁就认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1.如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应的递减。当计数值为0时,这个锁释放!

不可中断性

不可中断性就是当某一个线程获取到了锁后,如果其他线程还想获取这把锁,就只能等待,等待持有这把锁的线程执行完,再去获取锁。期间不能够中断这个线程。

5、加锁和释放锁的原理

获取锁和释放锁的时机:进入和退出同步代码块(包括抛出异常)

6、Synchronized的缺点

任何东西都有缺点:那我们就聊聊它的缺点吧,
1、效率低:锁的释放情况少,不能够被中断,不能设置超时时间
2、不够灵活,加锁和释放锁单一,每个锁仅有单一的条件,可能是不够的。

7、常见面试题

问题一:线程能同时访问一个对象的同步方法?

不能,需要排队等候。因为同一个对象需要获取对象锁,而对象锁只有一把

package synchronize;

/**
 * @author delingw
 * @version 1.0
 * 多个线程能同时访问一个对象的同步方法?
 */
public class SynchronizedNoUse implements Runnable {
    
    
    @Override
    public void run() {
    
    
        classBlock();
    }
    private synchronized void classBlock() {
    
    
        System.out.println("我是同步方法" + Thread.currentThread().getName());
        try {
    
    
            Thread.sleep(1);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束!");
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        SynchronizedNoUse synchronizedNoUse1 = new SynchronizedNoUse();

        Thread t1 = new Thread(synchronizedNoUse1);
        Thread t2 = new Thread(synchronizedNoUse1);
        t1.start();
        t2.start();
        // 执行结果
        while (t1.isAlive() || t2.isAlive()) {
    
    
        }
        System.out.println("执行结束!");
    }
}

结果:

我是同步方法Thread-0
Thread-0运行结束!
我是同步方法Thread-1
Thread-1运行结束!
执行结束!

问题二:线程能同时访问两个对象的同步方法?

可以同时访问,因为它们不是同一个对象,并不需要抢对象锁。

package synchronize;

/**
 * @author delingw
 * @version 1.0
 * 多个线程能同时访问两个对象的同步方法?
 */
public class SynchronizedNoUse implements Runnable {
    
    
    @Override
    public void run() {
    
    
        classBlock();
    }

    private synchronized void classBlock() {
    
    
        System.out.println("我是同步1方法" + Thread.currentThread().getName());
        try {
    
    
            Thread.sleep(1);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束!");
    }


    public static void main(String[] args) throws InterruptedException {
    
    
        SynchronizedNoUse synchronizedNoUse1 = new SynchronizedNoUse();
        SynchronizedNoUse synchronizedNoUse2 = new SynchronizedNoUse();
        Thread t1 = new Thread(synchronizedNoUse1);
        Thread t2 = new Thread(synchronizedNoUse2);
        t1.start();
        t2.start();
        // 执行结果
        while (t1.isAlive() || t2.isAlive()) {
    
    
        }
        System.out.println("执行结束!");
    }
}

结果:

我是同步1方法Thread-0
我是同步1方法Thread-1
Thread-1运行结束!
Thread-0运行结束!
执行结束!

问题三:线程访问的synchronized的静态方法?

需要排队 ,因为是类锁

package synchronize;

/**
 * @author delingw
 * @version 1.0
 * 多个线程访问的synchronized的静态方法?
 */
public class SynchronizedNoUse implements Runnable {
    
    
    @Override
    public void run() {
    
    
        classBlock();
    }

    private synchronized static void classBlock() {
    
    
        System.out.println("我是静态同步方法" + Thread.currentThread().getName());
        try {
    
    
            Thread.sleep(1);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束!");
    }


    public static void main(String[] args) throws InterruptedException {
    
    
        SynchronizedNoUse synchronizedNoUse1 = new SynchronizedNoUse();
        SynchronizedNoUse synchronizedNoUse2 = new SynchronizedNoUse();
        Thread t1 = new Thread(synchronizedNoUse1);
        Thread t2 = new Thread(synchronizedNoUse2);
        t1.start();
        t2.start();
        // 执行结果
        while (t1.isAlive() || t2.isAlive()) {
    
    
        }
        System.out.println("执行结束!");
    }
}

结果:

我是静态同步方法Thread-0
Thread-0运行结束!
我是静态同步方法Thread-1
Thread-1运行结束!
执行结束!

问题四:线程能同时访问同步方法与非同步方法?

同步方法同一时刻只能被一个对象调用,非同步方法没有限制,可以同时调用

package synchronize;

/**
 * @author delingw
 * @version 1.0
 * 线程能同时访问同步方法与非同步方法?
 */
public class SynchronizedNoUse implements Runnable {
    
    
    @Override
    public void run() {
    
    
        if (Thread.currentThread().getName().equals("Thread-0")) {
    
    
            classBlock1();
        }
        classBlock2();
    }

    private synchronized void classBlock1() {
    
    
        System.out.println("我是同步方法" + Thread.currentThread().getName());
        try {
    
    
            Thread.sleep(1);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束!");
    }

    private synchronized static void classBlock2() {
    
    
        System.out.println("我是非同步方法" + Thread.currentThread().getName());
        try {
    
    
            Thread.sleep(1);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束!");
    }


    public static void main(String[] args) throws InterruptedException {
    
    
        SynchronizedNoUse synchronizedNoUse1 = new SynchronizedNoUse();
//        SynchronizedNoUse synchronizedNoUse2 = new SynchronizedNoUse();
        Thread t1 = new Thread(synchronizedNoUse1);
        Thread t2 = new Thread(synchronizedNoUse1);
        t1.start();
        t2.start();
        // 执行结果
        while (t1.isAlive() || t2.isAlive()) {
    
    
        }
        System.out.println("执行结束!");
    }
}

结果:

我是同步方法Thread-0
我是非同步方法Thread-1
Thread-1运行结束!
Thread-0运行结束!
我是非同步方法Thread-0
Thread-0运行结束!
执行结束!

问题五:线程能访问同一个对象的不同的普通同步方法?

不能,因为两个线程调用不同的普通的同步方法时,要先获得锁,而synchronized默认的锁是this,所以还是对象锁,所以排队

package synchronize;

/**
 * @author delingw
 * @version 1.0
 * 线程能访问同一个对象的不同的普通同步方法?
 */
public class SynchronizedNoUse implements Runnable {
    
    
    @Override
    public void run() {
    
    
        if (Thread.currentThread().getName().equals("Thread-0")) {
    
    
            classBlock1();
        }
        classBlock2();
    }

    private synchronized void classBlock1() {
    
    
        System.out.println("我是同步方法1" + Thread.currentThread().getName());
        try {
    
    
            Thread.sleep(1);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束!");
    }

    private synchronized void classBlock2() {
    
    
        System.out.println("我是同步方法2" + Thread.currentThread().getName());
        try {
    
    
            Thread.sleep(1);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束!");
    }


    public static void main(String[] args) throws InterruptedException {
    
    
        SynchronizedNoUse synchronizedNoUse1 = new SynchronizedNoUse();
//        SynchronizedNoUse synchronizedNoUse2 = new SynchronizedNoUse();
        Thread t1 = new Thread(synchronizedNoUse1);
        Thread t2 = new Thread(synchronizedNoUse1);
        t1.start();
        t2.start();
        // 执行结果
        while (t1.isAlive() || t2.isAlive()) {
    
    
        }
        System.out.println("执行结束!");
    }
}

结果:

我是同步方法1Thread-0
Thread-0运行结束!
我是同步方法2Thread-0
Thread-0运行结束!
我是同步方法2Thread-1
Thread-1运行结束!
执行结束!

问题六:线程能同时访问静态synchronized和非静态synchronized方法?

静态是类锁,非静态是对象锁,所以能同时执行

	package synchronize;

/**
 * @author delingw
 * @version 1.0
 * 线程能同时访问静态synchronized和非静态synchronized方法?
 */
public class SynchronizedNoUse implements Runnable {
    
    
    @Override
    public void run() {
    
    
        if (Thread.currentThread().getName().equals("Thread-0")) {
    
    
            classBlock1();
        }
        classBlock2();
    }
    private synchronized static void classBlock1() {
    
    
        System.out.println("我是静态同步方法" + Thread.currentThread().getName());
        try {
    
    
            Thread.sleep(1);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束!");
    }
    private synchronized void classBlock2() {
    
    
        System.out.println("我是非静态同步方法" + Thread.currentThread().getName());
        try {
    
    
            Thread.sleep(1);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束!");
    }
    public static void main(String[] args) throws InterruptedException {
    
    
        SynchronizedNoUse synchronizedNoUse1 = new SynchronizedNoUse();
        SynchronizedNoUse synchronizedNoUse2 = new SynchronizedNoUse();
        Thread t1 = new Thread(synchronizedNoUse1);
        Thread t2 = new Thread(synchronizedNoUse2);
        t1.start();
        t2.start();
        // 执行结果
        while (t1.isAlive() || t2.isAlive()) {
    
    
        }
        System.out.println("执行结束!");
    }
}

结果:

我是静态同步方法Thread-0
我是非静态同步方法Thread-1
Thread-0运行结束!
Thread-1运行结束!
我是非静态同步方法Thread-0
Thread-0运行结束!
执行结束!

问题七:方法抛出异常,会释放锁吗?

会的 抛出异常后会立马释放锁

	package synchronize;

/**
 * @author delingw
 * @version 1.0
 * 方法抛出异常,会释放锁吗?
 */
public class SynchronizedNoUse implements Runnable {
    
    
    @Override
    public void run() {
    
    
        if (Thread.currentThread().getName().equals("Thread-0")) {
    
    
            classBlock1();
        }
        classBlock2();
    }

    private synchronized void classBlock1() {
    
    
        System.out.println("我是加锁方法1 " + Thread.currentThread().getName());
        try {
    
    
            Thread.sleep(10);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        throw new RuntimeException();
//        System.out.println(Thread.currentThread().getName() + "运行结束!");
    }

    private synchronized void classBlock2() {
    
    
        System.out.println("我是加锁方法2 " + Thread.currentThread().getName());
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束!");
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        SynchronizedNoUse synchronizedNoUse1 = new SynchronizedNoUse();
//        SynchronizedNoUse synchronizedNoUse2 = new SynchronizedNoUse();
        Thread t1 = new Thread(synchronizedNoUse1);
        Thread t2 = new Thread(synchronizedNoUse1);
        t1.start();
        t2.start();
        // 执行结果
        while (t1.isAlive() || t2.isAlive()) {
    
    
        }
        System.out.println("执行结束!");
    }
}

结果:

我是加锁方法1 Thread-0
我是加锁方法2 Thread-1
Exception in thread "Thread-0" java.lang.RuntimeException
	at synchronize.SynchronizedNoUse.classBlock1(SynchronizedNoUse.java:24)
	at synchronize.SynchronizedNoUse.run(SynchronizedNoUse.java:12)
	at java.lang.Thread.run(Thread.java:748)
Thread-1运行结束!
执行结束!

8、总结

JVM会自动通过使用monitor来加锁和解锁,保证了同一时刻只有一个线程可以执行指定代码,从而保证了线程安全,同时synchronized还具有可重入性和不可中断的性质。

猜你喜欢

转载自blog.csdn.net/qq_40805639/article/details/120593706