一、知识点
线程。
二、目标
-
理解进程和线程。
-
掌握创建多线程的方式。
-
理解线程的生命周期。
-
掌握死锁。
三、内容分析
-
重点
-
多线程的创建方式。
-
线程的生命周期。
-
死锁的形成条件。
-
-
难点
-
多线程的的理解。
-
死锁。
-
四、内容
1、线程
1.1 什么是进程
进程(Process):进程是计算机中的程序关于某数据集合上的一次运行活动,是操作系统进行资源分配与调度的基本单位。
可以简单理解为:进程是正在操作系统中运行的一个程序。
1.2 什么是线程
线程(thread):线程是进程的一个执行单元。
一个线程就是进程中一个单一顺序的控制流,是进程的一个分支。
进程与线程之间的关系:
进程是线程的容器,一个进程至少有一个线程,一个进程也可以有多个线程。
在操作系统中是以进程为单位分配内存,同一个进程的所有线程共享该进程所有资源。
如果把一个进程看作是一个工厂,线程就是工厂中的若干流水线,若干流水线共享工厂的某些资源。
1.3 创建线程的方式
1.3.1 继承Thread类
继承Thread类,重写run方法。使用start()方法开启线程,调用run()方法和调用普通方法没有区别。
// 没有使用多线程,分别打印两次1~20
for(int i = 1; i <= 20; i++) {
System.out.println(i);
}
for(int i = 1; i <= 20; i++) {
System.out.println(i);
}
// 使用多线程,两个for循环交替打印
public class MyThread extends Thread {
@Override
public void run() {
for(int i = 1; i <= 20; i++) {
System.out.println(Thread.currentThread().getName() + " : " + i);
}
}
public static void main(String[] args) {
Thread thread1 = new MyThread();
Thread thread2 = new MyThread();
thread1.start();
thread2.start();
}
}
对比效果:当我们没有使用多线程执行两个for循环,结果得按顺序一个for循环执行结束,开始另一个for循环。
当我们使用多线程执行两个for循环,两个for循环同时执行。
1.3.2 实现Runable接口
实现Runable接口,重写run方法。
public class MyThread implements Runnable {
@Override
public void run() {
for(int i = 1; i <= 20; i++) {
System.out.println(Thread.currentThread().getName() + " : " + i);
}
}
public static void main(String[] args) {
Thread thread1 = new Thread(new MyThread());
Thread thread2 = new Thread(new MyThread());
thread1.start();
thread2.start();
}
}
1.3.3 实现Callable接口
实现Callable接口,使用线程池创建多个线程。
public class MyThread implements Callable {
@Override
public Object call() throws Exception {
for(int i = 1; i <= 20; i++) {
System.out.println(Thread.currentThread().getName() + " : " + i);
}
return null;
}
public static void main(String[] args) {
//创建线程
MyThread myThread = new MyThread();
//ExecutorService类作用是管理线程
//Executors 是一个工具类,线程池工厂,用来创建不同类型的线程池对象
ExecutorService pool = Executors.newFixedThreadPool(2);
//submit方法:将执行的任务添加到线程池中
pool.submit(thread);
pool.submit(thread);
//关闭线程池服务
pool.shutdown();
}
}
1.4 线程原理
线程的并发执行是通过多个线程不断的切换CPU资源,这个速度非常快,我们感知不到,我们能感知的就是几个线程并发在执行。
1.5 线程生命周期
-
新建:线程被new出来。
-
就绪:线程具有执行的资格,即线程调用了start(),没有执行的权利,就是调用的线程的start()方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行。
-
运行:具备执行的资格和执行的权利,当就绪的线程被调用并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能。
-
阻塞:没有执行的资格和执行的权利,在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep()、wait()之后,线程就处于阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify或者notifyAll方法。唤醒的线程不会立刻执行run方法,它们需要再次等待CPU分配资源进入运行状态。
-
销毁/死亡:线程释放资源,如果线程正常执行完毕后或线程被提前强制性的终止或者出现异常导致结束,那么线程就要被销毁,释放资源 。
1.6 synchronized
使用多线程提高了效率,但是也带来一个问题,当多个线程共享资源的时候,会出现线程不安全问题。比如几个窗口同时卖100张电影票,这100张电影票是共享的,会造成两个窗口同时读取到同一张票的可能。
synchronized关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
-
存在线程安全问题的写法
// 存在线程安全问题的写法 public class SellTicket extends Thread { // 使用static保存100张票 private static int tickets = 100; @Override public void run() { while (true) { if(tickets <= 0) { break; } System.out.println(Thread.currentThread().getName() + "卖出了第几" + tickets-- + "号票"); try { Thread.sleep(100); // 模拟延时,买票是需要时间的。 } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { Thread thread1 = new SellTicket(); Thread thread2 = new SellTicket(); // 同时开启两个窗口买票,发现结果有重复的票号 thread1.start(); thread2.start(); } }
-
使用synchronized修饰方法体
public class SellTicket extends Thread { private static int tickets = 100; // 创建锁 private static Object object = new Object(); @Override public void run() { while (true) { synchronized (object) { if(tickets <= 0) { break; } System.out.println(Thread.currentThread().getName() + "卖出了第几" + tickets-- + "号票"); try { Thread.sleep(100); // 模拟延时,买票是需要时间的。 } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) { Thread thread1 = new SellTicket(); Thread thread2 = new SellTicket(); thread1.start(); thread2.start(); } }
-
使用synchronized修饰方法
public class SellTicket extends Thread { private static int tickets = 100; @Override public void run() { while (true) { if(sell()) { break; } } } public synchronized static boolean sell() { boolean sign = false; if(tickets > 0) { System.out.println(Thread.currentThread().getName() + "卖出了第几" + tickets-- + "号票"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } else { sign = true; } return sign; } public static void main(String[] args) { Thread thread1 = new SellTicket(); Thread thread2 = new SellTicket(); thread1.start(); thread2.start(); } }
1.7 死锁
1.7.1 定义
当两个或两个以上的线程因竞争相同资源而处于无限期的等待,这样就导致了多个线程的阻塞,出现程序无法正常运行和终止的情况。
假设场景:有一个羽毛球场和一个网球场,小明占用了羽毛球场,小红占用了网球场。小明在打羽毛球的过程中就想如果网球场空闲了,他就去打网球;同时小红也在想,如果羽毛球场空闲了,她就去打羽毛球。但是他们去另一个球场的前提是另一个球场空闲了出来,这时候他们保持着占用现有资源,然后又想获取对象的占用资源,这时候就造成死锁了。
public static void main(String[] args) {
String str1 = "羽毛球场";
String str2 = "网球场";
Thread thread1 = new Thread(){
@Override
public void run() {
synchronized (str1) {
System.out.println("小明占用了" + str1);
// 小明占用羽毛球场的同时,还想去尝试获取网球场
synchronized (str2) {
System.out.println("小明占用了" + str2);
}
}
}
};
Thread thread2 = new Thread(){
@Override
public void run() {
synchronized (str2) {
System.out.println("小红占用了" + str2);
// 小红占用网球场的同时,还想去尝试获取羽毛球场
synchronized (str1) {
System.out.println("小红占用了" + str1);
}
}
}
};
thread1.start();
thread2.start();
}
运行结果:程序一直处于运行状态,进入了死锁,卡住了。
1.7.2 产生死锁的必要条件
-
互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用。即一个资源每次只能被一个进程使用。
-
不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
-
请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
-
循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
1.7.3 修复死锁
-
预防死锁:通过设置某些限制条件,去破坏死锁的四个必要条件中的一个或者几个,来进行预防。
-
避免多锁:尽量避免使用多个锁,并且只有需要时才持有锁。否则嵌套的synchronized非常容易出现问题。
-
设计锁的顺序:如果能确保所有的线程都是按照相同的顺序获取锁,那就不会出现锁问题。