What

线程就是独立的执行路径;对同一份资源操作时,会存在资源抢夺问题,需要加入并发控制;每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。

  • 线程调度
    • 抢占式调度:让优先级高的线程先使用cpu,如果线程优先级相同,那么会随机选择一个(线程的随机性)。Java采用抢占式调度。
    • 分时式调度:所有线程轮流使用cpu的使用权,平均分配每个线程占用cpu的时间。

注意:线程开启不一定立即执行,由cpu调度执行

  • 线程分类
    • 用户线程
    • 守护线程

虚拟机必须确保用户线程执行完毕;虚拟机不用等待守护线程执行完毕。

线程的生命周期

操作系统的线程五态

并发 - 图1

JVM的线程六态

并发 - 图2

  • 新建(New)

创建后尚未启动。

  • 可运行(Runnable)

可能正在运行,也可能正在等待 CPU 时间片(包含了系统五态中的 就绪 & 运行)。

  • 阻塞(Blocking)

等待监视器锁(monitor lock),表示线程阻塞于锁。

  • 等待(Waiting)

等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。
阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁;而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。

触发条件 唤醒方法
Object#wait() 方法 Object#notify() / Object#notifyAll()
Thread#join() 方法 被调用的线程执行完毕
LockSupport#park() 方法 LockSupport.unpark(Thread)
  • 限期等待(Timed Waiting) | 触发条件 | 唤醒方法 | | —- | —- | | Thread.sleep() | 时间结束 | | Object#wait(long timeout) | 时间结束 / Object#notify() / Object#notifyAll() | | Thread#join(long millis) | 时间结束 / 被调用的线程执行完毕 | | LockSupport.parkNanos() | LockSupport.unpark(Thread) | | LockSupport.parkUntil() | LockSupport.unpark(Thread) |

  • 死亡(Terminated)

可以是线程结束任务之后自己结束,或者产生了异常而结束。

How

创建线程的三种方式

注意:

  1. 每调一次start()方法都会开辟一个新的线程;
  2. 业务代码中不推荐直接使用start()开启线程,而是推荐通过线程池开启线程,这样开销更小,资源利用率更高哦;
  3. 线程无论通过那种方式创建在执行时本质上在底层都是执行的Runnable接口的run()方法;

    • 继承Thread类:通过重写了run()方法;
    • 实现Callable接口:通过适配器FutureTask(适配器模式),将Callable接口适配为了Runnable接口;

      继承Thread类

      其实这种方式本质上也是实现了Runnable接口

      1. public class MyThread extends Thread{
      2. @Override
      3. public void run() {
      4. //具体线程操作...
      5. }
      6. public static void main(String[] args) {
      7. new MyThread().start();
      8. }
      9. }
  • 注意:不建议使用该种方式创建线程,会存在OOP单继承局限性,也不利于解耦。

    实现Runnable接口

    1. new Thread(new Runnable() {
    2. @Override
    3. public void run() {
    4. //具体线程操作...
    5. }
    6. }).start();
  • 相比于继承Thread类的实现方式,有如下优点

    • 避免了单继承的局限性;
    • 增强了程序的扩展性、解耦。

      实现Callable接口

      Callable接口类似于Runnable,因为它们都是为其实例可能由另一个线程执行的类设计的。 然而,与Runnable不同的是,Callable有返回值,也能抛出被检查的异常。
  • FutureTask执行 ```java public class CallableTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

    1. //使用适配类来通过Thread对象的starter方法运行线程任务
    2. MyCallable myCallable = new MyCallable();
    3. //futureTask是一个适配器,使其Callable能够兼容Runnable
    4. FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
    5. //两个线程同时执行结果被缓存,效率高
    6. new Thread(futureTask, "A").start();
    7. new Thread(futureTask, "B").start();
    8. /*获取线程任务执行完毕的返回结果
    9. 注意:这个get方法可能会产生阻塞!应该把它放在最后或者使用异步通信的方式来处理!*/
    10. Integer i = futureTask.get();
    11. System.out.println(i);

    } }

class MyCallable implements Callable {

  1. @Override
  2. public Integer call() throws Exception {
  3. //两个线程同时执行会打印几个call?一个,因为结果会被缓存
  4. System.out.println("call()");
  5. return 1024;
  6. }

}

  1. - **线程池执行**
  2. ```java
  3. public class CallableTest {
  4. public static void main(String[] args) throws ExecutionException, InterruptedException {
  5. //1.创建Callable接口实现类的实例对象
  6. MyCallable myCallable = new MyCallable();
  7. //2.创建执行服务(创建了一个包含3个线程的线程池)
  8. ExecutorService eServer = Executors.newFixedThreadPool(3);
  9. //3.提交执行(此处的返回结果就是call()方法的返回值,只是用Future对象包了一层而已)
  10. Future<Integer> result = eServer.submit(myCallable);
  11. //4.获取结果
  12. Integer i = result.get();
  13. System.out.println(i);
  14. //5.关闭服务
  15. eServer.shutdownNow();
  16. }
  17. }
  18. class MyCallable implements Callable<Integer> {
  19. @Override
  20. public Integer call() throws Exception {
  21. //两个线程同时执行会打印几个call?一个,因为结果会被缓存
  22. System.out.println("call()");
  23. return 1024;
  24. }
  25. }

