程序运行环境
maven
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
</dependencies>
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration >
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date{HH:mm:ss} [%t] %logger - %m%n</pattern>
</encoder>
</appender>
<logger name="com.laowa" level="debug" additivity="false">
<appender-ref ref="STDOUT"/>
</logger>
</configuration>
线程安全问题
- 一个程序运行多个线程本身是没有问题的,问题出现在多个线程访问共享资源(多个线程对共享资源读写操作时发生指令交错),一段代码内如果存在对共享资源的多线程读写操作,这段程序称为临界区
- 多个线程在临界区执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
@Slf4j
public class Demo {
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
counter++;
}
},"t1");
Thread t2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
counter++;
}
},"t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("counter={}",counter);
}
}
预期结果为10000,实际结果为6979
synchronized解决方案
synchronized是阻塞式的解决方案,俗称对象锁,他会采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其他线程再想获取这个对象锁就会被阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文的切换
@Slf4j
public class Demo {
static int counter = 0;
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
synchronized (lock){
for (int i = 0; i < 5000; i++) {
counter++;
}
}
},"t1");
Thread t2 = new Thread(()->{
synchronized (lock){
for (int i = 0; i < 5000; i++) {
counter++;
}
}
},"t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("counter={}",counter);
}
}
synchronized实际是用对象锁保证了临界区内代码的原子性,临界区的代码对外是不可分割的,不会被线程切换所打断
面向对象改进
@Slf4j
public class Demo {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
room.increment();
}
},"t1");
Thread t2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
room.decrement();
}
},"t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("counter={}",room.getCounter());
}
}
class Room{
private int counter = 0;
public void increment(){
//锁住自身对象,不用额外创建一个属性用于加锁
synchronized (this){
counter++;
}
}
public void decrement(){
synchronized (this){
counter--;
}
}
public int getCounter(){
return counter;
}
}
方法锁
public synchronized void m(){
}
等价于
public void m(){
synchronized(this){
}
}
两个方式都是对当前对象实例加锁
class Test{
public synchronized static void m(){
}
}
等价于
class Test{
public static void m(){
synchronized(Test.class){
}
}
}
两个方式都是锁住当前类的class对象
线程安全性分析
变量的线程安全性
- 成员变量和静态变量:如果被多个线程共享了,并且同时有读写操作,则需要考虑线程安全
- 局部变量:如果局部变量的作用范围逃离了方法的作用范围(作为返回值和参数)
常见的线程安全类
- String
- Integer
- StringBuffer(StringBuilder线程不安全)
- Random
- Vector
- HashTable(HashMap线程不安全)
- java.util.concurrent包下的类
这里所述的线程安全是指,多个线程调用他们同一个实例的某个方法时,是线程安全的。也可以他们的每个方法是原子的,但是多个方法组合在一起可能出现线程问题
多个方法组合导致线程不安全例子
Hashtable table = new Hashtable();
new Thread(()->{
if(table.get("key")!=null){
table.put("key",1);
}
}).start();
new Thread(()->{
if(table.get("key")!=null){
table.put("key",2);
}
}).start();
两个线程可能同时进入if条件中,同时执行了put操作,其中一个put操作就会失效
不可变类的线程安全性
String、Integer等都是不可变类,因为其内部的状态不可改变
对象内部的操作都不会在原有的对象内部进行改变,而是创建新的对象,内部的字符串不会收到任何的改动(没有写操作,不会有线程安全问题)
synchronized工作原理(Monitor)
Java对象头
普通对象:Mark Word(32bit)+Klass Word(32bit)
数组对象:Mark Word(32bit)+Klass Word(32bit)+array length(32bit)
其中Mark Word在不同状态下的内容为:
Monitor工作原理
Monitor翻译为监视器或管程,每个对象都会关联一个Monitor,synchronized的加锁就基于这个Monitor(不论synchornized是加在对象、类、方法上,实际都是对某个对象加了锁)
从字节码层面,synchornized在修饰同步代码块和修饰同步方法时,采用的方式不同
修饰同步代码块
public final class Demo{
public static void main(String[] args) {
Demo.getInstance();
}
private Demo(){}
private static Demo instance = null;
public static Demo getInstance(){
if(instance==null){
synchronized (Demo.class){
if(instance==null){
instance = new Demo();
}
}
}
return instance;
}
}
修饰同步方法
public final class Demo{
public static void main(String[] args) {
Demo.getInstance();
}
private Demo(){}
private static Demo instance = null;
public static synchronized Demo getInstance(){
if(instance==null){
instance = new Demo();
}
return instance;
}
}
等待/通知机制
场景
当一个线程的运行需要建立在某个变量的变化,或某个线程的运行基础之上(join是建立在父子线程的关系上,而且与锁无关),例如:
@Slf4j
public final class Demo{
static boolean flag = true;
static Object lock = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
synchronized (lock){
while (flag){
try {
// 使用sleep方法不会释放锁,当前线程就无效的占用资源
// 并且使用sleep时间不好掌控,如果太长导致对flag的变化不能即使的响应,如果太短则一直占用cpu空转,造成资源浪费
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 进行下一步操作
}
},"t1");
Thread t2 = new Thread(()->{
flag = false;
},"t2");
}
}
解决这样一个生产者-消费者的同步问题,使用sleep的方式就会有如下两个缺陷:
- 难以保证及时性,如果睡得太久,就无法对flag的改变做出及时的响应
- 难以降低开销,如果睡眠时间减少,线程能够及时做出反应,但是空转导致一直占用处理器资源,早曾无端的资源浪费
解决方案
Object中有两个方法:wait、notify;这两个方法就可以解决这个问题:当一个对象调用wait,当前执行的线程会被挂起,同时释放掉对象锁;只有另一个线程在调用该对象的notify方法,重新唤醒wait中的线程,这样就可以保证线程能够在满足执行需求时迅速的开始行动
@Slf4j
public final class Demo{
static boolean flag = true;
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
synchronized (lock){
while (flag){
try {
// 挂起,释放当前锁,等待其他线程的唤醒
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 进行下一步操作
log.debug("ok");
}
},"t1");
Thread t2 = new Thread(()->{
// 调用notify的线程需要先获取对象的锁
synchronized (lock){
flag = false;
lock.notify();
}
},"t2");
t1.start();
t2.start();
t1.join();
t2.join();
}
}
相关方法
方法名称 | 描述 |
---|---|
notify() | 通知一个在对象上等待的线程,使其从wait()方法返回;返回的前提是这个线程抢到了对象锁(即只有抢到锁的一个线程才会被唤醒) |
notifyAll() | 通知所有在该对象上等待的线程 |
wait() | 线程进入WAITING状态,只有等待另外线程调用notify或者线程中断才会返回 |
wait(long) | 等待一段时间,当时间达到参数指定的毫秒数还没有通知,就会返回 |
wait(long,int) | 对超时时间进行更细粒度的控制,可以达到纳秒 |
wait/notify原理
- 当Owner线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING状态
- 其他线程调用notify方法时,会通知WaitSet,随机挑选一个线程进入EntryList进行锁竞争
- 被唤醒的线程不会立即获得锁,仍然需要进入EntryList变成BLOCK状态
等待/通知范式
优化前的代码
@Slf4j
public final class Demo{
static boolean flag1 = true;
static boolean flag2 = true;
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
synchronized (lock){
if(flag1){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(flag1){
log.debug("1操作失败");
}else{
log.debug("执行操作1");
}
}
},"t1");
Thread t2 = new Thread(()->{
synchronized (lock){
if(flag2){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(flag2){
log.debug("2操作失败");
}else{
log.debug("执行操作2");
}
}
},"t2");
Thread t3 = new Thread(()->{
synchronized (lock){
flag2 = false;
lock.notify();
}
},"t2");
t1.start();
t2.start();
Thread.sleep(1000);
t3.start();
}
}
如果t3线程在唤醒时,t1争抢到了锁,就会出现这样的矛盾:t3的目的是唤醒t2,却错误的唤醒了t1,这种矛盾称为虚假唤醒;这样会导致t2没能正常工作,同时t1也无法再正常工作了
于是推出这样一种范式:
等待方
synchronized(lock){
while(条件不满足){
lock.wait()
}
处理操作
}
通知方
synchronized(lock){
改变条件
lock.notifyAll();
}
对应如上代码
@Slf4j
public final class Demo{
static boolean flag1 = true;
static boolean flag2 = true;
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
synchronized (lock){
while(flag1){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("执行操作1");
}
},"t1");
Thread t2 = new Thread(()->{
synchronized (lock){
while(flag2){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("执行操作2");
}
},"t2");
Thread t3 = new Thread(()->{
synchronized (lock){
flag2 = false;
lock.notifyAll();
}
},"t2");
t1.start();
t2.start();
Thread.sleep(1000);
t3.start();
}
}
这样一来,通过notifyAll可以保证所有线程都被唤醒;在while循环中,没有满足继续执行条件的线程会再次进入等待
Park/Unpark
LockSupport工具类中的park和unpark方法也能够对线程进行阻塞和唤醒
@Slf4j
public final class Demo{
static boolean flag1 = true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while(flag1){
LockSupport.park();
}
log.debug("执行操作1");
},"t1");
t1.start();
Thread.sleep(1000);
flag1 = false;
LockSupport.unpark(t1);
}
}
与wait/notify对比
- wait/noify必须配合Object Monitor一起使用,即必须获取对象锁,而park/unpark不用
- park/unpark是以线程为单位来阻塞和唤醒线程,而wait/notify只能随机唤醒一个线程;park/unpark比wait/notify更精确
- park/unpark可以先执行unpark,后park仍然可以接受唤醒;而wait/notify不能先执行notify,后wait不能接受唤醒
原理
每个线程都关联一个Parker对象,由三部分组成:_counter,_cond,_mutex
调用park
- 检查_counter,如果_counter为0,获得_mutex互斥锁
- 线程进入_cond条件变量阻塞
- 设置_counter=0
调用unpark
- 设置_counter=1
- 唤醒_cond条件变量中的线程
- 设置_counter为0
活跃性
死锁
假设有两个线程:
t1获得了A锁,想要获取B锁
t2获得了B锁,想要获得A锁
两个线程就会陷入矛盾,互不相让
@Slf4j
public final class Demo{
static Object lockA = new Object();
static Object lockB = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
synchronized (lockA){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB){
}
}
},"t1");
Thread t2 = new Thread(()->{
synchronized (lockB){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockA){
}
}
},"t2");
t1.start();
t2.start();
}
}
避免死锁的方法
- 避免一个线程同时获取多个锁
- 避免一个线程在锁内同时占有多个资源,尽量保证每个锁只占用一个资源
- 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
- 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况
活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束
@Slf4j
public final class Demo{
static volatile int count = 10;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while(count>0){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
log.debug("t1-{}",count);
}
},"t1");
Thread t2 = new Thread(()->{
while(count<20){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
log.debug("t2-{}",count);
}
},"t2");
t1.start();
t2.start();
}
}
饥饿
一个线程由于优先级太低,每次锁竞争都被其他线程抢走锁,导致该线程一直得不到运行