Java——多线程高并发系列之synchronized关键字

简介: Java——多线程高并发系列之synchronized关键字

文章目录:


写在前面

Demo1synchronized面对同一个实例对象)

Demo2synchronized面对多个实例对象)

Demo3synchronized面对一个 public static final 常量)

Demo4synchronized同步代码块分别位于实例方法、静态方法中)

Demo5synchronized同步实例方法体,默认的锁对象是this

Demo6synchronized同步静态方法体,默认的锁对象是当前类的运行时对象Test06.class

Demo7(同步方法与同步代码块如何选择)

Demo8synchronized解决脏读问题)

Demo9synchronized同步过程中线程出现异常, 会自动释放锁对象)

Demo10(使用synchronized实现死锁)

写在前面


Java 中的每个对象都有一个与之关联的内部锁(Intrinsic lock)。这种锁也称为监视器(Monitor),这种内部锁是一种排他锁,可以保障原子性,可见性与有序性。内部锁是通过 synchronized 关键字实现的,synchronized 关键字修饰代码块,修饰该方法。

修饰实例方法就称为同步实例方法
修饰静态方法称称为同步静态方法

下面,我用10Demo带大家彻底搞明白synchronized关键字的使用!!!

Demo1(synchronized面对同一个实例对象)


synchronized关键字当前修饰的是一个代码块,其中的this可以这样理解,目前,synchronized这个东西目前处在Test01类中,而我们程序的第一句就是 Test01 obj = new Test01(); 那么synchronized中的this代表的就是obj对象,obj对象是属于Test01类的,那这不就对应上this了吗?

package com.szh.synchronizedtest;
/**
 * synchronized同步代码块
 *  this表示的是obj锁对象
 */
public class Test01 {
    public static void main(String[] args) {
        //先创建Test01对象
        Test01 obj=new Test01();
        //创建两个线程,分别调用mm方法
        new Thread(new Runnable() { //Thread-0
            @Override
            public void run() {
                obj.mm(); //使用的锁对象this就是obj
            }
        }).start();
        new Thread(new Runnable() { //Thread-1
            @Override
            public void run() {
                obj.mm(); //使用的锁对象this也是obj
            }
        }).start();
    }
    public void mm() {
        synchronized (this) {
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + " ---> " + i);
            }
        }
    }
}

从程序的运行结果可以看出来,第一个线程这里先抢到CPU执行权,然后启动,进入mm方法后,对其上锁,此时第二个线程就进不来了,必须等待第一个线程执行完毕同步代码块之后才可以执行。

也就是Thread-0先打印了10行,之后Thread-1才打印。(我这里只是面对这个执行结果才这样说,线程的执行结果也可能是先Thread-1、再Thread-0

Demo2(synchronized面对多个实例对象)


这个例子中,synchronized仍然修饰了同步代码块,但是这里Test02new了两个实例对象,那这就不一样了。

new第一个Thread的时候,我们使用obj1实例对象去调用mm方法,这时候Thread-0线程进入同步代码块,对obj1对象上锁,执行for循环。

new第二个Thread的时候,我们使用obj2实例对象去调用mm方法,这时候Thread-1线程能不能进入同步代码块呢?(不能吧,你Thread-0线程不是已经上锁了吗?)答案当然能!!!Thread-0线程是对obj1对象上的锁,我先Thread-1面对的是obj2对象啊,这个对象还没有上锁呢,我这里为什么不能进入同步代码块呢???所以这里Thread-1完全可以进入同步代码块执行,此时对obj2对象上锁。

也就是说,synchronized关键字要想实现线程同步,这些线程必须面对同一个锁对象!!!

package com.szh.synchronizedtest;
/**
 * synchronized同步代码块
 *  this在这个代码案例中表示了两个不同的锁对象
 *  要想实现同步,必须是同一个锁对象
 */
public class Test02 {
    public static void main(String[] args) {
        //先创建Test01对象
        Test02 obj1=new Test02();
        Test02 obj2=new Test02();
        //创建两个线程,分别调用mm方法
        new Thread(new Runnable() { //Thread-0
            @Override
            public void run() {
                obj1.mm(); //使用的锁对象this就是obj1
            }
        }).start();
        new Thread(new Runnable() { //Thread-1
            @Override
            public void run() {
                obj2.mm(); //使用的锁对象this就是obj2
            }
        }).start();
    }
    public void mm() {
        synchronized (this) {
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + " ---> " + i);
            }
        }
    }
}

