0%

java并发01_基础概念

线程

线程创建与运行

创建线程的三种方式:

  1. 继承Thread类,重写run方法
  2. 通过Runable接口创建线程类
  3. 通过Callable和FutureTask创建线程
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
36
public class MyTest {

public static class SubThread extends Thread {
//重写run方法,run方法的方法体就是现场执行体
public void run() {
//...
}
}

public static class RunnableImpl implements Runnable {
//实现Runnable接口
public void run() {
//...
}
}

public static class CallableImpl implements Callable<Integer> {
//实现Callable接口
@Override
public Integer call() throws Exception {
return 1;
}
}

public static void main(String[] args) throws InterruptedException {
//第一种方式
SubThread subThread = new SubThread();
subThread.start();

//第二种方式
new Thread(new RunnableImpl());
//第三种
FutureTask<Integer> futureTask = new FutureTask<>(new CallableImpl());
new Thread(futureTask).start();
}
}

线程状态

Java定义了5中线程状态(新建、运行、等待(包括无限期等待和限期等待)、阻塞、结束):

  1. 新建(New):创建后尚未启动的线程处于此状态。
  2. 运行(Runable):包括了操作系统中的Running和Ready,线程可能正在执行,也可能在等待时间片。
  3. 等待
    • 无限期等待(Waiting):处于这种状态的线程不会被分配CUP时间,它们等待被其他线程显示唤醒。
      • 没有设置Timeout的Object.wait()方法。
      • 没有设置TImeout参数的Thread.join方法
      • LockSupport.park()方法
    • 限期等待(Timed Waiting):处于这种状态的线程不会被分配CUP时间,一定时间后由系统自动唤醒。
      • 设置Timeout的Object.wait()方法。
      • 设置TImeout参数的Thread.join方法
      • Thread.sleep()方法
      • LockSupport.parkNanos()方法
      • LockSupport.parkUntil()方法
  4. 阻塞(Blocked):线程等待着一个排它锁。
  5. 结束(Terminated):已经终止的线程状态。

用户线程与守护线程

Java中的线程分为两类,分别为daemon线程(守护线程)和user线程(用户线程)。

区别之一是当最后一个非守护线程结束时,JVM会正常退出,而不管当前是否有守护线程,也就是说守护线程是否结束并不影响JVM的退出

线程相关方法

wait()

当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回:

  1. 其他线程调用了该共享对象的notify()或者notifyAll()方法
  2. 其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。

调用wait()方法 的线程需要先获取该对象的监视器锁,否则调用wait()方法时调用线程会抛出IllegalMonitorStateException异常。

获取对象的监视器锁:

  1. 执行 synchronized 同步代码块时 ,使用该共享变量作为参数。

    1
    2
    3
    synchronized (obj){
    //do something
    }
  2. 调用该共享变量的方法,并且该方法使用了synchronized修饰。

    1
    2
    3
    synchronized void sync(int a,int b){
    //do something
    }

调用wait()会释放锁
当线程调用共享对象的wait()方法时,当前线程只会释放当前共享对象的锁,当前线程持有的其他共享对象的监视器锁并不会被释放。

notify()/notifyAll()

notify()函数
一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用 wait系列方法后被挂起的线程。 一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。

notifyAll()函数
notifyAll()方法则会唤醒所有在该共享变量上由于调用 wait 系列方法而被挂起的线程。

被唤醒的线程,不会直接从wait方法返回并执行,而是需要去竞争锁,获取到对象的锁以后才能继续执行。

虚假唤醒

一个线程可以从挂起状态变为可以运行状态(也就是被唤醒), 即使该线程没有被其他线程调用 notify()、 notifyAll()方法进行通知, 或者被中断, 或者等待超时, 这就是所谓的虚假唤醒 。

虽然虚假唤醒在应用实践中很少发生, 但要防患于未然, 做法就是不停地去测试该线 程被唤醒的条件是否满足, 不满足则继续等待, 也就是说在一个循环中调用 wait()方法进行防范。退出循环的条件是满足了唤醒该线程的条件 。

1
2
3
4
5
synchronized(obj){
while (条件不满足){
obj.wait();
}
}

join()方法

threadObj.join()
threadObj.join(long millis)

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(new RunnableImpl());
Thread threadB = new Thread(new RunnableImpl());

threadA.start();
threadB.start();

//主线程阻塞,等待子线程完成
threadA.join();
threadB.join();

}

sleep()方法

Thread.sleep(long millis)

