Java并发理论基础知识

什么是程序

程序是指令、数据及其组织形式的描述,进程是程序的实体。

程序是操作系统可执行的文件,也就是说操作系统找到可执行的文件,并把这些文件加载到内存中。

什么是进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

当代面向线程设计的计算机结构中,进程是线程的容器。

什么是线程

线程(thread)是操作系统能够进行运算调度的最小单位。

它被包含在进程之中,是进程中的实际运作单位。

一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

java代码演示(继承Thread方式):

public class T01_WhatIsThread {
    private static class T1 extends Thread {
        @Override
        public void run() {
           for(int i=0; i<5; i++) {
               try {
                   TimeUnit.MICROSECONDS.sleep(1);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               System.out.println("T1被调用");
           }
        }
    }
    public static void main(String[] args) {
        new T1().start();
        for(int i=0; i<5; i++) {
            try {
                TimeUnit.MICROSECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("main方法");
        }
    }
}
---------------输出结果----------
main方法
main方法
T1被调用
T1被调用
main方法
T1被调用
main方法
main方法
T1被调用
T1被调用

如果这里不使用线程,即直接调用T1的run方法,那么就是一条线程,会等T1的run方法执行完了才会执行下面的代码,即按顺序执行。

public class T01_WhatIsThread {
    private static class T1 extends Thread {
        @Override
        public void run() {
           for(int i=0; i<5; i++) {
               try {
                   TimeUnit.MICROSECONDS.sleep(1);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               System.out.println("T1被调用");
           }
        }
    }
    public static void main(String[] args) {
        new T1().run();  // 修改了这里
        for(int i=0; i<5; i++) {
            try {
                TimeUnit.MICROSECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("main方法");
        }
    }
}
---------------输出结果----------
T1被调用
T1被调用
T1被调用
T1被调用
T1被调用
main方法
main方法
main方法
main方法
main方法

为什么需要多线程

为了合理利用(压榨) CPU 的高性能。

在计算机里面,cpu、内存、I/O等设备的速度差异极大,为了平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献:

  • cpu增加了缓存,均衡与内存对的速度差异。
  • 操作系统增加了线程、进程,分时复用CPU,均衡CPU与I/O设备的速度差异。
  • 编译程序优化了指令次序(重排序),使缓存能得到更合理的使用。

但他们又带了可见性、原子性、有序性的问题。这三个问题也叫并发三要素

线程不安全示例

如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作的结果是不一致的。

以下代码演示了 1000 个线程同时对 cnt 执行自增操作,操作结束之后它的值会小于1000。

public class ThreadUnsafeExample {
    private int cnt = 0;
    public void add() {
        cnt++;
    }
    public int get() {
        return cnt;
    }
    public static void main(String[] args) throws InterruptedException {
        final int threadSize = 1000;
        ThreadUnsafeExample example = new ThreadUnsafeExample();
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < threadSize; i++) {
            executorService.execute(() -> {
                example.add();
            });
        }
        executorService.shutdown();
        System.out.println(example.get());
    }
}
---------输出结果-----------
975 

上面的并发代码出现问题的根本原因是上面提到的过的并发三要素

并发三要素

可见性:CPU缓存引起

可见性:一个线程对共享变量的修改,另一个线程能够立刻能看到。

// 初始值
int i = 0;
//线程1执行的代码
i ++;
//线程2执行的代码
j = i;

上面这个例子,如果未使用多线程,j 最后的结果肯定为 1。

但是在多线程中可能出现 i 取到初始值然后将值改成 1 后还未来得及将值写入主存中,j 就开始取值,取到的值还是最开始的初始值 0 而不是 1。

这就是可见性问题,线程1对变量 i 做了修改之后,线程2没有立即看到线程1修改的值。

原子性:分时复用引起

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

转账问题:比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。

如果没有原子性,账户A减去1000元之后,操作意外中止,账户B没有收到这个转来的1000元。

有序性:重排序引起

有序性:即程序执行的顺序按照代码的先后顺序执行。

int i = 0;              
boolean flag = false;
i = 1;                //语句1  
flag = true;          //语句2

上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。

从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?

不一定, 这里可能会发生指令重排序。

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型(顺序从上到下):

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

上述的 1 属于编译器重排序,2 和 3 属于处理器重排序。

这些重排序都可能会导致多线程程序出现内存可见性问题。

线程数设多少最合适

显而易见,线程数肯定不是越多越好。

public class T {
    private static double[] nums = new double[1_0000_0000];
    private static Random r = new Random();
    private static DecimalFormat df = new DecimalFormat("0.00");

    static {
        for (int i = 0; i < nums.length; i++) {
            nums[i] = r.nextDouble();
        }
    }

    // 单线程,直接for循环,本身就是一个单线程
    private static void m1() {
        long start = System.currentTimeMillis();
        double result = 0.0;
        for (int i = 0; i < nums.length; i++) {
            result += nums[i];
        }
        long end = System.currentTimeMillis();
        System.out.println("m1:" + (end - start) + " result:" + df.format(result));
    }