执行结果中可以看到,两个线程不同步,一会Thread-0、一会Thread-1

Demo3(synchronized面对一个 public static final 常量)


这个例子中,synchronized中传入了一个 public static final 修饰的常量,这个自然也是可以实现线程同步的。

因为在Test03这个类中,不管你new多少个对象,在某个线程中,使用哪个实例对象去调同步代码块对应的方法,对于我们 public static final 修饰的常量来说,在这个类中都是可见的!!!

当第一个线程(Thread-0run启动成功之后,synchronized将这个常量给上了锁,然后线程Thread-0进去执行for循环。

这个时候第二个线程(Thread-1run也启动成功了,它也想进入同步代码块执行,但是同步代码块面对的是常量,而常量此时已经被Thread-0给锁上了,那么你Thread-1肯定就进不去了呀,它就必须等待Thread-0执行完毕才可以进入同步代码块执行。

(我这里只是面对这个执行结果才这样说,线程的执行结果也可能是先Thread-1、再Thread-0

package com.szh.synchronizedtest;
/**
 * synchronized同步代码块
 *  使用一个常量对象作为锁对象
 *  this表示的是OBJ
 */
public class Test03 {
    //定义一个常量
    public static final Object OBJ=new Object();
    public static void main(String[] args) {
        //先创建Test01对象
        Test03 obj1=new Test03();
        Test03 obj2=new Test03();
        //创建两个线程,分别调用mm方法
        new Thread(new Runnable() { //Thread-0
            @Override
            public void run() {
                obj1.mm(); //使用的锁对象this就是OBJ常量,这与使用哪个对象调用mm方法无关,这个常量是大家共有的
            }
        }).start();
        new Thread(new Runnable() { //Thread-1
            @Override
            public void run() {
                obj2.mm(); //使用的锁对象this就是OBJ常量,这与使用哪个对象调用mm方法无关
            }
        }).start();
    }
    public void mm() {
        synchronized (OBJ) {
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + " ---> " + i);
            }
        }
    }
}

Demo4(synchronized同步代码块分别位于实例方法、静态方法中)


这个例子中,我分别将synchronized修饰的同步代码块放在了实例方法和静态方法中,下面,我来说一下我对执行结果的理解:

实例方法中的synchronized理解可以参考Demo3。而这个静态方法中,为什么也实现了线程同步呢?因为我们都知道,static 修饰的方法被称为静态方法,而静态方法一般是由它的所属类直接调用的,而在这个例子中它是属于Test04类的,所以

public static void sm() {

   synchronized (OBJ) {

       for (int i = 1; i <= 10; i++) {

           System.out.println(Thread.currentThread().getName() + " ---> " + i);

       }

   }

}

这个代码块就是由Test04直接调用的,而它的同步代码块锁的是OBJ这个常量,这个常量不正好也是Test04类所有的吗?所以说,这三个子线程中的同步代码块面对的都是Test04类所拥有的OBJ常量(就这一把锁)!!!

package com.szh.synchronizedtest;
/**
 * synchronized同步代码块
 *  这里使用一个常量作为锁对象,这个常量是所有对象都共享的,也就是同一个锁对象
 *  不管是实例方法还是静态方法,只要是同一个锁对象,就可以实现同步
 */
public class Test04 {
    //定义一个常量
    public static final Object OBJ=new Object();
    public static void main(String[] args) {
        //先创建Test01对象
        Test04 obj1=new Test04();
        Test04 obj2=new Test04();
        //创建两个线程,分别调用mm方法
        new Thread(new Runnable() { //Thread-0
            @Override
            public void run() {
                obj1.mm(); //使用的锁对象this就是OBJ常量,这与使用哪个对象调用mm方法无关,这个常量是大家共有的
            }
        }).start();
        new Thread(new Runnable() { //Thread-1
            @Override
            public void run() {
                obj2.mm(); //使用的锁对象this就是OBJ常量,这与使用哪个对象调用mm方法无关
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                sm(); //使用的锁对象this就是OBJ常量
            }
        }).start();
    }
    public void mm() {
        synchronized (OBJ) {
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + " ---> " + i);
            }
        }
    }
    public static void sm() {
        synchronized (OBJ) {
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + " ---> " + i);
            }
        }
    }
}

