Java synchronized 以及线程安全

一、Synchronized

Synchronized 是什么

synchronized 是 Java 中的一个关键字,用于实现线程同步,它可以应用于代码块或方法,以确保多个线程对共享资源的安全访问。
在 JDK1.6 之前,它有另一个名称叫做:重量级锁。但是从 1.6 版本起,它就在不断被优化。在此之后,锁便拥有了 4 种状态,根据锁的级别从低到高可分为:无锁、偏向锁、 轻量级锁、重量级锁。

无锁

没有对共享资源进行任何锁定,所有线程都可以去访问并修改同一资源,但同时只能有一个线程修改成功,其他线程不断尝试直至成功,并会将原内容覆盖。

偏向锁

偏向锁,指的就是偏向第一个加锁线程,对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,降低获取锁带来的性能开销。

轻量级锁

轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B 会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。

重量级锁

指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。

synchronized 锁升级的过程

  1. 首先,在锁对象的对象头里面有一个 threadid 字段,未访问时 threadid 为空,这时是无锁状态,任何线程都可竞争获取共享资源;

  2. 先得到共享资源的线程,其线程 ID 会被记录到 Mark Word 中,此时锁状态为偏向锁;

  3. 当后续还有线程去获取共享资源时,会先判断 threadid 是否与其线程 id 一致。如果一致则可以直接使用此对象;如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁;

  4. 自旋的线程在自旋过程中,成功获得资源 (即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于 轻量级锁的状态,如果自旋失败;

  5. 进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己。

优化过程参考文章

二、Synchronized 用法

同步代码块

使用 synchronized 关键字修饰的代码块,通过指定一个对象来确定同步范围。例如:

synchronized (obj) {
   // 需要同步的代码
}

在这个例子中,obj 是一个对象引用,同步块中的代码只有当线程获取了 obj 对象的锁时才能执行,确保了对共享资源的安全访问。

同步方法

使用 synchronized 关键字修饰的方法,整个方法体被视为同步代码块,锁定的对象是方法所属的对象。例如:

public synchronized void someMethod() {
 // 需要同步的代码
}

静态同步方法

对于静态方法,可以使用 synchronized 关键字来实现同步,此时锁定的对象是类对象。例如:

public static synchronized void someStaticMethod() {
   // 需要同步的代码
}

synchronized 的规则和细节:

  1. 当一个线程访问一个对象的 synchronized 代码块时,它获得的是括号中指定对象的锁。
  2. 当一个线程访问一个对象的 synchronized 实例方法时,它获得的是该对象的锁。
  3. 当一个线程访问一个类的 synchronized 静态方法时,它获得的是该类的锁。

虽然 synchronized 关键字可以简化多线程编程中的同步问题,但是因为锁的问题过度使用 synchronized 也可能导致性能问题,因为它可能会引入锁竞争和线程阻塞。

三、类锁

类锁是针对类的锁,它是针对类的所有实例的锁。当一个线程访问一个类的静态 synchronized 方法或代码块时,它获得了该类的锁,其他线程必须等待直到锁被释放。这意味着无论该类的实例有多少个,它们都共享同一个类锁。
换句话说,当一个线程获取了类锁时,其他线程无法同时访问这个类中的任何 synchronized 方法,无论是静态方法还是非静态方法。这是因为类锁是针对整个类的,而不是针对类的实例的。

四、对象锁

对象锁是针对类的实例(对象)的锁。每个 Java 对象都可以作为一个同步锁,通过 synchronized 关键字修饰的方法或代码块来获取对象锁。
当一个线程访问一个对象的 synchronized 方法或代码块时,它获得了该对象的锁,其他线程必须等待直到锁被释放。不同对象的对象锁是相互独立的,因此它们不会相互影响。

注意:不同对象的对象锁是相互独立的,因此它们不会相互影响。
既然不同对象的对象锁是相互独立的,不会相互影响。那么当不同的对象同时对共享资源进行修改时,如果没有适当的同步机制,就会存在线程安全问题。

下面看个简单的例子:

无锁的方法

public static class SharedResource {
   private static int COUNT;

    public void increment() {
        COUNT++;
        System.out.println(COUNT);
    }

}

public static void  sync1 (){
    SharedResource obj = new SharedResource();
    for (int i = 0; i < 100; i++) {
        new Thread(() -> {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            obj.increment();

        }).start();
    }
}

累加结果出现了错误:
在这里插入图片描述
然后我们把 increment 方法加上锁:

public static class SharedResource {
        private static int COUNT;

        public synchronized void increment() {
            COUNT++;
            System.out.println(COUNT);
        }

    }

    public static void  sync1 (){
        SharedResource obj = new SharedResource();
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                obj.increment();

            }).start();
        }
    }