当 一个执行中的线程调用了Thread的sleep方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与 CPU 的调度, 但是该线程所拥有的锁是持有不让出的。(不会释放锁

yield()方法

Thread.yield()

当一个线程调用 yield 方法时,当前线程会让出 CPU 使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,也有可能会调度到刚刚让出CPU的那个线程来获取CPU执行权

interrupt()方法(线程中断)

通过设置线程的中断标志并不能直接终止该线程的执行 ,而是被中断的线程根据中断状态自行处理。
interrupt()方法:

  1. 设置线程状态,
  2. 如果线程处于wait、sleep、join而被阻塞挂起,则会抛出InterruptedException
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 public static void testInterrupt() throws InterruptedException {
Thread threadA = new Thread(() -> {
int i = 0;
while (true) {
i++;
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threadA.start();
TimeUnit.SECONDS.sleep(10);
System.out.println("interrupt A");
threadA.interrupt();
//虽然调用了interrupt方法,但是线程A还是会继续运行,不会停止。
}

正确的线程退出方法

1
2
3
4
5
6
7
8
9
10
11
...
try {
//检测当前线程是否被中断
while (true && Thread.interrupted()) {
i++;
TimeUnit.SECONDS.sleep(1);
}
} catch (Exception e) {
e.printStackTrace();
}
...

线程组

Java中使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。默认情况下,所有的线程都属于主线程组。

中断线程组,线程组会去中断组里的所有线程。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class MyTest {
private static final Object mutex = new Object();
private static LinkedList<String> queue = new LinkedList<>();
private static int MAX_SIZE = 40;

public static void main(String[] args) throws InterruptedException {
ThreadGroup group = new ThreadGroup("mygroup");

Thread producer = new Thread(group, () -> {
int i = 0;
try {
while (!Thread.interrupted()) {
synchronized (mutex) {
while (queue.size() == MAX_SIZE) {
mutex.wait();
}
System.out.println("生产者开始生产数据 " + i);
queue.offer(String.valueOf(i));
i++;
mutex.notifyAll();
}
}

} catch (Exception e) {
e.printStackTrace();
}
}, "producer");

Thread consumer = new Thread(group, () -> {
try {
while (!Thread.interrupted()) {
synchronized (mutex) {
while (queue.size() == 0) {
mutex.wait();
}
String poll = queue.poll();
System.out.println("消费者开始消费数据 " + poll);
mutex.notifyAll();
}
}

} catch (Exception e) {
e.printStackTrace();
}
}, "consumer");
producer.start();
TimeUnit.SECONDS.sleep(2);
consumer.start();

TimeUnit.MINUTES.sleep(1);
group.interrupt();

}
}

ThreadLocal

Thread类中有一个threadLocals和一个inheritableThreadLocals,它们都是ThreadLocalMap类型的变量,而 ThreadLocalMap 是一个定制化的Hashmap。 在默认情况下,每个线程中的这两个变量都为 null,只有当前线程第一次调用ThreadLocal的set或者get方法时才会创建它们。

InheritableThreadLocal继承自 ThreadLocal , 其提供了一个特性,就是让子线程可 以访问在父线程中设置的本地变量 。

1
2
3
4
5
6

private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();

public static void main(String[] args) throws InterruptedException {
threadLocal.set("abc");
}

结合源码去看,实际上set是把threadLocal作为key,“abc”作为value,加入到线程的hashMap里面。

ThreadLocal使用不当可能导致内存泄漏

当一个线程调用ThreadLocal的 set 方法设置变量时,当前线程的ThreadLocalMap里就会存放一个记录, 这个记录的 key 为 ThreadLocal 的弱引用, value则为设置的值 。

如果当前线程一直存在且没有调用 ThreadLocal 的 remove 方法并且这时候在其他地方还有对 ThreadLocal 的引用 ,则当前线程的 ThreadLocalMap 变量里面会存在对ThreadLocal 变量的引用和对 value 对象的引用 ,它们是不会被释放的,这就会造成内存泄漏。

如上述代码所示,存在静态的threadLocal强引用,所以不会被释放。

锁概念

乐观锁与悲观锁

乐观锁和悲观锁是在数据库中引入的名词,但是在并发包锁里面也 引入了 类似的思想。

悲观锁指对数据被外界修改持保守态度 ,认为数据很容易就会被其他线程修改 ,所以在数据被处理前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态 。

乐观锁是相对悲观锁来说的,它认为数据在一般情况下不会造成冲 突 ,所以在访问记录前不会加排它锁,而是在进行数据提交更新时,才会正式对数据冲突与否进行检测 。

乐观锁并不会使用数据库提供的锁机制,一般在表 中 添加 version 宇段或者使用业务状态来实现 。乐观锁直到提交时才锁定,所以不会产生任何死锁 。

公平锁与非公平锁

根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁。

  • 公平锁:线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁 。
  • 非公平锁:在运行时闯入,也就是先来不一定先得。

ReentrantLock 提供了公平和非公平锁的实现。
非公平锁下,后来的线程会先去尝试获取一次锁,获取不到才会挂起。

公平锁,后来的线程先判断AQS队列里面是否有在排队的线程,没有的话去获取锁,有的话直接排队挂起。

在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销 。

独占锁和共享锁

读锁、写锁。

自旋锁

自旋锁是:
当前线程在获取锁时,如果发现锁已经被其他线程占有,它不马上阻塞自己,在不放弃 CPU 使用权的情况下,多次尝试获取,很有可能在后面几次尝试中其他线程己经释放了锁。

死锁概念