多线程
概念
进程是执行程序的实体,每个进程是一个应用程序,cpu的一个核心只能同时处理一个进程,当需要处理多个进程的时候会采用时间片轮调度法
![](https://i-blog.csdnimg.cn/blog_migrate/78ddeacc340070d7acb8ae21c3d9c936.jpeg)
在早期的计算机中,进程是拥有资源和独立运行的最小单位,也是程序执行的最小单位。但是,如果希望两个任务同时进行,就必须运行两个进程,由于每个进程都有一个自己的内存空间,进程之间的通信就变得非常麻烦(比如要共享某些数据)而且执行不同进程会产生上下文切换,非常耗时。
线程和进程示意图
![](https://i-blog.csdnimg.cn/blog_migrate/fa37e5c33069c77e8fc8c27ba23d71a3.gif)
线程出现后,一个进程可以有多个线程,线程是程序执行中一个单一的顺序控制流程,现在线程才是程序执行流的最小单元,各个线程之间共享程序的内存空间(也就是所在进程的内存空间),上下文切换速度也高于进程。
实际上一个Java程序启动后,会创建很多线程,不仅仅只运行一个主线程
创建和启动
使用Thread进行多线程构造,需要传入一个Runnable接口的实现(其实就是编写要在另一个线程执行的内容逻辑),由于只有一个未实现方法,可用lambda表达式
并且需要启动
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("我是线程:"+Thread.currentThread().getName());
System.out.println("我正在计算 0-10000 之间所有数的和...");
int sum = 0;
for (int i = 0; i <= 10000; i++) {
sum += i;
}
System.out.println("结果:"+sum);
});
t.start();
System.out.println("我是主线程!");
}
若使用run方法而不是start,则会在当前线程中执行该线程,即执行完run的线程才会往下运行,stop或者call也可结束程序
线程会等待获取CPU资源,一旦获取到,就开始按顺序执行给定的程序,当需要等待外部IO操作(比如Scanner获取输入的文本),就会暂时处于休眠状态,等待通知,或是调用sleep()方法来让当前线程休眠一段时间,sleep操作以毫秒为单位,可响应中断。
public static void main(String[] args) throws InterruptedException {
System.out.println("l");
Thread.sleep(1000); //休眠时间,以毫秒为单位,1000ms = 1s
System.out.println("b");
}
对象方法stop()获取当前线程并结束(推荐使用interrupt):
Thread me = Thread.currentThread();
me.stop();
休眠和中断
运行状态的线程可能出现如下的其他状态:
- 当CPU给予的运行时间结束时,会从运行状态回到就绪(可运行)状态,等待下一次获得CPU资源。
- 当线程进入休眠 / 阻塞(如等待IO请求) / 手动调用
wait()
方法时,会使得线程处于等待状态,当等待状态结束后会回到就绪状态。 - 当线程出现异常或错误 / 被
stop()
方法强行停止 / 所有代码执行结束时,会使得线程的运行终止。
注意对象方法和静态方法的区别
sleep方法是Thread的静态方法,它只作用于当前线程(它知道当前线程是哪个)
Thread.sleep(1000)//直接使代码所在线程休眠
通过调用sleep()
方法来将当前线程进入休眠,使得线程处于等待状态一段时间。此方法显示声明了会抛出一个InterruptedException异常
每一个Thread对象的对象方法中,都有一个interrupt()方法
Thread.currentThread().isInterrupted()
调用此方法后,会给指定线程添加一个中断标记以告知线程需要立即停止运行或是进行其他操作,由线程来响应此中断并进行相应的处理,我们前面提到的stop()方法是强制终止线程,这样的做法虽然简单粗暴,但是很有可能导致资源不能完全释放,而类似这样的发送通知来告知线程需要中断,让线程自行处理后续,会更加合理一些。
静态方法,中断复位,清楚中断标记,方便后续处理和迎接下一次中断
Thread.interrupted();
对象方法,suspend()和resume(),挂起和继续运行,不推荐使用,容易导致死锁。
Thread.currentThread().suspend()
//外部线程中:
thread.resume();
优先级、礼让和加入
线程的优先级
Java程序中的每个线程并不是平均分配CPU时间的,为了使得线程资源分配更加合理,Java采用的是抢占式调度方式,优先级越高的线程,优先使用CPU资源!希望CPU花费更多的时间去处理更重要的任务,而不太重要的任务,则可以先让出一部分资源。线程的优先级一般分为以下三种:
- MIN_PRIORITY 最低优先级
- MAX_PRIORITY 最高优先级
- NOM_PRIORITY 常规优先级
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("线程开始运行!");
});
t.start();
t.setPriority(Thread.MIN_PRIORITY); //通过使用setPriority方法来设定优先级
}
优先级越高的线程,获得CPU资源的概率会越大,并不是一定优先级越高的线程越先执行
礼让和加入
当线程当前工作不是特别重要的时候,可以使用yield()
静态方法将cpu资源优先让给其他同级别的线程,尽可能多地执行其他线程
当我们希望一个线程等另一个线程运行完了再继续执行,则可以使用对象方法join()
实现线程的加入(如t2让t1加入自己的线程并等待t1结束),
t1.join();
注意并不是真的合为一个线程,实质是t2在等待,直到t1运行结束
线程锁和同步
多线程情况下Java的内存管理:
线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的工作内存(本地内存),工作内存中存储了该线程读/写共享变量的副本。它类似于在计算机组成原理中的多处理器高速缓存机制
高速缓存保存内存中数据的副本来提供更快的数据访问,但是多个线程都涉及对同一块内存区域的改变,就可能导致各自的高速缓存中的数据不一样,从而在写回主存时发生冲突,这种引入高速缓存而发生的问题称为:缓存一致性
要去解决这样的问题,就必须采取某种同步机制,来限制不同线程对于共享变量的访问,希望的是保证共享变量value自增操作的原子性(原子性是指一个操作或多个操作要么全部执行,且执行的过程不会被任何因素打断,包括其他线程,要么就都不执行)
线程锁
通过synchronized
关键字来创造一个线程锁,传入内容必须是一个对象或是一个类,将锁中的代码块变为同步代码块
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
synchronized (Main_base.class){
value++;
}
}
System.out.println("线程1完成");
});
这样就可以解决问题,因为在同步代码块执行过程中,拿到了传入对象或类的锁(传入的如果是对象,就是对象锁,不同的对象代表不同的对象锁,如果是类,就是类锁,类锁只有一个,实际上类锁也是对象锁,是Class类实例,但是Class类实例同样的类无论怎么获取都是同一个),但是注意两个线程必须使用同一把锁。上了同一个锁的内容,同时只会有一个在执行。
当一个线程进入到同步代码块时,会获取到当前的锁,而这时如果其他使用同样的锁的同步代码块也想执行内容,就必须等待当前同步代码块的内容执行完毕,在执行完毕后会自动释放这把锁,而其他的线程才能拿到这把锁并开始执行同步代码块里面的内容。(实际上synchronized是一种悲观锁,随时都认为有其他线程在对数据进行修改,此外还有乐观锁,如CAS算法)
当传入对象不同时,获取到的是不同的锁,因此并不能保证自增操作的原子性,最后也得不到我们想要的结果。
synchronized关键字也可以作用于方法上,调用此方法时也会获取锁
private static synchronized void add(){
value++;
}
等价于
private static void add(){
synchronized(Main.class){ //Main.class相当于java入口函数的类
value++;
}
}
若不加静态前缀,则说明为对象锁,等价于
private void add(){
synchronized(this){
value++;
}
}
即,静态方法为类锁,非静态方法为对象锁
死锁
两个线程相互持有对方需要的锁,但是又迟迟不释放,导致程序卡住,线程被无限期地阻塞,因此程序不可能正常终止。
JDK自带指令:
jps 指令,查看当前java进程
jstack 进程Id 指令,查看指定进程栈信息
jconsole.exe 指令, 查看更多运行信息
不推荐使用 suspend()去挂起线程的原因,是因为suspend()在使线程暂停的同时,并不会去释放任何锁资源。其他线程都无法访问被它占用的锁。直到对应的线程执行resume()方法后,被挂起的线程才能继续,从而其它被阻塞在这个锁的线程才可以继续执行。但是,如果resume()操作出现在suspend()之前执行,那么线程将一直处于挂起状态,同时一直占用锁,这就产生了死锁。
wait()和notify()方法
这两个方法是生成的锁的对象(类)的方法,且只能在该锁的同步代码块中使用这两个方法。
public static void main(String[] args) throws InterruptedException {
Object o1 = new Object();
Thread t1 = new Thread(() -> {
synchronized (o1){
try {
System.out.println("开始等待");
o1.wait(); //进入等待状态并释放锁
System.out.println("等待结束!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
synchronized (o1){
System.out.println("开始唤醒!");
o1.notify(); //唤醒处于等待状态的线程
for (int i = 0; i < 50; i++) {
System.out.println(i);
}
//唤醒后依然需要等待这里的锁释放之前等待的线程才能继续
}
});
t1.start();
Thread.sleep(1000);
t2.start();
}
注意在o1.notify();
之后,锁依然在t2代码块中,t1只是结束了wait等待,还需要等待锁从t2中释放,并自己获得锁才能继续运行。
对象的
wait()
方法会暂时使得此线程进入等待状态,同时会释放当前代码块持有的锁,这时其他线程可以获取到此对象的锁,当其他线程调用对象的notify()
方法后,会唤醒刚才变成等待状态的线程(这时并没有立即释放锁)。注意,必须是在持有锁(同步代码块内部)的情况下使用,否则会抛出异常。
notifyAll()
其实和notify()
一样,也是用于唤醒,但是前者是唤醒所有调用wait()
后处于等待的线程,而后者是看运气随机选择一个。
ThreadLocal
想让每个线程的变量独立开互不影响时可以使用ThreadLocal类,每一个线程会创建自己工作区内存中的变量,不同的线程访问该ThreadLocal变量时,只能访问、获取到自己线程所属的变量,主要有.get()/.set()
方法
public static void main(String[] args) throws InterruptedException {
ThreadLocal<String> local = new ThreadLocal<>(); //注意这是一个泛型类,存储类型为我们要存放的变量类型
Thread t1 = new Thread(() -> {
local.set("lbwnb"); //将变量的值给予ThreadLocal
System.out.println("变量值已设定!");
System.out.println(local.get()); //尝试获取ThreadLocal中存放的变量
});
Thread t2 = new Thread(() -> {
System.out.println(local.get()); //尝试获取ThreadLocal中存放的变量
});
t1.start();
Thread.sleep(3000); //间隔三秒
t2.start();
}
第一个线程存放的内容,第一个线程可以获取,但是第二个线程无法获取,我们再来看看第一个线程存入后,若第二个线程也存放,也不会改变覆盖第一个线程存放的内容。
在线程中创建的子线程,无法获得父线程工作内存中的变量,可以使用InheritableThreadLocal代替ThreadLocal来解决
在InheritableThreadLocal存放的内容,会自动向子线程传递。
定时器
当需要特定的程序定时执行,如3秒后执行时。可以通过Thread.sleep()
来实现。
而有时会需要一个任务循环进行,如每隔3秒钟执行一次,可以在循环体上套一层while即可
java提供了更完善的定时器框架
import java.util.Timer;
import java.util.TimerTask;
public static void main(String[] args) {
Timer timer = new Timer(); //创建定时器对象
timer.schedule(new TimerTask() { //注意这个是一个抽象类,不是接口,无法使用lambda表达式简化,只能使用匿名内部类,实现run方法
@Override
public void run() {
System.out.println(Thread.currentThread().getName()); //打印当前线程名称
}
}, 1000); //执行一个延时任务
}
守护线程
当其他所有的非守护线程结束之后,守护线程是自动结束,也就是说,Java中所有的线程都执行完毕后,守护线程自动结束,因此守护线程不适合进行IO操作,随时有可能北关闭!
public static void main(String[] args) throws InterruptedException{
Thread t = new Thread(() -> {
while (true){
try {
System.out.println("程序正常运行中...");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.setDaemon(true); //设置为守护线程(必须在开始之前,中途是不允许转换的)
t.start();
for (int i = 0; i < 5; i++) {
Thread.sleep(1000);
}
}
线程必须在start之前北设置为守护线程。在守护线程中产生的线程也是守护的
集合类并行方法
public static void main(String[] args) {
List<Integer> list = new ArrayList<>(Arrays.asList(1, 4, 5, 2, 9, 3, 6, 0));
list
.parallelStream() //获得并行流 本来是Stream()
.forEach(i -> System.out.println(Thread.currentThread().getName()+" -> "+i));
}
可以使用多线程来执行流,但可能不会按原有的数组顺序进行处理。
Arrays.parallelSort()
多线程排序,能更快运行,并且更快。 除此外还有很多并行方法 Arrays.parallelxxx
生产者消费者模型
所谓的生产者消费者模型,是通过一个容器来解决生产者和消费者的强耦合问题。通俗的讲,就是生产者在不断的生产,消费者也在不断的消费,可是消费者消费的产品是生产者生产的,这就必然存在一个中间容器,我们可以把这个容器想象成是一个货架,当货架空的时候,生产者要生产产品,此时消费者在等待生产者往货架上生产产品,而当货架有货物的时候,消费者可以从货架上拿走商品,生产者此时等待货架出现空位,进而补货,这样不断的循环。
通过多线程编程,来模拟一个餐厅的2个厨师和3个顾客,假设厨师炒出一个菜的时间为3秒,顾客吃掉菜品的时间为4秒。
- 使用lambda表达式创建线程
- 使用main::method调用类内方法
- 使用wait和notify进行转让线程锁