线程中常见的方法

方法 说明
setPriority(int newPriority) 更改线程的优先等级
static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠
void join() 当前线程等待调用该方法的线程终止后再执行
static void yield() 暂停当前正在执行的线程对象,并执行其他线程
void interrupt() 中断线程(不推荐这种方式)
boolean isAlive() 查看线程是否处于活动状态
  • 休眠

static void sleep(long millis);
注意:

  • sleep存在异常InterruptedException;
  • sleep时间达到后,线程进入就绪状态;
  • sleep可以模拟网络延时,倒计时等;
  • 每个对象都有一个锁,sleep不会释放锁。
  • 礼让

static void yield();//暂停当前正在执行的线程对象,并由cpu重新调度线程执行
注意:

  • 线程礼让,让当前正在执行的线程暂停,但不阻塞;
  • 将线程从运行状态转为就绪状态;
  • 线程礼让不一定成功,因为cpu是随机调度,也有可能又调度到刚刚礼让的线程来执行。
  • 合并

void join(); //本线程阻塞等待此(调用的线程)线程执行完成后,再执行本线程。

  • 状态

State getState();//获取线程状态
注意:线程中断或结束,一旦进入死亡状态(TERMINATED),就不能再次启动了。

  • 优先级

setPriority(int newPriority);
getPriorty();
Java提供了一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按优先级决定应该调度哪个线程来执行。

  • 等级说明:线程优先等级用数字表示,范围是[0,10]
    • Thread.MIN_PRIORITY = 1;
    • Thread.NORM_PRIORITY = 5; //默认值
    • Thread.MAX_PRIORITY = 10;
  • 注意:

优先级低只是意味着获得调度的概率低,并不是优先级低就一定会后于优先级高的之后被调用,这需要看cpu的调度。当出现优先级低的先于优先级高的被调度,就出现了性能倒置的问题。

  • 守护线程

final void setDaemon(boolean on); //设置守护线程,默认是false,即非守护线程

  • 停止

    • 不推荐使用JDK提供的stop()、destroy()方法,因为它会释放锁,从而导致数据不一致的情况,而且已被官方废弃;
    • 建议线程自己停止下来,即,使用一个标记进行终止变量,当 flag=false,则终止线程运行。
  • 获取当前正在执行的线程对象

static native Thread currentThread();

线程协作

为什么需要线程协作?

多线程并发执行时,在默认情况下cpu是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要一些协调通信,以次来帮助我们达到多线程共同操作一份数据。

实现线程协作_等待唤醒机制

  • synchronized方式
    • Java的Object提供了几个方法来实现线程通讯:
      • wait(); //表示线程一直等待,直到其他线程通知其唤醒,与sleep不同,会释放锁;
      • wait(long timeout); //指定等待的最大时间(毫秒);
      • notify(); //唤醒一个处于等待状态的线程,不能指定某个具体的线程,所以只有一个线程在等待的时候它才有用武之地;
      • notifyAll(); //唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度。
    • 注意

