在上一篇文章中,我们说了造成线程不安全的原因,那今天我们就来聊聊线程安全问题的解决方案~
目录
2.3 synchronized VS Lock
一、volatile解决内存可见性和指令重排序
volatile修饰的变量,能够保证可见性和指令重排序问题。
代码在写入volatile修饰的变量时:
· 改变线程工作内存中volatile变量副本的值
· 将改变后的副本的值从工作内存中刷新到主内存
代码在读取volatile修饰的变量时:
· 从主内存读取volatile变量的最新值到线程的工作内存中
· 从工作内存中读取 volatile变量的副本
这样子的工作流程就解决了内存可见性问题,加上了volatile强制读写内存,速度是慢了,但是数据变得更准确了。
代码演示volatile用途:
public class ThreadDemo16 {
private static volatile boolean flag=true;
public static void main(String[] args) {
Thread thread1=new Thread(()->{
System.out.println("开始执行:"+ LocalDateTime.now());
while (flag){
}
System.out.println("flag被修改为false"+LocalDateTime.now());
});
thread1.start();
Thread thread2=new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("将flag修改为false:"+LocalDateTime.now());
flag=false;
});
thread2.start();
}
}
volatile缺点:
volatile虽然可以解决内存可见性问题和指令重排序的问题,但还是不能保证原子性,对于++和--这种非原子性的问题还是解决不了。
如下代码所示:
public class ThreadDemoVolatile {
static class Counter{
private volatile int num=0;
private int MAX_COUNT=0;
public Counter(int MAX_COUNT){
this.MAX_COUNT=MAX_COUNT;
}
//++方法
public void increment(){
for (int i = 0; i < MAX_COUNT; i++) {
num++;
}
}
//--方法
public void decrement(){
int temp=0;
for (int i = 0; i < MAX_COUNT; i++) {
num--;
}
}
public int getNum(){
return num;
}
}
public static void main(String[] args) throws InterruptedException {
ThreadDemo15.Counter counter=new ThreadDemo15.Counter(100000);
Thread thread1=new Thread(()->{
counter.increment();
});
Thread thread2=new Thread(()->{
counter.decrement();
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("最终结果:"+counter.getNum());
}
}
因此我们就引出了锁的概念~
二、锁(synchronized和lock)
java中的锁是解决线程安全问题的最主要手段,锁主要分为两种:①内存锁synchronized ②可重入锁lock。
2.1 synchronized
2.1.1 synchronized的特性
1) 互斥性(排他性)
synchronized会起到互斥的作用,某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同一个对象的synchronized就会阻塞等待。
· 进入synchronized修饰的代码块相当于加锁
· 退出synchronized修饰的代码块相当于解锁
2) 刷新内存
synchronized的工作过程:
1.获得互斥锁
2.从主内存拷贝变量的最新副本到工作内存中
3.执行代码
4.将更改后的共享变量的值刷新到主内存
5.释放主内存
由上述的工作过程可以看出来,synchronized也可以解决内存可见性问题。
代码示例:
import java.time.LocalDateTime;
public class ThreadDemo16 {
static class Counter{
public boolean flag=true;
}
public static void main(String[] args) {
Counter counter=new Counter();
Thread thread1=new Thread(()->{
System.out.println("开始执行:"+ LocalDateTime.now());
while (true) {
synchronized (counter) {
if(!counter.flag){
System.out.println("flag被修改为false"+LocalDateTime.now());
break;
}
}
}
});
thread1.start();
Thread thread2=new Thread(()->{
synchronized (ThreadDemo16.class) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("将flag修改为false:"+LocalDateTime.now());
counter.flag= false;
}
});
thread2.start();
}
}
3) 可重入性
synchronized对于同一条线程来说是可重入的,不会出现把自己锁死的问题。
/**
* synchronized的可重入性测试
*/
public class ThreadSynchronized4 {
public static void main(String[] args) {
synchronized (ThreadSynchronized4.class) {
System.out.println("当前线程已经得到了锁");
synchronized (ThreadSynchronized4.class) {
System.out.println("当前线程再次得到了锁");
}
}
}
}
synchronized使用的锁是存在java对象头中的。
2.1.2 synchronized的基本使用
1) 修饰普通方法
/**
* Synchronized修饰普通方法
*/
public class ThreadSynchronized2 {
private static int num=0;
static class Counter{
private static int MAX_COUNT=100000;
//++方法
public synchronized void increment(){
for (int i = 0; i < MAX_COUNT; i++) {
num++;
}
}
//--方法
public synchronized void decrement(){
int temp=0;
for (int i = 0; i < MAX_COUNT; i++) {
num--;
}
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter=new Counter();
Thread thread1=new Thread(()->{
counter.increment();
});
Thread thread2=new Thread(()->{
counter.decrement();
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("最终结果:"+num);
}
}
2) 修饰静态方法
/**
* Synchronized修饰静态方法
*/
public class ThreadSynchronized {
private static int num=0;
static class Counter{
private static int MAX_COUNT=100000;
//++方法
public static synchronized void increment(){
for (int i = 0; i < MAX_COUNT; i++) {
num++;
}
}
//--方法
public static synchronized void decrement(){
int temp=0;
for (int i = 0; i < MAX_COUNT; i++) {
num--;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1=new Thread(()->{
Counter.increment();
});
Thread thread2=new Thread(()->{
Counter.decrement();
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("最终结果:"+num);
}
}
3)修饰代码块
注意事项:
1.修饰代码块时,对于同一个业务的多个对象的加锁对象,注意加锁的应是同一个对象(即加同一把锁);
2.synchronized修饰代码块,代码块在静态方法中时,不能使用this。
synchronized中的对象可以有以下三种形式:
public class SynchronizedDemo {
public void method(){
//1.使用this锁当前对象
synchronized (this){
}
//2.锁当前类对象
synchronized (SynchronizedDemo.class){
}
//3.自定义的锁对象
Object obj=new Object();
synchronized (obj){
}
}
}
/**
* Synchronized修饰代码块
*/
public class ThreadSynchronized3 {
private static int num=0;
static class Counter{
//自定义锁对象
private Object myLock=new Object();
private static int MAX_COUNT=100000;
//++方法
public void increment(){
for (int i = 0; i < MAX_COUNT; i++) {
synchronized (myLock) {
num++;
}
}
}
//--方法
public void decrement(){
int temp=0;
for (int i = 0; i < MAX_COUNT; i++) {
synchronized (myLock) {
num--;
}
}
}
public static void fun(){
synchronized (Counter.class){
num++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter=new Counter();
Thread thread1=new Thread(()->{
counter.increment();
});
Thread thread2=new Thread(()->{
counter.decrement();
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("最终结果:"+num);
}
}
2.1.3 synchronized的底层实现
· 在JVM层面synchronized是依靠监视器实现的;
· 从操作系统层面来看,synchronized是基于操作系统的互斥锁(mutex)实现的。
监视器是一个概念或者说是一个机制,它用来保障在任何时候,只有一个线程能够执行指定区域的代码。
下面,我们来看以下synchronized在字节码层面是如何实现的:
public class SynchronizedToMonitorExample {
public static void main(String[] args) {
int count = 0;
synchronized (SynchronizedToMonitorExample.class) {
for (int i = 0; i < 10; i++) {
count++;
}
}
System.out.println(count);
}
}
将上述代码翻译成字节码之后是这样的:
从上述结果中我们看出,在main方法中多了monitorenter和moniterexit两个指令,他们分别表示进入监视器和退出监视器,由此得出在JVM层面synchronized是依靠监视器实现的。
JVM监视器的执行流程:
在Java中,synchronized是非公平锁,也是可重入锁。
非公平锁是指:线程获取锁的顺序不是按照访问顺序先来先得的,而是由线程自己竞争,随机获取到锁。
可重入锁是指:一个线程获取到锁之后,可以重复得到该锁。
在Java虚拟机中(hotSpot)中,Monitor 底层是由 C++实现的,它的实现对象是 ObjectMonitor,
ObjectMonitor 结构体的实现如下:
ObjectMonitor::ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0; //线程的重⼊次数
_object = NULL;
_owner = NULL; //标识拥有该monitor的线程
_WaitSet = NULL; //等待线程组成的双向循环链表,_WaitSet是第⼀个节点
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; //多线程竞争锁进⼊时的单向链表
FreeNext = NULL ;
_EntryList = NULL ; //_owner从该双向循环链表中唤醒线程结点,_EntryList
是第⼀个节点
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
监视器执⾏的流程如下:
1. 线程通过 CAS(对比并替换)尝试获取锁,如果获取成功,就将 _owner 字段设置为当前线程,说明当前线程已经持有锁,并将 _recursions 重⼊次数的属性 +1。如果获取失败则先通过⾃旋 CAS尝试获取锁,如果还是失败则将当前线程放⼊到 EntryList 监控队列(阻塞)。
2. 当拥有锁的线程执⾏了 wait ⽅法之后,线程释放锁,将 owner 变量恢复为 null 状态,同时将该线程放入 WaitSet 待授权队列中等待被唤醒。
3. 当调用 notify ⽅法时,随机唤醒 WaitSet 队列中的某⼀个线程,当调用 notifyAll 时唤醒所有的
WaitSet 中的线程尝试获取锁。
4. 线程执行完释放了锁之后,会唤醒 EntryList 中的所有线程尝试获取锁。
以上就是监视器的执⾏流程,执⾏流程如下图所示:
2.1.4 锁升级
在jdk1.6之前synchronized使用的较少,因为synchronized默认使用重量级锁实现,所以性能较差。jdk1.6对synchronized做了优化,实现了锁升级。
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
2.2 Lock(手动锁/可重入锁)
2.2.1 Lock的实现步骤
1.创建Lock 2.加锁lock.lock() 3.释放锁lock.unlock()。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 手动锁(可重入锁)
*/
public class ThreadLock {
public static void main(String[] args) {
//1、创建锁对象
Lock lock=new ReentrantLock();
//2、加锁操作
lock.lock();
try {
//业务代码(可能会非常复杂->导致异常)
System.out.println("你好,ReentrantLock");
}finally {//unlock一定要放在finally中
//3.释放锁
lock.unlock();
}
}
}
注意事项:
1.释放锁unlock操作一定要放在finally代码块中,防止因为业务代码有异常直接结束执行,而导致的锁资源永久占用的问题。
2.加锁lock.lock()一定要放在try之前,或者是try的首行。原因有两个:①如果没放在try的首行或try前面,如果因为try中代码异常导致加锁失败,还会执行finally中释放锁的操作;②释放锁的异常会覆盖try中的业务异常,增加排查难度。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 手动锁(可重入锁)
*/
public class ThreadLock {
public static void main(String[] args) {
//1、创建锁对象
Lock lock=new ReentrantLock();
try {
//业务代码(可能会非常复杂->导致异常)
int y=10/0;
//2、加锁操作
lock.lock();
}finally {//unlock一定要放在finally中
//3.释放锁
lock.unlock();
}
}
}
2.2.2 Lock公平锁和非公平锁
Lock可以指定锁的类型,默认情况下创建一个非公平锁,非公平锁的执行效率较高。
创建锁时传递参数true会创建一个公平锁。
源码如下:
2.2.3 Lock的应用
应用场景:对0进行10万次的++操作,再进行10万次的--操作。看一下公平锁和非公平锁的性能。
公平锁:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadLock2 {
private static int num=0;
static class Counter{
//1.创建锁对象
private Lock lock=new ReentrantLock(true);
private static int MAX_COUNT=100000;
//++方法
public void increment(){
for (int i = 0; i < MAX_COUNT; i++) {
//2.加锁操作
lock.lock();
try {
num++;
}finally {
//3.释放锁
lock.unlock();
}
}
}
//--方法
public void decrement(){
int temp=0;
for (int i = 0; i < MAX_COUNT; i++) {
//2.加锁操作
lock.lock();
try {
num--;
}finally {
//3.释放锁
lock.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter=new Counter();
long stime=System.currentTimeMillis();
Thread thread1=new Thread(()->{
counter.increment();
});
Thread thread2=new Thread(()->{
counter.decrement();
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
long etime=System.currentTimeMillis();
System.out.println("最终结果:" + num + "| 执行时间:" + (etime-stime));
}
}
非公平锁:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadLock2 {
private static int num=0;
static class Counter{
//1.创建锁对象
private Lock lock=new ReentrantLock();
private static int MAX_COUNT=100000;
//++方法
public void increment(){
for (int i = 0; i < MAX_COUNT; i++) {
//2.加锁操作
lock.lock();
try {
num++;
}finally {
//3.释放锁
lock.unlock();
}
}
}
//--方法
public void decrement(){
int temp=0;
for (int i = 0; i < MAX_COUNT; i++) {
//2.加锁操作
lock.lock();
try {
num--;
}finally {
//3.释放锁
lock.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter=new Counter();
long stime=System.currentTimeMillis();
Thread thread1=new Thread(()->{
counter.increment();
});
Thread thread2=new Thread(()->{
counter.decrement();
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
long etime=System.currentTimeMillis();
System.out.println("最终结果:" + num + "| 执行时间:" + (etime-stime));
}
}
由此我们可以看出,非公平锁的性能要好于公平锁的性能。
2.3 synchronized VS Lock
1.synchronized是JVM层面提供的锁,它是自动进行加锁和释放锁操作,对于开发者是无感的,而 Lock需要开发者自己进行加锁和释放锁的操作。
2.锁的类型不同,Lock默认是非公平锁,但可以设置为公平锁,而synchronized只能是非公平锁。
3.Lock更灵活,有更多的方法:比如tryLock()...
public class ThreadLock4 {
public static void main(String[] args) throws InterruptedException {
Lock lock=new ReentrantLock();
for (int i = 0; i < 15; i++) {
//每次等待1+i的时间获取锁,如果超时未获取到,返回false,否则返回true
boolean result = lock.tryLock(1+i, TimeUnit.SECONDS);
}
}
}
4.调用lock()方法和synchronized线程等待锁的状态不同,lock方法会让线程状态变为waiting,而synchronized会让线程状态变成blocked。
使用synchronized加锁:
public class ThreadLock4 {
public static void main(String[] args) throws InterruptedException {
Lock lock=new ReentrantLock();
Thread t1=new Thread(()->{
synchronized (ThreadLock4.class) {
System.out.println("线程1得到了锁");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1释放了锁");
}
});
t1.start();
Thread t2=new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (ThreadLock4.class) {
System.out.println("线程2获取到了锁");
}
});
t2.start();
Thread.sleep(1500);
System.out.println("线程2的状态:"+t2.getState());
}
}
调用lock方法加锁:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadLock4 {
public static void main(String[] args) throws InterruptedException {
Lock lock=new ReentrantLock();
Thread t1=new Thread(()->{
lock.lock();
System.out.println("线程1得到了锁");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("线程1释放锁");
lock.unlock();
}
});
t1.start();
Thread t2=new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
try {
System.out.println("线程2获取到了锁");
}finally {
lock.unlock();
}
});
t2.start();
Thread.sleep(1500);
System.out.println("线程2的状态:"+t2.getState());
}
}
5.synchronized可以修饰方法(静态方法、普通方法)和代码块,而Lock只能修饰代码块。