1. 进程和线程
1.1 串行和并行
串行:指多个任务时,各个任务按顺序执行,完成一个之后才能进行下一个。
并行:指多个任务可以同时执行。异步是多个任务并行的前提。
1.2 并发和并行
并发:在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。其中两种并发关系分别是同步和互斥,
- 互斥:进程间相互排斥的使用临界资源的现象,就叫互斥。
- 同步:进程之间的关系不是相互排斥临界资源的关系,而是相互依赖的关系。进一步的说明:就是前一个进程的输出作为后一个进程的输入,当第一个进程没有输出时第二个进程必须等待。具有同步关系的一组并发进程相互发送的信息称为消息或事件。
- 异步:异步和同步是相对的,同步就是顺序执行,执行完一个再执行下一个,需要等待、协调运行。异步就是彼此独立,在等待某事件的过程中继续做自己的事,不需要等待这一事件完成后再工作。线程就是实现异步的一个方式。
并行:在操作系统中,一组程序按独立异步的速度执行,无论从微观还是宏观,程序都是一起执行的。
来个比喻:并发和并行的区别就是一个人同时吃三个馒头和三个人同时吃三个馒头;
1.3 进程和线程
进程:具有独立的执行环境和一套完整的私有基本运行时资源,每个进程都有自己的存储空间。
线程:有时称为轻量级进程,创建新线程所需的资源少于创建新进程的资源。
进程与线程的区别:
- 进程是资源分配最小单位,线程是程序执行的最小单位;
- 进程有自己独立的地址空间,每启动一个进程,系统都会为其分配地址空间,建立数据表来维护代码段、堆栈段和数据段,线程没有独立的地址空间,它使用相同的地址空间共享数据;
- CPU切换一个线程比切换进程花费小;
- 创建一个线程比进程开销小;
- 线程占用的资源要⽐进程少很多;
- 线程之间通信更方便,同一个进程下,线程共享全局变量,静态变量等数据,进程之间的通信需要以通信的方式(IPC)进行;(但多线程程序处理好同步与互斥是个难点);
- 多进程程序更安全,生命力更强,一个进程死掉不会对另一个进程造成影响(源于有独立的地址空间),多线程程序更不易维护,一个线程死掉,整个进程就死掉了(因为共享地址空间);
- 进程对资源保护要求高,开销大,效率相对较低,线程资源保护要求不高,但开销小,效率高,可频繁切换。
线程状态如下所示:
1.4 线程的五种状态
(1)新建( new ):新创建了一个线程对象。
(2)可运行( runnable ):线程对象创建后,其他线程(比如 main 线程)调用了该对象的 start ()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取 cpu 的使用权 。
(3)运行( running ):可运行状态( runnable )的线程获得了 cpu 时间片( timeslice ) , 执行程序代码。
(4)阻塞( block ):阻塞状态是指线程因为某种原因放弃了 cpu 使用权,如当调用sleep()、wait()方法或同步锁时,线程进入阻塞状态,不再往下执行。阻塞事件解除后,线程重新进入可运行( runnable )状态,才有机会再次获得 cpu 使用权转到运行( running )状态。
(5)死亡( dead ):线程 run ()、 main () 方法执行结束,或者因异常退出了 run ()方法,则 该线程结束生命周期。死亡的线程不可再次复生。
1.5 线程池
线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建, 使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
2. 线程对象
创建线程的三种方式:继承Thread类,实现Runnable接口,实现Callable接口
2.1 线程创建方式一:继承Thread类
步骤:
(1)自定义线程类继承Thread类
(2)重写run()方法,编写线程执行体
(3)创建线程对象,调用start()方法启动线程
//线程的创建方式一:继承Thread类
public class TestThread1 extends Thread {
//重写run方法(ctrl+o)
@Override
public void run() {
//run方法线程体
for (int i = 0; i < 10; i++) {
System.out.println("我在执行线程---"+i);
}
}
public static void main(String[] args) {
//创建线程对象
TestThread1 testThread1 = new TestThread1();
//调用start()方法启动线程
testThread1.start();
}
}
2.2 线程创建方法二:实现Runnable接口
步骤:
(1)自定义一个类实现Runnable接口
(2)重写run()方法,编写线程执行体
(3)创建线程对象,调用start()方法启动线程
public class TestThread2 implements Runnable {
//重写run方法(ctrl+o)
@Override
public void run() {
//run方法线程体
for (int i = 0; i < 10; i++) {
System.out.println("我在执行线程---"+i);
}
}
public static void main(String[] args) {
//创建实现类对象
TestThread2 thread2 = new TestThread2();
//创建代理类对象(需要丢入实现类对象)
Thread thread = new Thread(thread2);
//调用start()方法启动线程
thread.start();
}
}
注意:实现 Runnable 接口这种方式更受欢迎,因为这不需要继承 Thread 类。在应用设计中已经继承了 别的对象的情况下,这需要多继承(而 Java 不支持多继承),只能实现接口。
- 多线程有两种实现方法,分别是继承Thread类与实现Runnable接口;两者在启动线程时不同,继承Thread类是子类对象.start(),而实现Runnable接口是传入目标对象+Thread对象.start()
- 同步的实现方面有两种, 分别是 synchronized, wait 与 notify。
2.3 线程的命名
(1)Thread thread = new Thread(helloRunnable,“我是子线程1”);
案例:
(2)Thread thread = new Thread(helloRunnable);
thread.setName(“我是子线程1”);
案例:
2.4 线程优先级
线程优先级的范围是1~10,默认的优先级是5,最高级是10。“高优先级线程”被分配CPU的概率高于“低优先级线程”。
//线程的优先级用数字表示
Thread.MIN_PRIORITY=1;
Thread.MAX_PRIORITY=10;
Thread.NORM_PRIORITY=5;
//获取优先级
getPriority();
//设置优先级大小
setPriority(int xxx);
实例:
2.5 线程休眠:sleep方法
Thread.sleep() 使当前线程在指定时间段内暂停执行。
实例:
2.6 线程礼让:yield方法
Thread.yield() 给当前正处于运行状态下的线程一个提醒,告知它可以将资源礼让给其他线程,但这仅仅是一种暗示,没有任何一种机制保证当前线程会将资源礼让。
实例:
sleep()和yield()方法的区别:
-
sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;
-
yield()方法只会给相同优先级或更高优先级的线程以运行的机会。
2.7 线程联合:join方法
join方法允许一个线程等待另一个线程的完成。如果t是Thread正在执行其线程对象,t.join()导致当前线程暂停执行,直到 t 的线程终止。
类似于插队,如下列vip线程插队到主线程
package Thread;
public class TestJoin implements Runnable {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("我是vip线程--->"+i);
}
}
public static void main(String[] args) throws InterruptedException {
TestJoin testJoin=new TestJoin();
Thread thread = new Thread(testJoin);
thread.start();
//主线程
for (int i = 0; i < 1000; i++) {
if(i==100){
thread.join(); //vip线程插队到主线程
}
System.out.println("我是主线程--->"+i);
}
}
}
运行结果:
2.8 线程停止:stop方法
停止一个线程意味着在线程处理任务完成之前停掉正在做的操作,也就是放弃当前操作。
推荐使用退出标识,使得线程正常退出,即当run方法完成后进程终止。
实例:
2.9 守护线程
- Java线程分为用户线程和守护线程两种;
- 用户线程是系统的工作线程,它会完成这个程序要完成的业务员操作;
- 守护线程是一种特殊的线程,是系统的守护者,在后台默默完成一些系统性的服务,比如垃圾回收线程。
如果用户线程全部结束,则意味着这个程序无事可做。守护线程要守护的对象已经不存在了,那么整个应用程序就结束了。
此处实例:龟兔赛跑
3. 线程同步
在多线程程序中,会出现多个线程抢占一个资源的情况,即出现并发现象,这时间有可能会造成冲突,也就是一个线程可能还没来得及将更改的资源保存,另一个线程的更改就开始了。可能造成数据不一致。因此引入多线程同步,也就是说多个线程只能一个对共享的资源进行更改,其他线程不能对数据进行修改。
3.1 线程冲突
当在不同线程中运行作用于相同数据的两个操作时,就会发生干扰,这意味着这两个操作由多个步骤组成,并且步骤顺序重叠。
- 检索的当前值c
- 将检索到的值加1
- 将增加的值存储回c
此处实例:售票员们卖100张票
3.2 同步方法与同步代码块
synchronized用于解决同步问题,当有多条线程同时访问共享数据时,如果进行同步,就会发生错误,Java提供的解决方案是:只要将操作共享数据的语句在某一时段让一个线程执行完,在执行过程中,其他线程不能进来执行可以。解决这个问题。这里在用synchronized时会有两种方式,一种是上面的同步方法,即用synchronized来修饰方法,另一种是提供的同步代码块。
//同步方法
public synchronized void method1(){}
//同步代码块
public void method2(){
synchronized (Obj){} //其中Obj称为同步监视器,可以是任何对象
}
常见面试题:synchronized 和 java.util.concurrent.locks.Lock 的异同
- 主要相同点:Lock 能完成 synchronized 所实现的所有功能。
- 主要不同点:
(1)synchronized 是托管给 JVM 执行的,lock 的锁定是通过代码实现的,它有比 synchronized 更精确的线程语义。
(2)synchronized 会自动释放锁,而 Lock 一定要求程序员手工释放 ,并且必须在 finally 语句中释放。
(3)synchronized 既可以加在方法上,也可以加载特定代码块上,而Lock 需要显示地指定起始位置和终止位置。
4. 线程死锁
-
死锁是指多个线程各自占有一些共享资源,并且相互等待其他线程占有的资源才能运行,导致线程出现死锁。
-
死锁不仅仅在线程之间会发生,存在资源独占的进程之间同样也可能出现死锁。通常来说,我们大多是聚焦在多线程场景中的死锁,指两个或多个线程之间,由于互相持有对方需要的锁,互相等待,而永久处于阻塞状态。
-
使用多线程的时候,一种非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了。
5. 线程协调
-
wait()方法和notify()方法;
-
发生死锁的条件
- 互斥条件
- 请求保持条件
- 不剥夺条件
- 循环等待条件
public class ThreadCoordinate {
public static void main(String[] args) {
// 实例化线程1
DeadLockA deadLockA = new DeadLockA();
Thread threadA = new Thread(deadLockA, "线程1");
// 实例化线程2
DeadLockB deadLockB = new DeadLockB();
Thread threadB = new Thread(deadLockB, "线程2");
threadA.start();
threadB.start();
}
}
/**
* 线程A
*/
class DeadLockA implements Runnable {
@Override
public void run() {
synchronized ("A") {
System.out.println("线程1获得了A锁");
try {
"A".wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized ("B") {
System.out.println("线程1获得了A锁和B锁");
}
}
}
}
/**
* 线程B
*/
class DeadLockB implements Runnable {
@Override
public void run() {
synchronized ("B") {
System.out.println("线程2获得了B锁");
synchronized ("A") {
System.out.println("线程2获得了B锁和A锁");
"A".notifyAll();
}
}
}
}
懒汉式单例模式:
- 在使用这个对象时,才去查看这个对象是否创建。如果没创建就马上创建;如果已经创建,就返回这个实例。
- 线程不安全,需要加上同步锁,影响了程序执行效率。
饿汉式单例模式:
- 在加载这个类的时候,就先创建好一个对象实例,等待调用。
- 天生线程安全,类加载的时候初始化一次对象,效率比懒汉式高
6. 高级并发对象
6.1 线程定义
- 实现Callable接口
前面两种线程定义方式都有这两个问题:
- 无法获取子线程的返回值
- run方法不可以抛出异常
为了解决这两个问题,我们就需要用到Callable这个接口了。
6.2 线程同步:锁对象
同步代码依赖于一种简单的可重入锁。这种锁易于使用,但有很多限制。java.util.concurrent.locks软件包支持更复杂的锁定习惯用法 。Lock对象的工作方式非常类似于同步代码所使用的隐式锁。与隐式锁一样,一次只能有一个线程拥有一个Lock对象。Lock对象还wait/notify通过其关联的Condition对象支持一种机制 。
- reentrantLock.lock()
- reentrantLock.unlock()
6.3 线程池
线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建, 使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
Java中创建和销毁一个线程是比较昂贵的操作,需要系统调用。频繁创建和销毁线程会影响系统性能。Java 通过 Executors 提供四种线程池,分别为:
- newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
- newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
- newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
- newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务, 保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPool implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.execute(new ThreadPool());
executorService.execute(new ThreadPool());
executorService.execute(new ThreadPool());
executorService.shutdown();
}
}
6.4 并发集合:BLockingQueue
BlockingQueue实现被设计为主要用于生产者-消费者队列,如果BlockingQueue是空的,从BlockingQueue取东西的操作将会被阻断进入等待状态,直到BlockingQueue进了东西才会被唤醒。同样,如果BlockingQueue是满的,任何试图往里存东西的操作也会被阻断进入等待状态,直到BlockingQueue里有空间时才会被唤醒。
import java.util.concurrent.BlockingQueue;
public class Consumer implements Runnable {
BlockingQueue<Product> blockingQueue;
public Consumer(BlockingQueue<Product> blockingQueue) {
this.blockingQueue = blockingQueue;
}
@Override
public void run() {
try {
while (true) {
Product product = blockingQueue.take();
System.out.println("消费产品: " + product.getName());
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
6.5 静态代理
真实对象和代理对象都要实现同一个接口,代理对象要代理真实角色。
好处:代理对象可以做很多真实对象做不了的事情,真实对象只专注做自己的事情,如婚庆公司和结婚人之间的关系
6.6 Lambda表达式
- Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。
- Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。
- 使用 Lambda 表达式可以使代码变的更加简洁紧凑。
Lambda表达式的重要特征:
- 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
- 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
- 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
- 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。
Lambda 表达式的简单例子:
// 1. 不需要参数,返回值为 5
() -> 5
// 2. 接收一个参数(数字类型),返回其2倍的值
x -> 2 * x
// 3. 接受2个参数(数字),并返回他们的差值
(x, y) -> x – y
// 4. 接收2个int型整数,返回他们的和
(int x, int y) -> x + y
// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)
(String s) -> System.out.print(s)