1.上述方法只能在同步方法或者同步代码块中使用,否则会抛异常IllegalMonitorStateException,这是为了避免 wait 和 notify 之间产生竞态 条件;
2.哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁了,所以它需要再次尝试去获取锁(很有可能需要面临其他线程的竞争),成功获取锁后才能在当初调用wait方法之后的地方恢复执行。
3.wait()方法一定不要放在if()语句块中,会出现虚假唤醒问题,即,当在if()中被wait的线程,再次唤醒时,不会判断是否需要wait 而直接执行if()外的逻辑,因为if()只会判断一次。解决方法是使用while()来代替if()。

  • 思考

上述方法为什么是定义在Object类中的呢?因为每个对象都有自己的一把锁,而每个对象都继承自Object类,把共有方法定义在父类就起到了代码复用的作用。

  • Lock锁方式

    • 常用方法:

      1. Lock lock = new ReentrantLock(); //创建lock对象
      2. Condition condition = lock.newCondition(); //创建对象监视器
      3. condition.await(); //等待
      4. condition.singnal(); //唤醒当前监视器绑定的线程
      5. condition.signalAll(); //唤醒全部
    • 扩展:如何有序调用线程?

可通过给每个线程创建一个 对象监视器,来实现唤醒指定线程,以达到有序调用线程的目的。

  • 对象监视器和线程是如何绑定的呢?

其实,在哪个线程中调用了condition.await()方法,那么,该线程就与该对象监视器绑定了,就必须使用这个对象监视器才能唤醒这个线程。

线程同步机制

线程安全问题

由于多个线程共享同一块存储空间,当多个线程共同写了共享数据,则会出现线程安全问题。

  • why?

Java中每个线程有独立的内存空间,被称为工作内存。而所有的数据源都位于主内存中,当线程去操作某些数据的时候,是先将主内存中的数据复制一份到自己的工作内存中,操作完后,如果做了修改,则再回写到主内存中。当多线程对同一个数据做处理时,由于拿到的数据已经被其他线程操作过了,而此时在工作内存中还是以前的数据,那么就会出现数据过期导致的不一致,就出现了线程不安全的情况,直观感受就是数据错乱。

保证线程安全的手段

  • 核心思想

通过 队列+ 来实现当一个线程获得对象的排它锁,独占资源,其他线程必须等待,直到使用后释放锁为止。 简单来说,就是在同一时间内只能有一个线程来操作主内存的某些数据。

  • 缺陷

    • 一个线程持有锁会导致其他所有需要此锁的线程挂起;
    • 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换 和 调度延时,引起性能问题;
    • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题。

      Synchronized

      平常我们只知道synchronize只是获得同步锁,实现多线程的互斥,而忽略了中间的内存的运作,而这个也看出synchronize跟volatile的最大区别:volatile只保证多线程内存的可见性,而synchronized既保证了多线程的并发有序性,又保证了多线程的内存可见性。
  • synchronized是做了什么的操作呢?

1.获得同步锁;
2.清空工作内存;
3.从主内存拷贝对象副本到工作内存;
4.执行代码(计算或者输出等);
5.刷新主内存数据;
6.释放同步锁。

同步方法
  • 使用:在方法中加入synchronized 关键字修饰;
  • 作用域:整个方法;
  • 锁对象
    • 方法对应的类的实例对象,即 this;
    • 如果是静态同步方法,则锁对象为该类的class对象。

每个类的实例对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,而继续执行;

  • 缺陷

方法锁针对的是各个类的实例对象,粒度大, 锁的资源太多,会照成不必要的线程阻塞,性能影响较大。
例如:
某对象中有两个同步方法,但是这两个方法操作的都是不同的数据,所以它们互相是不存在线程安全问题的。当线程A访问该类中某一同步方法时,此时线程B想访问另外一同步方法就只能阻塞等待,因为这两个同步方法用的是同一把锁,而此时正在被线程A持有。

