多线程(初阶四:synchronized关键字)

目录

一、加锁的目的

二、加锁和解锁

三、加锁后是否会出现线程安全问题

1、两个线程,针对不同对象加锁

2、一个线程加锁,一个线程不加锁

3、针对加锁操作的一些混淆理解

(1)多个线程调用同一个类的方法,对其方法里面的变量加锁

(2)Test类里的add方法里面,加锁的对象换成Test.class

四、联系其他的相关知识点

[1、StringBuffer 和 StringBuilder](#1、StringBuffer 和 StringBuilder)

2、C++加锁、解锁和java的区别


一、加锁的目的

因为加锁具有互斥 的特性,给一段代码加锁,当运行这段代码时,这里的代码在系统上的指令就会就会被打包在一起,等这些指令,执行完了,其他的指令操作才能进行。而这,也是加锁的目的:把几个操作打包成一个原子的操作。


二、加锁和解锁

我们想让一个变量自增10_0000次,用两个线程来实现这一操作,分工各一半,

没有加锁的操作,是有线程问题的,因为两个线程修改同一个变量的原因。代码如下:

java 复制代码
public class ThreadDemo1 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

执行效果:

count并不是我们预期的10_0000。

当我们给count++加上锁操作后的代码:

java 复制代码
public class ThreadDemo1 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized(locker) {
                    count++;
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized(locker){
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

执行效果:

是我们预期的效果。

加锁的最核心规则: 对于一个线程,针对一个对象进行加锁,但也有其他线程,也尝试对这个对象进行加锁,就会产生阻塞,这时,我们就把这已现象称为锁竞争 / 锁冲突

例如下图,都是针对locker对象进行加锁,这时就会产生锁竞争

加锁和解锁的执行过程,针对上述代码,简单的画图展示一下:

也就是把t1先上锁,把t1的这几个操作打包成一个原子,执行完它们才能执行t2。

注意: 给加锁的对象是任意的引用类型都可以的,我们也可以随便起个对象,但是要记住加锁的核心,两个线程之间加锁的对象,是否是同一个对象,是同一个对象,就会产生竞争,反之则不会。


三、加锁后是否会出现线程安全问题

1、两个线程,针对不同对象加锁

如下下代码,加锁用的不是同一个对象,则还是会存在线程安全问题

java 复制代码
public class ThreadDemo1 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized(locker1) {
                    count++;
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized(locker2){
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

执行结果:

我们可以看到,结果和我们预期的10_0000不同,所以,还是存在线程安全问题。

2、一个线程加锁,一个线程不加锁

如下代码,和上面代码差不多,做一些小小的改动

java 复制代码
public class ThreadDemo1 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
//        Object locker2 = new Object();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized(locker1) {
                    count++;
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

执行结果:

可以看到,和我们预期的结果不同,所以还是存在线程安全问题。

3、针对加锁操作的一些混淆理解

(1)多个线程调用同一个类的方法,对其方法里面的变量加锁

还是之前的代码模板,不过做了一些改动,把count放到Test t 对象中,在这里面count++,并且对其加锁,加锁对象是 this,其他线程再来调用Test中的方法。

java 复制代码
class Test {
    public int count = 0;
    public void add() {
        synchronized(this) {
            count++;
        }
    }
}
public class ThreadDemo2 {
    public static void main(String[] args) throws InterruptedException {
        Test t = new Test();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                t.add();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                t.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(t.count);
    }
}

执行效果:

和我们的预期效果一样,这说明,这种情况是线程安全的。

解释: 要解决这种情况的线程安全问题,核心还是上面所说的,看加锁是不是同一个对象,这里的Test类,add方法里对其加锁引用的对象是this,也就是当前Test类的实例对象,所有两个线程调用者方法的时候会产生锁竞争,结果也就可以达到我们的预期效果了。

(2)Test类里的add方法里面,加锁的对象换成Test.class

模板和之前差不多,只有一点小改动。代码如下:

java 复制代码
class Test {
    public int count = 0;
    public void add() {
        synchronized(Test.class) {
            count++;
        }
    }
}
public class ThreadDemo2 {
    public static void main(String[] args) throws InterruptedException {
        Test t = new Test();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                t.add();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                t.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(t.count);
    }
}

执行结果:

结果和我们预期的原因,是线程安全的情况。

解释: 这里的 Test.class 是类对象(反射那一块的内容),而 t1 和 t2 拿到的都是同一个对象,就会有锁竞争,还是能保障线程安全的。

(3)还可以把synchronized加到方法上

如图:

不过比较少用。


四、联系其他的相关知识点

1、StringBuffer 和 StringBuilder

StringBuffer是线程安全的,就是在一些关键方法上,加上了synchronized;StringBuilder不是线程安全的,就是在一些关键方法上,没加synchronized。

其实通俗的说是不是线程安全,并不严谨,例如上面的代码例子,一个线程加锁,一个线程不加锁,或者两个线程给不同的对象加锁,任然是线程不安全的。具体还是要看代码怎么写。

2、C++加锁、解锁和java的区别

C++: 它的加锁和解锁和java是不同的,在C++里,加锁:locker.lock() 解锁:locker.unlock(),他们的加锁和解锁是分开执行的,C++这种写法可能导致程序猿忘记调用unlock,或者unlock没执行到,这时就会产生很严重的bug,没解锁,其他加锁用和它一样对象的线程,就会一直等待。

java: 它的是使用synchronized方法进行加锁,解锁的,这些操作已经打包好了,当synchronized代码块执行完后,就会自动释放锁,就不会有忘记或者没的解锁这种情况。

相关推荐
Monodye8 分钟前
【Java】网络编程:TCP_IP协议详解(IP协议数据报文及如何解决IPv4不够的状况)
java·网络·数据结构·算法·系统架构
一丝晨光14 分钟前
逻辑运算符
java·c++·python·kotlin·c#·c·逻辑运算符
元气代码鼠15 分钟前
C语言程序设计(进阶)
c语言·开发语言·算法
霍霍哈嗨27 分钟前
【QT基础】创建项目&项目代码解释
开发语言·qt
friklogff28 分钟前
【C#生态园】从图像到视觉:Emgu.CV、AForge.NET、OpenCvSharp 全面解析
开发语言·c#·.net
无名指的等待71238 分钟前
SpringBoot中使用ElasticSearch
java·spring boot·后端
Tatakai251 小时前
Mybatis Plus分页查询返回total为0问题
java·spring·bug·mybatis
武子康1 小时前
大数据-133 - ClickHouse 基础概述 全面了解
java·大数据·分布式·clickhouse·flink·spark
.生产的驴1 小时前
SpringBoot 消息队列RabbitMQ 消费者确认机制 失败重试机制
java·spring boot·分布式·后端·rabbitmq·java-rabbitmq
虚拟搬运工1 小时前
Python类及元类的创建流程
开发语言·chrome·python