参考视频:https://www.bilibili.com/video/BV1aJ411V763?p=15&spm_id_from=pageDriver
并发编程的三个问题
可见性问题
可见性是指指一个线程对共享变量进行了修改,另外的线程没用需要看到更新后共享变量的值。
可见性问题是指一个线程对共享变量进行了修改,另外的线程没有立即看到更新后共享变量的值。
public class TestVisibility {
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (flag) {
}
}, "t1").start();
Thread.sleep(1000);
new Thread(() -> {
flag = false;
System.out.println("线程修改了flag的值,修改后是false");
}, "t2").start();
}
}
输出结果:
t2把共享变量flag的值修改成false了,但是t1一直没有发现,所以一直在循环不结束。
原子性
由.java文件编译成.class文件后,原来的一句java语句(一次操作)可能对应多条Java字节码指令。
原子性指的就是在一次操作或多次操作中,要么对应的所有的JVM字节码指令都执行,要么对应的所有的JVM字节码指令都不执行。
原子性问题就是一个线程对共享变量操作还没完成时,其他线程也来操作共享变量,干扰了前一个线程的工作。
public class TestAtomicity {
private static int number = 0;
public static void main(String[] args) throws InterruptedException {
Runnable increment = () -> {
for (int i = 0; i < 1000; i++) {
number ++;
}
};
ArrayList<Thread> threads = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(increment);
thread.start();
threads.add(thread);
}
for (Thread thread : threads) {
thread.join();
}
System.out.println("number = " + number);
}
}
输出结果:
最后的number小于5000了。
number++;
操作实际上包含3个步骤:
- 从主存中读取数据到工作内存
- 线程对工作内存中的数据进行++操作
- 将工作内存的数据写回到主内存
假设这么一种情况:线程1在执行完步骤1从主存读取数据到工作内存,number的值为2,再执行步骤2后,number的值为3,此时还没有执行到步骤3,但时间片用完了;轮到线程2执行了,线程2执行执行完步骤1、2、3,得到number的值为3,写回主存;线程1再继续执行步骤3,将number的值为3写入主存。
有序性
有序性是指程序中代码的执行顺序。
Java在编译和运行时会对没有数据依赖关系的代码进行优化,导致程序执行的顺序不一定就是我们编写代码时的顺序。重排序可以提高程序运算和处理速度。但线程并发执行时,指令重排序可能会让得到的结果与预期不一致。
Java内存模型JMM
Java Memory Model(Java内存模型/JMM),是Java虚拟机规范中定义的一种内存模型,屏蔽了底层不同计算机的区别,是一套规范。JMM描述了Java程序种各种变量的访问规则,以及在JVM种将变量存储到内存和从内存读取变量的底层细节。具体如下:
- 主内存:所有线程共享的,所有的共享变量存储于主存。
- 工作内存:每个线程独有的,工作内存只存储该线程对共享变量的副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量
Java内存模型是一套规范,可以保证并发编程中共享数据的可见性、原子性、有序性。主要有两个关键字synchronized、volatile。
主存和工作内存之间的交互
Java内存模型中定义了以下8种操作来完成,主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。
Lock、Read、Load、Use、Assign、Store、Write、Unlock
注意:对一个变量执行Lock操作,将会清空工作内存中此变量的值。对一个变量执行Unlock操作,必须先把此变量同步到主存中。
synchronized保证三大特性
synchronized保证可见性
public class TestVisibilitySynchronized {
//private static volatile boolean flag = true;
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (flag) {
System.out.println("flag = " + flag);
}
}).start();
Thread.sleep(1000);
new Thread(() -> {
flag = false;
System.out.println("线程修改了flag的值,修改后是false");
}).start();
}
}
输出结果:
println()方法中有synchronized。
synchronized会对应lock操作,刷新工作内存中共享变量的值,更新到与主内存中共享变量的值一致。
也可以使用
volatile
关键字来修饰共享变量,一旦某个线程更改了共享变量的值,就会同时更新到主内存中,其他线程的工作内存原来的共享变量副本记录将失效,再去主内存拷贝一份共享变量的最新副本,从而实现线程间共享变量的可见性。
synchronized保证原子性
public class TestAtomicitySynchronized {
private static int number = 0;
public static void main(String[] args) throws InterruptedException {
Runnable increment = () -> {
for (int i = 0; i < 1000; i++) {
synchronized (TestAtomicitySynchronized.class) {
number ++;
}
}
};
ArrayList<Thread> threads = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(increment);
thread.start();
threads.add(thread);
}
for (Thread thread : threads) {
thread.join();
}
System.out.println("number = " + number);
}
}
输出结果:
synchronized后,虽然进行了重排序,保证只有一个线程会进入同步代码块,也能保证有序性。
volatile
不能保证对数据操作的原子性,在多线程环境下volatile修饰的变量也是线程不安全的。多线程下保证线程安全还得用锁机制。
原子类也可以保证原子操作。JDK1.5开始提供了java.util.concurrent.atomic包,这个包下的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。
synchronized保证有序性
as-if-serial意思是:不管编译器和CPU如何重排序,必须保证单线程下程序的结果是正确的。编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。
加synchronized
后,一个函数内不具有数据依赖关系的操作语句依然会发生重排序,但是有同步代码块的存在,可以保证只有一个线程执行同步代码块中的代码,保证有序性。
在变量上加上volatile
也可以保证不会进行指令重排序。
synchronized的特性
可重入性
可重入性是指一个线程可以多次执行synchronized,重复获取同一把锁。
原理:synchronized的锁对象中有一个计数器(recursions变量),会记录线程获得几次锁。在执行完同步代码块时,计数器会减1,直至减到0。
好处:
- 避免死锁
- 方便封装代码
不可中断性
不可中断性是指一个线程获得锁后,其他线程想要获得该锁,必须处于阻塞(BLOCK)或者等待状态(WAITING),如果第一个线程不释放锁,其他线程会一直处于阻塞或等待状态。
- synchronized是不可中断的
public class SynchronizedInterruptible {
private static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "进入同步代码块");
try {
Thread.sleep(88888);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t1 = new Thread(runnable, "t1");
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(runnable, "t2");
t2.start();
System.out.println("停止线程前");
System.out.println(t1.getState());
System.out.println(t2.getState());
t2.interrupt();//强行中断t2
System.out.println("停止线程后");
System.out.println(t1.getState());
System.out.println(t2.getState());
}
}
输出结果:
- Lock的lock()方法是不可中断的
- Lock的tryLock()方法是可中断的,tryLock()方法会在指定时间内去请求锁资源,请求不到也不会阻塞。
public class LockInterruptible {
private static Lock lock= new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
test01();
//test02();
}
//Lock不可中断
public static void test01() throws InterruptedException {
Runnable runnable = () -> {
String name = Thread.currentThread().getName();
lock.lock();
System.out.println(name + "获得锁,进入锁执行");
try {
Thread.sleep(88888);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println(name + "释放锁");
}
};
Thread t1 = new Thread(runnable, "t1");
Thread t2 = new Thread(runnable, "t2");
t1.start();
Thread.sleep(1000);
t2.start();
System.out.println("停止线程前");
System.out.println(t1.getState());
System.out.println(t2.getState());
t2.interrupt();//强行中断t2
System.out.println("停止线程后");
System.out.println(t1.getState());
System.out.println(t2.getState());
}
//Lock可中断
public static void test02() throws InterruptedException {
Runnable runnable = () -> {
String name = Thread.currentThread().getName();
boolean b = false;
try {
b = lock.tryLock(3, TimeUnit.SECONDS);
//lock.lock();
if (b) {
System.out.println(name + "获得锁,进入锁执行");
Thread.sleep(88888);
} else {
System.out.println(name + "在指定时间内没有获得锁,执行其他操作");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (b) {
lock.unlock();
System.out.println(name + "释放锁");
}
}
};
Thread t1 = new Thread(runnable, "t1");
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(runnable, "t2");
t2.start();
}
}
test01()执行结果:
test02()执行结果:
Synchronized底层原理
synchronized修饰同步代码块
synchronized 同步语句块的实现使用的是 monitorenter
和 monitorexit
指令,其中monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位置。
当执行 monitorenter 指令时,线程试图获取锁也就是获取对象监视器 monitor 的持有权。如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1,如果线程已经拥有monitor的所有权,允许它重入,则monior的计数器会再加1。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外⼀个线程释放锁资源为止。
在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放。
synchronized出现异常也会释放锁。
synchronized修饰方法
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED
标识,该标识指明了该方法是⼀个同步方法,会隐式调用monitorenter
和monitorexit
。JVM 通过该ACC_SYNCHRONIZED 访问标志来辨别⼀个方法是否声明为同步方法,从而执行相应的同步调用。
常见面试题
synchronized和Lock的区别?
- synchronized是关键字,Lock是接口。
- synchronized会自动完成加锁和释放锁,Lock需要手动调用lock()、tryLock()、unLock()方法来加锁和释放锁。
- synchronized是不可中断的,Lock如果是调用lock()方法也是不可中断的,Lock调用tryLock()方法是可中断的。
- synchronized可以锁住方法和代码块,Lock只能锁住代码块。
- 通过Lock可以知道线程有没有拿到锁(看返回值),synchronized不能。
- Lock可以使用读锁提高多线程读效率。
- synchronized是非公平锁,ReentrantLock可以控制是否是公平锁。
synchronized和volatile的区别?
- synchronized可以修饰代码块、方法;volatile用于修饰变量。
- synchronized可以保证数据可见性、原子性;volatile只能保证数据可见性。
- synchronized用于解决多线程访问资源的同步性问题;volatile用于解决共享变量在多线程之间的可见性。
- volatile可以看出synchronized的轻量级实现,volatile的读写操作都是无锁的,不需要花费时间在获取锁和释放锁上,所以说volatile是低成本的。如果只是多个线程对共享变量的赋值,没有其他操作,可以用volatile修饰共享变量来代替synchronized。