同步块

想要进入上锁的房间(同步块),就必须要有这房间锁(锁对象)的钥匙(持有锁);
一把锁只有一把钥匙,且同时只能开一扇门(一个锁对象只能同时被一个线程持有);
但不同的房间也可以安装同一把锁(不同的代码块可以设置相同的锁对象)。

  • 使用方式:synchronized(Object o){…}

参数o称之为同步监视器,可以是任何对象,但应该使用共享资源作为同步监视器,否则无意义,无法实现线程安全;

  • 作用域:代码块内;
  • 锁对象:同步监视器指定的对象;
  • vs 同步方法

同步代码块锁针对的是同步监视器对象,粒度小,不会造成不必要的线程阻塞,相对于同步方法性能影响更小。

lock锁

  • 通过显示定义锁对象来实现同步,同步锁使用Lock对象充当;
  • Java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象;
  • ReentrantLock类实现类Lock接口,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁、释放锁。
  • 使用方式:使用Lock接口中的两个接口方法 lock、unlock来加锁和解锁;当然实际操作的是Lock的实现类对象,如ReentrantLock等,且建议在try-catch的finally中释放锁。
  • 作用域:调用lock与unlock方法之间;
  • 锁对象:具体调用的Lock实现类对象,如ReentrantLock对象等

  • synchronized vs Lock

    • 原始构成
      synchronized是关键字属于JVM层面;Lock是接属于api层面。
    • 使用方法
      synchronized是隐式锁,出了作用域自动释放;Lock是显示锁(手动开启和关闭)。
    • 等待是否可中断
      synchronized不可中断,除非抛出异常或者正常运行完成;
      ReentrantLock可中断(1.设置超时方法tryLoc(long timeout, TimeUnit unit);2.lockInterruptibly()放代码块中,调用interrupt()方法中断)
    • 加锁是否公平
      synchronized非公平锁;
      ReetrantLock默认是非公平锁,也可以通过构造函数显示指定为是公平锁。
    • 锁绑定多个条件Condition
      ReentrantLock用来实现分组唤醒、精确唤醒;
      synchronized不支持,要么随机唤醒一个线程,要么唤醒全部线程。
    • 性能
      Lock锁,JVM调度时间更少,性能更好。并且具有更好的扩展性(有很多不同作用的实现类);
    • 优先使用顺序
      Lock —> 同步代码块 —> 同步方法
    • 补充

在较新版本的JVM上,ReentrantLock和synchronized的性能是接近的,但Java编译器和虚拟机可以不断优化synchronized的实现,比如自动分析synchronized的使用,对于没有锁竞争的场景,自动省略对锁获取/释放的调用。
简单总结下,能用synchronized就用synchronized,不满足要求时再考虑Reentrant-Lock。

其它方式

原子类、volatile关键字、不变类、ThreadLocal 等

线程池

  • 核心思想(池化技术):

程序的运行,本质上就是占用系统的资源,硬件不变的前提下提高程序运行的效率,实际上就是去优化资源的使用。池化技术就是这一思想的落地实现之一。
找硬件获取资源,使用完后销毁,整个过程十分消耗性能。池化技术就是以这个作为优化点,事先准备好一些资源到内存中,用直接拿,用完再归还给内存,避免资源反复创建、销毁。

  • 线程池的几点好处:

1.线程复用:降低资源消耗、提高响应速度;
2.可控制最大并发数,性能稳定,方便管理线程。

  • 核心API:
    • Executors:工具类、线程池工厂类,用于创建并返回不同类型的线程池;
    • ExecutorService:真正的线程池接口,常见的子类ThreadPoolExecutor。该接口常见方法如下:
      • void execute(Runnable command):执行任务,没有返回值,一般用来执行Runnable方式创建的线程;
      • Future submit(Callable task):执行任务,有返回值,一般用于Callable方式创建的线程
    • void shutdown():关闭连接池

