并发编程

80 阅读7分钟

注解

@FunctionalInterface

只有一个抽象方法的接口会加,代表可以使用Lambda简化

快捷键

Reformat Code Ctrl Alt L

Declaration or Usages Ctrl B

进程与线程

JVM

  • 方法区,加载的字节码,静态变量,类信息,常量
  • 堆,对象 new创建的都在堆里
  • 栈在下面有介绍
注意
  • 对于i++而言(i为静态变量),实际上会产生如下的JVM字节码

    getstatic      i//获取静态变量i的值
    iconst_1      //准备常量1
    iadd          //自增
    putstatic      i//将修改后的值存入静态变量i
    

线程的并行并发

线程轮流使用CPU的做法叫并发,concurrent

在多核CPU下,每个核(core)都可以 调度运行线程,这个时候线程是可以并行的(parallel)

  • 并发是同一时间应对多件事情的能力
  • 并行是同一时间动手做多件事情的能力

同步与异步

从方法调用上来看

  • 需要等待返回结果,才能继续运行就是同步
  • 不需要等待返回结果,就能继续运行就是异步

同步在多线程中还有另一层意思,就是让多个线程保持步调一致

多线程可以让方法执行变成异步的

线程的使用

线程的创建与启动是分开的

Thread

Thread t = new Thread(){ 
    @Override
    public void run(){
        log.debug("running");
    }
};
​
t1.setName(T1);//设置线程的名字
​
t.start();

第二种创建使用线程的方法

Runnable

线程任务分开

  • Thread代表线程
  • Runnable可运行的任务(线程要执行的代码)
  • 使用Runnable更容易与线程池等高级API配合
  • 使用Runnable让任务类脱离了Thread继承体系,更灵活
//实现Runnable接口Runnable r = new Runnable(){
    @Override
    public void run(){
        log.debug("running");
    }
}
Thread t = new Thread(r,"T1")//第二个参数是线程的名字

使用Lambda化简

Runnable r = ()->{
    log.debug("running");
};
//进一步化简
​
Thread t = new Thread(()->{log.debug("running");},"T1");

FutureTask

可以获得返回结果,接受Callable类型的参数

FutureTask<Integer> r = new FutureTast<>(()->{
    log.debug("hello");
    return 100;
});
​
new Thread(t,"T1").start();
Integer result = r.get();

栈与栈帧

  • 栈(Java Virtual Machine Stacks)(Java虚拟机栈)

  • 每个线程启动后,Java虚拟机就会为其分配一块栈内存(线程栈)

    • 线程栈包括,程序计数器和栈帧
  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存(方法)

  • 每个线程只能有一个活动栈帧,对应着当前正在执行的方法

栈帧

  • 局部变量表
  • 返回地址
  • 锁记录
  • 操作数栈

线程上下文切换

(Thread Context Switch)

CPU不再执行当前的线程,转而执行另一个线程的代码

  • 线程的CPU时间片用完
  • 垃圾回收
  • 有更高优先级的线程要运行
  • 线程自己调用了sleep,yield,wait,join,park,synchronized,lock等方法

当Context Switch发生时,需要有操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的概念就是程序计数器(Program Counter Register),他的作用是记住下一条JVM指令的执行地址,是线程私有的。

  • 状态包括程序计数器,虚拟机栈中的每个栈帧信息,如局部变量,操作数栈,返回地址等
  • 频繁的进行Context Switch会影响性能

线程的方法

  • start()

    • 启动一个新线程,在新的线程运行run方法的代码
    • start方法只是让线程进入就绪,里面的代码不一定立刻执行(CPU的时间片还没分给它)
    • 每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStatusException
  • run()

    • 新线程启动时会调用run方法
  • join()

    • 等待线程运行结束
  • join(long n)

    • 等待线程结束,最多等待n毫秒
  • getId()

    • 获取线程唯一id
  • getName()

  • setPriority()

    • 最下优先级是1,最大是10,默认是5
    • 很不靠谱,只是一个提示,任务调度器可以忽略
    • CPU很忙,优先级高的会获得更多时间片,CPU很闲时没什么用
  • getState()

  • isInterrupted()

  • isAlive

  • interrupte()

    • 其他线程调用这个方法可以打断sleep,wait,join线程
  • currentThread()

  • sleep(long n)

    • 会让当前线程从Running状态变为Timed Waited(阻塞)状态
    • 被interrupt打断会抛出InterruptdeException
    • 睡眠结束后的线程的未必会立刻得到执行
    • 建议由TImeUnit替代
  • TimeUnit

    • 本质上是调用Thread.sleep()不过封装了不同单位
    • 由很多时间单位,可读性更高
    • TimeUnit.SECOND.sleep(1)睡一秒
  • yield()

    • 调用yield()会让线程从Running方法进入Runnable就绪状态,然后调度执行其他线程
    • 提示线程调度器让出当前线程对CPU的使用
    • 具体实现依赖于操作系统的任务调度器
    • 主要是为了测试

