多线程(英语:multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。
百度百科
进程与线程的概念
进程:是指程序或者任务的执行过程。
它持有资源(共享资源、文件)和线程
结合现实看一下,什么是进程?
以上几种应用运行的过程,都是一个进程。
但是,这些应用的安装文件、安装包、exe可执行程序等,都不是进程,只有这些应用执行起来,才可以叫做进程。
线程:是进程的一部分,也是操作系统运行的最小单位。
我们以学校为例。
每个班级都是一个独立的进程,班级中的每个学生都是一个线程,学生们都使用共同的教室,桌椅,被同样的老师教。
所以说,线程是系统运行的最小单位,一个进程中有n个线程,共享进程的资源。
学生之间,在学习之中互相追赶,但是,学习资源是有限的,比如:图书馆,老师等,当一个同学正在使用时,其他同学只能等待前一个同学使用完成后才能继续使用,这就是线程的互斥性。
而在体育活动中,比如拔河,多个线程之间需要共同协作,这就是线程的同步。
JAVA语言支持线程的方式与差别
java语言对线程提供了两种实现方式。
Thread 和 Runnable
首先,我们看一下API,里面介绍了线程的几种状态
NEW
至今尚未启动的线程处于这种状态。RUNNABLE
正在 Java 虚拟机中执行的线程处于这种状态。BLOCKED
受阻塞并等待某个监视器锁的线程处于这种状态。WAITING
无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。TIMED_WAITING
等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。TERMINATED
已退出的线程处于这种状态。
下面,我们通过一幅图直观的了解一下这六种状态。
然后,我们在分析一下Thread 和 Runnable的方法,来看看都属于这六种状态的哪个阶段。
我们看一下方法介绍,或许我们就有一个更加直观的了解。
首先,创建Thread对象,调用start方法,启动线程,此时,线程进入等待获取cpu资源状态,等到获取后,开始执行,若人为停止,譬如,调用sleep方法,这时候会导致线程阻塞,待sleep方法完成后,线程又开始执行,最后执行完毕。
了解完线程的生命周期,我们结合实际生活,然后来仔细看一下,在java中,Thread和Runnable两种方法的差异。
以学校招生为例。
北京某两所大学都计划在山东省招生5人,正巧,山东省有5名同学符合招生条件。然后两所大学分别开始招生。因为人数不够,所以两所学校开始同时招生。
首先,我们以Thread方法实现。(不考虑线程安全)
public class ThreadDemo extends Thread{
//模拟山东省有学生5名
private Integer number = 5;
//招生大学的名字
private String name;
public ThreadDemo(String name) {
super();
this.name = name;
}
@Override
public void run() {
//每个学校都有5个名额,
while(number > 0) {
number--;
System.out.println(Thread.currentThread().getName()
+"招收了1名学生还剩"+number +"名");
}
}
public static void main(String[] args) {
ThreadDemo th1 = new ThreadDemo("A大学");
ThreadDemo th2 = new ThreadDemo("B大学");
th1.start();
th2.start();
}
}
运行结果:
Thread实现方式显示,我们相当于创建每所大学的时候,都赋值了初始化的五个招生名额,各自在各自的线程里使用,这也就表示,这五个学生,并没有被两所学校共享,这就会导致,每个学校都招了五名学生,最后这五名学生两所大学都可以上了。当然,这是好事情,如果是剔除差生的话,就会导致有五名学生被冤枉,这是不符合我们需求的。
下面,我们换成Runnable实现。
class MyThread implements Runnable {
// 模拟山东省有学生5名
private int number = 5;
@Override
public void run() {
// 每个学校都有5个名额,
while (number > 0) {
number--;
System.out.println(Thread.currentThread().getName() +
"招收了1名学生还剩" + number + "名");
}
}
}
public class RunnableDemo {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread th1 = new Thread(myThread, "A大学");
Thread th2 = new Thread(myThread, "B大学");
th1.start();
th2.start();
}
}
我们接着看结果:
B大学招了四名学生,A大学招了一名学生。
因为线程的交叉执行,导致值的不统一,我们先不考虑这些,下面再讲,但是最后的结果是我们想要的。
所以,我们可以看出两种实现方式的差别。
Thread由于是单继承关系,会降低程序的扩展性
Runnbale可以被多个类实现并共享资源,适用于多个线程处理同一资源的情况。
线程可见性
首先,我们先明白几个定义。
可见性: 一个线程对共享变量值的修改,可以被其它的线程及时发现
共享变量:一个变量在多个线程的工作内存中都存在副本,那么,这个变量就是这几个线程的共享变量
说完以上两个名词的基本定义,我们不得不提一下JMM(JAVA内存模型),它描述了java程序中各种变量(线程访问变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节。
而且,我们要确定下面这些概念:
所以的变量都存储在主内存中
每个线程都有自己独立的工作内存,里面保存了该线程使用到变量的副本
看这幅图,线程1 - 线程2 共享变量之间值得传递必须经过以下的步骤
线程1更新工作内存1中共享变量的值,工作内存1中共享变量的值又更新到主内存中,主内存又更新到工作内存2中,最后到线程2.
说到这,估计,你就明白了,所有线程对共享变量的值必须从自己的工作内存中读写,不能直接从主内存中读写。
不同线程间共享变量的值的传递,必须经过主内存,否则无法直接读取其他线程的共享变量的值。
1、如何保证可见性
首先,我们要明确,要实现共享变量的可见性,我们要保证两点:
修改后的共享变量值可以及时的从修改的线程工作内存中刷新到主内存
其它线程的工作内存可以及时的获取到主内存中共享变量的新值
其实说直白点,就是线程可以及时的对主内存进行读写。
其它线程的工作内存可以及时的获取到主内存中共享变量的新值
java语言对可见性提供了两种方式
synchronized
volatile
2.synchronized
synchronized是多线程中相当重要的一个关键字,应该说是SVIP了。我们可以把它称为互斥锁,同步锁。它的特点就是保证了程序的原子性、可见性
JMM中,也有对synchronized的规定
线程解锁前,必须把共享变量的最新值刷新到主内存中
线程加锁前,将清空工作内存中共享变量的值,使用从主内存获取的最新共享变量值(加解锁需要用同一把锁)
说到这,我们说一个题外话 - 重排序。
什么是重排序?
在印象中,代码的顺序是按照我们的书写顺序依次执行的。其实不然,java程序在执行时,编译器或者处理器会为了提高性能,在保证as-if-serial原则的前提下,改变实际执行的顺序,这就是重排序。目前,有三种重排序
编译器优化的重排序 - 编译器优化
指令级并行重排序 - 处理器优化
内存系统的重排序 - 处理器优化
as-if-serial原则:无论如何重排序,程序执行的结果应该与代码顺序执行的结果一致。
编译器和处理器在运行时,都会保证java在单线程下遵循此原则。
但是,多线程情况下,程序交叉执行,重排序可能会造成可见性问题。
int number = 20; // 1.1
int count = 10; // 1.2
number+= count //1.3
这个代码中,1.1,1.2就可以重排序,但是1.3不可以,因为它会导致结果改变。
而程序在多线程运行时,由重排序问题导致的结果不一致现象就更奇妙了
public class RunnableDemo{
private boolean flag = false;
private int result = 10;
private int number = 1;
public void eat() {
flag = true;
number = 10;
}
public void drink() {
if(flag) {
result = number * 10;
}
System.out.println("result:"+result);
}
class MyThread extends Thread {
private boolean flag;
public MyThread(boolean flag) {
this.flag = flag;
}
@Override
public synchronized void run() {
if(flag) {
eat();
}else {
drink();
}
}
}
public static void main(String[] args) {
RunnableDemo runnableDemo = new RunnableDemo();
runnableDemo.new MyThread(true).start();
runnableDemo.new MyThread(false).start();
}
}
多运行几次,或者写个for循环,你会看到一个神奇的现象。有兴趣的同学可以自己多了解一下。在这就不多说了。
来,我们看一下synchronized关键字的作用。
首先,我们回到一开始所写的A、B两所大学招生的例子,Runnable实现多线程的方式,结果是什么呢?
A、B在招生第四名的时候,发生了冲突,相当于两个人同时去争抢了一个资源,而值没有及时更改,这在实际应用中是线程不安全的,所以这时候,synchronized出现了:我们在run方法,或者方法内部加上它。
class MyThread implements Runnable {
// 模拟山东省有学生5名
private int number = 5;
@Override
public synchronized void run() {
// 每个学校都有5个名额,
while (number > 0) {
number--;
System.out.println(Thread.currentThread().getName() +
"招收了1名学生还剩" + number + "名");
}
}
}
public class RunnableDemo {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread th1 = new Thread(myThread, "A大学");
Thread th2 = new Thread(myThread, "B大学");
th1.start();
th2.start();
}
}
这次,再让我们看一下结果。
多运行几遍,你会看到,不是A学校就是B学校,只有当一个学校招生完成了,另一个学校才能继续招生,这是线程安全的。当然,现实生活中,大家都是争着抢着的,但是其实我们要从根本理解,就不会有这种疑问了。
我们来看下 number--,其实这可以分解为以下几个过程。
num从内存中获取最新值
num-1
num-1的值赋给num
num把最新值更新到内存中
现在你是不是大概明白了?
仔细思考一下
synchronized锁几种方式(以上面代码为例,操作run方法)
对象锁:包括方法锁和同步代码块锁
方法锁:默认锁对象为this当前实例对象
@Override
public void run() {
// 每个学校都有5个名额,
synchronized (this) {
while (number > 0) {
number--;
System.out.println(Thread.currentThread().getName() +
"招收了1名学生还剩" + number + "名");
}
}
}
同步代码块锁:自己指定对象
Object lock = new Object();
@Override
public void run() {
// 每个学校都有5个名额,
synchronized (lock) {
while (number > 0) {
number--;
System.out.println(Thread.currentThread().getName() +
"招收了1名学生还剩" + number + "名");
}
}
}
类锁:synchronized修改静态的方法或指定锁为class对象
synchronized修饰静态方法
@Override
public synchronized void run() {
// 每个学校都有10个名额,
while (number > 0) {
number--;
System.out.println(Thread.currentThread().getName() +
"招收了1名学生还剩" + number + "名");
}
}
.class对象锁
@Override
public void run() {
// 每个学校都有10个名额,
synchronized (MyThread.class) {
while (number > 0) {
number--;
System.out.println(Thread.currentThread().getName() +
"招收了1名学生还剩" + number + "名");
}
}
}
最后的结果我们运行完成之后会发现,其实效果是一样的。每一个锁的结果,都保证了线程的顺序执行,保证一个线程运行完成之前不会被其它线程抢走,保证了线程的安全性。
下面,我们针对其中的一种锁的方式,做一个改进。
Object lock = new Object();
Object lock2 = new Object();
@Override
public void run() {
// 每个学校都有5个名额,
synchronized (lock) {
while (number > 0) {
number--;
System.out.println(Thread.currentThread().getName() +
"招收了1名学生还剩" + number + "名");
}
}
synchronized (lock2) {
while (number > 0) {
number--;
System.out.println(Thread.currentThread().getName() +
"招收了1名学生还剩" + number + "名");
}
}
}
最后,大家思考下,结果是什么呢?
3.volatile
volatile能保证变量的可见性
volatile不能保证复合操作的原子性
其实具体的来说,是通过加入内存屏障和禁止重排序来实现的。
对volatile变量执行写操作时,会在写操作后加入一条store屏障指令,处理器会把cpu中的缓存强制更新到主内存中,读操作时,会在读操作之前加入一条load屏障指令,必须去主内存中读取都能起到禁止重排序的作用。
通俗的讲,volatile变量每次被线程访问时,都会从主内存中重读该变量的值,而当变量发生变化时,又会主动把最新的值更新到主内存中,这样,就保证了不同的线程读取时,都会看到最新的值。
public class RunnableDemo{
private volatile int num = 0;
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public static void main(String[] args) {
RunnableDemo runnableDemo = new RunnableDemo();
for(int i=0;i<100;i++) {new Thread(new Runnable() {
@Override
public void run() {
runnableDemo.add();
}
}).start();}
System.out.println(runnableDemo.getNum());
}
private void add() {
num++;
}
}
多运行几次,看下结果。是不是有很多次小于100?
我们来简单总结下volatile使用规则:
对变量的写入操作不依赖当前值
如:num++等
当前变量的值不存在于不等式中
如:num > sum等
4.volatile和synchronized简单对比
volatile不需要加锁,比synchronized更轻量级,不会阻塞线程
从内存可见性来讲,volatile读相当于加锁,写相当于解锁
synchronized既能保证原子性,又能保证可见性,而volatile只能保证可见性
最后,写作不易,求一波关注