进程
进程是一个”执行中的程序”,是系统进行资源分配和调度的一个独立单位。
线程
什么是线程?操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。
线程和进程有什么区别?
线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片内存空间。
实现线程的三种方法
- 继承Thread类
- 实现Runnable接口,重写run()
- 实现Callable接口,重写call()
用Runnable和Thread中哪个?
该类需要继承其他类时选择Runnable,因为java中不支持多继承,但支持多实现
Runnable和Callable有什么区别?
Runnable接口的任务线程无返回值;实现Callable接口的任务线程能返回执行结果
call()可以抛出异常,run()若有异常只能在内部消化。callable接口支持返回执行结果,需要调用Future.get()方法实现,此方法会阻塞主线程直到获取结果,不调用此方法不会阻塞主线程。
public static void main(String[] args) {
//创建核心线程和最大线程数
ExecutorService executorService=Executors.newFixedThreadPool(2);
Callable<String> callable=new Callable<String>(){
@Override
public String call() throws Exception {
System.out.println("Thread_current"+Thread.currentThread());
return "helloworld";
}
};
System.out.println("start");
//执行任务并获取Future对象
Future<String> future = executorService.submit(callable);
try {
System.out.println(future.get());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
Thread类中的start()和run()方法有什么区别?
start()用来启动新创的线程,使进程进入就绪状态,而run()执行线程,使线程进入运行状态。
线程的几种状态
- 新建状态:new
- 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于”可运行线程池”中,变得可运行,只等待CPU的使用权。
- 运行状态(Running):就绪状态的线程获取了CPU
- 阻塞状态(Blocked):线程因为某种原因放弃CPU使用权,暂停运行,知道线程进入就绪状态,才有机会转到运行状态
阻塞的情况分三种:
- 等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入”等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能唤醒。
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入”锁池”中。(抽象理解)
- 其他阻塞:运行的线程执行sleep()或join(),或发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
- 死亡状态
多线程和单线程的区别和联系
答:单线程,在单核CPU中,将CPU分为多个小的时间片,每个时刻只有一个线程在执行,是一种微观的轮流占用CPU的机制
多线程会存在线程上下文切换,会导致程序执行速度变慢,即采用一个拥有两个线程的进程执行所需要的时间比一个线程执行两次所需要的时间要多一些
线程和进程的区别
- 进程是一个”执行中的程序”,是系统进行资源分配和调度的一个独立单位
- 线程是进程的一个实体,一个进程中拥有多个线程,线程之间共享地址空间和其他资源(所以通信和同步操作比进程更加容易)
- 线程上下文的切换比进程上下文切换要快很多
- 进程切换时,涉及到当前进程的CPU环境的保存和新被调度运行进程的CPU环境的设置。
- 线程切换仅需要保存和设置少量的寄存器容器内容,比设计存储管理方面的内容。
多线程产生死锁的4个必要条件
- 互斥条件:一个资源每次只能被一个线程使用
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程已获得的资源在未使用完之前,不能强行剥夺
- 循环条件:若干线程之间形成一种头尾相接的循环等待资源关系
如何避免死锁?
答:指定获取锁的顺序,比如某个线程只有获得A锁和B锁才能对某资源进行操作;在多线程条件下,如何避免死锁,比如规定只有获取A锁的线程才有资格获取B锁,按顺序获取锁就可以避免死锁。
线程的同步和异步
从字面上解释的同步和异步和线程中的同步和异步恰好是相反的。同步机制指,在A线程请求资源时发现资源被B线程占用着,A线程请求不到,只能等待B线程用完释放后才能使用。而异步指的是,A线程请求资源时发现被B线程占着,A线程依然能请求的到,A线程无需等待。
同步是最安全的,但性能低
异步不安全,容易导致死锁,但是性能高
同步和异步的例子:普通B/S模式(同步) AJAX(异步)
线程同步的几种方式
- synchronize关键字修饰语句块或者修饰方法
public class synchronizedClass implements Runnable{
public synchronized void get() {
// TODO Auto-generated method stub
System.out.println(Thread.currentThread().getId());
set();
}
public synchronized void set(){
System.out.println(Thread.currentThread().getId());
}
@Override
public void run() {
// TODO Auto-generated method stub
get();
}
public static void main(String[] args) {
synchronizedClass synclass=new synchronizedClass();
new Thread(synclass).start();
new Thread(synclass).start();
new Thread(synclass).start();
}
}
synchronized是一个关键字,还可以修饰变量、静态方法和代码块
一种同步机制是使用synchronize关键字,这种机制也成为互斥锁机制。
底层原理:通过monitorenter和monitorexit两个指令分别获取锁标记和释放锁标记,用一个字段完成锁的获取与释放。
synchronized(this) 和synchronized(object)的区别:this是对当前类做控制,即保证当前类是线程安全的,而对于非线程安全的类在调用时,为保证其线程安全性可以在调用时使用synchronize(object)确保被调用类的线程安全。
修饰类:锁住所有对象
修饰静态方法:锁住所有方法
修饰普通方法:锁住当前对象
修饰静态代码块:锁住当前对象
synchronized(this)和synchronized(XXX.class):前者锁的是当前对象,后者锁的是类本身
synchronized(this)和synchronized(XXX.this):都是锁住当前对象
- ReentrantLock可重入锁
jdk内置的一个锁对象,可以用来实现同步,基本使用方法如下:
public class ReentrantLockTest {
private ReentrantLock lock=new ReentrantLock();
public void execute(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"do something synchronize");
try {
Thread.sleep(5000l);
} catch (InterruptedException e) {
System.err.println(Thread.currentThread().getName()+"interupted");
e.printStackTrace();
}
} finally{
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockTest reentrantLockTest=new ReentrantLockTest();
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
reentrantLockTest.execute();
}
});
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
reentrantLockTest.execute();
}
});
thread1.start();
thread2.start();
}
}
输出结果: Thread-0 do something synchronize
// 隔了5秒钟 输入下面
Thread-1 do something synchronize
怎么理解可重入锁呢(synchronized也是同样的原理)?意思就是对于同一个线程在被一个锁锁住的情况下依旧可以调用其他的锁,而不会被挂起。底层实现是锁内部维护了一个计数器,对于同一个线程调用lock方法,计数器+1,调用unlock方法,计数器-1。当计数器为0时,则当前锁空闲,可以占用;反之线程进入等待状态。
- volatile关键字修饰变量
volatile是一个很老的关键字,几乎伴随着jdk的诞生而诞生,但开发不建议使用。Thread这个类的线程状态就是用volatile来修饰的。
volatile的第一条语义是保证线程之间变量的可见性,即A线程对变量X进行修改之后,在线程A后执行的其他线程也能看到变量X的变动。实现这个的原理是:线程对变量进行修改之后要立刻写到主内存,线程对变量读取时,要从主内存中读,而不是缓存。
volatile不能保证原子性。其实和volatile的第一条语义有些关系。最典型的例子就是i++:假如线程A对读取的i做了i+1的操作,在还没有赋值的情况下,线程B开始读取i,读到的值为0,执行完毕后又写回主内存,i的值仍为1。
volatile的第二条语义是:禁止指令重排序。
指令重排序(happen-before)
指令重排是一个复杂的,不可思议的问题,以我现在的理解就是同一段代码前面的代码如果执行速度过慢,CPU为了不影响进度可以先执行后面的代码。为什么指令要重排:1.在虚拟机层面,为了尽可能的减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响。2.在硬件层面,CPU会讲接受到的一批指令按照其规则重排序,同样是因为CPU的速度比缓冲速度快。
- 使用ThreadLocal
ThreadLocal是一种把变量放到线程本地的方式来实现线程同步的。
代码实现:
public class ThreadLocalTest {
private static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal=new ThreadLocal<SimpleDateFormat>(){
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
};
};
public static void main(String[] args) {
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
Date date=new Date();
System.out.println(dateFormatThreadLocal.get().format(date));
}
});
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
Date date=new Date();
System.out.println(dateFormatThreadLocal.get().format(date));
}
});
thread1.start();
thread2.start();
}
}
synchronized和ReentrantLock比较:
- ReentrantLock加锁的时候能通过tryLock()设置超时,如果超过这个时间并且没有获取到锁,就会放弃,synchronized没有这种功能。
- ReentrantLock可以使用多个Condition,而synchronize却只能有一个。
Condition对象的await()和singleAll()可以控制线程的执行流程,await()时,当前线程会被挂起,加入到Condition的等待队列,释放锁,直到另一个thread调用了signalAll()方法之后,Condition中等待队列的线程被取出并加入到AQS中,接下来另执行的线程执行完毕后释放锁,原线程已经被唤醒,再继续执行。
- ReentrantLock可以选择公平锁和非公平锁
- ReentrantLock可以获得正在等待线程的个数,计数器等
volatile和synchronized的区别
- volatile本质上是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronize则是锁定当前的变量,只有当前线程可以访问该变量,其他线程被阻塞住
- volatile仅能使用在变量级别,synchronize则可以使用在变量,方法
- volatile仅能实现变量的修改可见性,而synchronized则可以保证变量的修改可见性和原子性
- volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞
终止线程的三种方式
线程正常执行完毕,正常退出
意外死亡
使用interrupt方法终止线程
并发包下的工具类
CountDownLatch
是一个计数器,它的构造器需要设置一个数值,用来设定计数的次数。每次调用countDown()方法之后,这个计数器都会减1,CountDownLatch会一直阻塞着调用await()的线程,直到计数器值变为0。
CyclicBarrier
CylicBarrier阻塞调用的线程,直到条件满足时,阻塞的线程同时被打开。CylicBarrier最有特点的就是这个屏障(barrier)机制,它允许一组线程互相等待,直到达到某个公共屏障点。而barrier在释放等待线程后可以重用,所以称它为循环的barrier。
CyclicBarrier支持一个可选的Runnable命令,在一组线程中的最后一个线程到达之后(但在释放所有线程之前),该命令只在每个屏障点运行一次。若在继续所有参与线程之前更新共享状态,此屏障操作很有用。
CyclicBarrier cyclic1=new CyclicBarrier(5, new Runnable() {
@Override
public void run() {
for (int i = 0; i <10; i++) {
System.out.println(i);
}
}
});
调用await()的时候,这个线程就会被阻塞,当调用await()的线程数量达到屏障数的时候,主线程就会取消所有被阻塞线程的状态
代码实现:
public class CyclicBarrierTest {
public static void main(String[] args) {
Random random=new Random();
//设置屏障数为5
CyclicBarrier cyclic=new CyclicBarrier(5);
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
int secs=random.nextInt(5);
System.out.println(Thread.currentThread().getName()+" "+new Date()+" run,sleep"+secs+"secs");
try {
Thread.sleep(secs*1000);
cyclic.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (BrokenBarrierException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" "+new Date()+"run over");
}
}).start();
}
}
}
相比CountDownLatch,CyclicBarrier是可以被循环使用的,而且遇到线程中断等情况时,还可以利用reset(),重置计数器,从这些方面来说,CyclicBarrier会比CountDownLatch更加灵活一些。
AbstractQueuedSynchronizer
AQS是很多同步工具类的基础,比如ReentrentLock里的公平锁和非公平锁,Semaphore里的公平锁和非公平锁,CountDownLatch里的锁等他们底层都是使用AbstractQueuedSynchronizer完成的。
线程池
线程池是什么?
java.util.concurrent.ThreadPoolExecutor类就是一个线程池。客户端调用ThreadPoolExecutor.submit(Runnable task)提交任务,线程池内部维护的工作者线程的数量就是该线程池的线程池大小,有三种形态:
- 当前线程池大小:表示线程池中实际工作者线程的数量
- 最大线程池大小(maxinumPoolSize):表示线程池中允许存在的工作者线程的数量上限
- 核心线程大小(corePoolSize ):表示一个不大于最大线程池大小的工作者线程数量上限
如果运行的线程少于corePoolSize,则Executor始终首选添加新的线程,而不进行排队
如果运行的线程等于或者多于corePoolSize,则Executor始终首选将请求加入队列,而不是添加新线程
如果无法将请求加入队列,即队列已经满了,则创建新的线程,除非创建此线程超出maxinumPoolSize,在这种情况下,任务将被拒绝。