目录
1.概念
1.1.什么是进程
- 进程就是正在运行的程序,是系统进行资源分配的基本单位
- 目前操作系统都是支持多进程,可以同时执行多个进程,通过进程ID区分
- 单核CPU在同一时刻,只能有一个进程;宏观并行,微观串行
1.2.什么是线程
线程,又称轻量级进程(Light Weight Process)。 进程中的一条执行路径,也是CPU的基本调度单位。 一个进程由一个或多个线程组成,彼此间完成不同的工作, 同时执行,称为多线程。
迅雷是一个进程,当中的多个下载任务即为线程。 | |
Java虚拟机是一个进程,当中默认包含主线程(main),可通过代码创建多个独立线程,与main并发执行。 |
1.3.进程和线程的区别
- 进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位
- 一个程序运行后至少有一个线程
- 一个进程可以包含多个线程,但是至少需要有一个线程,否则这个进程是没有意义
- 进程间不能共享数据段地址,但是同进程的线程之间可以
1.4.线程的组成
- CPU时间片: 操作系统(OS)会为每个线程分配执行时间
- 运行数据:
- 堆空间: 存储线程需要的对象,多个线程可以共享堆中的数据
- 栈空间: 存储线程需使用的局部变量,每个线程都拥有独立的栈
- 线程的逻辑代码
1.5.线程的特点
①线程抢占式执行 :效率高 可防止单一线程长时间独占CPU
② 在单核CPU中,宏观上同时执行,微观上按顺序执行
2.java如何实现多线程
第一种方式: 继承Thread类并重写run方法
①继承Thread ②重写run方法 ③创建线程对象 ④开启线程
代码:
//1.继承Thread类
public class MyThread extends Thread{
//2.重写run方法:线程的执行任务
@Override
public void run(){
for (int i = 0 ; i < 20 ; i++){
System.out.println("i======"+i);
}
}
//==================================================================
//TODO 下方测试代码再创建一个类编写,此处是为方便整理
public static void main(String[] args) {
//3.创建线程对象
MyThread myThread = new MyThread();
//4.开启线程,---当获取cpu时间片,那么该线程就会执行run方法的任务代码
myThread.start();
for (int j = 0 ; j < 20 ; j++){
System.out.println("主方法main====="+j);
}
}
}
1.获取线程名称
第一种: 通过父类Thread中的getName()可以获取线程名称。必须为Thread的子类
第二种: 通过Thread类中的静态方法currentThread获取当前线程,getName()获取线程名。任意处获取线程名。
2.为线程起名
第一种:通过setName()为线程起名
第二种:通过构造函数
案例1:
需求:有四个窗口,它们各卖了100张票
第二种方式:实现Runnable接口
实现Runnable / 重写run方法 / 创建线程对象 /开启线程
记住: 能实现接口的就不要继承父类。因为父类只需要单继承。扩展性比较差
案例2:
需求:有四个窗口,它们共卖了100张票
- 以上会出现多个窗口公卖同一张票,而且也会出现超卖的现象。
- 多线程操作公共数据时出现的线程安全问题。后面再解决。
3.Thread类中常用的方法
(1)休眠:
public static void sleep(long millis) 当前线程主动休眠millis毫秒
(2)放弃:
public static void yield() 当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片
(3)加入:
public final void join() 允许其他线程加入到当前线程中,直到其他线程执行完毕后,当前线程才会执行。
(4)优先级:
线程对象.setPriority() 线程优先级1-10,默认为5,优先级越高,表示获取CPU的概率越高。
优先级有概率,就算设置了也无法决定硬件的CPU
(5)守护线程:
- 线程对象.setDaemon(true);设置为守护线程。
- 线程有两类:用户线程(前台线程)和守护线程(后台线程)
- 如果程序中所有前台线程都执行完毕了,后台线程也会自动结束。
- 垃圾回收线程属于守护线程。
4.多线程安全问题
- 当多线程并发访问临界资源时,如果破坏原子操作,可难会造成数据不一致
- 临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性
- 原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可打乱或缺省
多线程安全问题演示:
需求: A线程将“Hello”存入数组;B数据将“World”存入数组。
public class Test05 {
//创建数组
private static String arr[]=new String[2];
//下标
private static int index=0;
public static void main(String[] args) throws InterruptedException {
//需求: A线程将“Hello”存入数组;B数据将“World”存入数组。
Thread t1 = new Thread(new Runnable() { //匿名内部类
@Override
public void run() {
if (arr[index]==null){
arr[index]="hello";
index++;
}
}
});
Thread t2 = new Thread(new Runnable() { //匿名内部类
@Override
public void run() {
if (arr[index]==null){
arr[index]="world";
index++;
}
}
});
//开启线程
t1.start();
t2.start();
t1.join();
t2.join();
//t1和t2执行完毕才执行下方的打印
System.out.println(Arrays.asList(arr));
}
}
在程序应用中,如何保证线程的安全性?
(1)同步方式
同步代码块:synchronized(临界资源对象){ //对临界资源对象加锁
//代码(原子操作)
}
注意:
- 每个对象都有一个互斥锁标记,用来分配给线程的
- 只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步代码块
- 线程退出同步代码块,会释放相应的互斥锁标记
修改上方演示代码 :加入synchronized
public class Test05 {
//创建数组
private static String arr[]=new String[2];
//下标
private static int index=0;
public static void main(String[] args) throws InterruptedException {
//需求: A线程将“Hello”存入数组;B数据将“World”存入数组。
Thread t1 = new Thread(new Runnable() { //匿名内部类
@Override
public void run() {
//synchronized 同步(互斥锁)
synchronized (arr){ //同步代码块 必须获取()锁资源才能进入同步代码块中
//这里的操作必须原子操作要求
if (arr[index]==null){
arr[index]="hello";
index++;
}
}
}
});
Thread t2 = new Thread(new Runnable() { //匿名内部类
@Override
public void run() {
synchronized (arr){
if (arr[index]==null){
arr[index]="world";
index++;
}
}
}
});
//开启线程
t1.start();
t2.start();
t1.join();
t2.join();
//t1和t2执行完毕才执行下方的打印
System.out.println(Arrays.asList(arr));
}
}
解决案例2的线程安全问题 (this)临界资源对象