多线程基础概念介绍
进程
是程序或任务的执行的过程,具有动态性;
它持有资源(共享内存,共享文件)和线程;
如:打开QQ、搜狗输入法软件时,我们启动了两个进程。
线程
系统中最小的执行单元。 比如一个软件里边的各种任务就是线程;
同一进程中有多个线程;
线程共享进程的资源。
多线程
一个进程中可以开启多条线程,多条线程可以并行(同时)执行不同的任务;
多线程技术可以提高程序的执行效率。
线程创建的两种方式比较
- 继承Thread类
public class ExtendThread {
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
// mt.start();
MyThread mt2 = new MyThread();
mt2.start();
}
}
class MyThread extends Thread{
@Override
public void run(){
System.out.println("使用extends创建线程!!");
}
}
- 实现Runnable接口
public class ImplementThread {
public static void main(String[] args) {
Mythread2 mt = new Mythread2();
Thread th = new Thread(mt);
th.start();
Thread th2 = new Thread(mt);
th2.start();
}
}
class Mythread2 implements Runnable{
@Override
public void run() {
System.out.println("使用implements创建线程!");
}
}
注意事项:同一线程的start()方法不能重复调用
start()方法相关源码如下:
public synchronized void start() {
if (threadStatus != 0) //start()方法不能被重复调用,否则会抛出异常
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this); //加入到线程组中
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
}
private native void start0();
附:Thread常用方法介绍
两种实现方式对比:
Runnable方式可以避免Thread方式由于Java单继承特性带来的缺陷
Runnable方式实现可以被多个线程共享,适合于多个线程处理同一资源的情况
线程的生命周期
下图是线程生命周期的详细状态转换图(注意:不能从阻塞状态直接到运行状态)
下面将对每一个状态进行简单的介绍
创建
新建一个线程对象,如Thread td = new Thread();就绪
创建线程对象之后,调用start()方法(注意:此时线程只是进入了线程队列,等待获取CPU服务,具备了运行条件,但不一定已经开始运行了)运行
处于就绪状态的线程,一旦获取CPU资源,就会进入运行状态,开始执行run()方法里面的逻辑阻塞
一个正在执行的线程在某些情况下,由于某种原因而暂时让出了CPU资源,暂停了自己的执行,便进入了阻塞状态。如:调用了sleep()方法终止
线程的run()方法执行完毕,或者线程调用了stop()方法(现在已经不推荐使用这种方式),线程便进入了终止状态
附:更加详细的线程状态转换图
守护线程介绍
Java线程有两类
用户线程
运行在前台,执行具体的任务。如:用户的主线程,连接网络的子线程等守护线程
运行在后台,为其他前台线程服务。
特点:
一旦所有用户线程都结束运行,守护线程也会随JVM一起结束工作
应用:
数据库连接池中的监测线程
JVM虚拟机启动后的监测线程
常见:
垃圾回收线程
使用方法:
调用Thread类的setDaemon(true)方法来设置当前线程为守护线程
注意事项:
①setDaemon(true)必须在start()方法之前调用,否则会抛出异常IllegalThreadStateException
②在守护线程中产生的新线程也是守护线程
③不是所有的任务都可已分配给守护线程来执行,如:读写操作或计算逻辑
内存可见性
原子性
跟数据库事务的原子性概念差不多,即一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效)。
可见性
一个线程对共享变量值的修改,能够及时地被其他线程看到。
共享变量
如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量。
Java内存模型(JMM)
描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节。
注意:
- 所有的变量都存储在主内存中
- 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(即主内存中共享变量的副本)
共享变量可见性实现原理
线程1对共享变量的修改想要被线程2及时看到,必须经过以下两个步骤:
- 把工作内存1中更新过的共享变量刷新到主内存中
- 将主内存中最新的共享变量的值更新到工作内存2中
Java语言层面实现可见性的两种方式
- synchronized
synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程A每次运行到这个方法时,都要检查有没有其它正在用这个方法的线程B(或者C D等),有的话要等正在使用这个方法的线程B(或者C D)运行完这个方法后再运行此线程A,如果没有其他线程正在用这个方法则线程A可以运行这个方法。
线程执行互斥代码的过程:
①获得互斥锁
②清空工作内存
③从主内存拷贝变量的最新副本到工作内存
④执行代码
⑤将更改后的共享变量的值刷新到主内存
⑥释放互斥锁
synchronized 关键字的两种用法
①synchronized 方法
通过在方法声明中加入 synchronized关键字来声明 synchronized 方法。如:
public synchronized void accessVal(int newVal);
②synchronized 块
通过 synchronized关键字来声明synchronized 块。语法如下:
synchronized(syncObject)
{
//允许访问控制的代码
}
对synchronized (this)的一些理解(this指的是调用这个方法的对象)
- 当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
- 然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
- 尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
- 第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
- 以上规则对其它对象锁同样适用。
注意:
线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存重新读取最新的值
线程解锁时,必须把共享变量的最新值刷新到主内存中
- volatile
能够保证volatile变量的可见性;不能保证volatile变量复合操作的原子性
实现方式:
通过加入内存屏障和禁止重排序优化实现
①对volatile变量执行写操作时,会在写操作后加入一条store屏障指令
②对volatile变量执行读操作时,会在读操作前加入一条load屏障指令
线程写volatile变量的过程
①改变线程工作内存中volatile变量副本的值
②将改变后的副本的值从工作内存刷新到主内存
线程读volatile变量的过程:
①从主内存中读取volatile变量的最新值到线程的工作内存中
②从工作内存中读取volatile变量的副本的值
适用场合:
①对变量的写操作不依赖其当前值
不满足:number++、count = count+5
满足:boolean变量,记录温度变化的变量
②变量没有包含在具有其它变量的不变式中
不满足:不变式 low < up
两种实现方式比较
- volatile不用加锁,比synchronized更加轻量级,不会阻塞线程;
- 从内存可见性分析,volatile读操作相当于加锁,volatile写操作相当于解锁;
- synchronized既能保证可见性,又能够保证原子性;而volatile只能保证可见性,不能够保证原子性
参考资料
[1]Java总结篇系列:Java多线程(一)
[2]深入理解java中的synchronized关键字
[3]java中synchronized的用法详解
补充问题
1.Thread类的yield方法作用
yield()应该做的是让当前运行线程回到可运行状态,只允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
注意:sleep()可以使低优先级的线程得到执行的机会,当然也可以让同优先级、高优先级的线程有执行的机会。而yield()方法只能让同优先级的线程有执行的机会。
2.为什么每个线程要使用工作内存,而不直接访问主内存
①为了减少各线程对主内存的频繁访问,各线程只需要访问自己的工作内存即可以获取共享变量的副本值,将访问压力分摊到各线程的工作内存中,主内存只需要按照一定的规则同步共享变量的值到各线程的工作内存。
注意:
“synchronized” — 保证在块开始时都同步主内存的值到工作内存,而块结束时将变量同步回主内存
3.happens-before原则