背景
线程争抢:多个线程同时争抢相同的公共资源就是线程争抢,线程争抢会造成数据异常问题,解决线程争抢问题的最好的方案就是【加锁】
线程争抢示例
package com.jdw.java8.thread;
public class VolatileExample {
private static int counter = 0;
/**
* 自增
*/
public static void selfIncrementing() {
counter++;
}
public static void main(String[] args) {
// 启动两个线程对 counter 进行递增
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
selfIncrementing();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
selfIncrementing();
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终计数: " + counter);
// 理论实际值应该是20000,但由于线程争抢的原因,实际值都在1万几
}
}
解决办法:synchronized
synchronized 是 Java 中用于实现同步的关键字,它可以应用于方法和代码块。synchronized 的主要作用是确保在同一时刻最多只有一个线程能够执行被 synchronized 修饰的代码,从而保证线程安全。主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行
- 使用 synchronized 的三种方式(说2种也对!)
// 同步方法: 将整个方法声明为 synchronized。
// 修饰实例方法, 给当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁
public synchronized void synchronizedMethod() {
// 同步的代码块
}
// 修饰静态方法 (锁当前类)
// 给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁
// 这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享
public synchronized static void method() {
// 同步的代码块
}
// 同步代码块: 将需要同步的代码块包裹在 synchronized 关键字中。
public void someMethod() {
// 非同步代码块
//对括号里指定的对象/类加锁:synchronized(object) 表示进入同步代码库前要获得 给定对象的锁。
// synchronized(类.class) 表示进入同步代码前要获得 给定 Class 的锁
synchronized (lockObject) {
// 需要同步的代码块
}
// 非同步代码块
}
尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能
构造方法不能使用 synchronized 关键字修饰。构造方法本身就属于线程安全的,不存在同步的构造方法一说
示例代码分析:
1. synchronized 同步代码块的情况
通过查看字节码信息,我们发现synchronized 同步代码块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置
2. synchronized 修饰方法的的情况
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取而代之的是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM 会尝试获取当前 class 的锁
不过以上两者的本质都是对对象监视器 monitor 的获取
背景和使用场景: | |
---|---|
解决竞态条件(Race Condition): | 当多个线程同时访问共享资源时,如果不进行同步,可能导致竞态条件,即结果依赖于线程执行的时序。 |
确保原子性操作: | synchronized 可以确保一段代码或方法的执行在同一时刻只能由一个线程执行,从而保证了操作的原子性。 |
保护共享数据: | 当多个线程共享同一份数据时,使用 synchronized 可以防止数据不一致性的问题。 |
避免死锁: | synchronized 的使用可以避免死锁的发生,确保同一时刻只有一个线程可以获取所需的资源 |
synchronized 解决线程争抢示例
package com.jdw.java8.thread;
public class VolatileExample {
private static int counter = 0;
/**
* 自增
*/
public static synchronized void selfIncrementing() {
counter++;
}
public static void main(String[] args) {
// 启动两个线程对 counter 进行递增
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
selfIncrementing();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
selfIncrementing();
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终计数: " + counter);
// 加锁后,理论实际值与实际值一致
}
}
注意事项:
- 性能开销: synchronized 会引入一定的性能开销,因为它会导致线程阻塞和切换。
- 粒度控制: 同步的粒度要控制得当,避免同步代码块过大,影响程序性能。
- 死锁风险: 不当使用 synchronized 可能导致死锁,即多个线程互相等待对方释放锁。
- 静态方法锁: 静态方法使用 synchronized 会锁定类而不是实例,可能导致性能问题。
在并发编程中,synchronized 是一种重要的手段,但随着 Java 并发包的发展,更灵活的锁机制(如 ReentrantLock)也提供了更多选择。根据具体的场景和需求选择合适的同步方式是很重要的。
synchronized 是怎么加锁的(原理分析)
在 Java 中,每个对象都有一个关联的监视器对象(monitor),也称为内部锁或监视锁。当一个线程希望执行一个被 synchronized 关键字修饰的代码块时,它必须首先获得与指定对象关联的监视器对象(monitor)。这个过程涉及到以下几个步骤:
获取锁(获得监视器对象): 当线程希望执行一个 synchronized 代码块时,它会尝试获取与该对象关联的监视器对象 (字节码中执行到monitorenter指令时,会尝试获取Java对象所对应的monitor所有权)。如果这个监视器对象当前没有被其他线程占用,那么该线程将成功获取锁,进入临界区。
执行临界区代码: 一旦线程成功获取锁,它就可以执行 synchronized 代码块中的临界区代码。这段代码是由 synchronized 修饰的,同一时刻只有一个线程能够执行。
释放锁: 当线程退出 synchronized 代码块时,它将释放之前获取的锁,即释放与对象关联的监视器对象(字节码中执行monitorexit,就是释放monitor的所有权) 。这样,其他线程就有机会获取锁,进入临界区执行相应的代码。
这个过程确保了在同一时刻只有一个线程能够执行 synchronized 代码块中的关键代码,从而保证了数据的一致性和线程安全性。
值得注意的是,锁是与对象关联的。每个对象都有一个与之关联的锁,而 synchronized 的粒度可以是对象级别的方法或代码块。如果多个线程在不同对象上调用同步方法,它们之间的锁互不影响。如果多个线程在同一对象上调用同步方法,它们将争夺该对象上的锁。
锁升级
注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率
synchroized锁升级 | - |
---|---|
无锁 | 不加锁 |
偏向锁 | 不锁锁,只有一个线程争夺时,偏心某一个线程,这个线程来了不加锁 |
轻量级锁 | 少量线程来了之后,先尝试 自旋,不挂起线程 |
重量级锁 | 排队挂起线程 |
注:挂起线程和恢复线程的操作都需要转入内核态中完成这些操作,给系统的并发性带来很大的压力。在许多应用上共享数据的锁定状态,只会持续很短的一段时间,为了这段时间去挂起和恢复现场并不值得,我们就可以让后边请求的线程稍等一下,不要放弃处理器的执行时间,看看持有锁的线程是否很快就会释放,锁为了让线程等待,我们只需要让线程执行一个盲循环也就是我们说的自旋,这项技术就是所谓的【自旋锁】
Java对象头信息
对象头主要包括两部分数据:Mark Word(标记字段)锁信息都在Mark Word中记录、
Klass Pointer(类型指针)、
数组类型还有一个int类型的数组长度
锁相关信息主要在Mark Word(标记字段)中,之后帖子再详细讲述对象头!!!