【面试】并发编程总结1(生命周期+基本操作)

1 线程

线程是大多数操作系统调度的基本单元。

一个程序作为一个进程运行,程序运行过程中可能会创建多个线程。一个线程在一个时刻只能运行在一个CPU核心上。

为什么使用多线程?

①异步执行

②利用多CPU资源实现真正意义上的并列执行

线程的应用场景:

①使用多线程实现文件下载

②后台任务:如定时向大量(100W以上)的用户发送邮件

③异步处理:记录日志

④多步骤的任务处理:可根据步骤特征选用不同个数和特征的线程来协作处理,多任务的分割,由一个主线程分割给多个线程完成。

多线程的本质是:

合理利用多核心CPU资源来实现线程的并行处理,来实现同一个进程内的多个任务的并行执行,同时基于线程本身的异步执行特性,提升任务处理效率。

2 在Java中使用多线程的方式

  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现callable接口

2.1 继承Thread类

public class ThreadDemo extends Thread{
	@Override
	public void run() {
		System.out.println("当前线程:"+Thread.currentThread().getName());
	}
	
	public static void main(String [] args) {
		ThreadDemo demo = new ThreadDemo();
		demo.start();
	}

}

2.2 实现Runnable接口

public class RunableDemo implements Runnable{
	@Override
	public void run() {
		// TODO Auto-generated method stub
		System.out.println("当前线程:"+Thread.currentThread().getName());
	}

	public static void main(String[] args) {
		RunableDemo runnable = new RunableDemo();
		Thread thread = new Thread(runnable);
		thread.start();
	}
}

2.3 实现callable接口

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class CallableDemo implements Callable<String> {
	@Override
	public String call() throws Exception {
		System.out.println("当前线程:"+Thread.currentThread().getName());
		return "hello";
	}
	
	public static void main(String [] args) throws InterruptedException, ExecutionException {
		ExecutorService executorService = Executors.newFixedThreadPool(1); //线程池
		Future<String> future = executorService.submit(new CallableDemo());
		System.out.println(future.get());
		// future.get()是一个阻塞方法。阻塞到Callable 方法返回值之后输出
	}
}

区别:

Callable实现了,线程返回一个返回值(带类型)

Runnable是接口。(多重实现——可以用于继承了父类的子类中实现线程)

Thread是继承。(单继承)

3 Java线程的生命周期

Java线程从创建到销毁,一共经历6个状态(不一定每一种状态都经历):

  1. NEW:初始状态,线程被构建,但是还没有调用start方法
  2. RUNNABLED:运行状态,JAVA线程把操作系统中的就绪和运行两种状态统一称为“运行中”
  3. BLOCKED:阻塞状态,表示线程进入等待状态,也就是线程因为某种原因放弃了CPU使用权,阻塞也分为几种情况
  4. WAITING:等待状态
  5. TIME_WAITING:超时等待状态,超时以后自动返回
  6. TERMINATED:终止状态,表示当前线程执行完毕

 

4 线程的基本操作和原理

4.1 Thread.join()

1.Thread.join的作用是保证线程执行结果的可见性

4.1.1 示例

对于下述代码的执行结果,有可能是1,也有可能是4。主要原因在于t1和t2不一定是顺序执行的,假如t2先执行完了,再执行t1,就会出现i=1的情况。

package demoThread;

public class ThreadJoinDemo {
	private static int x=0;
	private static int i=0;
	public static void main(String[] args) throws InterruptedException {
		Thread t1 = new Thread(()->{
			i=1;
			x=2;
			
		});
		Thread t2 = new Thread(()->{
			i=x+2;
		});
		t1.start();
		t2.start();
		
		Thread.sleep(1000);
		System.out.println("result:"+i);
	}

}

但是如果加入了Thread.join()方法,就能够保证使用了join的线程先执行完。如下述代码所示:

package demoThread;

public class ThreadJoinDemo {
	private static int x=0;
	private static int i=0;
	public static void main(String[] args) throws InterruptedException {
		Thread t1 = new Thread(()->{
			i=1;
			x=2;
			
		});
		Thread t2 = new Thread(()->{
			i=x+2;
		});
		t1.start();
		t1.join(); //保证t1线程的执行结果对t2可见(t1线程一定比t2优先执行)
		
		t2.start();
		Thread.sleep(1000);
		System.out.println("result:"+i);
	}

}

