进程与线程、并发与并行
一个应用程序至少有一个进程,一个进程至少有一个线程
进程:是程序的一次运行活动,是系统资源分配和调度的一个独立单位,有独立的地址空间和系统资源。
线程:是进程的一个实体(轻量级进程),是CPU调度的基本单位。多个线程共享同一个进程的资源。
并发(Concurrent):当系统只有一个CPU,把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。.这种方式我们称之为并发。(一个人同时喂两个孩子吃饭,每一时刻只能喂一个孩子)
并行(Parallel):当系统有一个以上CPU时,一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(两个人喂两个孩子吃饭,每自分工)
线程的生命周期
线程的5种状态:
1.新建状态(New) : 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
2.就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
3.运行状态(Running): 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
4.阻塞状态(Blocked) : 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(1)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
(2)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(3)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5. 死亡状态(Dead) : 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
线程五种状态转换:
1、线程的实现有两种方式,一是继承Thread类,二是实现Runnable接口,但不管怎样,当我们new了这个对象后,线程就进入了初始状态;
2、当该对象调用了start()方法,就进入可运行状态;
3、进入可运行状态后,当该对象被操作系统选中,获得CPU时间片就会进入运行状态;
4、进入运行状态后情况就比较复杂了
4.1、run()方法或main()方法结束后,线程就进入终止状态;
4.2、当线程调用了自身的sleep()方法或其他线程的join()方法,就会进入阻塞状态(该状态既停止当前线程,但并不释放所占有的资源)。当sleep()结束或join()结束后,该线程进入可运行状态,继续等待OS分配时间片;
4.3、线程调用了yield()方法,意思是放弃当前获得的CPU时间片,回到可运行状态,这时与其他进程处于同等竞争状态,OS有可能会接着又让这个进程进入运行状态;
4.4、当线程刚进入可运行状态(注意,还没运行),发现将要调用的资源被synchronized(同步),获取不到锁标记,将会立即进入锁池状态,等待获取锁标记(这时的锁池里也许已经有了其他线程在等待获取锁标记,这时它们处于队列状态,既先到先得),一旦线程获得锁标记后,就转入可运行状态,等待OS分配CPU时间片;
4.5、当线程调用wait()方法后会进入等待队列(进入这个状态会释放所占有的所有资源,与阻塞状态不同),进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒(由于notify()只是唤醒一个线程,但我们由不能确定具体唤醒的是哪一个线程,也许我们需要唤醒的线程不能够被唤醒,因此在实际使用时,一般都用notifyAll()方法,唤醒有所线程),线程被唤醒后会进入锁池,等待获取锁标记。
wait()与yield()、sleep()的比较
wait()的作用是让当前线程由“运行状态”进入“等待(阻塞)状态”的同时,也会释放同步锁;而yield()的作用是让步,让线程由“运行状态”进入到“就绪状态”;而sleep()的作用是也是让当前线程由“运行状态”进入到“休眠(阻塞)状态”。
区别:wait()会释放它所持有对象的同步锁,而yield()、sleep()则不会释放锁。
并发包下的常用类
关键字:volatile
共享变量,保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的;修改volatile变量时会强制将修改后的值刷新的主内存中,其他线程工作内存中对应的变量值失效
关键字:synchronized
Java中,提供两种方式实现同步互斥访问(互斥锁):synchronized和Lock
synchronized关键字来标记一个方法或者代码块,当某个线程调用该对象的synchronized方法或者访问synchronized代码块时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,只有等待这个方法执行完毕或者代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法或者代码块
1、修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
情况1
synchronized(this){ //锁实例
//业务逻辑
}
情况2
public synchronized void func(){ //锁实例方法
//业务逻辑
}
2、修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
情况3
synchronized(Test.class){ //锁类
//业务逻辑
}
情况4
public static synchronized void fun(){ //锁静态方法
//业务逻辑
}
3、修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
情况5
private String lock = new String("lock");
synchronized(lock ){ //每个lock对象一把锁
//业务逻辑
}
情况6
private static String lock = new String("lock");
synchronized(lock ){ //所有lock对象都是同一把锁
//业务逻辑
}
接口Lock
Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。(可重入锁:ReentrantLock)
(可利用Tair和Redis实现分布式锁,锁住一个key)
示例:
public class Test {
private ArrayList
private Lock lock = new ReentrantLock(); //实现一个全局的类实例锁
public static void main(String[] args){
final Test test = new Test();
new Thread(){ //线程1
public void run() {
test.insert(Thread.currentThread());
};
}.start();
new Thread(){ //线程2
public void run() {
test.insert(Thread.currentThread());
};
}.start();
}
public void insert(Thread thread) {
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();
}
}
}
类ThreadLocal
ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
例:
publicclass ThreadLocalTest {
ThreadLocal localMap =new ThreadLocal() {
//每个线程下的ThreadLocalMap里有一个Entry的key是localMap
@Override
protected HashMapinitialValue() {
//returnsuper.initialValue();// return null; 会造成NullPointerException
System.out.println(Thread.currentThread().getName()+ " localMap initialValue...");
return new HashMap();
}
};
ThreadLocal localList= new ThreadLocal() {
//每个线程下的ThreadLocalMap里有一个Entry的key是localList
@Override
protected ArrayListinitialValue() {
System.out.println(Thread.currentThread().getName()+ " localList initialValue...");
return new ArrayList();
}
};
(1)ThreadLocal只是操作Thread中的ThreadLocalMap对象的集合;
(2)ThreadLocalMap变量属于线程的内部属性,不同的线程拥有完全不同的ThreadLocalMap变量;
(3)线程中的ThreadLocalMap变量的值是在ThreadLocal对象进行set或者get操作时创建的;
(4)使用当前线程的ThreadLocalMap的关键在于使用当前的ThreadLocal的实例作为key来存储value值;
(5) ThreadLocal模式至少从两个方面完成了数据访问隔离,即纵向隔离(线程与线程之间的ThreadLocalMap不同)和横向隔离(不同的ThreadLocal实例之间的互相隔离);
(6)一个线程中的所有的局部变量其实存通过一个 Entry 保存在同一个map属性中;
(7)当线程拥有的局部变量超过了容量的阀值2/3(没有扩大容量时是10个),会涉及到ThreadLocalMap中Entry的回收;
类ConcurrentHashMap
线程不安全的HashMap
没有操作的原子性(锁机制),多个线程同时检测到总数量超过门限值的时候就会同时调用resize,出现后线程覆盖前,最后一个生效。
效率低下的HashTable容器
HashTable容器使用synchronized来保证线程安全,高并发多线程环境下,效率低下。因为当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态。
锁分段技术ConcurrentHashMap
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。
这里每一个segment所指向的数据结构,其实就是一个Hashtable,所以说每一个segment都有一把独立的锁,来保证当访问不同的segment时互不影响
接口BlockingQueue
BlockingQueue既然是Queue的子接口,生产者/消费者模型;多线程场景下,通过队列实现共享数据,消费者消费到一定程度上的时候,必须要暂停等待一下了(使消费者线程处于WAITING状态),BlockingQueue出现了,提高生产消费效率和线程安全;阻塞队列所谓的"阻塞",指的是某些情况下线程会挂起(即阻塞),一旦条件满足,被挂起的线程又会自动唤醒。使用BlockingQueue,不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,这些内容BlockingQueue都已经做好了
两个实现类,基于数组的阻塞队列ArrayBlockingQueue,基于链表的阻塞队列LinkedBlockingQueue
接口Callable和Future
Callable和Runnable的区别
(1)Callable规定的方法是call(),而Runnable规定的方法是run()。
(2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
(3)call()方法可抛出异常,而run()方法是不能抛出异常的。
(4)运行Callable任务可拿到一个Future对象。
Future的理解
例如我有一个任务,提交给了Future,Future替我完成这个任务。期间我自己可以去做任何想做的事情。一段时间之后,我就便可以从Future那儿取出结果。就相当于下了一张订货单,一段时间后可以拿着提订单来提货,这期间可以干别的任何事情。其中Future接口就是订货单,真正处理订单的是Executor类,它根据Future接口的要求来生产产品。
ExecutorService executor = Executors.newFixedThreadPool(10);
Future future = executor.submit(new Callable(){
public String call() throws Exception{
//真正的任务执行,返回值类型为String
return "";
}
});
try {
String result = future.get();//取得结果,用future.get()
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
扩展:
Guava定义了ListenableFuture接口并继承了JDK concurrent包下的Future 接口,增加了回调方法,对异常、失败和成功的回调处理
ListeningExecutorService executorService =MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
final ListenableFuture listenableFuture =executorService.submit(new Callable() {
@Override
public Integer call() throws Exception{
System.out.println("callexecute..");
TimeUnit.SECONDS.sleep(1);
return 7;
}
});
listenableFuture.addListener(new Runnable() {
@Override
public void run() {
try {
System.out.println("getlistenable future's result " + listenableFuture.get());
} catch (InterruptedException e){
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}, executorService);
Futures.addCallback(listenableFuture, new FutureCallback(){
@Override
public void onSuccess(Integer result){
System.out.println("getlistenable future's result with callback " + result);
}
@Override
public void onFailure(Throwable t) {
t.printStackTrace();
}
});
类CountDownLatch
CountDownLatch是一个同步的辅助类,允许一个或多个线程,等待其他一组线程完成操作,再继续执行。
实现原理:使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成了任务,然后在CountDownLatch上等待的线程就可以恢复执行任务。
public static void main(String[] args) {
//所有线程阻塞,然后统一开始
CountDownLatch begin = new CountDownLatch(1);
//主线程阻塞,直到所有分线程执行完毕
CountDownLatch end = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
begin.await();
System.out.println(Thread.currentThread().getName() + " 起跑");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+ " 到达终点");
end.countDown();
} catch (InterruptedExceptione) {
e.printStackTrace();
}
}
});
thread.start();
}
try {
System.out.println("1秒后统一开始");
Thread.sleep(1000);
begin.countDown();
end.await();
System.out.println("停止比赛");
}catch (InterruptedException e) {
e.printStackTrace();
}
}
注:分线程通过第一个CountDownLatch实现了阻塞,直到主线程调用了countDown()方法,所有分线程才继续执行。
然后主线程通过第二个CountDownLatch实现阻塞,直到所有分线程都调用了countDown()方法。
类CyclicBarrier
CyclicBarrier是一个同步的辅助类,允许一组线程相互之间等待,达到一个共同点,再继续执行。
实现原理:在CyclicBarrier的内部定义了一个Lock对象,每当一个线程调用await方法时,将拦截的线程数减1,然后判断剩余拦截数是否为初始值parties,如果不是,进入Lock对象的条件队列等待。如果是,执行barrierAction对象的Runnable方法,然后将锁的条件队列中的所有线程放入锁等待队列中,这些线程会依次的获取锁、释放锁。
线程池ThreadPoolExecutor
Java中的线程池类有两个,分别是:ThreadPoolExecutor和ScheduledThreadPoolExecutor,这两个类都继承自ExecutorService。利用这两个类,可以创建各种不同的Java线程池,为了方便我们创建线程池,Java API提供了Executors工厂类来帮助我们创建各种各样的线程池。下面我们分别介绍一下这三个类。