这里可以看到,虽然我们new线程的顺序是Thread-0Thread-1Thread-2,但是执行顺序却是012的线程顺序,所以多线程的执行结果并不是一定按照代码顺序来的。

Demo5(synchronized同步实例方法体,默认的锁对象是this


这个例子中,synchronizedthis)就不再多说了,它锁的就是Test05类的那个实例对象obj

public synchronized void mm2()这个实例方法中,大家不要被迷惑,这里它修饰的是一个实例方法,而实例方法不就是由类的实例对象调用的吗?那说白了,它锁的不还是本类中new的那个obj对象吗?是的吧!!!

package com.szh.synchronizedtest;
/**
 * synchronized同步实例方法体,默认的锁对象是this
 *  this表示的obj锁对象
 */
public class Test05 {
    public static void main(String[] args) {
        //先创建Test01对象
        Test05 obj=new Test05();
        //创建两个线程,分别调用mm方法
        new Thread(new Runnable() { //Thread-0
            @Override
            public void run() {
                obj.mm(); //使用的锁对象this就是obj
            }
        }).start();
        new Thread(new Runnable() { //Thread-1
            @Override
            public void run() {
                obj.mm2(); //使用的锁对象this也是obj
            }
        }).start();
    }
    public void mm() {
        synchronized (this) {
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + " ---> " + i);
            }
        }
    }
    public synchronized void mm2() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(Thread.currentThread().getName() + " ---> " + i);
        }
    }
}

Demo6(synchronized同步静态方法体,默认的锁对象是当前类的运行时对象Test06.class


如果这样写:synchronizedXXX.class)则表示的是当前类的运行时类对象作为锁对象,而在反射中简单的理解,XXX.class不就是这个类的字节码文件吗?

public synchronized static void mm2() {} 这样写,则说明synchronized修饰了静态方法体,而静态方法是属于类的,所以它这里锁的也是当前类的运行时对象。也就是说synchronized在这两种方式中面对的都是类锁!!!

package com.szh.synchronizedtest;
/**
 * synchronized同步静态方法体,默认的锁对象是当前类的运行时对象Test06.class
 *  这个也可以称为类锁
 */
public class Test06 {
    public static void main(String[] args) {
        //先创建Test01对象
        Test06 obj=new Test06();
        //创建两个线程,分别调用mm方法
        new Thread(new Runnable() { //Thread-0
            @Override
            public void run() {
                obj.mm(); //使用的锁对象是Test06.class
            }
        }).start();
        new Thread(new Runnable() { //Thread-1
            @Override
            public void run() {
                Test06.mm2(); //使用的锁对象是Test06.class
            }
        }).start();
    }
    public void mm() {
        //使用当前类的运行时类对象作为锁对象,简单的理解为将Test06类的字节码文件作为锁对象
        synchronized (Test06.class) {
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + " ---> " + i);
            }
        }
    }
    //synchronized修饰静态方法,同步静态方法,默认的锁对象为运行时类对象Test06.class
    public synchronized static void mm2() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(Thread.currentThread().getName() + " ---> " + i);
        }
    }
}

