Java多线程

说起线程,首先要提及进程,进程是程序(任务)的执行过程(动态性),它持有资源(共享内存,共享文件)和线程(载体),

  • 线程是系统中最小的执行单元
  • 同一进程中有多个线程
  • 线程共享进程的资源
  • 线程的交互:互斥,同步

Java对线程的支持

  • Java对线程的支持主要体现在java.lang包中的Thread类和Runnable接口上
  • Thread类和Runnable接口中有一个共通的run()方法
  • run()方法提供了线程实际工作执行的代码

线程常用的方法

  • 线程的创建
    • Thread()
    • Thread(String name)
    • Thread(Runnable target)
    • Thread(Runnable target, String name)
  • 线程的方法
    • void start(),启动线程
    • static void sleep(long millis),指定线程休眠的时间,以毫秒为单位
    • static void sleep(long millis, int nanos),第二个参数将休眠时间精确到纳秒
    • void join(),使其他线程等待当前线程终止
    • void join(long millis),指明时间阈值,即其他线程最长需要等待的时间
    • void join(long millis, int nanos),第二个参数将等待时间精确到纳秒
    • static void yield(),当前运行线程释放处理器资源,并且重新竞争处理器资源
  • 获取线程引用
    • static Thread currentThread(),返回当前运行的线程引用

线程创建的两种方式

1.继承Thread类:

class MyThread extends Thread{
    ...
    @Override
    public void run(){
        ...
    }
}

MyThread mt = new MyThread();
mt.start();

2.实现Runnable接口:

class MyThread implements Runnable{
    ...
    @Override
    public void run(){
        ...
    }
}

MyThread mt = new MyThread();
Thread td = new Thread(mt);
td.start();

3.两种方式的比较:
Runnable方式可以避免Thread方式由于Java单继承带来的缺陷;
Runnable的代码可以被多个线程(Thread实例)共享,适合于多个线程处理同一资源的情况。

线程的生命周期

1.创建:新建一个线程对象,如Thread td = new Thread()。
2.就绪:创建了线程对象后,调用了线程的start()方法(注意:此时线程只是进入了线程队列,等待获取CPU服务,具备了运行的条件,但并不一定已经开始运行了)。
3.运行:处于就绪状态的线程,一旦获取了CPU资源,便进入到运行状态,开始执行run()方法里面的逻辑。
4.阻塞:一个正在执行的线程在某些情况下,由于某种原因而让出了CPU资源,暂停了自己的执行,便进入了阻塞状态,如调用了sleep()方法。
5.终止:线程的run()方法执行完毕,或者线程调用了stop()方法(已弃用),线程便进入终止状态。

守护线程

Java线程有两类,一类是用户线程,它运行在前台,执行具体的任务,比如程序的主线程、连接网络的子线程等都是用户线程;还有一类是守护线程,它运行在后台,为其他前台线程服务:

  • 特点:一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作
  • 应用:数据库连接池中的检测线程、JVM虚拟机启动后的检测线程等
  • 最常见的守护线程:垃圾回收线程
  • 设置守护线程:可以通过调用Thread类的setDaemon(true)方法来设置当前的线程为守护线程
  • setDaemon(true)必须在start()方法之前调用,否则会抛出IllegalThreadStateException异常
  • 在守护线程中产生的新线程也是守护线程
  • 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑

Java内存模型(JMM)

Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节。

  • 所有的变量都存储在主内存中
  • 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
  • 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写
  • 不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成
  • 线程1对共享变量的修改要想被线程2及时看到,必须要经过两个步骤:1.把工作内存1中更新过的共享变量刷新到主内存中;2.将主内存中最新的共享变量的值更新到工作内存2中

内存可见性

可见性:一个线程对共享变量值的修改,能够及时地被其他线程看到。
共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量。
Java语言层面支持的可见性实现方式: synchronized,volatile。
JMM关于synchronized的两条规定:

  • 线程解锁前,必须把共享变量的最新值刷新到主内存中
  • 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意:加锁与解锁需要是同一把锁)

通过以上两点来保证线程解锁前对共享变量的修改在下次加锁时对其他线程可见。
线程执行互斥代码的过程:
1. 获得互斥锁
2. 清空工作内存
3. 从主内存拷贝变量的最新副本到工作内存
4. 执行代码
5. 将更改后的共享变量的值刷新到主内存
6. 释放互斥锁

volatile如何实现内存可见性:
深入来说:通过加入内存屏障和禁止重排序优化来实现。通俗地讲:volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存。这样任何时刻,不同的线程总能看到该变量的最新值。注意:volatile不能保证原子性。

  • 对volatile变量执行写操作时,会在写操作后加入一条store屏障指令
  • 对volatile变量执行读操作时,会在读操作前加入一条load屏障指令

线程写volatile变量的过程:
1. 改变线程工作内存中volatile变量副本的值
2. 将改变后的副本的值从工作内存刷新到主内存
线程读volatile变量的过程:
1. 从主内存中读取volatile变量的最新值到线程的工作内存中
2. 从工作内存中读取volatile变量的副本

synchronized和volatile的比较:

  • volatile不需要加锁,比synchronized更轻量级,不会阻塞线程
  • 从内存可见性角度讲,volatile读相当于加锁,volatile写相当于解锁
  • synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性

保证变量操作的原子性:

  • 使用synchronized关键字
  • 使用ReentrantLock(java.util.concurrent.locks包下)
  • 使用AtomicInteger(java.util.concurrent.atomic包下)

要在多线程中安全的使用volatile变量,必须同时满足:
1. 对变量的写入操作不依赖其当前值
- 不满足:number++、count=count*5等
- 满足:boolean变量、记录温度变化的变量等
2. 该变量没有包含在具有其他变量的不变式中

重排序

重排序:代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而作的优化,
1. 编译器优化的重排序(编译器优化)
2. 指令级并行重排序(处理器优化)
3. 内存系统的重排序(处理器优化)
as-if-serial:无论如何重排序,程序执行的结果应该与代码顺序执行的结果一致(Java编译器、运行时和处理器都会保证Java在单线程下遵循as-if-serial语义)
重排序不会给单线程带来内存可见性问题,而在多线程中程序交错执行时,重排序可能会造成内存可见性问题。
导致共享变量在线程间不可见的原因有:
1. 线程的交叉执行(通过synchronized的原子性来解决)
2. 重排序结合线程交叉执行(通过synchronized的原子性来解决)
3. 共享变量更新后的值没有在工作内存与主内存间及时更新(通过synchronized的可见性来解决)

你可能感兴趣的:(Java多线程)