Java多线性与并发笔记

本文最后更新于:2022年6月11日 下午

[TOC]

1.进程 线程

进程让操作系统的并发性成为可能;而线程让进程的内部并发成为可能。

一个进程包括多个线程,但是这些线程共同享有进程的资源和地址空间。

进程是操作系统进行资源分配的基本单位,而线程是操作系统调度的基本单位。

2.多线程并发

性能问题:

对于单核CPU:

  • CPU密集型任务,如解压文件,一直占用CPU,单线程比多线程性能高,因为多线程涉及到线程切换,线程切换导致的开销可能会让性能下降。

  • 交互任务型任务,当然是多线程比单线程性能高。

对于多核CPU:

  • 对于CPU密集型任务,多线程优于单线程,因为能更充分的利用多核资源。

多线程编程能够提升程序性能,但是编程复杂,需要考虑线程安全问题。

3.如何创建线程?创建线程的方法

Java中应用程序和进程的概念

在Java中,一个应用程序对应一个JVM实例(JVM进程),名字默认为java.exe 或 javaw.exe。

Java采用单线程编程模型,一般来说,程序中只会创建一个线程,称为主线程main。

注意:虽然只有一个主线程执行任务,但不代表JVM只有一个线程,JVM实例在创建的时候,同时会创建很多其他的线程(比如垃圾回收器线程)。

创建线程

  • 继承Thread类,重写run方法(定义需要执行的任务);使用自己写的线程类创建线程实例t,调用t.start()方法启动线程。注意:调用t.run()方法,和普通方法调用没有区别,不会创建新的线程。
  • 实现Runnable接口,重写run方法。Runnable的含义是“任务”,通过实现Runnable接口,定义一个子任务,然后把子任务交给Thread去执行,还是调用t.start()方法。
  • 新线程创建的过程不会阻塞主线程的后续的执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {
public static void main(String[] args) {
System.out.println("主线程ID:"+Thread.currentThread().getId());
MyRunnable runnable = new MyRunnable(); // 创建一个子任务实例
Thread thread = new Thread(runnable); // 把子任务交给Thread去执行
thread.start(); // 创建新线程,执行子任务
}
}


class MyRunnable implements Runnable{

public MyRunnable() {

}

@Override
public void run() {
System.out.println("子线程ID:"+Thread.currentThread().getId());
}
}

创建进程

  • 使用ProcessBuilder创建进程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 创建进程
* @throws IOException
*/
@Test
public void testCreateProcess() throws IOException {
// 创建一个ProcessBuilder,传入命令参数
ProcessBuilder pb = new ProcessBuilder("cmd", "/c", "ipconfig/all");
// 调用ProcessBuilder.start()方法,返回一个Process对象
Process process= pb.start();

Scanner scanner = new Scanner(process.getInputStream());

while (scanner.hasNextLine())
{
System.out.println(scanner.nextLine());
}
scanner.close();
}
  • 使用Runtime类的exec方法创建进程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Runtime {
private static Runtime currentRuntime = new Runtime();

/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}

/** Don't let anyone else instantiate this class */
private Runtime() {}

可以看到,上面的Runtime类的构造方法是私有的,只能在类内实例化对象,并且对象currentRuntime是静态的,就是只有一个,所以Runtime是一个单例的,只能通过getRuntime()方法获取单例对象。

Runtime类的exec方法:

1
2
3
4
5
6
7
8
public Process exec(String[] cmdarray, String[] envp, File dir)
throws IOException {
// 通过ProcessBuilder的start方法创建ProcessImpl对象。
return new ProcessBuilder(cmdarray)
.environment(envp)
.directory(dir)
.start();
}

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    /**
* 使用Runtime创建进程,实际上exec()方法的内部,还是调用ProcessBuilder类的start方法创建进程的
* @throws IOException
*/
@Test
public void testCreateRuntimeProcess() throws IOException {
String cmd = "cmd "+"/c "+"ipconfig/all";
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec(cmd);

Scanner scanner = new Scanner(process.getInputStream());

while (scanner.hasNextLine())
{
System.out.println(scanner.nextLine());
}
scanner.close();
}
}

Java内存模型(JMM)总结

Java内存模型解决的问题:

1、多线程读同步与可见性问题:

线程缓存导致的可见性问题:

解决方法:

  • 使用volatile关键字,使得在线程中修改的变量立即刷新写回到主内存中,并且每个线程在每次使用volatile变量前都立即从主内存刷新。
  • Java中的synchronized关键字:同步快的可见性是由“如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值”、“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)”这两条规则获得的。

重排序导致的可见性问题:

  • volatile关键字:本身包含了禁止指令重排序的语义
  • synchronized是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入

ConcurrentHashMap如何保证线程安全?

由于HashMap是线程不安全的,在多线程情况下使用会出现很多问题。

