高并发与多线程的技术总结

进程与线程、并发与并行

高并发与多线程的技术总结_第1张图片

一个应用程序至少有一个进程,一个进程至少有一个线程

进程:是程序的一次运行活动,是系统资源分配和调度的一个独立单位,有独立的地址空间和系统资源。

线程:是进程的一个实体(轻量级进程),是CPU调度的基本单位。多个线程共享同一个进程的资源。


高并发与多线程的技术总结_第2张图片


并发(Concurrent):当系统只有一个CPU,把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。.这种方式我们称之为并发。(一个人同时喂两个孩子吃饭,每一时刻只能喂一个孩子)


并行(Parallel):当系统有一个以上CPU时,一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(两个人喂两个孩子吃饭,每自分工)



线程的生命周期

高并发与多线程的技术总结_第3张图片


线程的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 arrayList = newArrayList();

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();

}

};


高并发与多线程的技术总结_第4张图片


(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锁。


高并发与多线程的技术总结_第5张图片

这里每一个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工厂类来帮助我们创建各种各样的线程池。下面我们分别介绍一下这三个类。

高并发与多线程的技术总结_第6张图片

你可能感兴趣的:(高并发与多线程的技术总结)