4.1.2 Join方法工作流程

底层使用 wait(0) 方法对主线程进行阻塞,当前线程结束之后,才在JVM中使用notify方法唤醒被阻塞的线程。(ensure_join方法中有个lock(notify_all(主线程))去唤醒主线程)。

为什么阻塞的是主线程?

Thread.join的本质其实是wait/notifyall

4.2 Thread.sleep()

4.2.1 示例

sleep的单位是ms,因此1秒=1000ms。

使线程暂停执行一段时间,直到等待的时间结束才恢复执行或在这段时间内被中断(InterruptedException)。

如下述代码所示:

package demoThread;

public class SleepDemo extends Thread {

	public static void main(String[] args) {
		new SleepDemo().start();

	}
	@Override
	public void run() {
		// TODO Auto-generated method stub
		System.out.println("begin:"+System.currentTimeMillis());
		try {
			Thread.sleep(3000); //睡3秒
			System.out.println("end:"+System.currentTimeMillis());
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}
	

}

运行结果:

begin:1617955794912
end:1617955797913

 4.2.2 Thread.sleep的工作流程

  • 挂起线程并修改其运行状态(为阻塞状态)
  • 用sleep()提供的参数来设置一个定时器。
  • 当时间结束,定时器会触发,内核收到中断后修改线程的运行状态。例如线程会被标志为就绪而进入就绪队列等待调度

4.2.3 相关问题

  •  假设现在是 2019-11-18 12:00:00.000,如果我调用一下Thread.Sleep(1000) ,在 2019-11-18 12:00:01.000 的时候,这个线程会不会被唤醒?

会睡眠1秒。但是1s之后,有可能会有其他进程使用cpu资源,此时需要等那个进程结束才可以使用。还可能被其他进程抢占。

  • Thread.Sleep(0) 的意义

其作用类似于:Thread.yeild()。含义是出让CPU,触发操作系统重新进行CPU竞争。

操作系统中,CPU竞争有很多种策略。Unix系统使用的是时间片算法(所有进程排成队列,操作系统会依次分配),而
Windows则属于抢占式(如果一个进程得到了CPU资源,那么除非自己放弃资源,否则会霸占一直到运行结束)的。

4.3 Thread.wait()及notify()

如何实现:一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行响应的操作

4.3.1 生产者消费者示例

在下面的实例中,生产者和消费者共用一个队列,完成消费生产过程。

当bags数量为空时,消费者进入阻塞状态,生产者生产bags,并调用notifyAll方法唤醒阻塞的消费者。

当bags数量达到最大值(size)时,生产者进入阻塞状态,消费者消费bags之后,调用notifyAll方法唤醒阻塞的生产者。

Producer.java 
import java.util.*;
public class Producer implements Runnable{
	private Queue<String> bags;
	private int size;
	public Producer(Queue<String> bags, int size) {
		this.bags = bags;
		this.size = size;
	}
	
	@Override
	public void run() {
		int i=0;
		while(true) {
			i++;
			synchronized (bags) {
				while(bags.size()==size) {
					System.out.println("bags已经满了");
					try {
						bags.wait();
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					
				}
				
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				
				System.out.println("生产者生产bag:"+i);
				bags.add("bag"+i);
				
//				唤醒处于阻塞状态的消费者
				bags.notifyAll();
				
			}
		}
	}
	
	
	
}
Consumer.java
import java.util.Queue;

public class Consumer implements Runnable{
	private Queue<String> bags;
	private int size;
	public Consumer(Queue<String> bags, int size) {
		this.bags = bags;
		this.size = size;
	}
	