❌ Executors创建线程池

  1. public class ThreadPoolTest {
  2. public static void main(String[] args) {
  3. //创建只有一个线程的线程池
  4. ExecutorService threadPool1 = Executors.newSingleThreadExecutor();
  5. //创建一个指定大小的线程池
  6. ExecutorService threadPool2 = Executors.newFixedThreadPool(5);
  7. //创建一个可伸缩线程池,性能与线程数量成正比
  8. ExecutorService threadPool3 = Executors.newCachedThreadPool();
  9. try {
  10. for (int i = 0; i < 10; i++) {
  11. //使用线程池来创建线程
  12. threadPool2.execute(new Runnable() {
  13. @Override
  14. public void run() {
  15. System.out.println(Thread.currentThread().getName() + "OK!");
  16. }
  17. });
  18. }
  19. }catch (Exception e){
  20. e.printStackTrace();
  21. }finally {
  22. //线程池用完,程序结束,关闭线程池
  23. threadPool2.shutdown();
  24. }
  25. }
  26. }
  • 阿里巴巴开发手册中明确指出:

强制要求,线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式能让coder更加明确线程池的运行规则,规避资源耗尽的风险。

  • Executors返回的线程池对象的弊端如下:
    1. FixedThreadPool和SingleThreadPool:

允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。

  1. CachedThreadPool和ScheduleThreadPool:
    允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

    ✅ ThreadPoolExecutor创建线程池

    Executors类创建线程池底层就是用的ThreadPoolExecutor类,只是底层封装了默认的参数,我们因该根据不同的业务场景和硬件条件来设置不同的参数,所以需要用ThreadPoolExecutor类来创建线程池。
  • 七大构造参数: | int corePoolSize | 核心线程数 | | | —- | —- | —- | | int maximumPoolSize | 最大线程数 | 当任务数超过 corePoolSize + workQueue ,线程池扩容(最大达到maximumPoolSize),且新扩容的线程优先执行非workQueue的任务; | | long keepAliveTime | 线程存活时间 | 超时没人调用的非核心线程就会被释放 | | TimeUnit unit | 时间单位 | 存活时间的单位 | | BlockingQueue workQueue | 阻塞队列 | 当任务数超过corePoolSize,会将其放入workQueue中 | | ThreadFactory threadFactory | 线程创建工厂 | 一般不动,设为默认Executors.defaultThreadFactory() | | RejectedExecutionHandler handler | 拒绝策略 | 当任务数超过 阻塞队列容量 + 最大线程数 后 触发拒绝策略 | | 一言堂:当核心线程数满了,就放工作队列,当工作队列也满了,就扩展线程数(且率先执行非工作队列的任务),当最多能扩展的线程数也满了,就触发拒绝策略。 | | |

  • 四种拒绝策略:

以下拒绝策略均实现了RejectedExecutionHandler接口,且都是ThreadPoolExecutor的静态内部类

拒绝策略 说明
AbortPolicy 不处理这个任务,直接抛出异常RejectedExecutionException
CallerRunsPolicy 哪来的去哪里!(即哪个线程调用就回哪个线程去处理)
DiscardPolicy 丢掉任务,不会抛出异常!
DiscardOldestPolicy 尝试去和最早的竞争,竞争失败丢掉任务,也不会抛出异常!
  • 如何确定线程池的最佳大小?
    • CPU密集型:since :《on java8》
      几个物理核心(不是超线程),就是创建几个线程,可以保持CPU的效率最高!因为此时每条线程都是并行处理,不存在并发操作,减小了反复切换任务的开销。
    • IO 密集型
      • 方案一:判断程序中有多少个十分消耗IO的线程,然后一般线程池的大小就按这个数量的两倍设置为佳;
      • 方案二:参考公式:cpu核心数/(1-阻塞系数);一般阻塞系数取值在0.8~0.9之间。

