多线程相关知识点总结
1. 线程的概念:
在早期的操作系统中并没有线程的概念,进程是拥有资源和独立运行的最小单位,也是程序执行的最小单位。任务调度采用的是时间片轮转的抢占式调度方式,而进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。
后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明了线程,线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。一个标准的线程由线程ID,当前指令指针PC,寄存器和堆栈组成。而进程由内存空间(代码,数据,进程空间,打开的文件)和一个或多个线程组成。
进程与线程的区别
-
线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
-
一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线
-
进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见;
-
调度和切换:线程上下文切换比进程上下文切换要快得多
2. 线程的生命周期:
新建 New — 就绪 Runnable — 运行 Running — 阻塞 Blocked — 死亡 Dead
-
新建 New:就是刚使用new方法,new出来的线程;
-
就绪 Runnable:就是调用的线程的start()方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行;
-
运行 Running:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能;
-
阻塞 Blocked:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep()、wait()之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify或者notifyAll()方法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态;
-
死亡 Dead:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源;
线程进入阻塞状态的原因:
1.等待I/O流的输入输出
2.等待网络资源,即网速问题
3.调用sleep()方法,需要等sleep时间结束
4.调用wait()方法,需要调用notify()唤醒线程
5.其他线程执行join()方法,当前线程则会阻塞,需要等其他线程执行完。
线程进入死亡状态的原因:
1.线程正常完成工作
2.调用stop()方法,强行停止线程
3.外部原因中断线程
3. 实现现线程的三种方式:
1.继承Thread:
自定义线程类继承Thread类
重写run()方法,编写线程执行体
创建线程对象,调用start()方法启动线程;
代码示例:
public class ThreadDemo extends Thread{
@Override
public void run() {
//run方法线程体
for (int i = 0; i < 20; i++) {
System.out.println("run: 我在吃饭====" + i);
}
}
//main
public static void main(String[] args) {
//创建一个线程对象
ThreadDemo testThread = new ThreadDemo();
//调用start()开启线程
testThread.start();
for (int i = 0; i < 50; i++) {
System.out.println("main:我在看电影====" + i);
}
}
}
执行结果:
可以看出,main()方法中的程序和run()方法中的程序交替执行
此时,我们将statr()方法,改成run()方法,再看结果:
可以看出,run()方法执行完之后,main()方法才开始执行:
综上可得出:
run() 和 start() 的区别:
2.实现Runnable:
推荐使用Runnable对象,因为Java单继承的局限性
自定义线程类实现Runnable接口
实现run()方法,编写线程执行体
创建线程对象,调用start()方法启动对象;
代码示例:
public class RunnableDemo implements Runnable{
@Override
public void run() {
//run方法线程体
for (int i = 0; i < 20; i++) {
System.out.println("run: 我在吃饭====" + i);
}
}
public static void main(String[] args) {
//创建一个线程对象
ThreadDemo testThread = new ThreadDemo();
//调用start()开启线程
testThread.start();
for (int i = 0; i < 50; i++) {
System.out.println("main:我在看电影====" + i);
}
}
}
执行结果:
3.实现Callable:
可以定义返回值
可以抛出异常
实现Callable有两种:
1. 使用线程池:
代码示例:
public class CallableDemo implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
//run方法线程体
for (int i = 0; i < 20; i++) {
System.out.println("run: 我在吃饭====" + i);
}
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
CallableDemo callableDemo = new CallableDemo();
//1.创建执行服务
ExecutorService es = Executors.newFixedThreadPool(1);
//2.执行提交
Future<Boolean> r1 = es.submit(callableDemo);
//3.获取结果
Boolean res = r1.get();
System.out.println(res);
//4,关闭服务
es.shutdown();
}
}
执行结果:
2. 使用FutureTask包装:
代码示例:
public class CallableDemo2 implements Callable {
@Override
public Object call() throws Exception {
//run方法线程体
for (int i = 0; i < 20; i++) {
System.out.println("run: 我在吃饭====" + i);
}
return 666;
}
public static void main(String[] args) {
CallableDemo2 testCallable = new CallableDemo2();
//创建多个FutureTask对象,才能多次执行线程
FutureTask futureTask1 = new FutureTask(testCallable);
FutureTask futureTask2 = new FutureTask(testCallable);
new Thread(futureTask1).start();
new Thread(futureTask2).start();
try {
System.out.println(futureTask1.get());
System.out.println(futureTask2.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
执行结果:
剩余待完善。。。
wait()和 sleep()的区别:
线程休眠 线程礼让
join()的使用
线程的优先级
用户线程和守护线程
线程同步(线程锁 synchronized)
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制 synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可,但同时也会存在一些问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起;
- 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换 和 调度延时,引起性能问题;
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引发新能问题;
同步块:sunchronized(Obj){}
Obj称之为 同步监视器
- Obj可以是任何对象,但是推荐使用共享资源作为同步监视器;
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class;
同步监视器的执行过程:
1.第一个线程访问,锁定同步监视器,执行其中的代码;
2.第二个线程访问,发现同步监视器被锁定,无法访问;
3.第一个线程访问完毕,解锁线程同步监视器;
4.第二个线程访问,发现同步监视器没有锁,然后锁定并访问;
CopyOnWriteArrayList线程安全的集合
死锁
概念:
多个线程各自占有一些资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止的情形,叫做死锁。某一个同步块同时拥有"两个以上对象的锁"时,就可能发生"死锁"的问题。
产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用;
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺;
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系;
只要想办法破掉以上的一个或者多个就可以避免死锁的产生!
Lock锁
ReentrantLock 可重入锁
synchronized 与 Lock 的对比:
- Lock是显式锁(需要手动开启和关闭),synchronized是隐式锁,出了作用域自动释放;
- Lock只有代码块锁,synchronized有代码块锁和方法锁;
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类);
- 优先使用顺序:Lock锁 > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)
生产者和消费者问题