Demo7(同步方法与同步代码块如何选择)


这个就不再给出例子了,就一句话:同步代码块,锁的粒度细,执行效率高。同步方法,锁的粒度粗, 执行效率低。

Demo8(synchronized解决脏读问题)


这个例子中,就是说如果我们在子线程中设置了用户名和密码,我们对写方法的代码块进行了同步,如果不对读方法的代码块进行同步了话,那么这个时候,子线程设置了用户名和密码,而main主线程并不知道你子线程设置的用户名和密码,它此时是可以从getValue中读取到静态内部类PublicValue中提前定义好的 root12345678(因为读方法并没有同步,子线程对setValue方法上了锁,但是getValue还可以顺利的读)。这显然不对啊,我们希望的是子线程设置完新的用户名密码之后,在main主线程中也可以读取到新的值,而不是读取到的脏数据。(将getValue方法前的synchronized删掉,main主线程输出结果将会是 root12345678

这个时候,我们就需要将读方法的代码块也定义为同步的!!!这个时候,子线程一旦对setValue写方法上锁之后,那么main主线程此时再想从getValue方法中读取就不行了,因为getValue方法也上了锁,而这两个方法的synchronized都是面对的实例方法上的锁,而同步实例方法默认的锁对象就是this PublicValue publicValue=new PublicValue(); )。所以这样等到子线程设置完新值之后,main主线程就可以读取到了。

package com.szh.synchronizedtest;
/**
 * 脏读
 *  出现读取属性值出现了一些意外, 读取的是中间值,而不是修改之后的值
 *  出现脏读的原因是: 对共享数据的修改 与 对共享数据的读取 不同步
 * 解决方法:
 *  不仅对修改数据的代码块进行同步, 还要对读取数据的代码块同步
 */
public class Test08 {
    public static void main(String[] args) throws InterruptedException {
        //开启子线程设置用户名和密码
        PublicValue publicValue=new PublicValue();
        SubThread t1=new SubThread(publicValue);
        t1.start();
        //为了确定设置成功
        Thread.sleep(100);
        //在main线程中读取用户名和密码
        publicValue.getValue();
    }
    static class SubThread extends Thread {
        private PublicValue publicValue;
        public SubThread(PublicValue publicValue) {
            this.publicValue=publicValue;
        }
        @Override
        public void run() {
            publicValue.setValue("admin","666");
        }
    }
    static class PublicValue {
        private String name="root";
        private String pwd="12345678";
        public synchronized void getValue() {
            System.out.println(Thread.currentThread().getName() + ", getter -- name: "
                            + name + ", pwd: " + pwd);
        }
        public synchronized void setValue(String name,String pwd) {
            this.name=name;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.pwd=pwd;
            System.out.println(Thread.currentThread().getName() + ", setter -- name: "
                    + name + ", pwd: " + pwd);
        }
    }
}

Demo9(synchronized同步过程中线程出现异常, 会自动释放锁对象)


这个例子说的是,同步过程中,如果发生了运行时异常,则占有锁的那个线程会自动释放占有的锁对象。

Thread-0在启动之后,执行同步代码块,它锁的是当前类的运行时对象(Test09.class),也就是当前类的字节码文件(类锁),所以这个时候,Thread-1启动后,它调用mm2()方法时,是无法执行mm2()方法的,因为这个方法是静态方法,默认的锁对象也是当前类的运行时对象(Test09.class),所以这两个子线程面对的是同一个锁!!!

i=5 时,Integer.parseInt("abc");这段代码会抛出字符串转换为数字异常,这个时候发生了异常,那么Thread-0子线程就会释放它所占有的Test09类锁,那么此时Thread-1就可以顺序执行了!!!(详情见运行结果图)