通过 Runtime.getRuntime().availableProcessors(); 可获取当前机器的核心数,注意,是逻辑核心。

  • 示例:

    1. public class ThreadPoolTest {
    2. public static void main(String[] args) {
    3. ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
    4. 2, //核心线程池大小为2
    5. 5, //最大线程池大小为5
    6. 3, //线程存活时间
    7. TimeUnit.SECONDS, //时间单位为秒
    8. new LinkedBlockingDeque<>(4), //阻塞队列可容纳任务为4
    9. Executors.defaultThreadFactory(), //默认线程创建工厂(一般不用动)
    10. new ThreadPoolExecutor.AbortPolicy() //拒绝策略
    11. );
    12. try {
    13. for (int i = 0; i < 10; i++) {
    14. //使用线程池来创建线程
    15. threadPool.execute(new Runnable() {
    16. @Override
    17. public void run() {
    18. System.out.println(Thread.currentThread().getName() + "OK!");
    19. }
    20. });
    21. }
    22. } catch (Exception e) {
    23. e.printStackTrace();
    24. } finally {
    25. //线程池用完,程序结束,关闭线程池
    26. threadPool.shutdown();
    27. }
    28. }
    29. }
    • 说明:
      1. 当使用线程数超过 核心线数(2)+ 队列任务数(4)后,扩容线程池(最大到5)来执行任务;
      2. 使用线程数超过 最大核心线程数(5) + 工作队列可放线程数(4) 后 触发拒绝策略;
      3. 当非核心线程超过3秒未被使用就将其释放

        Why

        • 锁到底是什么

        锁其实就是对象的访问控制权限,每个对象都对应一把锁。当线程T1 获取了对象A 的锁,在获得锁的这段时间内只能允许线程T1 能操作对象A的资源。

        • 注意:如果锁的是静态方法,那么锁的对象就是 类的Class对象,因为每个类只有一个Class对象,所以锁对象是同一把锁。
          • 锁的两种特性: 互斥性 & 不可见性

公平锁 vs 非公平锁

  • 非公平锁
    线程可以插队 ,性能好,可以保证更大的吞吐量,但容易导致饥饿(优先级低的线程一直无法获取锁)。
  • 公平锁
    线程不能够插队,必须遵循先来后到! 可以避免饥饿,但性能差(如:一个只需要执行2s的线程,需要等待一个执行20s的线程操作完成后才能获取锁)。

导致饥饿的原因

  1. cpu一直不分配时间片;
  2. 线程一直抢不到锁;
  3. wait()之后一直没有被唤醒。

可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

自旋锁

自旋锁(spinlock)是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁(这里的锁其实不是常规意义上的锁,而是跳出循环),这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗cpu。

  • 执行函数:CAS(V,E,N)

pfb.png

  • 手写自旋锁

    1. public class MySpinLock {
    2. //原子引用线程
    3. AtomicReference<Thread> atomicReference = new AtomicReference<>();
    4. /**
    5. * 上锁
    6. */
    7. public void LockMe() {
    8. Thread thread = Thread.currentThread();
    9. while (!atomicReference.compareAndSet(null, thread)) {
    10. System.out.println(thread.getName() + "线程获取锁失败!");
    11. }
    12. System.out.println(thread.getName() + "线程获取锁成功!");
    13. }
    14. /**
    15. * 解锁
    16. */
    17. public void unLockMe() {
    18. Thread thread = Thread.currentThread();
    19. atomicReference.compareAndSet(thread, null);
    20. System.out.println(thread.getName() + "线程释放锁!");
    21. }
    22. }

读写锁

