多线程编程
本文系统介绍了Java多线程编程的核心概念与实践方法。主要内容包括:1.进程与线程的基本概念及区别,进程是资源分配单位,线程是执行单位;2.线程创建的5种方式(继承Thread类、实现Runnable/Callable接口等)及线程状态管理;3.线程安全问题产生原因(抢占执行、非原子操作等)及解决方案(volatile、synchronized);4.线程间通信机制(wait/notify)和经典
什么是进程
进程是程序运行时的一个实例,包含了正在运行的程序代码和相关数据等。可以 把进程看做程序的一次运行过程。在操作系统内部,进程为操作系统资源分配最小单位。
进程是系统的软件管理资源,也叫任务,可以在windows任务管理器上看到。
什么是线程
包含在进程中,是进程中的实际执行单位,是操作系统的最小运行单位,独立在cpu上调度执行。一个线程就是一个执行流.一个进程中至少一个线程.
PCB(进程控制块):是操作系统用于管理和跟踪进程的核心数据结构,包含进程ID、指令信息、上下文以及各种标识符等关键信息 .
进程是以链表形式组织的一组PCB,线程是一个PCB;
为什么创建线程
1. 单核CPU算力遇到瓶颈,需要使用多核CPU提高算力。并发编程可以充分利用CPU资源,所以并发编程成为刚需;
现代计算机通常配备有多核CPU,意味着能够并行执行多个指令流。创建多个线程,可以使不同的线程在不同的核心上并行运行,充分利用CPU加快处理速度。
2.每次创建进程,都涉及到资源的分配和调度,频繁的创建销毁对系统的开销很大(时间,空间)。在进程中创建线程,让线程共享进程的系统资源(硬盘,内存,网络带宽等),又能相对独立地执行各自的指令序列,省去了频繁申请资源和释放资源的开销;
并发
并行:cpu 的多个核心同时运行,每个核心都可以执行一个线程, 微观时间上同时运行
并发:一个核心根据时间片不断切换执行线程,宏观时间上同时执行,微观上串行执行
以上两种方式统称并发执行,因为在实际开发中不需要区分。
抢占式执行
当线程数量大于cpu核心数量时,就会抢占进程的cpu资源,在微观上无法满足所有线程的同时执行,此时线程之间会产生竞争,抢占资源的过程会影响效率。操作系统内核的调度器对线程执行的调度,可以近似看成“随机”的过程。
进程和线程的区别
1.进程包含线程,每个进程都至少有一个线程,就是主线程;
2.进程之间不共享系统资源,同一个进程里的线程共享资源(内存 ,硬盘,网络带宽等资源,内存资源就是存储的对象变量等);
3.进程是操作系统中资源分配的最小单位,线程是系统调度执行的最小单位;
4.一个进程挂了一般不会影响到其他进程执行(称为隔离性),但一个线程挂了,可能会让整个进程里的线程都崩溃,因为所有线程共享进程里的资源 ,如果一个线程把所有资源都占用了,那其他线程也就崩了。
Thread类
线程构造方法
Thread (); //创建 线程对象
Thread (String name ); //创建并命名
Thread(Runnable target); //用Runnable 对象创建
Thread(Runnable target,String name);
创建多线程的方式
1.继承Thread类
1.实现Thread里的抽象方法run
2.创建Thread对象
3.调用start()
public class thread {
static class MyThread extends Thread {//自定义内部类继承Thread类,就是为了重写run()
@Override
public void run() { //run()方法记录线程要执行的任务
for (int i = 0; i < 4; i++) {
System.out.println("t hello");
}
}
}
public static void main(String[] args) { //每个进程都至少有一个线程,主线程main
Thread t = new MyThread();//再创建一个线程的对象
t.start(); //调用start才在进程内部真正创建t线程,线程在内部自动调用run()方法执行任务
//执行完run里的任务,t线程结束被销毁
System.out.println("main hh"); //打印完这个语句main线程结束
//run()是回调函数,作为参数被传递,被动调用
}
}
控制台打印结果
2.实现Runnable接口
实现Runnable 接口和继承Thread类都要重写里面的抽象方法run。但这种方法的好处是解耦合,将线程任务的制定和线程的启动过程分开。
Runnable接口代表一个可以由线程执行的任务。它只有一个方法run()。Runnable专注于定义“做什么”,而管理线程的生命周期(如启动、运行、停止等),可以是Thread类也可以交给线程池等。
避免单继承限制:Java不支持多重继承,但允许一个类实现多个接口。如果一个类已经继承了某个类,则无法再继承Thread类。但是,它仍然可以通过实现Runnable接口来定义一个可在线程中执行的任务
public class ThreadRunnable {
static class MyThread implements Runnable{ //自定义内部类实现接口
@Override
public void run() {
System.out.println(" t hello");
}
}
public static void main(String[] args) {
//将MyThread对象传参给Thread构造对象
Thread t = new Thread(new MyThread());
t.start();
System.out.println("main xixi");
}
}
用匿名内部类形式和lambda对上面两种方式的变形
public class nim {
static Thread t1 =new Thread(){ //匿名内部类继承Thread类,对象类型为Thread类(多态形式)
@Override
public void run() {
System.out.println("nim");
}
};
static Thread t2 = new Thread(new Runnable() {//匿名内部类实现Runnable接口,都是多态
@Override
public void run() {
System.out.println("runnable");
}
});
//lambda
static Thread tt2 = new Thread(()->{ //对匿名内部类的简化,实现的接口和重写的方法名啥的全省略了,只有()构造函数和方法体
System.out.println("lambda");
});
public static void main(String[] args) {
t1.start();
t2.start();
tt2.start();
System.out.println("main");
}
}
3.实现Callable接口
获取结果时使用,任务有返回值,可以抛出受检异常
public class CallableBasic {
public static void main(String[] args) throws Exception {
// 创建Callable任务
Callable<Integer> task = () -> {
Thread.sleep(1000);
return 42; // 返回计算结果
};
// 包装为FutureTask
FutureTask<Integer> futureTask = new FutureTask<>(task);
// 创建线程并启动
Thread thread = new Thread(futureTask);
thread.start();
// 获取结果(会阻塞直到计算完成)
Integer result = futureTask.get();
System.out.println("计算结果: " + result);
}
}
成员状态
不同系统命名方式可能不一样 ,java中是这样命名
线程属性
属性的设置要在线程启动之前
属性 | 获取方法 | |
---|---|---|
id | getId(); | JVM为线程设置,不能手动设置ID,为线程的唯一标识,不同线程不会重复 |
name | getName(); | 线程名字 |
state | getState (); | 状态jvm为线程设置,不能手动设置 |
priority | getPriority(); | 优先级高的线程更容易被调度到 |
是否存活 | isAlive(); | run()方法是否运行结束(不是NEW,TERMINATED状态都是活着的) |
是否为后台线程 | isDeamon(); | 前台线程:自己没结束,进程也不能结束,所有线程都默认为前台线程; 后台线程:前台线程结束了, 进程和里面的后台线程都结束。 |
线程是否中断 |
isInterrupted(); interrupted(); |
在循环或阻塞操作中检查中断状态,确保线程能优雅退出 |
常用方法
run();//记录线程要执行的任务
start(); //调用这个方法才在系统底层真正创建线程,一个线程对象只能启动一次
join();//等待一个线程结束,a线程在b线程里调用join()方法,b等a
join(long millis);//等待指定时间
public static void main(String[] args) throws InterruptedException {
Runnable target = () -> {
for (int i = 0; i < 3; i++) {
try {
System.out.println(Thread.currentThread().getName()
+ ": 我还在⼯作!");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": 我结束了!");
};
Thread thread1 = new Thread(target, "李四");
Thread thread2 = new Thread(target, "王五");
thread1.start();
thread2.start();//同时开启两个线程,抢占式执行
thread2.join();//在main线程里调用thread2.join(),没有指定时间的话,main线程阻塞等待,只有当thread2执行完了,后面的代码才能执行,main线程才能结束
//这里不影响thread1的运行,因为没有在thread1里面调用thread2.join()
thread1.join();//调用两个线程的join(),main等待这两个线程之间运行的最大时间
System.out.println(12);
//运行结果
李四: 我还在⼯作!
王五: 我还在⼯作!
李四: 我还在⼯作!
王五: 我还在⼯作!
李四: 我还在⼯作!
王五: 我还在⼯作!
李四: 我结束了!
王五: 我结束了!
12 //thread1执行完,main结束阻塞
//如果以下顺序 ,thread1和2串行执行
thread1.start();
thread1.join();//等thread1执行完才执行main线程后面的代码
thread2.start();
thread2.join();
}
String getName();//得到线程的名字
setName( String name );//给线程取个名字,方便调试,知道哪个线程出了问题
static void sleep(long millis)thows InterruptedException;//线程睡眠/阻塞指定时间
static Thread currentThread();// 返回当前线程对象的引用,Thread.currentThread()
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName());//main
}
}
isAlive();//指的是系统中的线程(PCB)是否存在,跟Thread对象生命周期不一样,用start()方法,可以判断线程被创建,但是线程是否被销毁,用isAlive()判断
//在多线程编程里 ,可以自定义一个变量记录当前线程是否被中断,但这样维护变量比较麻烦.在Thread的内部有一个Boolean变量作为是否被中断的标记位,默认为false,表示没有被中断,可以替代自定义
static interrupted (); //Thread类调用,查看中断标志,清除标记,改为false
isInterrupted(); //对象调用 ,查看标记,但不清除标记,就是不改为false
interrupt(); //将标志位改为true,提示线程中断,不是强制性中断
thread 收到通知的方式有两种,分为此时正在阻塞(异常通知)或者没被阻塞(直接中断)
1.如果线程因为调用wait/join/sleep等方法而阻塞挂起,则以InterruptedException异常的形式通知,线程被唤醒,并清除中断标志,恢复为未被中断的状态(阻塞方法的擦除功能),这时捕获异常自定义处理方法(因为不强制中断),可以选择忽略这个异常, 也可以跳出循环结束线程.
2.如果没有sleep()这些阻塞方法,设置标记位为true,不会自动停止线程,需主动检查isInterrupted(),然后自行处理。
总结
行为 | interrupt() |
isInterrupted() |
interrupted() |
---|---|---|---|
作用 | 设置中断标志为 true |
检查中断标志(不清除) | 检查并清除中断标志 |
调用方式 | 实例方法 | 实例方法 | 静态方法 |
阻塞时的响应 | 抛出 InterruptedException |
无特殊行为 | 无特殊行为 |
典型用途 | 通知线程终止 | 循环条件检查 | 一次性状态检查 |
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t1.start();
t1=null; //对象没有引用被GC回收,但内核线程还在sleep没被销毁
Thread t2=new Thread(()->{
});
t2.start();
Thread.sleep(1000);//t2线程瞬间执行完任务,内核的线程和PCB被销毁,但是线程对象要等休眠结束才会被GC回收
}
public class MyRunnable implements Runnable{
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){ //检查当前线程是否中断
System.out.println("转帐");
}
}
}
public class Interrupted {
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
System.out.println(Thread.currentThread().getName()
+ ": 让李四开始转账。");
thread.start();
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()
+ ": ⽼板来电话了,得赶紧通知李四对⽅是个骗⼦!");
thread.interrupt(); //修改标准位,通知线程中断
}
}
Thread t=new Thread(()->{
while (!Thread.currentThread().isInterrupted()){
System.out.println("转帐");
try {
Thread.sleep(10); //当线程处于阻塞状态时,会将线程唤醒,抛出中断异常,
// sleep()会擦除设置,将标志位恢复成false
} catch (InterruptedException e) {//捕获异常,处理中断,可以直接break,
//return,终止线程继续,
// 也可以忽略这个中断提醒继续执行
break;
}
}
});
t.start();
Thread.sleep(10);
t.interrupt();
线程安全
这里对安全的简单定义:代码运行结果符合预期,有规律. 在多线程环境下,存在线程安全问题
不安全的原因
1.线程抢占式执行(罪魁祸首,操作系统内核设计决定,我们不能改变)
2.导致操作非原子性;
3.多个线程对同一个变量/数据 进行修改就会因为操作非原子性导致结果具有随机性;
int count=0;
count++;
要实现count自增,计算机底层要完成三个步骤
第一步将内存中的变量加载到寄存器,第二步变量加一,第三步将变量再加载到内存中存储。
如果此时有两个线程想要让这个变量分别实现3次自增操作,当A线程完成了前两步,还没将count加载到内存时,B线程抢夺cpu资源实现count++,那等A线程将count加载到内存时会将B存的值覆盖,最后的值一定不等于6。
4.内存可见性问题
public class Volatile {
static int count=0;
//static volatile int count=0;
public static void main(String[] args) {
Thread t1=new Thread(()->{
while (count==0){ //count变量第一次从内存被加载到寄存器后,就不会再次加载
//因为这里的while循环体为空,count的加载的时间速度比count的比较速度慢个几千倍,
//而while里没有比变量加载更耗时的操作,所以系统判断count并没有进行修改,
//后续比较就不再进行变量加载操作,以此加快运行时间。
//当这里有IO/sleep()/wait()的操作时,加载操作就不会被优化,因为这些操作耗费的时间远大于加载变量,那进行这样的优化就没有意义。
}
System.out.println("t1线程结束");
});
Thread t2=new Thread(()->{
System.out.println("请输入");
Scanner in=new Scanner(System.in);
count=in.nextInt();//当t2 线程对count变量进行修改后,
//t1线程不能察觉到count的变化,所以就陷入while死循环,t1线程无法结束。
//可以给变量加上volatile的关键字,就可以让系统不进行编译优化
});
t1.start();
t2.start();
}
}
对于大型项目来说,编译器的优化确实可以节省很多时间 ,但是在多线程情况下容易bug. volatile关键字表示不稳定的, 提醒编译器不要进行优化,确保每次操作变量都重新加载一次,强制读写内存,就能够保证线程之间的内存可见性,速度变慢了,但是数据变准确了。
编译器优化有很多种情况,volatile只是针对变量优化的情况。
5.指令重排序问题
编译期间为了加快运行速度会对一些指令进行重排序,但不会影响运行结果正确性。但在多线程环境下,指令重排序会概率性出 bug。加volatile关键字,不仅能解决内存可见性问题,也能禁止对变量的读写操作指令重排序,保证运行结果的正确。
解决线程不安全
volatile
表示不稳定的,修饰变量,在多线程情况下保证内存可见性,禁止指令重排序。编译器不确定啥时执行了优化,所以针对变量,volatile加上比较保险。
synchornized
表示锁,锁里的代码就只能一个线程执行,保证操作原子性。
1.使用synchornized关键字,给一个对象上锁,本质是修改指定对象的''对象头''。java中可以给任意对象上锁
语法: synchornized(对象){ }
()括号里的对象仅仅是起标识作用,如果跟其他线程的锁的对象一样就会产生锁竞争/阻塞
进代码块就已经加锁,出来就相当于解锁, synchornized是可重入锁,对同一个对象嵌套加多个锁,不会产生死锁
修饰静态方法相当于是对类对象加锁,修饰普通方法相当于对this(当前调用方法的对象)加锁 ,当两个线程竞争同一把锁会产生阻塞等待.
public class SynchronizedDemo {
private Object locker = new Object();
public void method() {
synchronized (locker) {}
}
}
2.Java标准库中的线程安全类:
ConCurrentHashMap: 高并发优化的线程安全哈希表
StringBuffer : 线程安全的长度可变字符串,所有方法用synchornized修饰,并发性很差,加锁系统开销大,适用于多线程环境
wait() 和notify()
多线程编程中每个线程是抢占式执行,执行顺序具有随机性,可以用wait和notify方法协调线程之间的执行顺序.
wait()/wait(long time) :让当前线程进入等待状态,可以指定等待时间
notify()/notifyAll() :唤醒在当前对象上等待的线程/全部线程
注意
1.wait()和notity()都是Object的方法
2.搭配synchornized使用,不然会抛出异常
wait()结束等待
1.其他线程调用该对象的notity()唤醒
2.指定等待时间,超时就自动唤醒
3.其他线程调用该等待线程的interrupted(),wait()会抛出 InterruptedException 异常.
notify()
在synchornized的同步代码块中调用,通知那些可能等待该对象锁的其他线程停止等待,
如果有多个线程等待,则有线程调度器随机挑选出一个呈wait状态的线程,并不存在先来后到;
调用notify()后,当前线程不会立马释放该对象锁,而要等到该执行notify()的线程将程序执行完,退出同步代码块之后才释放对象锁.(现实场景就是一个人在厕所里随机喊外面的一个人,对他说我快上完了了,马上就是你了. 他可能是提前喊的,过一会才出厕所,把厕所让给别人)
notifAll()
唤醒所有等待同一个对象锁的线程,等释放锁后会产生锁竞争
sleep()和wait() 的区别
1.sleep()和wait()都能让线程阻塞一段时间,但sleep让当前线程休眠,wait()用于线程间通信;
2.sleep()是Thread的静态方法,wait()是Object的方法;
3.wait()需要搭配synchornized使用,sleep()不需要;
static class WaitTask implements Runnable {
private Object locker;
public WaitTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
while (true) {
try {
System.out.println("wait 开始");
locker.wait();
System.out.println("wait 结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
static class NotifyTask implements Runnable {
private Object locker;
public NotifyTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
System.out.println("notify 开始");
locker.notify();
System.out.println("notify 结束");
}
}
}
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(new WaitTask(locker));
Thread t2 = new Thread(new NotifyTask(locker));
t1.start();
Thread.sleep(1000);
t2.start();
}
死锁
四个条件同时满足产生死锁
(基本特性)
1.锁具有互斥性,对一个对象加锁了,其他线程就不能对这个对象同时加锁。
2.锁具有不可剥夺性,给对象加了锁,得执行完锁里的代码逻辑才能释放锁
(代码结构)
3.嵌套锁不同的对象
4.循环等待
避免死锁
锁的基本特性保证了操作的原子性,不能改变,只能改变代码结构,避免线程嵌套锁不同对象的同时,不让里面的线程循环等待,如果避免不了多个锁嵌套,就得规定加锁的执行顺序来避免相互之间循环等待。
设计模式
单例模式
某个类在进程中只有唯一实例,不允许创建多个实例.也常用饿汉模式和懒汉模式实现单例模式。
饿汉模式:
在程序加载的时候创建实例,实例为静态的。
class Singleton {
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
private Singleton(){
//将构造方法私有化
}
}
public class demo{
public static void main(String[] args) {
Singleton s1 =Singleton.getInstance();
Singleton s2=Singleton.getInstance();
System.out.println(s1.equals(s2));//true
}
}
懒汉模式:
首次使用的时候才创建,如果不使用,就节省一次创建的成本。
//单线程
public class SingletonLazy { //单线程环境
private static SingletonLazy instance=null;
public static SingletonLazy getInstance(){
if(instance==null){
instance=new SingletonLazy();
}
return instance;
}
private SingletonLazy(){};//私有化构造方法
}
饿汉模式一开始对象就扎根创建,可能会把程序的启动拖慢,懒汉模式创建对象是分散的,可能 就不会出现程序启动卡顿。但是懒汉模式在多线程模式下可能会创建多个对象。
饿汉模式的实例是静态的,随着类的加载被创建,主线程main在调用时,对象就已经被创建好了,就不存在问题。但是懒汉模式在多个线程同时调用getInstance()方法时,由于创建对象操作不具有原子性,导致一个线程在if判断后new对象之前,别的线程就把对象创建好了,这样可能创建多个对象,需要给方法加锁。
//多线程
public class SingletonLazy {
private static volatile SingletonLazy instance=null;//加上volatile更万无一失
public static SingletonLazy getInstance(){
if (instance!=null){//这个if,避免重复加锁导致增加不必要开销
synchronized (instance){
if (instance == null) {//第一次调用getInstance()创建对象时,多个线程通过第一个if判断后竞争锁,一个线程创建后释放锁,需要再用一个if判断对象是否存在
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy(){}//私有化构造方法,只能创建一个对象
}
这里的双重if判断,避免了多次加锁带来的开销(第一个if),也能保证只创建一次对象(第二个if);
volatile 关键字避免内存可见性问题,确保每次访问变量时都按照程序代码的顺序直接从内存读取或写入,而不是依赖可能过期的寄存器缓存值.
阻塞队列
class BlockingQueue {
private int[] items = new int[1000];
private volatile int size;
private volatile int head;
private volatile int tail;
public void put(int value) throws InterruptedException {
synchronized (this) {
while (size == items.length) {
//不停判断队列是否有空位放,没有就wait阻塞
wait();
}
items[tail] = value;
size++;
tail = (tail + 1) % items.length;
notifyAll(); //唤醒阻塞的线程
}
}
public int take(int value) throws InterruptedException {
int ret = 0;
synchronized (this) {
while (size == 0) {
wait();
}
ret = items[head];
size--;
head = (head + 1) % items.length;
notifyAll();
}
return ret;
}
}
线程池
频繁创建销毁线程,对系统来说也是很大的开销。创建线程需要内核态和用户态进行交互。比较耗时,当创建线程放到线程池中,不着急销毁,当需要的时候就拿出来使用,线程长时间空闲就销毁,变成和用户态的交互,节省系统开销.
Executors
创建线程池是运用了工程模式
工厂模式的核心思想是:将对象的创建与使用分离,通过专门的工厂类来负责对象的实例化过程.
Executors 创建各种线程池的工厂类,标准库中的线程池
返回值 ExecutorService
submit () ;//注册一个任务到线程池中,通过 ExecutorService.submit ()将注册⼀个任务到线程池中.
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
ThreadPoolExecutor
cpu操作密集型:最大核心数不能超过
IO操作密集型:可以创建很多个
int corePoolSize = 5; // 核心线程数
int maxPoolSize = 10; // 最大线程数
long keepAliveTime = 60; // 空闲线程存活时间(秒)
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100); // 任务队列
ThreadFactory threadFactory = Executors.defaultThreadFactory(); // 线程工厂
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); // 拒绝策略
ExecutorService customPool = new ThreadPoolExecutor(
corePoolSize,
maxPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
workQueue,
threadFactory,
handler
);
定时器
定时器是实际开发中常用组件,如网络通信中,对方100ms内没有返回数据,则断开连接尝试重连,如一个Map,希望里面的某个key在3s后自动过期(自动删除),这样的场景需要用到定时器.
标准库中实现定时器的类Timer,核心方法,schedule()
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello");
}
}, 3000);
schedule第一个参数指定要执行的任务,第二个参数指定等待的时间,单位毫秒.
TimerTask 为一个抽象类,实现了 Runnable 接口,因此本质上是一个可运行的任务.
更多推荐
所有评论(0)