进程的概念
操作系统(OS)中并发(同时)执行的多个程序任务
进程的特点
- 宏观并行,微观串行
在一个时间段内,CPU会将时间段划分为若干个时间片,一个时间片是能被一个程序拥有,且只有拥有时间片的程序才能执行自身内容,所以当时间片的划分足够细小,交替频率足够快,就会形成并行的假象,时间上仍然是串行.
线程的概念
- 是进程的基本组成部分
是进程中并发执行的多个任务
线程的特点
- 宏观并行,微观串行
一个时间片只能被一个进程拥有,一个进程一次又只能执行一个线程. 由于进程之间交替执行,所以线程之间必定也是交替执行
多线程
- 只存在多线程,不存在多进程
正在执行中的程序才叫进程,其他的都是等待执行的程序
无论是否拥有时间片,线程任务都叫线程
线程执行的组成
- 时间片
CPU调度分配, 线程争抢拥有 - 数据
堆: 堆共享
栈: 栈独立 - 代码
书写逻辑
线程的创建
- 继承Thread, 重写run方法
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 1; i <=100 ; i++) {
System.out.println(i);
}
}
}
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 1; i <=100 ; i++) {
System.out.println(i);
}
}
}
- 实现Runnable,重写run方法. 在Thread对象的构造中传入任务对象
package com.by.dao.impl;
/**
* 线程任务
*/
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i=1;i<=100;i++) {
System.out.println(i);
}
}
}
package com.by.test;
import com.by.dao.impl.MyRunnable;
import javax.print.attribute.standard.RequestingUserName;
public class Test2 {
public static void main(String[] args) {
/*//先创建任务对象
Runnable r = new MyRunnable();
//将任务对象传入线程对象
Thread t1 = new Thread(r);*/
//任务只会执行一次时,可以通过匿名内部类或者lambda简化书写
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <=100 ; i++) {
System.out.println("t1:: "+i);
}
}
});
Thread t2=new Thread(()->{
for (int i = 101; i <=200 ; i++) {
System.out.println("t2> "+i);
}
});
t1.start();
t2.start();
}
}
更推荐使用第二种创建方式: 更符合类的单一职责,将线程对象的创建与线程任务的书写分离,更有利于后期的维护
使用
- 当开启多个线程之后, 线程之间会争抢时间片,拿到时间片的线程执行自身内容,其他线程无法执行,只能继续尝试争抢时间片,直到线程内容执行结束,才会脱离争夺队列
- 主函数也成为主线程,其一定是首个拥有时间片的线程
- 当开启多个线程之后,JVM执行结束的标志将从主函数执行结束转换为所有线程执行结束
- 开启线程需要调用
线程对象.start()
方法
线程状态
基础状态
等待状态
也称为阻塞状态
- sleep()
Thread.sleep(毫秒数): 使当前线程释放自身时间片, 进入有限期休眠状态,在休眠时间内,该线程无法争抢时间片,休眠结束之后,才可以进入到就绪状态
1秒=1000毫秒
该方法需要处理非运行时异常, run方法不可上抛异常,所以必须通过try-catch处理解决
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
//让当前线程休眠3秒钟
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
System.out.println("休眠异常");
}
for (int i = 1; i <=100 ; i++) {
System.out.println("t1:: "+i);
}
}
});
- join()
线程对象.join(): 使调用者线程在当前线程之前执行, 当前线程只有等调用者线程执行结束进入死亡状态之后才有可能回到就绪状态.
该方法需要处理非运行时异常,run方法无法上抛,必须通过try-catch处理
package com.by.test;
public class Test3 {
public static void main(String[] args) {
//以下代码执行顺序:t1->t2->t3
Thread t1=new Thread(()->{
for (int i = 0; i < 30; i++) {
System.out.println("t1: "+i);
}
});
Thread t2=new Thread(()->{
//使t1线程在t2线程之前执行
try {
t1.join();
} catch (InterruptedException e) {
System.out.println("join失败");
}
for (int i = 101; i < 130; i++) {
System.out.println("t2>"+i);
}
});
Thread t3=new Thread(()->{
try {
t2.join();
} catch (InterruptedException e) {
System.out.println("join失败");
}
for (char i = 65; i <=90; i++) {
System.out.println("t3::"+i);
}
});
t1.start();
t2.start();
t3.start();
}
}
sleep和join的区别?
- sleep方法进入的是有限期等待状态,join方法进入的是无限期等待状态
- sleep是静态方法,可以直接通过类名调用,join是非静态方法,必须通过线程对象调用
线程池
前言: 当一个任务需要多次执行时,如果将任务放置于线程对象Thread中,会浪费内存空间导致不合理的并发,线程池可以解决该问题
作用
管理盛放线程任务, 将需要执行的任务提交执行,任务结束之后池与任务并不会立即销毁,任务对象会回到池中等待下次执行,直到线程池关闭,内部任务才会失效
创建-API
- ExecutorService: 线程池接口
submit(线程任务对象): 提交线程任务使其执行
shutdown(): 关闭线程池 - Executors: 线程池工具类,用来获取线程池对象
newCachedThreadPool(): 获取一个不固定并发数量的线程池对象
newFixedThreadPool(int ):获取一个固定并发数量的线程池对象
不固定并发数量的线程池: 所有提交到池中的任务都会同时并发
固定并发数量的线程池: 对应并发数量的任务先并发执行,超出的任务需要等待执行,等池中执行的任务结束让位之后,超出部分的任务才会进入池中执行
线程任务
- Runnable: run()
无返回值,不能上抛异常
package com.by.test;
import com.by.dao.impl.MyRunnable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test4 {
public static void main(String[] args) {
//获取一个不固定并发数量的线程池
// ExecutorService es1 = Executors.newCachedThreadPool();
ExecutorService es1 = Executors.newFixedThreadPool(2);
Runnable r1=new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("r1>>"+i);
}
}
};
Runnable r2=new Runnable() {
@Override
public void run() {
for (int i = 50; i < 100; i++) {
System.out.println("r2:::"+i);
}
}
};
//提交任务执行
es1.submit(r1);
es1.submit(r2);
es1.submit(r2);
//关闭线程池
es1.shutdown();
}
}
- Callable: call()
有返回值,可以上抛异常,默认上抛Exception
返回值会存放于一个Future对象中,可以通过Future对象.get()
获取内部的返回值
Callable<返回值类型> c1=new Callable<返回值类型>() {
@Override
public 返回值类型 call() throws Exception {
//...
return 值;
}
};
package com.by.test;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Test5 {
public static void main(String[] args) throws Exception {
//创建线程池
ExecutorService es = Executors.newCachedThreadPool();
//线程任务1: 计算1-100的和并返回
Callable<Integer> c1=new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum=0;
for (int i = 0; i < 101; i++) {
sum += i;
}
return sum;
}
};
//提交任务执行并接收
Future<Integer> f =es.submit(c1);
System.out.println(f.get());
System.out.println(es.submit(c1).get());
//关闭线程池
es.shutdown();
}
}
线程安全问题
当多个线程同时访问同一个临界资源时,原子操作可能被破坏,会导致数据丢失, 就会触发线程安全问题
- 临界资源: 被多个线程同时访问的对象
- 原子操作: 线程访问临界资源的过程中不可更改和缺失的操作
互斥锁
- 每个对象都默认拥有互斥锁, 该锁默认不开启.
- 当开启互斥锁之后,线程想要访问对象,则在需要拥有时间片的基础上也拥有锁标记,锁标记只能被一个线程拥有,拥有时间片和锁标记的线程才能执行自身内容,在此期间,其他线程只能等正在执行的线程执行结束释放锁标记和时间片之后才能进入就绪状态
- synchronized: 开启互斥锁的关键字
同步方法
思路: 在被线程同时访问的方法上加锁
访问修饰符 synchronized 返回值类型 方法名(参数列表){
}
package com.by.util;
import java.util.ArrayList;
import java.util.List;
/**
* 工具类-操作集合属性
*/
public class MyList {
private List<Integer> list = new ArrayList<>();
/**
* 给集合属性添加元素
* @param n 添加的元素值
synchronized: 同步方法
*/
public synchronized void insert(int n){
list.add(n);
}
/**
* 查看集合内容
*/
public void query(){
System.out.println("集合长度: " + list.size());
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i)+" ");
}
}
}
同步代码块
思路: 让参与临界资源对象访问的线程自身加锁
synchronized(临界资源对象){
//需要被认定为原子操作的代码
}
使用: 所有访问同一临界资源的线程都需要同时添加同步代码块
package com.by.test2;
import com.by.util.MyList;
public class TestMyList {
public static void main(String[] args)throws Exception {
//创建两个线程,同时操作工具类,线程1负责往集合中添加元素1-5,线程2负责往集合中添加元素6-10
//添加结束之后查看集合内容
//创建工具类对象
MyList m = new MyList();
Thread t1=new Thread(()->{
for (int i = 1; i <=5 ; i++) {
synchronized (m) {
m.insert(i);
}
}
});
Thread t2=new Thread(()->{
for (int i = 6; i <=10 ; i++) {
synchronized (m) {
m.insert(i);
}
}
});
t1.start();
t2.start();
//使t1和t2线程先进行添加操作
t1.join();
t2.join();
//查看集合元素
m.query();
}
}
/*
* 张三上厕所
* 李四上厕所
*
* 原子操作: 脱裤子-->蹲下来-->上厕所-->擦屁股-->穿裤子-->冲水-->走人
*
*临界资源: 厕所-坑位
*
*解决方式1:给厕所大门加锁
*解决方式2:自己给坑位加锁
*
*
* */
区别
- 同步方法: 线程执行需要同时争抢时间片和锁标记,写法简单但效率较慢
- 同步代码块: 线程只需要争抢时间片, 开启互斥锁的线程默认拥有锁标记, 效率较快但写法相对繁琐
线程安全的集合类
悲观锁: 悲观的认为集合一定会出现线程安全问题,所以直接加锁
乐观锁: 乐观的认为集合一定不会出现线程安全问题,如果安全问题发生,再利用算法解决问题(无锁机制)
JDK5.0,发布了一批无锁机制的线程安全的集合类
都来自于java.util.concurrent包
- ConcurrentHashMap: CAS算法
compare and swap: 比较并交换
原有值,预期值,结果值: 当原有值与预期值相等时才会将结果值放入内存
int i=1;
i++;
原有值: 1 预期值: 1 结果值:2
- CopyOnWriteArrayList:
当集合进行写(增删改)操作时,会先复制出一个副本,在副本中进行写操作,如果过程中出现线程安全问题,则舍弃当前副本,重新复制新的副本重复操作,直至副本中无异常,再将集合引用地址转换向副本地址,一次确保原集合中一定不会发生安全问题
特点: 舍弃写的效率提高读的效率,适用于读操作远多于写操作时 - CopyOnWriteArraySet:
原理与CopyOnWriteArrayList一致, 在写时会对元素进行去重