java多线程_内置锁01_线程安全和synchronized

文章通过一个线程不安全的自增操作案例,解释了线程安全的概念。当多个线程并发访问同一对象时,如果没有正确同步,可能会导致数据不一致。文章介绍了Java中的`synchronized`关键字,用于解决线程安全问题,包括同步方法和同步代码块的使用,以及它们如何确保在同一时刻只有一个线程执行特定代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

什么是线程安全?

什么是线程安全了?当多个线程并发访问某个java对象的时候。无论cpu按什么样的顺序调度这些线程,得到的结果都是一样的,就是线程安全的;反之,则是线程不安全的。

这里我们来讨论一个线程不安全的案例,通过多个线程对一个整数进行自增操作。

我们启动两个线程A、B;A线程对整数自增10万次。B线程对整数自增10万次;假设整数的初始值为0,那么当A、B自增结束之后,整数的值应该是20万。那么事实究竟是否如此了?我们通过程序验证一下:

public class Demo003_a {
    public static void main(String[] args) {
        new Demo003_a().testAddUnsafe();
    }

    public Integer counter = 0; //初始值为0

    /**
     * @desc 测试多线程累加
     * */
    public void testAddUnsafe(){

        Runnable run = new Runnable() {
            @Override
            public void run() {
                //累加1万次
                for(int i=0;i<10000;i++)
                    counter++;
            }
        };

        //启动2个线程进行累加
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        t1.start();
        t2.start();
        //等待t1、t2运行结束
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter);
    }
}

第一次运行结果:14860
第二次运行结果:10000

结果是显而易见的,自增运算并不是线程安全的。why?

自增运算看起来是一个原子操作。然而事实上,自增运算符是一个复合操作。它至少包含3个JVM指令(内存取值、寄存器加1、存值到内存)。这里我们简单描述一下其出现错误的原因。


时间	|线程A                                     |	线程B
	|1、从内存读取变量的值;                      | 	
	|2、寄存器执行加1指令;                       |1、从内存读取变量的值;
	|3、cpu时间片使用完;                        |	2、寄存器执行加1指令;
	|4、阻塞中...;                             | 3、将寄存器结果写入内存;
	|5、竞争到cpu时间片,将寄存器结果写入内存;      |	
	

上面列出了A、B线程执行过程中,可能发生的一种情况,很明显。线程A的结果覆盖了线程B的结果。

synchronized(内置锁) 关键字

有什么办法可以解决上述线程不安全的情况了? 对不安全的指令进行加锁是经常使用的解决办法。以上面的例子为例,我们可以对counter++指令进行加锁,即需要获得锁,才可以执行该指令。这样就保证了同一个时刻,只有一个线程处理加锁的指令,从而保证了线程安全。

synchronized 是java对象的内置锁,每个java对象都有一把内置锁。关于内置锁的详细原理,可以在 “java多线程_内置锁02_对象结构和内置锁” 一文中详细了解。在此,我们只需要知道,通过内置锁可以保证锁定的指令在同一时刻只有一个线程去执行。

java中,synchronized 内置锁可以加在方法、静态方法、代码块中。

synchronized 保护方法

在编写java方法的时候加上synchronized关键字,则该方法同一时刻,只能由一个线程执行。

public class Demo003_a {
    public static void main(String[] args) {
        new Demo003_a().testSynchronized1();
    }

    public Integer counter = 0; //初始值为0

    /**
     *
     * @desc 同步方法测试
     * */
    public void testSynchronized1(){
        Runnable run = new Runnable() {
            @Override
            public void run() {
                add();
            }
            //同步方法,使用匿名内部类的对象的锁(即run对象的内置锁)
            public synchronized void add(){
                //累加1万次
                for(int i=0;i<10000;i++)
                    counter++;
            }
        };

        //启动2个线程进行累加
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        t1.start();
        t2.start();
        //等待t1、t2运行结束
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter);
    }
}

可以观察到,不论如何运行程序;最终累加的结果都是 2000.

synchronized 保护代码块

在方法上面加锁的缺点是,整个方法中的指令都会被限制为单线程访问。这无疑会降低系统的性能——可能有些代码并不需要安全保护。为了提高系统的性能,我们要尽可能减少锁定的范围。

在代码块上面加锁,是比较优雅的一种做法。

public class Demo003_a {
    public static void main(String[] args) {
        new Demo003_a().testSynchronized1();
    }

    public Integer counter = 0; //初始值为0

    /**
     *
     * @desc 同步方法测试
     * */
    public void testSynchronized1(){
        Runnable run = new Runnable() {
            @Override
            public void run() {

                //处理其它的事情...

                //同步方法,使用匿名内部类的对象的锁(即run对象的内置锁)
                synchronized(this) {
                    //累加1万次
                    for(int i=0;i<10000;i++)
                        counter++;
                }

                //处理其它的事情...
            }

        };

        //启动2个线程进行累加
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        t1.start();
        t2.start();
        //等待t1、t2运行结束
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter);
    }
}

上面的 synchronized(this) 表示使用当前对象的内置锁。

synchronized 保护静态方法

静态方法和普通方法的区别在于,静态方法属于 Class 类的实例(Class类的实例是类加载器在加载类到JVM虚拟机的时候创建的),而普通方法是属于当前编写的类的实例。所以synchronized加在静态方法上面,则使用的是Class实例的内置锁。

使用方法和一般方法一摸一样。

public static  Integer counter = 0; //初始值为0

public static synchronized void add(){
    for(int i=0;i<10000;i++)
        counter++;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值