因此,在多线程情况下如何获得线程安全的HashMap呢?

  • Collections.synchronizedMap
  • HashTable
  • ConcurrentHashMap

前两种方法由于全局锁的问题,存在很严重的性能问题。

而ConcurrentHashMap采用分段锁,不是锁整个hash表,而是只锁一部分。

0 JDK7中的ConcurrentHashMap

ConcurrentHashMap中有一个Segment数组,Segment可以看做是一个HashMap。

Segment继承了ReentrantLock,本身就是一个锁。

每个Segment有一个HashEntry[]数组,数组中的每个HashEntry就是用来存储key-value键值对。

HashEntry还可以指向下一个HashEntry。

当一个线程向某个segment中put元素时,segment会将自己上锁,只允许一个线程访问,而对其他的segment的访问没有影响。

这就是锁分段带来的好处。

而且segment的put方法是线程安全的,因为在put方法中,先会去尝试获取当前segment的锁,然后才去执行添加元素和扩容等操作。

扩容操作是针对当前上锁的segment的HashEntry数组进行的,所以是线程安全的。

1 JDK8中的ConcurrentHashMap的初始化

首先执行静态代码块

1
ConcurrentHashMap<Object, Object> concurrentHashMap = new ConcurrentHashMap<>();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private static final sun.misc.Unsafe U;
private static final long SIZECTL;
private static final long TRANSFERINDEX;
private static final long BASECOUNT;
private static final long CELLSBUSY;
private static final long CELLVALUE;
private static final long ABASE;
private static final int ASHIFT;

static {
try {
U = sun.misc.Unsafe.getUnsafe();
Class<?> k = ConcurrentHashMap.class;
SIZECTL = U.objectFieldOffset
(k.getDeclaredField("sizeCtl"));
TRANSFERINDEX = U.objectFieldOffset
(k.getDeclaredField("transferIndex"));
BASECOUNT = U.objectFieldOffset
(k.getDeclaredField("baseCount"));
CELLSBUSY = U.objectFieldOffset
(k.getDeclaredField("cellsBusy"));
Class<?> ck = CounterCell.class;
CELLVALUE = U.objectFieldOffset
(ck.getDeclaredField("value"));
Class<?> ak = Node[].class;
ABASE = U.arrayBaseOffset(ak);
int scale = U.arrayIndexScale(ak);
if ((scale & (scale - 1)) != 0)
throw new Error("data type scale not a power of two");
ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
} catch (Exception e) {
throw new Error(e);
}
}

内部的数据结构是Node数组,JDK8不在使用segment 数组的概念,而是Node数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* The array of bins. Lazily initialized upon first insertion.
* Size is always a power of two. Accessed directly by iterators.
*
* hash表,在第一次put数据的时候才初始化,他的大小总是2的倍数。
*/
transient volatile Node<K,V>[] table;

/**
* 用来存储一个键值对
*
* Key-value entry. This class is never exported out as a
* user-mutable Map.Entry (i.e., one supporting setValue; see
* MapEntry below), but can be used for read-only traversals used
* in bulk tasks. Subclasses of Node with a negative hash field
* are special, and contain null keys and values (but are never
* exported). Otherwise, keys and vals are never null.
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
}

线程安全的hash初始化

hash表 table数组会在第一次执行put方法的实际进行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* Maps the specified key to the specified value in this table.
* Neither the key nor the value can be null.
*
* <p>The value can be retrieved by calling the {@code get} method
* with a key that is equal to the original key.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with {@code key}, or
* {@code null} if there was no mapping for {@code key}
* @throws NullPointerException if the specified key or value is null
*/
public V put(K key, V value) {
return putVal(key, value, false);
}

/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
// 计算key的hash值
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 如果table是空,初始化之
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 省略...
}
// 省略...
}

调用initTable()方法进行初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* Initializes table, using the size recorded in sizeCtl.
*/
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
// #1
while ((tab = table) == null || tab.length == 0) {
// sizeCtl的默认值是0,所以最先走到这的线程会进入到下面的else if判断中
// #2
if ((sc = sizeCtl) < 0) // 首先判断sizeCtl是否小于0,若<0, 当前线程直接变为就绪状态
Thread.yield(); // lost initialization race; just spin
// 尝试原子性的将指定对象(this)的内存偏移量为SIZECTL的int变量值从sc更新为-1
// 也就是将成员变量sizeCtl的值改为-1
// #3
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {//CAS的方式
try {
// 双重检查,原因会在下文分析
// #4
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY; // 默认初始容量为16
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
// #5
table = tab = nt; // 创建hash表,并赋值给成员变量table
sc = n - (n >>> 2);
}
} finally {
// #6
sizeCtl = sc;
}
break;
}
}
return tab;
}

https://juejin.cn/post/6844903813892014087

Java线程池解析

https://juejin.cn/post/6844903889678893063


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!