java多线程
基本知识
进程介绍
不管是开发的应用程序还是运行的其他的应用程序,都需要先把程序安装在本地的硬盘上。然后找到这个程序的启动文件,启动程序的时候,其实是电脑把当前的这个程序加载到内存中,在内存中需要给当前的程序分配一段独立的运行空间。这片空间就专门负责当前这个程序的运行。
不同的应用程序运行的过程中都需要在内存中分配自己独立的运行空间,彼此之间不会相互的影响。每个独立应用程序在内存的独立空间称为当前应用程序运行的一个进程。
进程:它是内存中的一段独立的空间,可以负责当前应用程序的运行。当前这个进程负责调度当前程序中的所有运行细节。
线程介绍
在一个进程中,每个独立的功能都需要独立的去运行,这时又需要把当前这个进程划分成多个运行区域,每个独立的小区域(小单元)称为一个线程。
线程:它是位于进程中,负责当前进程中的某个具备独立运行资格的空间。
进程是负责整个程序的运行,而线程是程序中具体的某个独立功能的运行。一个进程中至少应该有一个线程。
多线程介绍
现在的操作系统都是多用户,多任务的操作系统。每个任务就是一个进程。而在这个进程中就会有线程。
真正可以完成程序运行和功能的实现靠的是进程中的线程。
多线程:在一个进程中,同时开启多个线程,让多个线程同时去完成某些任务(功能)。
多线程的目的:提高程序的运行效率。
多线程运行的原理
cpu在线程中做时间片的切换。
其实真正电脑中的程序的运行不是同时在运行的。CPU负责程序的运行,而CPU在运行程序的过程中某个时刻点上,它其实只能运行一个程序。而不是多个程序。而CPU它可以在多个程序之间进行高速的切换。而切换频率和速度太快,导致人的肉看看不到。
每个程序就是进程,而每个进程中会有多个线程,而CPU是在这些线程之间进行切换。
了解了CPU对一个任务的执行过程,可以知道,多线程可以提高程序的运行效率,但不能无限制的开线程。
实现线程的两种方式
1、继承Thread的原理
- import java.util.Random;
- public class MyThreadWithExtends extends Thread {
- String flag;
- public MyThreadWithExtends(String flag){
- this.flag = flag;
- }
- @Override
- public void run() {
- String tname = Thread.currentThread().getName();
- System.out.println(tname+"线程的run方法被调用……");
- Random random = new Random();
- for(int i=0;i<20;i++){
- try {
- Thread.sleep(random.nextInt(10)*100);
- System.out.println(tname+ "...."+ flag);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- public static void main(String[] args) {
- Thread thread1 = new MyThreadWithExtends("a");
- Thread thread2 = new MyThreadWithExtends("b");
- thread1.start();
- thread2.start();
- }
- }
2、声明实现 Runnable 接口的类
- public class MyThreadWithImpliment implements Runnable {
- int x;
- public MyThreadWithImpliment(int x) {
- this.x = x;
- }
- @Override
- public void run() {
- String name = Thread.currentThread().getName();
- System.out.println("线程" + name + "的run方法被调用……");
- for (int i = 0; i < 10; i++) {
- System.out.println(x);
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- public static void main(String[] args) {
- Thread thread1 = new Thread(new MyThreadWithImpliment(1), "thread-1");
- Thread thread2 = new Thread(new MyThreadWithImpliment(2), "thread-2");
- thread1.start();
- thread2.start();
- }
- }
java同步关键词
synchronized
加同步格式:
synchronized(任意对象(锁) ){
代码块中放操作共享数据的代码
}
- public class MySynchronized {
- public static void main(String[] args) {
- final MySynchronized mySynchronized = new MySynchronized();
- final MySynchronized mySynchronized2 = new MySynchronized();
- new Thread("thread1") {
- public void run() {
- synchronized (mySynchronized) {
- try {
- System.out.println(this.getName()+" start");
- int i =1/0; //如果发生异常,jvm会将锁释放
- Thread.sleep(5000);
- System.out.println(this.getName()+"醒了");
- System.out.println(this.getName()+" end");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }.start();
- new Thread("thread2") {
- public void run() {
- synchronized (mySynchronized) { //争抢同一把锁时,线程1没释放之前,线程2只能等待
- // synchronized (mySynchronized2) { //如果不是一把锁,可以看到两句话同时打印
- System.out.println(this.getName()+" start");
- System.out.println(this.getName()+" end");
- }
- }
- }.start();
- }
- }
synchronized的缺陷
synchronized是java中的一个关键字,也就是说是Java语言内置的特性。
如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
2)线程执行发生异常,此时JVM会让线程自动释放锁。
例子1:
如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能等待,这会影响程序执行效率。
因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以实现。
例子2:
当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。
但是采用synchronized关键字来实现同步的话,就会导致一个问题:
如果多个线程都只是进行读操作,当一个线程在进行读操作时,其他线程只能等待无法进行读操作。
因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以实现。
另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。
总的来说,也就是说Lock提供了比synchronized更多的功能。
lock
lock和synchronized的区别
1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
java.util.concurrent.locks包下常用的类
- Lock
首先要说明的就是Lock,通过查看Lock的源码可知,Lock是一个接口:
- public interface Lock {
- void lock();
- void lockInterruptibly() throws InterruptedException;
- boolean tryLock();
- boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
- void unlock();
- }
Lock接口中每个方法的使用:
lock()、tryLock()、tryLock(long time, TimeUnit unit)、lockInterruptibly()是用来获取锁的。 unLock()方法是用来释放锁的。
四个获取锁方法的区别:
lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。
因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。
而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。
- ReentrantLock
直接使用lock接口的话,我们需要实现很多方法,不太方便,ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法,ReentrantLock,意思是"可重入锁"。
以下是ReentrantLock的使用案例:
例子1,lock()的正确使用方法
- import java.util.ArrayList;
- import java.util.concurrent.locks.Lock;
- import java.util.concurrent.locks.ReentrantLock;
- public class MyLockTest {
- private static ArrayList<Integer> arrayList = new ArrayList<Integer>();
- static Lock lock = new ReentrantLock(); // 注意这个地方
- public static <E> void main(String[] args) {
- new Thread() {
- public void run() {
- Thread thread = Thread.currentThread();
- lock.lock();
- try {
- System.out.println(thread.getName() + "得到了锁");
- for (int i = 0; i < 5; i++) {
- arrayList.add(i);
- }
- } catch (Exception e) {
- // TODO: handle exception
- } finally {
- System.out.println(thread.getName() + "释放了锁");
- lock.unlock();
- }
- };
- }.start();
- new Thread() {
- public void run() {
- Thread thread = Thread.currentThread();
- lock.lock();
- try {
- System.out.println(thread.getName() + "得到了锁");
- for (int i = 0; i < 5; i++) {
- arrayList.add(i);
- }
- } catch (Exception e) {
- // TODO: handle exception
- } finally {
- System.out.println(thread.getName() + "释放了锁");
- lock.unlock();
- }
- };
- }.start();
- }
- }
例子2,tryLock()的使用方法
- import java.util.ArrayList;
- import java.util.concurrent.locks.Lock;
- import java.util.concurrent.locks.ReentrantLock;
- /**
- * 观察现象:一个线程获得锁后,另一个线程取不到锁,不会一直等待
- * @author
- */
- public class MyTryLock {
- private static ArrayList<Integer> arrayList = new ArrayList<Integer>();
- static Lock lock = new ReentrantLock(); // 注意这个地方
- public static void main(String[] args) {
- new Thread() {
- public void run() {
- Thread thread = Thread.currentThread();
- boolean tryLock = lock.tryLock();
- System.out.println(thread.getName()+" "+tryLock);
- if (tryLock) {
- try {
- System.out.println(thread.getName() + "得到了锁");
- for (int i = 0; i < 5; i++) {
- arrayList.add(i);
- }
- } catch (Exception e) {
- // TODO: handle exception
- } finally {
- System.out.println(thread.getName() + "释放了锁");
- lock.unlock();
- }
- }
- };
- }.start();
- new Thread() {
- public void run() {
- Thread thread = Thread.currentThread();
- boolean tryLock = lock.tryLock();
- System.out.println(thread.getName()+" "+tryLock);
- if (tryLock) {
- try {
- System.out.println(thread.getName() + "得到了锁");
- for (int i = 0; i < 5; i++) {
- arrayList.add(i);
- }
- } catch (Exception e) {
- // TODO: handle exception
- } finally {
- System.out.println(thread.getName() + "释放了锁");
- lock.unlock();
- }
- }
- };
- }.start();
- }
- }
例子3,lockInterruptibly()响应中断的使用方法:
- import java.util.concurrent.locks.Lock;
- import java.util.concurrent.locks.ReentrantLock;
- /**
- * 观察现象:如果thread-0得到了锁,阻塞。。。thread-1尝试获取锁,如果拿不到,则可以被中断等待
- */
- public class MyInterruptibly {
- private Lock lock = new ReentrantLock();
- public static void main(String[] args) {
- MyInterruptibly test = new MyInterruptibly();
- MyThread thread0 = new MyThread(test);
- MyThread thread1 = new MyThread(test);
- thread0.start();
- thread1.start();
- try {
- Thread.sleep(2000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- thread1.interrupt();
- System.out.println("=====================");
- }
- public void insert(Thread thread) throws InterruptedException{
- lock.lockInterruptibly(); //注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出
- try {
- System.out.println(thread.getName()+"得到了锁");
- long startTime = System.currentTimeMillis();
- for( ; ;) {
- if(System.currentTimeMillis() - startTime >= Integer.MAX_VALUE)
- break;
- //插入数据
- }
- }
- finally {
- System.out.println(Thread.currentThread().getName()+"执行finally");
- lock.unlock();
- System.out.println(thread.getName()+"释放了锁");
- }
- }
- }
- class MyThread extends Thread {
- private MyInterruptibly test = null;
- public MyThread(MyInterruptibly test) {
- this.test = test;
- }
- @Override
- public void run() {
- try {
- test.insert(Thread.currentThread());
- } catch (Exception e) {
- System.out.println(Thread.currentThread().getName()+"被中断");
- }
- }
- }
ReadWriteLock也是一个接口,在它里面只定义了两个方法:
public interface ReadWriteLock { /** * Returns the lock used for reading. * * @return the lock used for reading. */ Lock readLock();
/** * Returns the lock used for writing. * * @return the lock used for writing. */ Lock writeLock(); } |
ReentrantReadWriteLock里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁。
下面通过几个例子来看一下ReentrantReadWriteLock具体用法。
例子1: 假如有多个线程要同时进行读操作的话,先看一下synchronized达到的效果
不过要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
java并发包
java并发包介绍
java并发包消息队列及在开源软件中的应用
BlockingQueue也是java.util.concurrent下的主要用来控制线程同步的工具。
主要的方法是:put、take一对阻塞存取;add、poll一对非阻塞存取。
1)add(anObject):把anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则抛出
2)offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.
3)put(anObject):把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续.
4)poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null
5)take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到Blocking有新的对象被加入为止
int remainingCapacity();返回队列剩余的容量,在队列插入和获取的时候,不要瞎搞,数 据可能不准
boolean remove(Object o); 从队列移除元素,如果存在,即移除一个或者更多,队列改 变了返回true
public boolean contains(Object o); 查看队列是否存在这个元素,存在返回true
int drainTo(Collection<? super E> c); 传入的集合中的元素,如果在队列中存在,那么将 队列中的元素移动到集合中
int drainTo(Collection<? super E> c, int maxElements); 和上面方法的区别在于,制定了移 动的数量
BlockingQueue有四个具体的实现类,常用的两种实现类为:
LinkedBlockingQueue和ArrayBlockingQueue区别:
java并发编程总结
不应用线程池的缺点
有些开发者图省事,遇到需要多线程处理的地方,直接new Thread(...).start(),对于一般场景是没问题的,但如果是在并发请求很高的情况下,就会有些隐患:
- 新建线程的开销。线程虽然比进程要轻量许多,但对于JVM来说,新建一个线程的代价还是挺大的,决不同于新建一个对象
- 资源消耗量。没有一个池来限制线程的数量,会导致线程的数量直接取决于应用的并发量,这样有潜在的线程数据巨大的可能,那么资源消耗量将是巨大的
- 稳定性。当线程数量超过系统资源所能承受的程度,稳定性就会成问题
制定执行策略
在每个需要多线程处理的地方,不管并发量有多大,需要考虑线程的执行策略
线程池的类型
不管是通过Executors创建线程池,还是通过Spring来管理,都得清楚知道有哪几种线程池:
- FixedThreadPool:定长线程池,提交任务时创建线程,直到池的最大容量,如果有线程非预期结束,会补充新线程
- CachedThreadPool:可变线程池,它犹如一个弹簧,如果没有任务需求时,它回收空闲线程,如果需求增加,则按需增加线程,不对池的大小做限制
- SingleThreadExecutor:单线程。处理不过来的任务会进入FIFO队列等待执行
- SecheduledThreadPool:周期性线程池。支持执行周期性线程任务
线程池饱和策略
线程无依赖性
本文作者:莲藕淹,转载请注明原文链接:https://www.cnblogs.com/meanshift/p/15558237.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY