目录
什么是程序
程序是指令、数据及其组织形式的描述,进程是程序的实体。
程序是操作系统可执行的文件,也就是说操作系统找到可执行的文件,并把这些文件加载到内存中。
什么是进程
进程(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 属于处理器重排序。
这些重排序都可能会导致多线程程序出现内存可见性问题。
线程数设多少最合适
显而易见,线程数肯定不是越多越好。
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。