2.1 资源共享导致线程安全
2.1.1 多线程、并行、并发
多线程指一个进程中启动了不止一个线程;
并行(concurrent)指不同的线程执行相同的代码,类似不同的人干相同的事;
并发(parallel)指不同的线程执行不同的代码,类似不同的人干不同的事;
无论是并行还是并发都可能产生不同线程同时访问相同的资源的情况,线程安全就产生于不同的线程同时访问相同的资源,资源可以是数据库、IO、内存,本章涉及的资源都是内存;
2.1.2 JVM内存分布
不同线程可以共享操作系统分配给进程的内存空间,但是各个线程之间如何共享、是否完全共享需要由java虚拟机决定,粗粒度看一下JVM的内存分布:
方法区和堆是线程共享的内存区域,栈内存在线程之间不共享,那么如果多个线程同时访问方法区和堆中的数据就会出现数据不一致的问题,也就是线程安全问题;
把方法区、堆、虚拟机栈的内存看做主内存,CPU在执行线程中的命令时需要将主内存,先进行load操作,将数据从主内存加载到CPU的高速缓存(寄存器),使用完后进行save操作,将数据保存到主内存,load和save操作会造成线程共享的方法区和堆数据不一致问题及线程安全问题;
方法区存放的是类的描述信息,堆存放的是类的实例信息,直白一点就是多个线程同时访问同一个对象的类属性和实例属性,会有线程安全问题;
2.1.3 线程安全场景
Tomcat容器,容器中有一个线程池,用来处理不同的请求,但是容器中的Servlet默认是单实例的,也就是servlet的类变量和实例变量,是线程不安全的;
Spring容器,bean的scope默认时singleton,也就是单例的,如果多个线程对spring容器的bean进行访问时会有线程安全问题;
2.2 如何保证线程安全
访问共享资源的代码块叫做临界区,如果能够保证同一时刻只有一个线程执行临界区内的代码就能保证同一时刻只有一个线程在访问共享资源;
对临界区进行加锁,线程获取到锁时才可以执行临界区的代码,这样就可以通过控制锁的获取和释放来保证同一时刻只有一个线程执行临界区的代码;
锁有2种具体形式:synchronized(同步)和Lock
说明:synchronized也是锁,好比手机指纹、人脸一样,实质上都是锁只是叫法不一样
2.2.1 不添加线程安全控制
>多线程启动类
1 /** 2 * 模拟线程池,启动多个线程驱动任务执行 3 */ 4 public class ThreadSecuritySynchronized { 5 public static void main(String[] args) throws Exception { 6 int cycleCount = 10000; 7 //创建线程共享的内存资源 8 SharedResource sharedResource = new SharedResource(); 9 //开启2个线程分别增加和减少共享资源的计数器 10 //lambda表达式:Runnable是一个函数式接口,作为入参时可以用lambda表达式 11 Thread de = new Thread(() -> { 12 for (int i = 0; i < cycleCount; i++) { 13 sharedResource.decrease(); 14 } 15 }); 16 Thread in = new Thread(() -> { 17 for (int i = 0; i < cycleCount; i++) { 18 sharedResource.increase(); 19 } 20 }); 21 //线程联合,等de和in线程都执行完成后再继续执行 22 de.join(); 23 in.join(); 24 System.out.println(sharedResource.getCount()); 25 } 26 }
>共享资源类
1 /** 2 * 共享的资源类 3 * 2个线程分别增加和减少相同次数 4 * 如果最终结果为0则表示增加和减少操作是线程安全的 5 * 如果最后结果不为0则表示增加和减少操作不是线程安全的 6 */ 7 public class SharedResource { 8 //共享资源的内存空间 9 private static int count; 10 //获取内存中的数据 11 public static int getCount() { 12 return count; 13 } 14 //修改 15 public static void increase() 16 { 17 SharedResource.count++; 18 } 19 //修改 20 public void decrease() 21 { 22 SharedResource.count--; 23 } 24 }
>运行结果:运行多次结果都不为0,说明SharedResource的更新操作不是线程安全的
2.2.2 synchronized同步
每个对象都有一把对象锁和一把类锁,同一把锁只能被一个线程占有(但是一个线程可同时占有多把锁),线程要执行synchronized修饰的代码必须取到相应的锁,否则线程就进入同步阻塞;
synchronized可以修饰方法和代码块,修饰方法时对应的锁就是方法宿主对象的对象锁和类锁,修饰代码块时对应的锁是synchronized(object)参数对象的对象锁和类锁;
synchronized修饰实例方法及实例方法内的代码块时对应的是对象锁
synchronized修饰类方法及类方法内的代码块时对应的是类锁
1. 分别给类方法和实例方法添加synchronized关键字
>共享资源类
1 package com.kepus.javabasic.concurrent.threadSecurity; 2 3 /** 4 * 共享的资源类 5 * 2个线程分别增加和减少相同次数 6 * 如果最终结果为0则表示增加和减少操作是线程安全的 7 * 如果最后结果不为0则表示增加和减少操作不是线程安全的 8 */ 9 public class SharedResource { 10 //共享资源的内存空间 11 private static int count; 12 //获取内存中的数据 13 public static int getCount() { 14 return count; 15 } 16 //修改:添加synchronized 17 public synchronized static void increase() 18 { 19 SharedResource.count++; 20 } 21 //修改:添加synchronized 22 public synchronized void decrease() 23 { 24 SharedResource.count--; 25 } 26 }
>运行结果:运行多次结果都不为0,说明SharedResource的更新操作仍然不是线程安全的,因为synchronized分别修饰实例方法和类方法使用的是不同的锁
2. 用synchronized关键字修饰,且都为实例方法(都为静态方法一样)
>共享资源类
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 package com.kepus.javabasic.concurrent.threadSecurity; 2 3 /** 4 * 共享的资源类 5 * 2个线程分别增加和减少相同次数 6 * 如果最终结果为0则表示增加和减少操作是线程安全的 7 * 如果最后结果不为0则表示增加和减少操作不是线程安全的 8 */ 9 public class SharedResource { 10 //共享资源的内存空间 11 private static int count; 12 //获取内存中的数据 13 public static int getCount() { 14 return count; 15 } 16 //修改:添加synchronized,方法改为实例方法 17 public synchronized void increase() 18 { 19 SharedResource.count++; 20 } 21 //修改:添加synchronized 22 public synchronized void decrease() 23 { 24 SharedResource.count--; 25 } 26 }
>运行结果:运行多次结果都为0,SharedResource的更新操作是线程安全的
2. 都为静态方法且synchronized修饰静态方法内的代码块
>共享资源类
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 package com.kepus.javabasic.concurrent.threadSecurity; 2 3 /** 4 * 共享的资源类 5 * 2个线程分别增加和减少相同次数 6 * 如果最终结果为0则表示增加和减少操作是线程安全的 7 * 如果最后结果不为0则表示增加和减少操作不是线程安全的 8 */ 9 public class SharedResource { 10 //共享资源的内存空间 11 private static int count; 12 //获取内存中的数据 13 public static int getCount() { 14 return count; 15 } 16 //修改 17 public static void increase() 18 { 19 //synchronized修饰静态代码块(SharedResource.class可以改为其他类,只要和其他synchronized保持一致即可) 20 synchronized (SharedResource.class) 21 { 22 SharedResource.count++; 23 } 24 } 25 //修改 26 public static void decrease() 27 { 28 //synchronized修饰静态代码块(SharedResource.class可以改为其他类,只要和其他synchronized保持一致即可) 29 synchronized (SharedResource.class) 30 { 31 SharedResource.count--; 32 } 33 } 34 }
>运行结果:多次运行结果都为0
2. 都为实例方法且synchronized修饰实例方法内的代码块
>共享资源类
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 package com.kepus.javabasic.concurrent.threadSecurity; 2 3 /** 4 * 共享的资源类 5 * 2个线程分别增加和减少相同次数 6 * 如果最终结果为0则表示增加和减少操作是线程安全的 7 * 如果最后结果不为0则表示增加和减少操作不是线程安全的 8 */ 9 public class SharedResource { 10 //共享资源的内存空间 11 private static int count; 12 //获取内存中的数据 13 public static int getCount() { 14 return count; 15 } 16 //修改 17 public void increase() 18 { 19 //synchronized修饰实例代码块(this可以改为其他对象,只要和其他synchronized保持一致即可) 20 synchronized (this) 21 { 22 SharedResource.count++; 23 } 24 } 25 //修改 26 public void decrease() 27 { 28 //synchronized修饰实例代码块(this可以改为其他对象,只要和其他synchronized保持一致即可) 29 synchronized (this) 30 { 31 SharedResource.count--; 32 } 33 } 34 }
>运行结果:多次运行结果都为0
2.2.3 Lock锁
Lock作为Java提供的一个接口,功能类似synchronized同步,但是功能比synchronized强大,可以精细化控制读写、可以响应中断、可以实现线程公平获取锁等;
1. Lock接口的6个方法
Lock作为接口提供了公共抽象方法,方法功能分为获取锁、释放锁、新建锁条件3类,其中获取锁分阻塞式获取和非阻塞式获取,阻塞式获取分为能够响应中断式和不能够响应中断式;
1. lock(): 获取锁、阻塞式、不响应中断
2. tryLock(): 获取锁、非阻塞式
3. tryLock(time, timeUnit) throws InterruptedException:获取锁、定时阻塞、响应中断
4. lockInterruptibly() throws InterruptedException:获取锁、阻塞式、响应中断
5. unLock():释放锁
6. newCondition():新建锁条件,Condition有await和signal方法,功能类似Object的wait和notify方法,常用于线程协作
2. 锁分类
根据Lock实现类的特性,锁可以分为可重入锁、公平锁、中断锁、读写锁;
1. 可重入锁:同一把锁可以锁多个临界区代码块,线程获取到锁后,可以执行同一把锁锁定的所有临界区; synchronized和Lock都是可重入锁;
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 import java.util.concurrent.locks.Lock; 2 import java.util.concurrent.locks.ReentrantLock; 3 public class ThreadSecurityLock { 4 private static Lock lock = new ReentrantLock(); 5 public static void main(String[] args ) throws Exception 6 { 7 firstGate(); 8 } 9 //synchronized和lock都是可重入锁 10 public synchronized static void firstGate(){ 11 lock.lock(); 12 try{ 13 System.out.println("into first gate."); 14 secondGate(); 15 System.out.println("out first gate."); 16 }finally { 17 lock.unlock(); 18 } 19 } 20 //在firstGate中获取到锁,可以直接进入secondGate,因为是相同的锁 21 public synchronized static void secondGate(){ 22 lock.lock(); 23 try{ 24 System.out.println("into second gate."); 25 System.out.println("out second gate."); 26 }finally { 27 lock.unlock(); 28 } 29 } 30 }
2. 公平锁:多个线程因获取锁进入阻塞状态时,阻塞时间最长的线程优先获取到锁叫做公平锁,所有阻塞线程均等机会获取到锁叫做非公平锁; synchronized是非公平锁,Lock默认是非公平锁,可以通过设置参数改为公平锁;
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 import java.util.concurrent.TimeUnit; 2 import java.util.concurrent.locks.Lock; 3 import java.util.concurrent.locks.ReentrantLock; 4 public class ThreadSecurityLock { 5 //构造函数入参为true,指定锁为公平锁 6 private static Lock lock = new ReentrantLock(true); 7 public static void main(String[] args ) throws Exception 8 { 9 new Thread(()->{ 10 lock.lock(); 11 try{ 12 //休眠10秒,让其他线程进入阻塞 13 TimeUnit.SECONDS.sleep(5); 14 } catch (InterruptedException e) { 15 e.printStackTrace(); 16 } finally { 17 lock.unlock(); 18 } 19 }).start(); 20 //休眠是为了线程有序进入阻塞 21 TimeUnit.MILLISECONDS.sleep(1); 22 new Thread(()->{ 23 lock.lock(); 24 try{ 25 System.out.println("第一个获取锁的阻塞线程"); 26 }finally { 27 lock.unlock(); 28 } 29 }).start(); 30 //休眠是为了线程有序进入阻塞 31 TimeUnit.MILLISECONDS.sleep(1); 32 new Thread(()->{ 33 lock.lock(); 34 try{ 35 System.out.println("第二个获取锁的阻塞线程"); 36 }finally { 37 lock.unlock(); 38 } 39 }).start(); 40 //休眠是为了线程有序进入阻塞 41 TimeUnit.MILLISECONDS.sleep(1); 42 new Thread(()->{ 43 lock.lock(); 44 try{ 45 System.out.println("第三个获取锁的阻塞线程"); 46 }finally { 47 lock.unlock(); 48 } 49 }).start(); 50 } 51 }
3. 中断锁:线程在因获取锁而进入阻塞状态时,中断锁能够响应中断退出阻塞,非中断锁进入阻塞后除非获取到锁否则无法退出阻塞状态;synchronized是非中断锁,Lock不同的获取锁方法可以响应中断也可以不响应中断
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 import java.util.concurrent.TimeUnit; 2 import java.util.concurrent.locks.Lock; 3 import java.util.concurrent.locks.ReentrantLock; 4 public class ThreadSecurityLock { 5 private static Lock lock = new ReentrantLock(); 6 public static void main(String[] args ) throws Exception { 7 new Thread(() -> { 8 lock.lock(); 9 try { 10 //休眠10秒,让其他线程进入阻塞,然后可以响应中断 11 TimeUnit.SECONDS.sleep(5); 12 } catch (InterruptedException e) { 13 e.printStackTrace(); 14 } finally { 15 lock.unlock(); 16 } 17 }).start(); 18 //休眠是为了线程有序进入阻塞 19 TimeUnit.MILLISECONDS.sleep(1); 20 Thread thread = new Thread(() -> { 21 try { 22 lock.lockInterruptibly(); 23 try { 24 System.out.println("第一个获取锁的阻塞线程"); 25 } finally { 26 lock.unlock(); 27 } 28 //获取锁,执行后退出 29 return; 30 } catch (InterruptedException e) { 31 System.out.println("中断了..."); 32 } 33 System.out.println("中断后执行..."); 34 }); 35 thread.start(); 36 //休眠是为了线程有序进入阻塞 37 TimeUnit.MILLISECONDS.sleep(1); 38 //中断线程 39 thread.interrupt(); 40 } 41 }
4. 读写锁:线程安全问题是因为不同的线程同时更新共享资源,如果所有线程只是读取共享资源的数据不会有线程安全问题,读写锁可以多个线程同时读,但不能同时读写;(A线程获取读锁时其他线程可以同时读,A线程获取写锁时其他线程只能等待)
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 import java.util.concurrent.TimeUnit; 2 import java.util.concurrent.locks.ReadWriteLock; 3 import java.util.concurrent.locks.ReentrantReadWriteLock; 4 public class ThreadSecurityLock { 5 private static ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); 6 public static void main(String[] args ) throws Exception { 7 new Thread(() -> { 8 readWriteLock.readLock().lock(); 9 try{ 10 //获取到读锁不释放,是为了验证另一个获取读锁的线程可以执行,另一个获取写锁的线程不能执行 11 TimeUnit.SECONDS.sleep(60); 12 } catch (InterruptedException e) { 13 System.out.println("只可能因sleep而中断..."); 14 } finally { 15 readWriteLock.readLock().unlock(); 16 } 17 }).start(); 18 TimeUnit.MILLISECONDS.sleep(1); 19 new Thread(() -> { 20 //获取读锁 21 readWriteLock.readLock().lock(); 22 try { 23 for (int i = 0; i < 10; i++) { 24 TimeUnit.SECONDS.sleep(1); 25 System.out.println("获取读锁,执行中.."); 26 } 27 } catch (InterruptedException e) { 28 System.out.println("只可能因sleep而中断..."); 29 } finally { 30 readWriteLock.readLock().unlock(); 31 } 32 }).start(); 33 new Thread(() -> { 34 //获取写锁 35 readWriteLock.writeLock().lock(); 36 try { 37 System.out.println("这句户不能执行.."); 38 }finally { 39 readWriteLock.writeLock().lock(); 40 } 41 }).start(); 42 } 43 }
2.2.2 synchronized和Lock对比
synchronized是Java的关键字,Lock是Java中的一个接口,有不同的实现类;
synchronized:
1. 释放锁的情况:临界区代码执行完毕,临界区代码执行过程发生异常;
2. 线程因synchronized同步阻塞时不能响应中断,也就是线程一旦因synchronized进入同步阻塞状态只能等待其他线程释放锁,没有其他办法;
Lock:
1. Lock锁需要手动释放;
2. Lock可以通过tryLock返回获取锁的结果;
3. Lock可以通过lockInterruptibly在因获取不到锁进入同步阻塞时响应中断;
说明:
1. 响应中断指在线程因获取锁失败进入同步阻塞状态时是否能够中断阻塞状态,无论synchronized还是Lock获取锁后执行过程因调用wait和Condition的await方法进入等待阻塞时都能构响应中断;
2. 线程安全产生的根源是不同的线程访问相同的内存资源,如果只是查看不会产生安全问题,同时更新内存资源才会产生安全问题,那么就可以通过将共享对象改造为不可变对象来避免线程安全;
3. 不可变对象的一般特点:a.所有的字段为final、private b.没有set方法 c.final修饰类,不允许扩展