package com.szh.synchronizedtest;
/**
 * synchronized同步静态方法体,默认的锁对象是当前类的运行时对象Test09.class
 *  这个也可以称为类锁
 *  同步过程中线程出现异常, 会自动释放锁对象
 */
public class Test09 {
    public static void main(String[] args) {
        //先创建Test01对象
        Test09 obj=new Test09();
        //创建两个线程,分别调用mm方法
        new Thread(new Runnable() { //Thread-0
            @Override
            public void run() {
                obj.mm(); //使用的锁对象是Test06.class
            }
        }).start();
        new Thread(new Runnable() { //Thread-1
            @Override
            public void run() {
                Test09.mm2(); //使用的锁对象是Test06.class
            }
        }).start();
    }
    public void mm() {
        //使用当前类的运行时类对象作为锁对象,简单的理解为将Test06类的字节码文件作为锁对象
        synchronized (Test09.class) {
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + " ---> " + i);
                if (i==5) {
                    //把字符串转换为 int 类型时,如果字符串不符合 数字格式会产生异常
                    Integer.parseInt("abc");
                }
            }
        }
    }
    //synchronized修饰静态方法,同步静态方法,默认的锁对象为运行时类对象Test06.class
    public synchronized static void mm2() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(Thread.currentThread().getName() + " ---> " + i);
        }
    }
}


Demo10(使用synchronized实现死锁)


这个例子,首先在子线程中有两个常量 lock1lock2,那么你SubThread无论new多少个子线程对象,它们都是共享这两个常量的。这里对两个线程分别命名为了 ab,然后启动这两个子线程,它们两个会抢夺CPU资源去执行自己的run方法,假如说 t1 线程先抢到了,那么它首先对 lock1 上了锁,这个时候,t2线程抢到了,那么它首先对 lock2 上了锁,这个时候,假如t1又抢到了,那么它想对 lock2 上锁,此时 lock2 已经被t2 上过锁了,所以 t1 会在这里等待,一直等到 t2 释放lock2 的锁;此时 t2 获得了 CPU 的运行权,那么它继续执行run方法,它此时相对 lock1 上锁,而 lock1 已经被t1 上了锁,所以 t2 会在这里等待,一直等到 t1 释放lock1 的锁;但是我们前面说了呀,t1在等待 t2 释放 lock2 的锁,t2在等待t1 释放 lock1 的锁,那我等你,你等我,谁也退出不了,谁也前进不成,那不就死到这里了吗?这就是死锁问题!!!

package com.szh.synchronizedtest;
/**
 * 死锁
 */
public class Test10 {
    public static void main(String[] args) {
        SubThread t1=new SubThread();
        SubThread t2=new SubThread();
        t1.setName("a");
        t2.setName("b");
        t1.start();
        t2.start();
    }
    static class SubThread extends Thread {
        private static final Object lock1=new Object();
        private static final Object lock2=new Object();
        @Override
        public void run() {
            if ("a".equals(Thread.currentThread().getName())) {
                synchronized (lock1) {
                    System.out.println("a线程获得了lock1锁,还需要获得lock2锁");
                    synchronized (lock2) {
                        System.out.println("a线程又获得了lock2锁???");
                    }
                }
            }
            if ("b".equals(Thread.currentThread().getName())) {
                synchronized (lock2) {
                    System.out.println("b线程获得了lock2锁,还需要获得lock1锁");
                    synchronized (lock1) {
                        System.out.println("b线程又获得了lock1锁???");
                    }
                }
            }
        }
    }
}

