JAVA高并发学习笔记(一)

一、并发和并行

从字面上理解的话,并发就是同时发生,并行是指同时执行(这里的执行指的是执行过程)。而在CPU中的效果就是:

并发:多个线程在同一个CPU中抢行(准确说是由CPU在指挥),每个线程之间交叉执行,同一时间只有一个线程在执行。反复切换,这个速度极快。

并行:其实就是几个线程在多个CPU中同时执行,不存在互相切换的过程。

通过交通来理解,就是有本来有三条车道:并发是突然变成了一条车道,然后三条车道要这个交错的驶入这一条道路。而并行就是三条车道不变,大家各跑各的,互不干扰。(在这里强烈鄙视一下随意变道、加塞的车!会导致别的车道崩溃的好不好!)

二、JAVA的内存模型(JMM)

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中的线程

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();
    }
}

而控制台输出的结果是:

JAVA高并发学习笔记(一)_第1张图片

实例化的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();
    }
}

 

JAVA高并发学习笔记(一)_第2张图片

五、volatile关键字

volatile关键字实现了JMM中的原子性、可见性和有序性。

原子性:前面提到的32位虚拟机,两个线程同时修改long类型数据的时候,会出现数据错误。而如果这个long用volatile声明后,就可以很好的避免这个问题。

可见性:用volatile关键字声明一个变量的时候,代表我们告诉虚拟机“这个变量可能被其它程序或者线程引用”。而为了在这个变量被一个线程修改后,其它变量可以获取到他的状态,虚拟机需要进行一系列的操作。

有序性:声明了volatile的变量,虚拟机会非常小心的处理“重排”的现象。

而关于有序性的印证,我们需要虚拟机以Server模式启动,这样虚拟机会进行系统优化,就可能发生重排的现象。(可以使用Java虚拟机参数 -server来切换到server模式)

六、synchronized关键字

关键字synchronized的作用是实现线程间的同步。他主要是给代码加锁,以保证这一段代码每一次只可以有一个线程进入,从而保证线程安全。

synchronized有三种主要的实现方法:

1、指定加锁对象:给指定的对象加锁;

2、直接作用于实例方法:相当于给当前实例加锁;

3、直接作用于静态方法:相当于给当前的类加锁,就是类名.class的那个类;

 

 

你可能感兴趣的:(JAVA)