Java多线程-synchronized使用分析

synchronized是Jdk的内置同步锁,用于实现多线程对共享资源的同步访问。

使用方式:

1.synchronized普通方法

// 同步方法
public synchronized void test() {
    // 同步代码块
    // 锁对象为当前对象
}

2.synchronized静态方法

// 同步静态方法
public synchronized static void test() {
    // 同步代码块
    // 锁对象为当前类对象
}

3.synchronized普通代码块

public void test() {
        synchronized (this){
            // 同步代码块
            // 锁对象为当前对象
        }
    }

4.synchronized静态代码块

public static void test() {
    synchronized (Demo.class){
        // 同步代码块
        // 锁对象为当前类对象
    }
}

同步锁

  • 在Java中每一个对象有且仅有一个同步锁,同步锁是依赖于对象而存在的。

  • 同步锁使用场景:多个线程对同一个对象中的实例变量进行并发访问。

对某个方法或某个代码块使用synchronized时,那么当某个线程访问时,就会尝试获取相应对象的同步锁,实现多个线程对加锁部分代码的互斥访问,也就是在某个时间点,对象的同步锁只能被一个线程持有。

例:现有A、B两个线程都需要访问加锁资源,如果A线程先抢到了同步锁,那么B就会获取失败,处于阻塞状态,只能等到A线程释放了该锁资源之后,B线程才能获取到该同步锁,进而访问共享资源。

锁对象

synchronized是对存在同步问题的对象进行加锁。
从上面4种不同synchronized方式可以看出,实现对共享资源同步操作的加锁机制:

  • 非静态方法的锁对象是当前类实例;
  • 静态方法的锁对象是当前类;
  • 非静态代码块和静态代码块的锁对象则基于括号里配置的实例;

可重入锁

可重入就是某个线程已经获取某个锁,可以再次获取相同的锁而不会发生死锁。如下,一个线程进入同步方法test1则已经持有了锁,再调用同步方法test2,两个方法的同步锁都是demo对象,然而并不会发生死锁。test3中同步代码块同是如此(经典的案例就是双重检查锁的单例模式)。

public class Demo {
    public static void main(String[] args) {
        Demo demo = new Demo();
        demo.test1();
    }

    public synchronized void test1() {
        // 同步代码块
        test2();
    }

    public synchronized void test2() {
        // 同步代码块
    }
    
    public synchronized void test3(){
        synchronized (this){
            synchronized (this){
                // 同步代码块
            }
        }
    }
    
}

Java的可重入锁还有ReentrantLock。

原子性和可见性

综上,synchronized能够实现多线程对共享资源操作的原子性,使多个线程互斥地访问共享资源。此外,synchronized还具有内存可见性。

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        myThread.start();
        while (true) {
            if (myThread.flag) {
                System.out.println("主线程读到flag=" + myThread.flag);
                break;
            }
        }

    }
}

class MyThread extends Thread {
    public boolean flag = false;

    @Override
    public void run() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("MyThread线程修改后flag=" + flag);
    }
}

以上程序,启动子线程MyThread,并修改子线程中的变量。主线程在启动子线程后监听读取子线程对象中的变量flag,如果flag==true,则结束监听。

但是运行程序后会发现,在子线程中正常修改了flag后,主线程依旧处于while死循环状态。
以上存在问题就是:

  • 1.子线程从主存读取flag到自己的工作空间,然后修改flag值,子线程还未来得及将修改后的flag值更新到主存,主线程就已经从主存把flag读到自己的工作内存,工作内存中的flag是false。

  • 2.即使子线程修改flag后并同步到主存,主线程依然使用的的自己工作内存中的flag。(这里可以先去了解下JMM)

以上问题产生的原因就是:多线程操作共享变量时,彼此不可见。

所以如果能让flag被修改后及时更新到主存,并且主线程每次都是从主存访问flag,那么主线程就能正常读取到更新后的值,及时结束while循环。

内存可见性

某个线程更改了某个对象的状态,也需要访问该对象的其他线程能够及时地看到该对象修改后的状态。

JMM关于synchronized的规定:
  • 线程加锁时,将清空工作内存中共享变量的值,从而重新读取最新的值;
  • 线程解锁前必须把共享变量的最新值刷新到主内存中;
线程执行同步代码块的过程:
  • 1.获得互斥锁
  • 2.清空工作内存
  • 3 从主内存中拷贝最新的变量副本到工作内存中
  • 4 执行代码块
  • 5.将更改后的共享变量的值刷新到主存中
  • 6.释放互斥锁

要解决上述while死循环问题,就是对flag的访问使用synchronized加锁实现可见性,如下:

synchronized (myThread){
    while (true) {
        if (myThread.flag) {
            System.out.println("主线程读到flag=" + myThread.flag);
            break;
        }
    }
}

但是,有的小伙伴可能会这么想,使用如下的方式在子线程中对flag的修改进行加锁是否可行呢?

synchronized (this){
    flag = true;
}

根据以上的可见性描述,执行完上述同步代码后,子线程会及时将flag的值同步到主存。但是主线程依旧使用的自己工作空间中的副本变量,并没有再次从主线程读取flag。所以在子线程中对flag的修改加锁也不会解决上述的主线程的可见性问题。

在之后锁机制章节中还会对synchronized的实现原理和JDK1.6的优化进行进一步的分析。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值