并发与并行
需要通过不断切换需要运行的线程让其运行的方式叫做并发。
在多cpu系统中,可以让两个以上的线程同时运行,这种方式叫做并行
线程与进程
每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销。一个进程包含1-n个线程
同意类线程共享代码和数据空间。每个线程有独立的运行栈和程序计数器,线程的切换开销小。
多进程是指操作系统可同时运行多个任务(程序)
多线程是指同一程序中有多个顺序流在执行
进程
一个进程对一个程序,每个进程都有一定的内存地址空间,并且只能使用它自己的内存空间,各个进程间互不干扰。并且进程保存了程序每个时刻的运行状态,这样就为进程切换提供了可能。当进程暂停时,他会保存当前进程的状态,在下一次重新切换回来时,便根据保存的状态进行回复,然后继续执行。从而实现并发。
线程
让一个线程去执行一个子任务。也就是说,一个进程包含多个线程,每个线程负责一个独立的子任务。也就是说,进程让并行成为可能,线程让内部并发成为可能。
注意:
一个进程虽然包含多个线程,但是这些线程共享进程战友的资源和地址空间。
进程是操作系统进行资源分配的最小单位,而线程是操作系统进行资源调度的最小单位。
由于多个线程是共同占有所属进程的资源和地址空间的,那么就会出现多个线程同时访问某个资源的同步问题,这就是线程同步问题。
线程和进程的区别
a.地址空间和其他资源: 进程间相互独立,同一进程的个县城间共享。能够进程内的线程在其他进程中不可见。
b.通信: 进程间通信,线程间可直接读写进程数据段来进行通信----需要进程同步和互斥的手段的辅助,以保证数据的一致性。
c.调度和切换: 线程上下文切换要比进程上下文切换快
d,在多线程操作系统中,进程不是一个可执行的实体。
线程对象和线程
线程对象时jvm生成的一个Object子类。线程是cpu分配给这个对象的一个运行过后才能。
线程的实现方式
实现多线程的方式主要有四种:继承Thread类,实现Runnable接口,实现Callable接口通过FutureTask包装器来创建Thread线程,使用ExecutorService、Callable、Future实现有结果的多线程。其中前两种方式线程执行完后没有返回值,后两种方式存在返回值
1. 继承Thread类创建线程
Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方式就是调用Thread类的start方法。start()是一个native方法,它将启动一个线程,并执行run()。这种实现方式通过继承Thread类,并重写run()。
public class My extends Thread{
public void run(){
System.out.println("aaaaa");
}
}
My my = new My();
my.start
2. 实现Runnable接口创建多线程
实现Runnable接口,解决了Java语言单继承的问题。
可以达到资源共享的效果。
public class My extends SuperClass implements Runnable{
public void run(){
System.out.print("sadada");
}
}
My my = new My();
Thread thread = new Thread(my);
thread.start();
//事实上,在给Thread传入Runnable target参数之后,Thread的run(),会调用target.run()
public void run(){
if(target!=null){
target.run();
}
}
3. 实现Callable接口通过FutureTask包装器创建Thread线程
//Callable接口源码,只有一个方法
public interface Callable<V>{
V call() throws Exception;
}
//实例类
public class SomeCallable<V> extends OtherClass implements Callable<V>{
@Override
public V call() throws Exceptin{
System.out.println();
return null;
}
}
//创建并运行
Callable<V> calllable = new SomeCallable<V>();
//使用Callable<V>创建一个FutureTask<V>对象
FutureTask<V> task = new FutureTask<V>(callable);
//FutureTask<V>是一个包装类,它通过接受Callable<V>来创建,它同时实现了Future和Runnable接口
//根据Future创建一个Thread对象
Thread thread = new Thread(task);
thread.start();
4. 使用ExecutorService、Callable、Future创建有返回结果的多线程
ExecutorService、Callable、Future三个接口实际都是属于Executor框架。可返回值得任务必须实现Callable接口。无需返回值的的任务必须实现Runnable接口。执行Callable任务后获取一个Futrue的对象,在该对象上调用get就可以获取Callable任务返回的Object了。
get()方法是阻塞的,也就是说线程无返回结果的时候,get()会一直阻塞等待。
再结合线程池接口ExecutorService就可以实现有返回值的多线程
public class Test{
public static void main(String []aaa) throws Exception{
Date date = new Date();
int taskSize = 5;
//创建一个线程池
ExecutorService pool = Executors.new FixedThreadPool(taskSize);
//创建多个有返回值的任务
List<Future> list = new ArrayList<Future>();
for(int a = 1 ;a < taskSize;a++){
Callable c = new MyCallable(i + "");
//执行任务并获取Future对象
Future f = pool.submit(c);
list.add(f);
}
//关闭线程池
pool.shutdown();
for(Future future : list){
System.out.print(f.get().toString);
}
}
}
class myCallable implement Callable<Object>{
private String taskNum;
MyCallable(String num){
this.taskNum = num;
}
@Override
public Object call() throws Exception{
Thread.sleep(1000);
return null
}
}
上面的代码中的Executors类,提供了一系列的工厂方法用于创建多线程,返回的线程是也都实现了ExecutorService接口。
//创建固定数目线程的线程池
public static ExecutorService newFixedThreadPool(int nThreads);
//创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用,则创建一个新线程并添加到池中。终止并从缓存中一处那些已有60s未被使用的线程。
public static ExecutorServie newCachedThreadPool();
//创建一个单线程化得Executor
public static ExecutorService newSingleThreadExecutor();
//创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来代替Timer类
public static ScheduledExecutorServie newScheduledThreadPool(int corePoolSize);
ExecutorServie提供了submit()方法,传递一个Callable或Runnable,返回Futrue。如果Executor后台线程池还没有完成Callable的计算,这调用返回Future对象的get()方法,知道阻塞计算完成。
Thread和Runnable的区别和联系
Thread创建的线程对象只能被start()一次。
联系
Thread实现了Runnable接口
都需要重写run方法
runnable的优势
- 实现runnable接口,适合多个相同程序代码去处理同一资源,把虚拟cpu(线程)同程序的代码,数据有效的分离。
- 避免Java单继承特性带来的问题。
- 代码可以被多个线程共享,代码和数据都是独立的。当多个线程执行的代码来自同一个类的实例时,就称他们为共享相同的代码。多个线程操作相同的数据,与其代码无关。
runnable实现资源共享
public static void main(String []asdsad){
My m = new My();
//两个Thread对象,包装同一个My线程对象,最终的结果只是打印一遍 5 4 3 2 1 0
new Thread(m).start();
new Thread(m).start();
}
class MyThread implements Runnable{
private int aa = 5;
public void run(){
while(true){
System.out.print("dasdad" + aa--);
if(aa<0){
break;
}
}
}
}
//Thread实现
public static void main(String [] aa){
//创建了两个没有任何关系的线程对象并启动,结果会将 5 4 3 2 1 0 打印两遍
new My().start();
new My().start();
}
class My extends Thread{
private int aa = 5;
@Override
public void run(){
while(true){
sout("sadada"+ aa--);
if(aa<0)
break;
}
}
}
多次start同一个线程会怎么样
同一个线程只能调用start()方法一次,第二次开始跑出IllegalThreadStateException。启动一个线程,需要调用start(),此时,当前线程会被添加到线程组中,进入就绪状态,等待线程调度器的调用,若获取到了资源,则可进入运行状态。
CountDownLatch、CyclicBarrier、Semaphore
CountDownLatch CyclicBarrier两个类似。都可以作为一个计数器,当达到指定数量的线程执行到某一行代码才能继续执行,否则在此处阻塞等待。
CountDownLatch常用方法 countDown() 将CountDownLatch中的参数减一,有一个线程执行到此处则便进行减一操作。当执行到await()时,阻塞等待。当参数减小到零时,所有阻塞的线程,再次开始抢锁,执行剩下的代码。
CyclicBarrier常用方法 await() 当线程执行到此处时,参数减一并进行阻塞操作。
Semaphore 信号量,可以指定同时最多有多少个线程并行执行。availablePermits()获取当前允许并行执行的最大线程数量。其返回值大于一的时候,证明有可用的线程执行位置。调用acquire()方法,获取位置,同时会将其参数减一,之后执行需要执行的代码。执行完毕之后,在finally模块中,调用release()方法,将之前获取的执行权限交回,同时,参数加一。
线程的状态
新建 new
新建一个线程对象 new Thread()
可运行状态 runnable
线程对象创建后,其他线程调用了该对象的start()方法,该线程就进入了可运行线程池中,等待被线程调用选中,获取CPU的使用权
运行 running
可运行状态的线程获取到CPU时间片之后,执行程序代码
阻塞 block
阻塞状态是指线程因为某种原因放弃CPU使用权,暂时停止执行。直到线程进入可运行状态,才有机会再次会的CPU时间片转到运行状态。
阻塞分为三种:
等待阻塞:运行的线程执行wait()方法。JVM会把该线程放入等待队列中。
同步阻塞:运行的线程在获取对象的同不锁的时候,如果该同步锁被别的线程占用,则jvm会将该线程放入锁池中
其他阻塞:运行的线程执行Thread.sleep(long s) 或者t2.join()方法,或者发出I/O请求时,jvm会将该线程置为阻塞状态。当sleep状态超时,join等待线程终止或超时,i/o处理完毕时,线程重新进入可运行状态。
死亡 dead
线程run方法执行完毕,或者因为异常退出run方法,则该线程生命周期结束。死亡的线程不可再生
常见的线程池
线程之间的区别和联系
线程池的实现原理
多线程同步,锁
synchronized和ReentrantLock的区别
synchronized锁普通方法和静态方法
死锁的原理和排查方法
用户线程和守护线程
用户线程用来处理业务逻辑,守护线程主要用来进行一些监控等操作
GC线程
常见面试题
1.假如有Thread1、Thread2、Thread3、Thread4四条线程分别统计C、D、E、F四个盘的大小,所有线程都统计完毕交给Thread5线程去做汇总,应当如何实现?
,java.util.concurrent
线程组ThreadGroup
ThreadGroup实现了Thread.UncaughtExceptionHandler接口,所以可以extends定义自己的线程组,并重写异常处理方法。以此来达到同一类型线程的分组和统一的线程异常捕获处理。