从字面上理解的话,并发就是同时发生,并行是指同时执行(这里的执行指的是执行过程)。而在CPU中的效果就是:
并发:多个线程在同一个CPU中抢行(准确说是由CPU在指挥),每个线程之间交叉执行,同一时间只有一个线程在执行。反复切换,这个速度极快。
并行:其实就是几个线程在多个CPU中同时执行,不存在互相切换的过程。
通过交通来理解,就是有本来有三条车道:并发是突然变成了一条车道,然后三条车道要这个交错的驶入这一条道路。而并行就是三条车道不变,大家各跑各的,互不干扰。(在这里强烈鄙视一下随意变道、加塞的车!会导致别的车道崩溃的好不好!)
JMM的存在是为了保证在多线程的情况下,多个线程之间可以有效地、正确地协同工作。而JMM的关键就是要围绕原子性、可见性和有序性来建立。
1、原子性(Atomicity)
原子性是指一个操作是不可以被中断的,也就是最小粒子的操作。一般给int赋值这种操作,就是原子性的。例如在32位的Java虚拟机下,对long的操作就是非原子性的(因为long类型的长度是64位,而32位系统无法一次执行完,就可能导致错误,比如把a的前32位和b的后32位拼在一起,出线了一个莫名其妙的数字)
2、可见性(Visibility)
可见性是指当一个线程修改了某一个共享变量的时候,其他的线程要能够立刻知道这个变量被修改了以及修改的值。
3、有序性(Ordering)
是指在程序编译的时候,有可能会把代码顺序进行改变,但是这个改变在串行情况下,并不会影响程序的执行结果。可是在并行的情况下,就可能会出现问题。
例如:
class OrderExample{
int a = 0;
boolean flag = false;
public void writer(){
a = 1;
flag = true;
}
public void reader(){
if(flag){
int i = a + 1;
}
}
}
假如有两个线程A和B,A调用的writer方法,而紧接着B调用了reader方法。如果这个时候出现指令重拍的现象,那么有可能B调用的reader方法中,flag已经是true了,但是a可能还不是1。(因为a = 1 和 flag = true换了位置)
至于为什么要指令重排,那是为了性能考虑。具体原因涉及到汇编、计算机结构等,理解不能。
4、哪些指令不可以被重排
1)程序顺序原则:一个线程内保证语义的串行性;
2)volatile规则:volatile变量的写,先发生于读,这保证了volatile变量的可见性;
3)锁规则:解锁必然发生在随后的加锁前;
4)传递性:A优先于B,B优先于C,那么A一定优先于C;
5)线程的start()方法优先于它的每一个条件;
6)线程的所有操作优先于线程的终结;
7)线程的中断先于被中断的线程的代码;
8)对象的构造函数执行、结束先于finalize()方法;
进程是一个容器,可以容纳多个线程。而线程可以理解成是轻量级的进程,是程序执行的最小单位。
JAVA程序本身就是一个多线程的。每次启动一个main方法(哪怕就是简单的“HELLO WORLD”),都会有好几个线程在执行。例如:主线程和垃圾回收(GC)。
1、创建一个新的线程
JAVA中创建新的线程有两种方法:继承Thread类,和实现Runnable接口。
1)继承Thread:
继承Thread类,并且重写run方法。然后实例化出来的对象调用start()方法(注意不是run()方法)。这个thread类也可以是一个匿名内部类
public class ThreadDemo1 {
private static class MyThread extends Thread{
@Override
public void run() {
super.run();
//打印当前的进程
System.out.println("这是一个新的线程:"+Thread.currentThread().getName());
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.setName("|我的线程|");
myThread.run();
myThread.start();
//匿名内部类
Thread thread = new Thread() {
@Override
public void run() {
super.run();
System.out.println("我是一个匿名内部类的新线程:" + Thread.currentThread().getName());
}
};
thread.setName("|匿名内部类|");
thread.run();
thread.start();
}
}
而控制台输出的结果是:
实例化的MyThread对象是可以调用run方法的,但是并不会创建一个新的线程,而是在原有线程(这里是主线程)中执行。所以必须要使用run来调用。
2)实现Runnable接口
通常情况下,继承Thread类就可以使用,但是考虑到JAVA的单继承、多实现原则,这会导致一些很麻烦的事情。所以出现了一个Runnable接口。而除了这一点区别外,Thread类是面向线程的,而Runnable接口是面向业务实现的。
public class RunnableDemo {
private static class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("我是一个Runnable接口:" + Thread.currentThread().getName());
}
}
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.setName("|我的Runnable接口|");
myRunnable.run();
//依然是在main线程调用的MyRunnable的run()方法
thread.run();
thread.start();
}
}
volatile关键字实现了JMM中的原子性、可见性和有序性。
原子性:前面提到的32位虚拟机,两个线程同时修改long类型数据的时候,会出现数据错误。而如果这个long用volatile声明后,就可以很好的避免这个问题。
可见性:用volatile关键字声明一个变量的时候,代表我们告诉虚拟机“这个变量可能被其它程序或者线程引用”。而为了在这个变量被一个线程修改后,其它变量可以获取到他的状态,虚拟机需要进行一系列的操作。
有序性:声明了volatile的变量,虚拟机会非常小心的处理“重排”的现象。
而关于有序性的印证,我们需要虚拟机以Server模式启动,这样虚拟机会进行系统优化,就可能发生重排的现象。(可以使用Java虚拟机参数 -server来切换到server模式)
关键字synchronized的作用是实现线程间的同步。他主要是给代码加锁,以保证这一段代码每一次只可以有一个线程进入,从而保证线程安全。
synchronized有三种主要的实现方法:
1、指定加锁对象:给指定的对象加锁;
2、直接作用于实例方法:相当于给当前实例加锁;
3、直接作用于静态方法:相当于给当前的类加锁,就是类名.class的那个类;