ReadWriteLock维护一对关联的locks ,一个用于只读操作,一个用于写入。 read lock可以由多个读线程同时进行属于共享锁,而write lock 同时只能单独线程执行属于独占锁。

  • 注意:读-读 可以共存; 写-写 不能共存; 读-写 不能共存(否则会出现幻读,无法保证数据的实时一致性)。
  • 示例

    1. /**
    2. * 加锁的自定义集合
    3. */
    4. public class MyCacheLock {
    5. private volatile Map<String, Object> map = new HashMap<>();
    6. //读写锁(比ReentrantLock更加细粒度的控制)
    7. private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    8. /**
    9. * 存储(写入的时候,同时只能有一个线程写)
    10. */
    11. public void put(String key, Object value) {
    12. readWriteLock.writeLock().lock(); //加锁
    13. try {
    14. System.out.println(Thread.currentThread().getName() + "线程写入:" + key);
    15. map.put(key, value);
    16. System.out.println(Thread.currentThread().getName() + "线程写入完成!");
    17. } catch (Exception e) {
    18. e.printStackTrace();
    19. } finally {
    20. readWriteLock.writeLock().unlock(); //释放写锁
    21. }
    22. }
    23. /**
    24. * 读取(所有人都可以读)
    25. */
    26. public Object get(String key) {
    27. readWriteLock.readLock().lock(); //加读锁
    28. try {
    29. System.out.println(Thread.currentThread().getName() + "线程读取:" + key);
    30. Object o = map.get(key);
    31. System.out.println(Thread.currentThread().getName() + "线程读取完成!");
    32. return o;
    33. } catch (Exception e) {
    34. e.printStackTrace();
    35. } finally {
    36. readWriteLock.readLock().unlock(); //释放读锁
    37. }
    38. return null;
    39. }
    40. }

死锁

多个线程各自持有一些共享资源,并且互相等待其他线程的资源才能运行,而导致两个或多个线程都在等待对方释放资源,都停止执行的情形。

  • 具体导致原因:

某个同步代码块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”问题。

  • 产生死锁的四个必要条件: | 条件 | 描述 | | —- | —- | | 互斥条件 | 一个资源每次只能被一个进程使用 | | 请求与保持条件 | 一个进程因请求资源而阻塞时,对已获得得资源保持不放 | | 不剥夺条件 | 进程已获得的资源,在未使用完之前,不能强行剥夺 | | 循环等待条件 | 若干进程之间形成一种头尾相接的循环等待资源关系 |

  • 避免死锁的方法:

只要破坏掉产生死锁的四个必要条件中任意一个或多个条件就可以避免死锁发生。

  • 问题排查
    • 使用 jsp -l 命令 定位进程号
    • 使用 jstack PID 找到堆栈信息中的有效信息

死锁堆栈信息图示:
wec.png

  • 死锁示例: ```java /**

    • 死锁Demo
    • 下面例子会出现,T1线程获取到lockA锁,等待被T2持有的lockB锁;T2线程持有lockB锁,等待获取被T1线程持有的lockA锁。 */ public class DeadLockDemo {

      public static void main(String[] args) {

      1. String lockA = "lockA";
      2. String lockB = "lockB";
      3. new Thread(new MyThread(lockA, lockB), "T1").start();
      4. new Thread(new MyThread(lockB, lockA), "T2").start();

      } }

class MyThread implements Runnable {

  1. private final String lockA;
  2. private final String lockB;
  3. public MyThread(String lockA, String lockB) {
  4. this.lockA = lockA;
  5. this.lockB = lockB;
  6. }
  7. @Override
  8. public void run() {
  9. synchronized (lockA) {
  10. System.out.println(Thread.currentThread().getName() + "线程:" + "获取锁" + lockA + "成功!");
  11. try {
  12. //休眠1s,保证另外一个线程先获取另外一把锁
  13. TimeUnit.SECONDS.sleep(1);
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. System.out.println(Thread.currentThread().getName() + "线程:" + "正在获取锁" + lockB + "中...");
  18. synchronized (lockB) {
  19. System.out.println(Thread.currentThread().getName() + "线程:" + "获取锁" + lockB + "成功!");
  20. }
  21. }
  22. }

} ```

线程的安全策略

不可变类 如果一个类初始化后,所有属性和类都是final不可变的,则它是线程安全,不需要任何同步,活性高。
线程栈内使用 方法内局部变量
线程内参数传递
ThreadLocal持有
同步锁 synchronized synchronized的代码串行执行,线程安全,但活性低
volatile volatile变量锁外双重检测(JDK1.5+),降低锁竞争
读写条件分离 锁粒度分级,排序锁
CAS 无锁,循环设新值,如果旧值变化,则重设,乐观并发。


三大特性

保证线程安全必须考虑的三大特性


描述
原子性
可见效
有序性