线程安全的主要诱因
- 存在共享数据(也称临界资源)
- 存在多条线程共同操作这些共享数据
解决问题的根本方法:
同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据迸行操作。
synchronized
synchronized锁的不是代码,锁的都是对象
互斥锁的特性
- 互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块(复合操作)进行访问。互斥性也称为操作的原子性。
- 可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起不一致。
根据获取的锁的分类:获取对象锁和获取类锁
获取对象锁的两种用法
- 同步代码块( synchronized (this), synchronized(类实例对象)),锁是小括号()中的实例对象
- 同步非静态方法( synchronized method),锁是当前对象的实例对象。
获取类锁的两种用法
- 同步代码块( synchronized(类cass)),锁是小括号()中的类对象(Cass对象)
- 同步静态方法( synchronized static method),锁是当前对象的类对象( Class对象)
不同锁的对象:
package com.lmm.Multithreading;
import java.text.SimpleDateFormat;
import java.util.Date;
public class SyncThread implements Runnable {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
if (threadName.startsWith("A")) {// 异步方法
async();
} else if (threadName.startsWith("B")) {
syncObjectBlock1();
} else if (threadName.startsWith("C")) {
syncObjectMethod1();
} else if (threadName.startsWith("D")) {
syncClassBlock1();
} else if (threadName.startsWith("E")) {
syncClassMethod1();
}
}
/**
* 异步方法
*/
private void async() {
try {
System.out.println(Thread.currentThread().getName()
+ "_Async_Start: "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()
+ "_Async_End: "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 方法中有 synchronized(this|object) {} 同步代码块 被修饰的代码谁先开始,谁先结束,其他抢占
*/
private void syncObjectBlock1() {
System.out.println(Thread.currentThread().getName()
+ "_SyncObjectBlock1: "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
synchronized (this) {// 获取对象锁
try {
System.out.println(Thread.currentThread().getName()
+ "_SyncObjectBlock1_Start: "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()
+ "_SyncObjectBlock1_End: "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* synchronized 修饰非静态同步方法 谁先开始,谁先运行
*/
private synchronized void syncObjectMethod1() {
System.out.println(Thread.currentThread().getName()
+ "_SyncObjectMethod1: "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
try {
System.out.println(Thread.currentThread().getName()
+ "_SyncObjectMethod1_Start: "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()
+ "_SyncObjectMethod1_End: "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 效果同synchronized修饰同步代码块
private void syncClassBlock1() {
System.out.println(Thread.currentThread().getName()
+ "_SyncClassBlock1: "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
synchronized (SyncThread.class) {
try {
System.out.println(Thread.currentThread().getName()
+ "_SyncClassBlock1_Start: "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()
+ "_SyncClassBlock1_End: "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 效果同synchronized修饰非静态方法
private synchronized static void syncClassMethod1() {
System.out.println(Thread.currentThread().getName()
+ "_SyncClassMethod1: "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
try {
System.out.println(Thread.currentThread().getName()
+ "_SyncClassMethod1_Start: "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()
+ "_SyncClassMethod1_End: "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试代码
package com.lmm.Multithreading;
public class SyncDemo {
public static void main(String... args) {
System.out
.println("共享同一个对象锁,被synchronized修饰的区域,一旦start只能等到end,才能由另一个被synchronized修饰的区域start");
SyncThread syncThread = new SyncThread();
Thread A_thread1 = new Thread(syncThread, "A_thread1");
Thread A_thread2 = new Thread(syncThread, "A_thread2");
Thread B_thread1 = new Thread(syncThread, "B_thread1");
Thread B_thread2 = new Thread(syncThread, "B_thread2");
Thread C_thread1 = new Thread(syncThread, "C_thread1");
Thread C_thread2 = new Thread(syncThread, "C_thread2");
Thread D_thread1 = new Thread(syncThread, "D_thread1");
Thread D_thread2 = new Thread(syncThread, "D_thread2");
Thread E_thread1 = new Thread(syncThread, "E_thread1");
Thread E_thread2 = new Thread(syncThread, "E_thread2");
A_thread1.start();
A_thread2.start();
B_thread1.start();
B_thread2.start();
C_thread1.start();
C_thread2.start();
D_thread1.start();
D_thread2.start();
E_thread1.start();
E_thread2.start();
System.out.println("同一个类的不同对象锁之间互不干扰,是异步的");
Thread A_thread3 = new Thread(new SyncThread(), "A_thread3");
Thread A_thread4 = new Thread(new SyncThread(), "A_thread4");
Thread B_thread3 = new Thread(new SyncThread(), "B_thread3");
Thread B_thread4 = new Thread(new SyncThread(), "B_thread4");
Thread C_thread3 = new Thread(new SyncThread(), "C_thread3");
Thread C_thread4 = new Thread(new SyncThread(), "C_thread4");
A_thread3.start();
A_thread4.start();
B_thread3.start();
B_thread4.start();
C_thread3.start();
C_thread4.start();
System.out.println("一个类只有一个类锁,所以在新建类对象的时候其实是同一把类锁,所以表现为同步的");
Thread D_thread3 = new Thread(new SyncThread(), "D_thread3");
Thread D_thread4 = new Thread(new SyncThread(), "D_thread4");
Thread E_thread3 = new Thread(new SyncThread(), "E_thread3");
Thread E_thread4 = new Thread(new SyncThread(), "E_thread4");
D_thread3.start();
D_thread4.start();
E_thread3.start();
E_thread4.start();
}
}
测试结果(每次运行都不同,但是由于锁,有些先后顺序是一定的,可以按ABCDE分开执行试试)
共享同一个对象锁,被synchronized修饰的区域,一旦start只能等到end,才能由另一个被synchronized修饰的区域start
同一个类的不同对象锁之间互不干扰,是异步的
一个类只有一个类锁,所以在新建类对象的时候其实是同一把类锁,所以表现为同步的
E_thread1_SyncClassMethod1: 10:06:23
A_thread1_Async_Start: 10:06:23
D_thread1_SyncClassBlock1: 10:06:23
D_thread3_SyncClassBlock1: 10:06:23
D_thread4_SyncClassBlock1: 10:06:23
E_thread1_SyncClassMethod1_Start: 10:06:23
A_thread2_Async_Start: 10:06:23
B_thread4_SyncObjectBlock1: 10:06:23
A_thread3_Async_Start: 10:06:23
C_thread3_SyncObjectMethod1: 10:06:23
B_thread4_SyncObjectBlock1_Start: 10:06:23
C_thread4_SyncObjectMethod1: 10:06:23
B_thread1_SyncObjectBlock1: 10:06:23
B_thread3_SyncObjectBlock1: 10:06:23
D_thread2_SyncClassBlock1: 10:06:23
A_thread4_Async_Start: 10:06:23
C_thread1_SyncObjectMethod1: 10:06:23
B_thread2_SyncObjectBlock1: 10:06:23
C_thread1_SyncObjectMethod1_Start: 10:06:23
C_thread4_SyncObjectMethod1_Start: 10:06:23
B_thread3_SyncObjectBlock1_Start: 10:06:23
C_thread3_SyncObjectMethod1_Start: 10:06:23
B_thread3_SyncObjectBlock1_End: 10:06:24
A_thread3_Async_End: 10:06:24
C_thread1_SyncObjectMethod1_End: 10:06:24
E_thread1_SyncClassMethod1_End: 10:06:24
B_thread2_SyncObjectBlock1_Start: 10:06:24
A_thread1_Async_End: 10:06:24
B_thread4_SyncObjectBlock1_End: 10:06:24
D_thread2_SyncClassBlock1_Start: 10:06:24
C_thread3_SyncObjectMethod1_End: 10:06:24
A_thread2_Async_End: 10:06:24
A_thread4_Async_End: 10:06:24
C_thread4_SyncObjectMethod1_End: 10:06:24
B_thread2_SyncObjectBlock1_End: 10:06:25
B_thread1_SyncObjectBlock1_Start: 10:06:25
D_thread2_SyncClassBlock1_End: 10:06:25
D_thread4_SyncClassBlock1_Start: 10:06:25
B_thread1_SyncObjectBlock1_End: 10:06:26
C_thread2_SyncObjectMethod1: 10:06:26
C_thread2_SyncObjectMethod1_Start: 10:06:26
D_thread4_SyncClassBlock1_End: 10:06:26
D_thread3_SyncClassBlock1_Start: 10:06:26
C_thread2_SyncObjectMethod1_End: 10:06:27
D_thread3_SyncClassBlock1_End: 10:06:27
D_thread1_SyncClassBlock1_Start: 10:06:27
D_thread1_SyncClassBlock1_End: 10:06:28
E_thread4_SyncClassMethod1: 10:06:28
E_thread4_SyncClassMethod1_Start: 10:06:28
E_thread4_SyncClassMethod1_End: 10:06:29
E_thread3_SyncClassMethod1: 10:06:29
E_thread3_SyncClassMethod1_Start: 10:06:29
E_thread3_SyncClassMethod1_End: 10:06:30
E_thread2_SyncClassMethod1: 10:06:30
E_thread2_SyncClassMethod1_Start: 10:06:30
E_thread2_SyncClassMethod1_End: 10:06:31
对象锁和类锁的总结
- 有线程访问对象的同步代码块时,另外的线程可以访问该对象的非同步代码块
- 若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象的同步代码块的线程会被阻塞
- 若锁住的是同一个对象,一个线程在访问对象的同步方法时,另一个访问对象同步方法的线程会被阻塞
- 一个访问对象同步方法的线程会被阻塞,反之亦然;
- 同一个类的不同对象的对象锁互不干扰
- 类锁由于也是一种特殊的对象锁,因此表现和上述1,2,3,4一致,而由于—个类只有一把对象锁,所以同一个类的不同对象使用类锁将会是同步的
- 类锁和对象锁互不干扰
synchronized底层实现原理
Java对象头
对象头含有三部分:Mark Word(存储对象自身运行时数据)、Class Metadata Address(存储类元数据的指针)、Array length(数组长度,只有数组类型才有)。
重点在Mark Word部分,Mark Word数据结构被设计成非固定的,会随着对象的不同状态而变化,如下图所示。
Monitor
Monitor可以理解为一种同步工具,也可理解为一种同步机制,常常被描述为一个Java对象。
- 互斥:一个Monitor在一个时刻只能被一个线程持有,即Monitor中的所有方法都是互斥的。
- signal机制:如果条件变量不满足,允许一个正在持有Monitor的线程暂时释放持有权,当条件变量满足时,当前线程可以唤醒正在等待该条件变量的线程,然后重新获取Monitor的持有权。
所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。
Monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的转换,成本非常高。
自旋锁与自适应自旋锁
自旋锁
- 许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得
- 通过让线程执行忙循环等待锁的释放,不让出CPU
- 缺点:若锁被其他线程长时间占用,会带来许多性能上的开销
自适应自旋锁
- 自旋的次数不再固定
- 由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
锁消除
JVM编译时,对运行上下文进行扫描,去除不可能存在竞争的锁
package com.lmm.Multithreading;
/**
* @author lmm E-mail:violet_mmhh@163.com
* @time 时间:2019年8月26日
* @function 功能:
*/
public class StringBufferWithoutSync {
public void add(String str1, String str2) {
// StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用
// 因此sb属于不可能共享的资源,JVM会自动消除内部的锁
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
public static void main(String[] args) {
StringBufferWithoutSync withoutSync = new StringBufferWithoutSync();
for (int i = 0; i < 1000; i++) {
withoutSync.add("aaa", "bbb");
}
}
}
锁粗化
通过扩大加锁的范围,避免反复加锁和解锁
public class CoarseSync {
public static String copyString100Times(String target) {
int i = 0;
StringBuffer sb = new StringBuffer();
while (i < 100) {
sb.append(target);
}
return sb.toString();
}
}
synchronized的四种状态
总共有4种锁状态,级别由低到高依次为:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。这几个状态会随着竞争情况逐渐升级。(注意:锁可以升级但不能降级)
无锁是指没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
偏向锁是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。
核心思想:如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word结构也变成偏向锁结构,当该线程再次请求锁的时候,无需再做任何的同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程ID等于Mark Word的ThReadID即可,这样就省略了大量有关锁申请的操作。(因此它不适用于锁竞争比较激烈的场合)。
轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。
适用场景:线程交替执行同步块。
若存在同一时间访问同一锁的情况,就会导致轻量级锁升级为重量级锁。
重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
锁的内存语义
- 当线程释放锁的时候,java内存模型会把该线程对应的本地内存中的共享变量刷新到主内存中。
- 而当线程获取锁的时候,java内存模型会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主存中读取共享变量。
总结
synchronized和ReentrantLock的区别
ReentrantLock(再入锁/重入锁)
jdk中独占锁的实现除了使用关键字synchronized外,还可以使用ReentrantLock。虽然在性能上ReentrantLock和synchronized没有什么区别,但ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。
- import java.util.concurrent.locks.*;
- 与CountDownLatch、FutureTask、Semaphore一样基于AQS(AbstractQueuedSynchronizer)实现;
- 能够实现比synchronized更细粒度的控制,如控制fairness
- 调用Lock()之后,必须调用Unlock()释放锁;
- 性能未必比synchronized高,并且也是可重入(当一个线程试图获取他已经获取的锁时,这个获取就会自动成功)的;
ReentrantLock的公平性设置
- ReentrantLock fairLock = new ReentrantLock(true);//给一个true的参数就会变成公平锁
- 参数为true的时候,倾向于将锁赋予等待时间最长的线程
- 公平锁:获取锁的顺序按照先后lock()方法的顺序(慎用)
- 非公平锁:抢占的顺序不一定,看运气
- synchronized是非公平锁。
ReentrantLock 将锁对象化
- 判断是否有线程或者某个特定线程,在排队等待获取锁。
- 带超时获取锁的尝试。
- 感知到有没有成功获取锁。
能否将wait、notify和notifyall对象化(答案是肯定的)
通过import java.util.concurrent.locks.Condition实现
总结
- synchronized是关键字,ReentrantLock是类;
- ReentrantLock可以获取锁的等待时间进行设置,避免死锁;
- ReentrantLock可以获取各种锁的信息;
- ReentrantLock可以灵活的实现多路通知;
- 机制:synchronized操作Mark Word,ReentrantLock底层调用Unsafe类的pack()方法
volatile和synchronized的区别
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
- volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
volatile的两个作用(参考)
保证内存的可见性
volatile的特殊规则就是:
- read、load、use动作必须连续出现。
- assign、store、write动作必须连续出现。
所以,使用volatile变量能够保证:
- 每次
读取前
必须先从主内存刷新最新的值。 - 每次
写入后
必须立即同步回主内存当中。
也就是说,volatile关键字修饰的变量看到的随时是自己的最新值。线程1中对变量v的最新修改,对线程2是可见的。
防止指令重排序
volatile关键字通过“内存屏障”
来防止指令被重排序。
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。然而,对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,Java内存模型采取保守策略。
下面是基于保守策略的JMM内存屏障插入策略:
- 在每个volatile写操作的前面插入一个StoreStore屏障。
- 在每个volatile写操作的后面插入一个StoreLoad屏障。
- 在每个volatile读操作的后面插入一个LoadLoad屏障。
- 在每个volatile读操作的后面插入一个LoadStore屏障。