Start与run

Thread t = new Thread("T1"){
    @Override
    void run(){
​
    }
}
t.run()//不用start直接用t.run()这是run方法不会在t线程中运行,只会再当前线程中运行

yield与sleep

yield的就绪状态,任务调度器依旧可以调用

sleep的阻塞状态,任务调度器无法调用

Interrupte

  • 打断wait,sleep,join的程序,isInterrupted会被置为false

  • 打断正常运行的程序被打断之后会将isInterrupted置为true

    • Thread t1 = new Thread(()->{
          while(true){
          };
      });
      //这个时候调用,while会正常执行,不会结束,但isInterrupted会被置为true
      t1.interrupt();
      ​
      //应当这样写while
          while(true){
              boolean interrupted = Thread.currentThread().isInterrupted();
              if(interrupted){
                  break;
              }
          }
      

两阶段终止模式

Two Phase Termination

如何在线程T1中优雅的终止线程T2,优雅指的是给线程T2一个处理后事的机会

错误思路

  • 使用线程的stop()方法停止线程
  • 使用System.exit(int)方法停止线程,会使整个程序停下来

打断park线程

不推荐使用

已经过时

  • stop()
  • suspend()
  • resume()

主线程与守护线程

默认情况下,Java进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使

守护线程的代码还没有执行完,也会强制结束

  • 垃圾回收器线程就是一种守护线程
  • Tomcat中的Acceptor和Poller线程都是守护线程,所以Tomcat接收到shutdown命令后,不会等待它它们处理完当前请求

线程的状态

NEW

Thread t1 = new Thread()

RUNNABLE

TERMINATED

TIMED_WAITING

  • sleep()这种有时限的

WAITING

  • join()这种不知道什么时候截止的

BLOCKED

共享问题

银行存取钱问题

临界区Critical Section

  • 一个程序运行多个线程本身是没问题的

  • 问题出在多个线程访问共享资源

    • 多个线程读共享资源其实也没有什么问题
    • 在多个线程对共享资源读写操作时发生指令错误,就会出现问题
  • 一段代码内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

竞态条件Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

为了避免竞态条件的发生,有多种手段可以达到目的

  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案: 原子变量

synchronized解决方案

应用之互斥

使用synchronized,即对象锁,它采用互斥的方法让同一时刻至多只有一个线程能持有对象锁,其它线程再想获得这个对象锁就会阻塞住,保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程的上下文切换

  • synchronized实际上时使用对象锁保证了临界区代码的原子性
  • 所有线程的临界区代码都要加对象锁,因为线程的blocked状态是因为,该线程获取对象锁时,该对象锁已经被持有,故会变成阻塞状态,如果线程吗没有加对象锁,不会有去获取对象锁的行为,故不会被锁住

虽然java中的互斥与同步都可以采用synchronized关键字来完成,但是它们还是有区别的

  • 互斥是避免临界区的竞态条件发生,同一时刻只能由一个线程执行临界区代码
  • 同步是由于线程的执行先后顺序不同,需要一个线程等待其它的线程运行的某一个点

语法

synchronized(对象){
    临界区
}
//this是对象,Room.class是类对象,synchronized不仅可以锁对象还可以锁类对象
//例子
class Room{
    private int counter=0;
    public void increment(){
        syncronized(this){
            counter++;
        }
    }
    public void decrecement(){
        syncronized(this){
            counter--;
        }
    }
    //等价于
    public synchronized void decrecement(){
            counter--;
    }
    public int getCounter(){
        syncronized(this){
            return counter;
        }
    }
}