累加结果正常:
在这里插入图片描述
我们使用不同对象去调用 increment 方法:

public static class SharedResource {
   private static int COUNT;

    public synchronized void increment() {
        COUNT++;
        System.out.println(COUNT);
    }

}

public static void  sync1 (){
    for (int i = 0; i < 100; i++) {
        SharedResource obj = new SharedResource();
        new Thread(() -> {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            obj.increment();

        }).start();
    }
}

累加结果又出错了:
在这里插入图片描述
从以上的测试可以看,即使使用了 synchronized 关键字,也是会存在线程不安全的情况。
简单来说要保证线程安全,需要保证获取的锁是同一个锁。
要解决这个问题,可以使用 synchronized 关键字来锁定某个共享的对象,或者使用 java.util.concurrent 包中的锁和原子类来确保对共享变量的安全访问。

使用 synchronized 关键字来锁定某个共享的对象:

public static class SharedResource {
    private static int COUNT = 0;

    public  void increment(Object lock) {
        synchronized (lock){
            COUNT++;
            System.out.println(COUNT);
        }

    }

}

public static void  sync1 (){
	//创建一个公共对象作为锁对象
    Object lock = new Object();
    for (int i = 0; i < 100; i++) {
        SharedResource obj = new SharedResource();
        new Thread(() -> {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            obj.increment(lock);

        }).start();
    }
}

结果:
在这里插入图片描述

使用 java.util.concurrent.locks 包中的 ReentrantLock 锁实现安全访问:

public static class SharedResource {
    private static int COUNT = 0;

    public  void increment(ReentrantLock lock) {
        lock.lock();
        try {
            COUNT++;
            System.out.println(COUNT);
        } finally {
            lock.unlock();
        }

    }

}

public static void  sync1 (){
    //创建一个公共锁对象
    ReentrantLock lock = new ReentrantLock();
    for (int i = 0; i < 100; i++) {
        SharedResource obj = new SharedResource();
        new Thread(() -> {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            obj.increment(lock);

        }).start();
    }
}

结果:
在这里插入图片描述

使用 java.util.concurrent.atomic 包中提供的 AtomicInteger 实现安全访问:

public static class SharedResource {
    private static AtomicInteger count = new AtomicInteger(0);

    public  void increment() {
        System.out.println(count.getAndIncrement());
    }

}

public static void  sync1 (){
    for (int i = 0; i < 100; i++) {
        SharedResource obj = new SharedResource();
        new Thread(() -> {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            obj.increment();

        }).start();
    }
}

结果(虽然 AtomicInteger 能够保证每次对整数的操作是原子的,但是线程的调度和执行顺序是不确定的):
在这里插入图片描述

五、java.util.concurrent 包

java.util.concurrent 是 Java 标准库中提供的用于并发编程的工具包。这个包提供了许多用于处理并发编程问题的类和接口,包括线程池、并发集合、同步器、原子变量等等。这些工具旨在简化并发编程,并提供更高效和更安全的并发操作。

以下是 java.util.concurrent 包中一些常用的类和接口:

  1. 线程池(ThreadPool):Executor框架及其实现类,如ExecutorServiceThreadPoolExecutor 等,用于管理和调度线程的执行。线程池可以有效地重用线程、管理线程数量,并提供任务执行的调度和控制能力。

  2. 并发集合(Concurrent Collections):java.util.concurrent 包提供了一系列并发安全的集合类,如 ConcurrentHashMapConcurrentLinkedQueue 等,用于在多线程环境下安全地操作集合数据。

  3. 同步器(Synchronizers):包括 CountDownLatchSemaphoreCyclicBarrier 等,用于协调多个线程之间的操作,实现线程之间的同步。

  4. 原子变量(Atomic Variables):java.util.concurrent.atomic 包提供了一系列原子变量类,如 AtomicIntegerAtomicLong 等,用于在多线程环境下进行原子操作,避免使用锁的情况下进行线程安全的操作。

  5. 并发工具类(Concurrent Utilities):包括 CopyOnWriteArrayListLinkedTransferQueue 等,提供了一些高效的并发工具,用于解决特定的并发编程问题。

  6. locks: java.util.concurrent.locks 包提供了一系列用于多线程同步的锁机制,这些锁机制相较于传统的 synchronized 关键字提供了更灵活的锁定和解锁操作。这个包中最重要的接口是 Lock ,它有几个常见的实现类,比如 ReentrantLockReentrantReadWriteLock.ReadLockReentrantReadWriteLock.WriteLock 等。

java.util.concurrent 包中的类和接口提供了一套强大的工具,用于简化并发编程、提高性能,并降低编写线程安全代码的复杂性。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值