相关文章
|
6天前
|
存储 网络协议 安全
Java网络编程,多线程,IO流综合小项目一一ChatBoxes
**项目介绍**:本项目实现了一个基于TCP协议的C/S架构控制台聊天室,支持局域网内多客户端同时聊天。用户需注册并登录,用户名唯一,密码格式为字母开头加纯数字。登录后可实时聊天,服务端负责验证用户信息并转发消息。 **项目亮点**: - **C/S架构**:客户端与服务端通过TCP连接通信。 - **多线程**:采用多线程处理多个客户端的并发请求,确保实时交互。 - **IO流**:使用BufferedReader和BufferedWriter进行数据传输,确保高效稳定的通信。 - **线程安全**:通过同步代码块和锁机制保证共享数据的安全性。
58 23
|
20天前
|
缓存 安全 Java
Volatile关键字与Java原子性的迷宫之旅
通过合理使用 `volatile`和原子操作,可以在提升程序性能的同时,确保程序的正确性和线程安全性。希望本文能帮助您更好地理解和应用这些并发编程中的关键概念。
40 21
|
10天前
|
Java C语言
课时8:Java程序基本概念(标识符与关键字)
课时8介绍Java程序中的标识符与关键字。标识符由字母、数字、下划线和美元符号组成,不能以数字开头且不能使用Java保留字。建议使用有意义的命名,如student_name、age。关键字是特殊标记,如蓝色字体所示。未使用的关键字有goto、const;特殊单词null、true、false不算关键字。JDK1.4后新增assert,JDK1.5后新增enum。
|
1月前
|
安全 Java 开发者
【JAVA】封装多线程原理
Java 中的多线程封装旨在简化使用、提高安全性和增强可维护性。通过抽象和隐藏底层细节,提供简洁接口。常见封装方式包括基于 Runnable 和 Callable 接口的任务封装,以及线程池的封装。Runnable 适用于无返回值任务,Callable 支持有返回值任务。线程池(如 ExecutorService)则用于管理和复用线程,减少性能开销。示例代码展示了如何实现这些封装,使多线程编程更加高效和安全。
|
2月前
|
Java 编译器 开发者
Java中的this关键字详解:深入理解与应用
本文深入解析了Java中`this`关键字的多种用法
212 9
|
2月前
|
JSON Java 数据挖掘
利用 Java 代码获取淘宝关键字 API 接口
在数字化商业时代,精准把握市场动态与消费者需求是企业成功的关键。淘宝作为中国最大的电商平台之一,其海量数据中蕴含丰富的商业洞察。本文介绍如何通过Java代码高效、合规地获取淘宝关键字API接口数据,帮助商家优化产品布局、制定营销策略。主要内容包括: 1. **淘宝关键字API的价值**:洞察用户需求、优化产品标题与详情、制定营销策略。 2. **获取API接口的步骤**:注册账号、申请权限、搭建Java开发环境、编写调用代码、解析响应数据。 3. **注意事项**:遵守法律法规与平台规则,处理API调用限制。 通过这些步骤,商家可以在激烈的市场竞争中脱颖而出。
|
2月前
|
缓存 安全 算法
Java 多线程 面试题
Java 多线程 相关基础面试题
|
10月前
|
消息中间件 Java Linux
2024年最全BATJ真题突击:Java基础+JVM+分布式高并发+网络编程+Linux(1),2024年最新意外的惊喜
2024年最全BATJ真题突击:Java基础+JVM+分布式高并发+网络编程+Linux(1),2024年最新意外的惊喜
|
9月前
|
缓存 NoSQL Java
Java高并发实战:利用线程池和Redis实现高效数据入库
Java高并发实战:利用线程池和Redis实现高效数据入库
603 0
|
7月前
|
监控 算法 Java
企业应用面临高并发等挑战,优化Java后台系统性能至关重要
随着互联网技术的发展,企业应用面临高并发等挑战,优化Java后台系统性能至关重要。本文提供三大技巧:1)优化JVM,如选用合适版本(如OpenJDK 11)、调整参数(如使用G1垃圾收集器)及监控性能;2)优化代码与算法,减少对象创建、合理使用集合及采用高效算法(如快速排序);3)数据库优化,包括索引、查询及分页策略改进,全面提升系统效能。
88 0