    // 双线程,两个线程一个线程执行一半
    static double result1 = 0.0, result2 = 0.0, result = 0.0;

    private static void m2() throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < nums.length >> 1; i++) {
                result1 += nums[i];
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = nums.length >> 1; i < nums.length; i++) {
                result2 += nums[i];
            }
        });
        long start = System.currentTimeMillis();
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        // 使用join,才能让线程结束后才执行result的加法,否则result=0.0。
        result = result1 + result2;
        long end = System.currentTimeMillis();
        System.out.println("m2:" + (end - start) + " result:" + df.format(result));
    }
	
    // 10000 条线程
    private static void m3() throws InterruptedException {
        final int threadCount = 10000; // 10000 条线程
        Thread[] threads = new Thread[threadCount];
        double[] results = new double[threadCount];
        final int segmentCount = nums.length / threadCount;
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        for (int i = 0; i < threadCount; i++) {
            int m = i;
            threads[i] = new Thread(() -> {
                for (int j = m * segmentCount; j < (m + 1) * segmentCount; j++) {
                    results[m] += nums[j];
                }
                countDownLatch.countDown(); // 这个必须放在 run 方法里面
            });
        }
        double result = 0.0;
        long start = System.currentTimeMillis();
        for (Thread t : threads) {
            t.start();
        }
        countDownLatch.await();
        for (int i = 0; i < results.length; i++) {
            result += results[i];
        }
        long end = System.currentTimeMillis();
        System.out.println("m3:" + (end - start) + " result:" + df.format(result));

    }

    public static void main(String[] args) throws InterruptedException {
        m1(); // 单线程
        m2(); // 俩线程
        m3(); // 10000条线程
    }
}
--------输出结果----------
m1:126 result:49997433.84
m2:73 result:49997433.84
m3:807 result:49997433.84

上面的m1是单线程,m2是俩线程,m3是10000条线程。可以看出线程并不是也多越好。

那么线程设置多少才最合适呢。我的电脑是8核16线程,那我是不是设置16个线程效率最高。

// 16 条线程
private static void m4() throws InterruptedException {
    final int threadCount = 16;
    Thread[] threads = new Thread[threadCount];
    double[] results = new double[threadCount];
    final int segmentCount = nums.length / threadCount;
    CountDownLatch countDownLatch = new CountDownLatch(threadCount);
    for (int i = 0; i < threadCount; i++) {
        int m = i;
        threads[i] = new Thread(() -> {
            for (int j = m * segmentCount; j < (m + 1) * segmentCount; j++) {
                results[m] += nums[j];
            }
            countDownLatch.countDown(); // 这个必须放在 run 方法里面
        });
    }
    double result = 0.0;
    long start = System.currentTimeMillis();
    for (Thread t : threads) {
        t.start();
    }
    countDownLatch.await();
    for (int i = 0; i < results.length; i++) {
        result += results[i];
    }
    long end = System.currentTimeMillis();
    System.out.println("m3:" + (end - start) + " result:" + df.format(result));
}

------输出结果---------
m4:27 result:49997234.81  

得到结果确实别上面都快了不少,理论上我们用上了所有的线程,所以效率最高。

但是由于我们的电脑可能不仅仅只运行java代码,可能还运行其他的程序,所以不可能16个线程全部空着等待java程序使用。

并且处于安全的考虑,我们还应该留20%左右的线程留用。

所以最合适的线程数还是得根据实际的环境,真实的压测才能找出来。

Java是怎么解决并发问题的:JMM(Java内存模型)

Java内存模型规范了JVM如何何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括:

  • volatile、synchronized 和 final 三个关键字
  • Happens-Before 规则

上面讲到了三个并发问题,我们来看看Java到底是怎么解决的,解决到哪种程度:

可见性

Java提供了volatile关键字来保证可见性。

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

原子性

我们先看几个例子:

x = 1;  //语句1,一个操作。将数值10赋值给x。
y = x;  //语句2,两个操作,读取x的值,然后将值赋给y。
x++;    //语句3,三个操作,读取x的值,进行加1操作,重新赋值给x。

上面三个语句中只有语句1具备原子性。

然后我们就可以知道,只有简单的读取和赋值操作是原子性。

有序性

可以通过volatile关键字来保证一定的有序性。JMM是通过Happens-Before 规则来保证有序性的。

PS:可以通过 synchronized 和 Lock 来保证可见性、原子性、有序性。

synchronized 和 Lock 能够保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中,从而可以保证可见性。

synchronized 和 Lock 能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。 

synchronized 和 Lock 能够保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
Happens-Before 规则
  • 单一线程原则:在一个线程内,在程序前面的操作先行发生于后面的操作。
  • 管程锁定规则:一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
  • volatile 变量规则:对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
  • 线程启动规则:Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。
  • 线程加入规则:Thread 对象的结束先行发生于 join() 方法返回。
  • 线程中断规则:对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否有中断发生。
  • 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
  • 传递性:如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值