	@Override
	public void run() {
		while(true) {
			synchronized (bags) {
				while(bags.isEmpty()) {
					System.out.println("bags为空");
					try {
						bags.wait();
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				String bag = bags.remove();
				System.out.println("消费者消费bag"+bag);
				
//				唤醒处于阻塞状态的生产者
				bags.notifyAll();
			}
		}
	}

}
WaitNotifyDemo.java
import java.util.*;
public class WaitNotifyDemo {

	public static void main(String[] args) {
		Queue<String> queue = new LinkedList<>();
		int size = 10;
		Producer pro = new Producer(queue, size);
		Consumer con = new Consumer(queue, size);
		
		Thread t1 = new Thread(pro);
		Thread t2 = new Thread(con);
		t1.start();
		t2.start();
	}

}

4.3.2 为什么wait/notify需要加synchronized

1.从刚刚的案例来看,其实wait/notify本质上其实是一种条件的竞争,至少来说,wait和notify方法一定是互斥存在的,既然要实现互斥,那么synchronized就是一个很好的解决方法
2. wait和notify是用于实现多个线程之间的通信,而通信必然会存在一个通信的载体,比如我们小时候玩的游戏,用两个纸杯,中间用一根线连接,然后可以实现比较远距离的对话。而这根线就是载体,那么在这里也是一样,wait/notify是基于synchronized来实现通信的。也就是两者必须要在同一个频道也就是同一个锁的范围内

4.4 Thread.interrupted 和 Thread.interrupt(如何正确终止一个线程)

可以尝试使用构建全局变量实现线程间的通信。从而实现终止线程。不能使用stop()方法

import java.util.concurrent.TimeUnit;

public class Stopdemo {
	
	static volatile boolean stop = false;
	public static void main(String[] args) throws InterruptedException {
		Thread t1 = new Thread(new StopThread());
		t1.start();
		TimeUnit.SECONDS.sleep(2);
		stop = true;

	}
	static class StopThread implements Runnable{

		@Override
		public void run() {
			while(!stop) { //while(true)
				System.out.println("持续运行");
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			
		}
		
	}

}

4.4.1 interrupt方法

当其他线程通过调用当前线程的interrupt方法,表示向当前线程打个招呼,告诉他可以中断线程的执行了,至于什么时候中断,取决于当前线程自己。

在JVM中,设置了interrupt这样一个属性。

Thread.currentThread().isInterrupted()默认是flase。

但我们可以通过调用Thread.interrupt();方法来显式加上使interrupt值变为true。

public class StopDemo2 {
	private static int i;
	public static void main(String[] args) {
		Thread thread = new Thread(()->{
			while(!Thread.currentThread().isInterrupted()) { //判断中断标识
				i++;
			}
		});
		thread.start();
		thread.interrupt(); //中断(友好)

	}

}

除了while之外。线程处于阻塞状态下时,中断才有意义。(Join、Wait等

所以除了while循环中判断中断标识来结束循环之外,阻塞时,可以调用中断,当JVM发现某段代码中可能存在阻塞并且在代码外调用了中断时,就会唤醒进程,并抛出 InterruptedException。

如下的这种情况就会抛出InterruptedException异常:

package demoThread;

public class StopDemo2 {
	private static int i;
	public static void main(String[] args) {
		Thread thread = new Thread(()->{
			try {
				Thread.sleep(100000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});
		thread.start();
		thread.interrupt();

	}

}

线程被中断的唯一情形是:sleep里面的run方法结束。

而当while(true)/Join/Wait/Sleep等时,就需要中断来结束线程。

4.4.2 interrupted方法

Thread.interrupted()对设置中断标识的线程复位,并且返回当前的中断状态。

假如Thread.currentThread().isInterrupted()==true,就说明被中断过。

当我们使用Thread.interrupted()方法后,会对中断标识复位,使其状态变为没有中断过。

import java.util.concurrent.TimeUnit;

public class StopDemo2 {
	private static int i;
	public static void main(String[] args) throws InterruptedException {
		Thread thread = new Thread(()->{
			while(true) {
				
				if(Thread.currentThread().isInterrupted()) {
					System.out.println("before:"+Thread.currentThread().isInterrupted());
					Thread.interrupted();  //对中断标志复位
					System.out.println("after:"+Thread.currentThread().isInterrupted());
				}
			}
		});
		thread.start();
		TimeUnit.SECONDS.sleep(1);
		thread.interrupt(); //中断
	}

}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值