线程

  1. 1. 进程与线程的关系
  2. 2. 线程的基本状态
  3. 3. 线程创建方式
  4. 4. 线程同步
  5. 5. volatile和synchronized的区别
  6. 6. synchronized与lock的异同
  7. 7. sleep() 和 yield() 的区别
  8. 8. sleep() 和 wait() 有的区别
  9. 9. 线程池

进程与线程的关系

  1. 进程是操作系统资源分配的基本单位
  2. 线程是cpu调度和分配的基本单位
  3. 一个进程可以有多个线程
  4. 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
  5. 线程的划分尺度小于进程,使得多线程程序的并发性高

线程的基本状态

  1. 新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
  2. 就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
  3. 运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
  4. 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
  • 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
  • 同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
  • 其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
  1. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

线程创建方式

  1. 继承Thread类

​ 1)创建一个线程子类继承Thread类

​ 2)重写run() 方法,把需要线程执行的程序放入run方法,线程启动后方法里的程序就会运行

​ 3)创建该类的实例,并调用对象的start()方法启动线程

1
2
3
4
5
6
7
8
9
10
11
12
public class ThreadDemo extends Thread {
@Override
public void run() {
super.run();
System.out.println("需要运行的程序。。。。。。。。");
}

public static void main(String[] args) {
Thread thread = new ThreadDemo();
thread.start();
}
}
  1. 实现Runnable接口

​ 1)定义一个线程类实现Runnable接口,并重写该接口的run()方法,方法中依然是包含指定执行的程序

​ 2)创建一个Runnable实现类实例,将其作为target参数传入,并创建Thread类实例

​ 3)调用Thread类实例的start()方法启动线程

1
2
3
4
5
6
7
8
9
10
11
12
public class RunnableDemo implements Runnable{
@Override
public void run() {
System.out.println("我是Runnable接口......");
}

public static void main(String[] args) {
RunnableDemo demo = new RunnableDemo();
Thread thread = new Thread(demo);
thread.start();
}
}
  1. 使用Callable和Future创建线程

​ 1)创建Callable接口的实现类,实现call() 方法

​ 2)创建Callable实现类实例,通过FutureTask类来包装Callable对象,该对象封装了Callable对象的call()方法的返回值

​ 3)将创建的FutureTask对象作为target参数传入,创建Thread线程实例并启动新线程

​ 4)调用FutureTask对象的get方法获取返回值。

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
class ThreadUtil implements Callable {

private StudentService studentService;

public ThreadUtil(StudentService studentService) {
this.studentService = studentService;
}

@Override
public List<Student> call() throws Exception {
List<Student> students = studentService.allStudentsList();
return students;
}
}

public class ThreadTest {

private StudentService studentService = new StudentService();

public void testThread1() {
// 1.获取FutureTask对象
ThreadUtil threadUtil = new ThreadUtil(studentService);
FutureTask futureTask = new FutureTask(threadUtil);

// 2.开启线程
new Thread(futureTask).start();

try {
// 3.使用Futura#get()方法获取线程的返回值
List<Student> studentList = (List<Student>) futureTask.get();
studentList.forEach(student -> System.out.println(student));
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}

线程同步

  1. wait():使一个线程处于等待状态,并且释放所持有的对象的lock。
  2. sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。
  3. notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
  4. notifyAll():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。
  5. yield():暂停当前线程,以便其他线程有机会执行,不过不能指定暂停的时间,并且也不能保证当前线程马上停止。yield方法只是将Running状态转变为Runnable状态。
  6. join():父线程等待子线程执行完成后再执行,换句话说就是将异步执行的线程合并为同步的线程。
  7. synchronized:Java中的关键字,是一种同步锁。用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
  8. volatile:保证了变量的可见性(visibility)。被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见,避免出现脏读的现象
  9. lock:加锁限定线程间的互斥,保持线程同步实现线程安全

Java 并发编程:线程间的协作(wait/notify/sleep/yield/join)

volatile和synchronized的区别

  1. volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住

  2. volatile仅能使用在变量级别,synchronized则可以使用在变量、方法

  3. volatile仅能实现变量的修改可见性,而synchronized则可以保证变量的修改可见性和原子性

  4. volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞

  5. 当一个域的值依赖于它之前的值时,volatile就无法工作了,如n=n+1,n++等。如果某个域的值受到其他域的值的限制,那么volatile也无法工作,如Range类的lower和upper边界,必须遵循lower<=upper的限制。

  6. 使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变的域。

synchronized与lock的异同

  1. Lock能完成synchronized所实现的所有功能
  2. Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放,否则会引起死锁。
  3. synchronized既可以加在方法上,也可以加载特定代码块上,而lock需要显示地指定起始位置和终止位置。
  4. synchronized是托管给JVM执行的,lock的锁定是通过代码实现的,它有比synchronized更精确的线程语义。
  5. lock接口的实现类ReentrantLock,不仅具有和synchronized相同的并发性和内存语义,还多了超时的获取锁、定时锁、等候和中断锁等。
  6. 在竞争不是很激烈的情况下,synchronized的性能优于ReentrantLock,竞争激烈的情况下synchronized的性能会下降的非常快,而ReentrantLock则基本不变。

sleep() 和 yield() 的区别

  1. sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
  2. 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;
  3. sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;
  4. sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。

sleep() 和 wait() 有的区别

  1. sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,把执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。
  2. wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。

线程池

  1. 定义

​ 1)线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。

  1. 优点

​ 1)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

​ 2)提高响应速度。当任务到达时,任务可以不需要等到线程创建就能执行。

​ 3)提高线程的可管理性,线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

  1. 分类

​ 1)newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

​ 2)newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

​ 3)newScheduledThreadPool:创建一个可定期或者延时执行任务的定长线程池,支持定时及周期性任务执行。

​ 4)newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

  1. 状态

​ 1)Running:能接受新任务以及处理已添加的任务

​ 2)Shutdown:不接受新任务,可以处理已添加的任务

​ 3)Stop:不接受新任务,不处理已添加的任务,并且中断正在处理的任务

​ 4)Tidying:所有任务已终止,ctl记录为”任务数量”为0,ctl负责记录线程池的运行状态与活动线程数量

​ 5)Terminated:线程池彻底终止