标签:play map对象 阶段 主题 cin 设计 header 数据量 分发
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。Java 虚拟机允许应用程序并发地运行多个执行线程。
Thread类:该类的对象代表一个线程
主要方法:
Start()方法: Java虚拟机调用该线程的 run 方法,使该线程开始执行(启动线程)
sleep()方法: 限时等待 休眠,sleep有时间
join()方法: 当前线程进入等待状态,没有时间,要等到该线程终止。
①继承Thread类重写run方法
②实现Runnable接口,实现run方法
③实现Callable接口,新建当前类对象
当多线程共同访问同一个对象(临界资源)的时候, 如果破坏了不可分割的操作(原子操作),就可能发生数据不一致,有可能出现多个线程先后更改数据,造成所得到的数据是脏数据
解决方法:用锁。
在Java中通常实现锁有两种方式,一种是synchronized关键字,另一种是Lock。
①使用 synchronized。必须要获取当前对象的互斥锁标记,如果得不到就被阻塞,直到得到互斥锁标记。线程执行完同步方法,会自动归还互斥锁标记
②使用Lock。 Lock接口的常用实现类 ReentrantLock /ri?‘entr?nt/ :互斥锁
两者的区别:
①首先最大的不同:synchronized是基于JVM层面实现的,而Lock是基于JDK层面实现的。
②synchronized是一个关键字,Lock是一个接口.
③synchronized代码块执行完成之后会自动释放锁对象,Lock必须手动调用方法释放锁对象。
④synchronized代码块出现了异常也会自动释放锁对象,Lock接口中出现异常也需要手动释放锁对象。
⑤在并发量比较小的情况下,使用synchronized;但是在并发量比较高的情况下,其性能下降会很严重,此时推荐使用ReentrantLock。
package day20; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class TestLock { public static void main(String[] args) throws Exception { MyList2 list = new MyList2(); Thread t1 = new Thread(new Runnable(){ public void run(){ list.add("C"); } }); Thread t2 = new Thread(new Runnable(){ public void run(){ list.add("D"); } }); t1.start(); t2.start(); t1.join(); t2.join(); list.add("E"); list.print(); } } class MyList2{ String[] data = {"A","B","","","",""}; int index = 2; Lock lock = new ReentrantLock();//Lock接口,ReentrantLock为实现类 public void add(String s){ try{ lock.lock();//加锁 //lock.tryLock();尝试加锁,失败时返回false,此时可进行其他操作,但有可能造成活锁。 data[index] = s ; try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } index++; } finally{ lock.unlock();//释放锁,为了避免锁内的代码块出现异常后直接返回而没有释放锁的问题,将此句代码放到Finally中 } } public void print(){ for(int i = 0 ; i < data.length ; i++){ System.out.println(data[i]); } } }
o.notify()/o.notifyAll():从等待状态中释放一个/全部线程
以上三个方法必须出现在对o加锁的同步代码块中,
1 永远在synchronized的方法或对象里使用wait、notify和notifyAll,不然Java虚拟机会生成 IllegalMonitorStateException。
2 永远在while循环里而不是if语句下使用wait。这样,循环会在线程睡眠前后都检查wait的条件,并在条件实际上并未改变的情况下处理唤醒通知。
3 永远在多线程间共享的对象(在生产者消费者模型里即缓冲区队列)上使用wait。
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class TestProducerConsumer { public static void main(String[] args) { MyStack stack = new MyStack(); Runnable task1 = new Runnable(){ public void run(){ for(char c = ‘A‘ ; c<=‘Z‘ ; c++){ stack.push(c+""); } } }; Runnable task2 = new Runnable(){ public void run(){ for(int i = 1 ; i <= 26; i++){ stack.pop(); } } }; new Thread(task1).start(); new Thread(task1).start(); new Thread(task2).start(); new Thread(task2).start(); } } class MyStack{ String[] data = {"","","","","",""}; int index; Lock lock = new ReentrantLock(); Condition full = lock.newCondition();//获得Condition实例 Condition empty = lock.newCondition(); public void push(String s){ try { lock.lock(); while (data.length == index) { try { full.await();//(不符合条件的等待)满了即等待 } catch (InterruptedException e) { e.printStackTrace(); } } System.out.print(s + " pushed "); data[index] = s; index++; print(); empty.signalAll();//通知消费者 } finally{ lock.unlock(); } } public void pop(){ try { lock.lock(); while (index == 0) { try { empty.await(); } catch (InterruptedException e) { e.printStackTrace(); } } index--; String o = data[index]; data[index] = ""; System.out.print(o + " poped "); print(); full.signalAll(); //通知生产者 } finally{ lock.unlock(); } } public void print(){ for(int i = 0 ; i < data.length ; i++){ System.out.print(data[i]+" "); } System.out.println(); } }
条件(Conditio也称为条件队列或条件变量 )为一个线程的暂停执行(“等待”)提供了一种方法,直到另一个线程通知某些状态现在可能为真。
Condition取代了对象监视器方法的使用。可以使用两个Condition实例来实现
一个Condition实例本质上绑定到一个锁。 要获得特定Condition实例的Condition实例,使用其newCondition()方法。
void | await() 导致当前线程等到发信号或 interrupted 。 |
---|---|
void | signal() 唤醒一个等待线程。 |
void | signalAll() 唤醒所有等待线程。 |
例如,假设我们有一个有限的缓冲区,它支持put
和take
方法。 如果在一个空的缓冲区尝试一个take
,则线程将阻塞直到一个项目可用; 如果put
试图在一个完整的缓冲区,那么线程将阻塞,直到空间变得可用。 我们希望在单独的等待集中等待put
线程和take
线程,以便我们可以在缓冲区中的项目或空间可用的时候使用仅通知单个线程的优化。 这可以使用两个Condition
实例来实现。
class BoundedBuffer { final Lock lock = new ReentrantLock(); final Condition notFull = lock.newCondition(); final Condition notEmpty = lock.newCondition(); final Object[] items = new Object[100]; int putptr, takeptr, count; public void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) notFull.await();//满了,put等待 items[putptr] = x; if (++putptr == items.length) putptr = 0; ++count; notEmpty.signal();//唤醒take } finally { lock.unlock(); } } public Object take() throws InterruptedException { lock.lock(); try { while (count == 0) notEmpty.await();//空了,take等待 Object x = items[takeptr]; if (++takeptr == items.length) takeptr = 0; --count; notFull.signal();//唤醒put return x; } finally { lock.unlock(); } } }
public class TestNumberCharPrint { public static void main(String[] args) throws InterruptedException { final Object o = new Object();//全局对象,用于分别不同时间拿到锁标记来交替 Runnable task1 = new Runnable(){ public void run(){ synchronized (o) {//加锁保证此处原子操作 for (int i = 1; i <= 52; i++) { System.out.println(i); if (i % 2 ==0){ o.notifyAll();//释放字母线程 try { if(i!=52) o.wait();//若等于52时进入等待,则此线程已完成全部任务单还没结束 } catch (InterruptedException e) { e.printStackTrace(); } } } } } }; Runnable task2 = new Runnable(){ public void run(){ synchronized (o) { for (char c = ‘A‘; c <= ‘Z‘; c++) { System.out.println(c); o.notifyAll();//释放数字线程 try { if (c!=‘Z‘) o.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }; Thread t1 = new Thread(task1); Thread t2 = new Thread(task2); t1.start(); Thread.sleep(1); t2.start(); } }
Lock接口的一个实现类
ReadWriteLock维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。
读锁和写锁不能被同时加载,写锁加载则不能读,读锁加载则不能写。
若写锁未被加载,读取锁可以多个读线程同时保持,
若读锁未被加载,写入锁也是独占的,不能同时写。
package day20; import java.util.ArrayList; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class TestMyList { public static void main(String[] args) { CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(); list.add("A"); } } //改造ArrayList为线程安全的(部分方法,通过加读或写锁来实现) class MyList extends ArrayList{ ReadWriteLock rwl = new ReentrantReadWriteLock();//读写锁 Lock rl = rwl.readLock();//读锁 Lock wl = rwl.writeLock();//写锁 @Override public int size() { try{ rl.lock(); return super.size(); } finally{ rl.unlock(); } } @Override public Object get(int index) { try{ rl.lock(); return super.get(index); } finally{ rl.unlock(); } } @Override public boolean add(Object e) { try{ wl.lock(); return super.add(e); } finally{ wl.unlock(); } } @Override public Object remove(int index) { try{ wl.lock(); return super.remove(index); } finally{ wl.unlock(); } } @Override public void clear() { try{ wl.lock(); super.clear(); } finally{ wl.unlock(); } } }
CopyOnWriteArrayList 利用复制数组的方式实现数组元素的修改, 写效率低 读效率高(读操作远多于写操作) 总体效率提高
CopyOnWriteArraySet 线程安全的Set集合
ConcurrentHashMap 分段锁,将HashMap的数组链表分为16段,多个线程读取和写入同一段时,需依次进行(需等待),读取或写入不同段时互不影响,由于HashCode相等的概率不大,所以效率远高于HashTable。
ConcurrentLinkedQueue 线程安全的队列(链表实现的) 利用一个无锁算法(CAS,和预期值比较,不同则重试)实现线程安全——效率高
常用方法:
add() :添加元素
offer():添加元素 优先使用
remove():删除元素
poll ():删除元素,优先使用
element():获取队列的头元素
peek():获取队列的头元素 优先使用
实现类:LinkedList ConcurrentLinkedQueue
put () 添加元素到队列中 如果队列满,则等待
take()删除队列头元素 , 如果队列空,则等待
实现类:ArrayBlockingQueue 数组实现 有界队列 put方法可能会等待
LinkedBlockingQueue 链表实现 无界队列 put方法不等待
package day21; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class TestBlockingQueue { public static void main(String[] args) { BlockingQueue<String> queue = new ArrayBlockingQueue<String>(6);//队列 Runnable task1 = new Runnable(){ public void run(){ for(int i = 1 ; i<= 100; i++){ try { queue.put("A"+i); } catch (InterruptedException e) { e.printStackTrace(); } } } }; Runnable task2 = new Runnable(){ public void run(){ for(int i = 1 ; i<= 100; i++){ try { queue.take(); } catch (InterruptedException e) { e.printStackTrace(); } } } }; ExecutorService es = Executors.newFixedThreadPool(2); es.submit(task1); es.submit(task2); es.shutdown(); } }
Collection |- List (ArrayList LinkedList Vector CopyOnWriteArrayList) |- Set (HashSet LinkedHashSet CopyOnWriteArraySet) |- SortedSet (TreeSet) |- Queue(LinkedList ConcurrentLinkedQueue) |- BlockingQueue (ArrayBlockingQueue LinkedBlockingQueue)
Map (HashMap LinkedHashMap Hashtable Properties ConcurrentHashMap ) |- SortedMap (TreeMap)
i++是先把数i读到另外一个寄存器,加1运算后再写回到原寄存器,中间过程被另外一个线程打断时就不是原子操作了,会造成结果不一致
package day21; import java.util.concurrent.atomic.AtomicInteger; public class TestAtomicInteger { static int i = 0 ; //error! static AtomicInteger a = new AtomicInteger(0);//利用AtomicInteger解决,还有AtomicBoolean等,利用了不加锁的比较算法(不是预期值时,撤回,重新加) static Integer b = Integer.valueOf(0);//error!无临界资源,因为Integer+1后成为了另外一个对象Integer,可以定义其他类型的对象来加锁,如下列的obj static MyObject obj = new MyObject(); public static void main(String[] args) throws Exception{ Thread[] ts = new Thread[10]; for(int k = 0 ; k<ts.length ; k++){ ts[k] = new Thread(new Runnable(){ public void run(){ for(int k = 1 ; k <= 10000; k++){ i++; a.incrementAndGet(); synchronized(b){ b = Integer.valueOf(b.intValue()+1); } synchronized(obj){ obj.x++; } } } }); ts[k].start(); } for(int k=0; k <ts.length ; k++){ ts[k].join(); } System.out.println(i); System.out.println(a); System.out.println(b); System.out.println(obj.x); } } class MyObject{ public int x=0; }
Fork/Join框架是Java7提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
Fork就是把一个大任务切分为若干子任务并行的执行,Join就是合并这些子任务的执行结果,最后得到这个大任务的结果。Fork/Join的运行流程图如下:
工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。工作窃取的运行流程图如下:
优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。
ForkJoinPool是ExecutorService的实现类,因此是一种特殊的线程池。ForkJoinPool提供了如下两个常用的构造器。
public ForkJoinPool(int parallelism):创建一个包含parallelism个并行线程的ForkJoinPool public ForkJoinPool() :以Runtime.getRuntime().availableProcessors()的返回值作为parallelism来创建ForkJoinPool 创建ForkJoinPool实例后,可以调用ForkJoinPool的
submit(ForkJoinTask<T> task)或者invoke(ForkJoinTask<T> task)来执行指定任务。其中ForkJoinTask代表一个可以并行、合并的任务。ForkJoinTask是一个抽象类,它有两个抽象子类:RecursiveAction和RecursiveTask。
RecursiveTask代表有返回值的任务————join()方法来启动RecursiveAction代表没有返回值的任务。————fork()方法来启动
Recursive 递归的,循环的 fork叉,搬走
RecursiveTask的例子:
import java.util.concurrent.ForkJoinPool; import java.util.concurrent.RecursiveTask; public class TestForkJoinAdd { public static void main(String[] args) { System.out.println(Runtime.getRuntime().availableProcessors()); ForkJoinPool pool = new ForkJoinPool();//线程池 AddTask main = new AddTask(1 , 100000);//任务 Long result = pool.invoke(main); //<T> T invoke(ForkJoinTask<T> task) 执行给定的任务,在完成后返回其结果。 System.out.println(result); } } class AddTask extends RecursiveTask<Long>{ int start; int end; static final int THRESHOLD = 1000; public AddTask(int start, int end) { super(); this.start = start; this.end = end; } @Override public Long compute() { //如果start和end之间差距低于THRESHOLD 直接计算,THRESHOLD是定下的是否分割任务的临界值 if (end - start <= THRESHOLD){ long result = 0 ; for(int i = start ; i<= end ; i++){ result += i; } return result; } //否则 就要把任务分割为两个子任务 else{ int middle = (start+end)/2; AddTask task1 = new AddTask(start , middle); AddTask task2 = new AddTask(middle+1 , end); invokeAll(task1 , task2); long r1 = task1.join(); long r2 = task2.join(); return r1+r2; } } }
Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量(控制线程的数量)。
方法 acquire( int permits ) 参数作用,及动态添加 permits 许可数量
acquire( int permits ) 中的参数是什么意思呢? new Semaphore(6) 表示初始化了 6个通路, semaphore.acquire(2) 表示每次线程进入将会占用2个通路,semaphore.release(2) 运行时表示归还2个通路。没有通路,则线程就无法进入代码块。
void | acquire() 从该信号量获取许可证,阻止直到可用,或线程为 interrupted 。 |
---|---|
void | acquire(int permits) 从该信号量获取给定数量的许可证,阻止直到所有可用,否则线程为 interrupted 。 |
void | release() 释放许可证,将其返回到信号量。 |
---|---|
void | release(int permits) 释放给定数量的许可证,将其返回到信号量。 |
package day21; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicBoolean; public class TestSemaphore { public static void main(String[] args) { List<PhoneRoom> rooms = new ArrayList<>(); rooms.add(new PhoneRoom("Room 1")); rooms.add(new PhoneRoom("Room 2")); rooms.add(new PhoneRoom("Room 3")); rooms.add(new PhoneRoom("Room 4")); rooms.add(new PhoneRoom("Room 5")); Semaphore s = new Semaphore(5); class Task implements Runnable{ public void run(){ try { s.acquire(); } catch (InterruptedException e1) { e1.printStackTrace(); } for(int i = 0 ; i < rooms.size() ; i++){ PhoneRoom room = rooms.get(i); if (room.isFree()){ room.setFree(false); System.out.println(Thread.currentThread().getName()+" entered "+room.getName()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" exited "+room.getName()); room.setFree(true); s.release(); return; } } } } for(int i = 1 ; i <= 10 ; i++){ Runnable task = new Task(); Thread t = new Thread(task); t.start(); } } } class PhoneRoom{ AtomicBoolean isFree = new AtomicBoolean(true);//原子操作的boolean,只允许一个线程拿到 String name; public PhoneRoom(String name) { super(); this.name = name; } public boolean isFree() { return isFree.get(); } public void setFree(boolean flag) { this.isFree.set(flag); } public String getName(){ return name; } }
Package java.util.concurrent.atomic
一个小型工具包,支持单个变量上的无锁线程安全编程。
常用:
AtomicBoolean | 一个 boolean 值可以用原子更新。 |
---|---|
AtomicInteger | 可能原子更新的 int 值。 |
java.util.concurrent.CountDownLatch
允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。
A CountDownLatch
用给定的计数初始化。 await
方法阻塞,直到由于countDown()
方法的调用而导致当前计数达到零,之后所有等待线程被释放,并且任何后续的await
调用立即返回。 这是一个一次性的现象 - 计数无法重置。 如果您需要重置计数的版本,请考虑使用CyclicBarrier
CountDownLatch是一个线程计数器,在创建线程的时候可以设置任务数量 每执行完一个线程 调用方法让计数器减一 如果计数器减为了0 主线程再向下执行。
CountDownLatch主要有两个方法:countDown()和await()。countDown()方法用于使计数器减一,一般是执行任务的线程调用,await()方法则使调用该方法的线程处于等待状态,一般是主线程调用。
package day21; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; public class TestCountDownLatch { public static void main(String[] args) { CountDownLatch cdl = new CountDownLatch(2); CyclicBarrier cb = new CyclicBarrier(3); Thread t1 = new Thread(){ public void run(){ for(int i = 1 ; i <= 100 ; i++){ System.out.println("### "+i); if (i == 50) cdl.countDown(); try { Thread.sleep(100); if (i == 99) cb.await(); } catch (Exception e) { e.printStackTrace(); } } } }; Thread t2 = new Thread(){ public void run(){ for(int i = 1 ; i <= 100 ; i++){ System.out.println("$$$ "+i); if (i == 50) cdl.countDown(); try { Thread.sleep(100); if (i==99) cb.await(); } catch (Exception e) { e.printStackTrace(); } } } }; Thread t3 = new Thread(){ public void run(){ try { cdl.await(); } catch (InterruptedException e1) { e1.printStackTrace(); } for(int i = 1 ; i <= 100 ; i++){ System.out.println("*** "+i); try { Thread.sleep(100); if (i==99) cb.await(); } catch (Exception e) { e.printStackTrace(); } } } }; t1.start(); t2.start(); t3.start(); } }
java.lang.ThreadLocal<T>
ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。
它只有4个方法 :
T | get() 返回当前线程的此线程局部变量的副本中的值。 |
---|---|
protected T | initialValue() 返回此线程局部变量的当前线程的“初始值”。 |
void | remove() 删除此线程局部变量的当前线程的值。 |
void | set(T value) 将当前线程的此线程局部变量的副本设置为指定的值。 |
static <S> ThreadLocal<S> | withInitial(Supplier<? extends S> supplier) Creates a thread local variable. |
set(T value)和T get()分别为设置和获得当前线程的线程局部变量的值。
remove()是将当前线程局部变量的值删除,目的是为了减少内存的占。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
T initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
ThreadLocal是如何做到为每一个线程维护变量的副本的呢?
其实实现的思路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。 我们自己就可以提供一个简单的实现版本:
package com.test; public class TestNum { // ①通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值 private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() { public Integer initialValue() { return 0; } }; // ②获取下一个序列值 public int getNextNum() { seqNum.set(seqNum.get() + 1); return seqNum.get(); } public static void main(String[] args) { TestNum sn = new TestNum(); // ③ 3个线程共享sn,各自产生序列号 TestClient t1 = new TestClient(sn); TestClient t2 = new TestClient(sn); TestClient t3 = new TestClient(sn); t1.start(); t2.start(); t3.start(); } private static class TestClient extends Thread { private TestNum sn; public TestClient(TestNum sn) { this.sn = sn; } public void run() { for (int i = 0; i < 3; i++) { // ④每个线程打出3个序列值 System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn[" + sn.getNextNum() + "]"); } } } }
ThreadLocal和线程同步机制相比有什么优势呢?ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。
在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。
由于ThreadLocal中可以持有任何类型的对象,低版本JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK 5.0通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用,
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
Spring使用ThreadLocal解决线程安全问题我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。
一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图所示:
同一线程贯通三层这样你就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有关联的对象引用到的都是同一个变量。
/** * Sets the current thread‘s copy of this thread-local variable * to the specified value. Most subclasses will have no need to * override this method, relying solely on the {@link #initialValue} * method to set the values of thread-locals. * * @param value the value to be stored in the current thread‘s copy of * this thread-local. */ public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
ThreadLocal类可以让你创建的变量只被同一个线程进行读和写操作,线程与线程之间独立,单一线程内共享数据,是线程安全的,
①作用:单一线程内共享数据。
? ThreadLocal可以操作线程内部的Map,可以存取值,而在web环境下一个业务操作过程中的类与方法的调用都是处于一个线程内部的,那么就可以使用ThreadLocal
? 将一个对象存入当前Thread,然后处于当前线程下的任意类和任意方法中获得该对象。
? 2.TreadLocal原理:
? ThreadLocal是如何操作Thread当中的Map对象的呢?
? ThreadLocal#set(Value):
? 把当前的ThreadLocal作为key,将Value作为值 存储到当前线程的Map属性中了。
? ThreadLocal#get() :
? 将ThreadLocal本身作为key 去查询Thread的Map对象。
? ThreadLcoal#remove:
? 将ThreadLocal本身作为key 重Thread 的Map属性删除
· 原子性原子,即一个不可再被分割的颗粒。在Java中原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
· 有序性程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
· 可见性当多个线程访问同一个变量时,如果其中一个线程对其作了修改,其他线程能立即获取到最新的值。
创建,就绪,运行,阻塞,死亡
New、start()、run()、sleep或join、run执行完,或者遇到异常
sleep是Thread类的方法
wait是object类的方法,进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒
主线程创建并启动子线程,如果子线程中需要进行大量的耗时计算,主线程往往早于子线程结束。这时,如果主线程想等待子线程执行结束之后再结束,比如子线程处理一个数据,主线程要取得这个数据,就要调用join() 方法(谁需要谁等待)。
sleep(long)方法在睡眠时不释放对象锁,
join(long)方法在等待的过程中释放对象锁
wait和sleep的区别
wait | sleep |
---|---|
wait()方法是Object类里的方法 | sleep()是Thread类的static(静态)的方法 |
wait()睡眠时,释放对象锁 | sleep()睡眠时,保持对象锁,仍然占有该锁 |
常用于线程间通信 | 常用于暂停执行 |
wait和notify/notifyAll是成对出现的, 必须在synchronize块中被调用 |
阻塞状态实际上是一种比较特殊的等待状态,
处于其他等待状态的线程是在等着别的线程执行结束,等着拿CPU的使用权;而处于阻塞状态的线程等待的不仅仅是CPU的使用权,主要是锁标记,没有拿到锁标记,即便是CPU有空也没有办法执行。(关于锁见下节:线程同步)
等待和阻塞的区别
等待 | 阻塞 |
---|---|
已经拿到锁对象,或者说不存在拿不到执行不了的情况 | 等待拿到锁对象 |
等待被唤醒 | 等待拿到锁对象 |
已经终止的线程会处于该种状态。
· 悲观锁:每次操作都会加锁,会造成线程阻塞。
· 乐观锁:每次操作不加锁,而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止,不会造成线程阻塞。
在一个方法中执行多个操作(3个以上)
单个任务执行时间都很长
多任务并行执行的代码思路
进入service方法之后
创建三个线程
三个线程分别执行三个查询
需要得到查询结果封装一个map中统一返回
创建线程的方式
①Thread
②Runable
③Callable
3.主线程等待
Future + Callable
CountDownLatch 类
可以认为是一个线程计数器 在创建线程的时候可以设置任务数量 每次执行完一个线程 调用方法让计数器减一 如果计数器减为了0 主线程向下执行
java.util.concurrent
All Known Subinterfaces:
所有已知实现类:
AbstractExecutorService , ForkJoinPool , ScheduledThreadPoolExecutor , ThreadPoolExecutor
提供了一种将任务提交从每个任务的运行机制分解的方式,包括线程使用,调度等的Executor
。通常使用Executor而不是显式创建线程。
例如,不是为一组任务调用new Thread(new(RunnableTask())).start()
,您可以使用:
Executor executor = anExecutor;
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());
方法:void execute(Runnable command)
在将来的某个时间执行给定的命令。 该命令可以在一个新线程,一个合并的线程中或在调用线程中执行,由Executor实现。 command - 可运行的任务
concurrent ,该包中提供的Executor
实现了ExecutorService
,这是一个更广泛的界面。 ThreadPoolExecutor
类提供了一个可扩展的线程池实现。 Executors
类为这些执行人员提供了方便的工厂方法。
ThreadPoolExecutor是线程池的核心类,此类的构造方法如下:
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue); public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory); public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler); public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
corePoolSize:核心线程池的大小,如果核心线程池有空闲位置,这时新的任务就会被核心线程池新建一个线程执行,执行完毕后不会销毁线程,线程会进入缓存队列等待再次被运行。
maximunPoolSize:线程池能创建最大的线程数量。如果核心线程池和缓存队列都已经满了,新的任务进来就会创建新的线程来执行。但是数量不能超过maximunPoolSize,否侧会采取拒绝接受任务策略,我们下面会具体分析。
keepAliveTime:非核心线程能够空闲的最长时间,超过时间,线程终止。这个参数默认只有在线程数量超过核心线程池大小时才会起作用。只要线程数量不超过核心线程大小,就不会起作用。
unit:时间单位,和keepAliveTime配合使用。
workQueue:缓存队列,用来存放等待被执行的任务。
threadFactory:线程工厂,用来创建线程,一般有三种选择策略。
ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;
handler:拒绝处理策略,线程数量大于最大线程数就会采用拒绝处理策略,四种策略为
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
二.线程池实现原理
线程池图:
1.线程池状态
线程池和线程一样拥有自己的状态,
在ThreadPoolExecutor类中定义了一个volatile变量runState来表示线程池的状态,线程池有四种状态,分别为RUNNING、SHUTDOWN、STOP、TERMINATED。
线程池创建后处于RUNNING状态。
调用shutdown后处于SHUTDOWN状态,线程池不能接受新的任务,会等待缓冲队列的任务完成。
调用shutdownNow后处于STOP状态,线程池不能接受新的任务,并尝试终止正在执行的任务。
当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。
总结:
如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;
如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;
如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。
Executors是工具类,里面全是静态方法,直接通过类名来调用。
不定长的线程池——newCachedThreadPool()线程数有任务数决定,不够时新建线程,结束后所有线程不销毁
定长的线程池——newFixedThreadPool(2)创建的线程池线程数量固定
package day20; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class TestExecutor { public static void main(String[] args) { //ExecutorService es = Executors.newFixedThreadPool(2); //Executors是工具类,里面全是静态方法,直接通过类名来调用newFixedThreadPool(2)创建的线程池是固定的线程数,结束后所有线程不销毁 ExecutorService es = Executors.newCachedThreadPool(); //newCachedThreadPool()创建的是不定长的线程池,线程数有任务数决定,不够时新建线程,结束后所有线程不销毁 Runnable r1 = new Runnable(){ public void run(){ for(int i = 1 ; i <= 100 ; i++){ System.out.println("### "+i); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }; Runnable r2 = new Runnable(){ public void run(){ for(int i = 1 ; i <= 100 ; i++){ System.out.println("$$$ "+i); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }; Runnable r3 = new Runnable(){ public void run(){ for(int i = 1 ; i <= 100 ; i++){ System.out.println("*** "+i); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }; es.submit(r1);//线程池的提交方法,将线程对象提交来执行 es.submit(r2); es.submit(r3); es.shutdown();//关闭线程池 } }
此接口有个V call()方法,返回值V是泛型,返回值用Future对象来接收。Future中有个方法有get()方法可以拿到返回值。
Callable有返回值,可抛异常,在多线程并发的时候,异步通信,利用Future来接收返回值
package day20; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class TestCallable { public static void main(String[] args) throws Exception{ ExecutorService es = Executors.newCachedThreadPool(); Callable<Integer> task1 = new Callable<Integer>(){ //Callable接口 public Integer call() throws Exception{ //方法call相当于run方法,只不过有返回值 System.out.println("task1 start working"); int result = 0 ; for(int i = 1 ; i < 100 ; i+=2){ result += i; Thread.sleep(100); } System.out.println("task1 end working"); return result; } }; Callable<Integer> task2 = new Callable<Integer>(){ public Integer call() throws Exception{ System.out.println("task2 start working"); int result = 0 ; for(int i = 2 ; i <= 100 ; i+=2){ result += i; Thread.sleep(100); } System.out.println("task2 end working"); return result; } }; Future<Integer> f1 = es.submit(task1);//返回值对象用Future来接收 Future<Integer> f2 = es.submit(task2); System.out.println("main do sth"); int result = f1.get()+f2.get(); System.out.println(result); es.shutdown(); } }
第二,线程中的基本概念,线程的生命周期
第三,单线程和多线程
第四,线程池的原理解析
第五,常见的几种线程池的特点以及各自的应用场景
一、
线程,程序执行流的最小执行单位,是行程中的实际运作单位,
线程和进程究竟有什么区别呢?
首先,进程是一个动态的过程,是一个活动的实体。简单来说,一个应用程序的运行就可以被看做是一个进程,
而线程,是运行中的实际的任务执行者。可以说,进程中包含了多个可以同时运行的线程。
二、
线程的生命周期,线程的生命周期可以利用以下的图解来更好的理解:
第一步,是用new Thread()的方法新建一个线程,在线程创建完成之后,线程就进入了就绪(Runnable)状态,此时创建出来的线程进入抢占CPU资源的状态,当线程抢到了CPU的执行权之后,线程就进入了运行状态(Running),当该线程的任务执行完成之后或者是非常态的调用的stop()方法之后,线程就进入了死亡状态。而我们在图解中可以看出,线程还具有一个则色的过程,这是怎么回事呢?当面对以下几种情况的时候,容易造成线程阻塞,第一种,当线程主动调用了sleep()方法时,线程会进入则阻塞状态,除此之外,当线程中主动调用了阻塞时的IO方法时,这个方法有一个返回参数,当参数返回之前,线程也会进入阻塞状态,还有一种情况,当线程进入正在等待某个通知时,会进入阻塞状态。那么,为什么会有阻塞状态出现呢?我们都知道,CPU的资源是十分宝贵的,所以,当线程正在进行某种不确定时长的任务时,Java就会收回CPU的执行权,从而合理应用CPU的资源。我们根据图可以看出,线程在阻塞过程结束之后,会重新进入就绪状态,重新抢夺CPU资源。这时候,我们可能会产生一个疑问,如何跳出阻塞过程呢?又以上几种可能造成线程阻塞的情况来看,都是存在一个时间限制的,当sleep()方法的睡眠时长过去后,线程就自动跳出了阻塞状态,第二种则是在返回了一个参数之后,在获取到了等待的通知时,就自动跳出了线程的阻塞过程
三、
什么是单线程和多线程?
单线程,顾名思义即是只有一条线程在执行任务,这种情况在我们日常的工作学习中很少遇到,所以我们只是简单做一下了解
多线程,创建多条线程同时执行任务,这种方式在我们的日常生活中比较常见。但是,在多线程的使用过程中,还有许多需要我们了解的概念。比如,在理解上并行和并发的区别,以及在实际应用的过程中多线程的安全问题,对此,我们需要进行详细的了解。
并行和并发:在我们看来,都是可以同时执行多种任务,那么,到底他们二者有什么区别呢?
并发,从宏观方面来说,并发就是同时进行多种时间,实际上,这几种时间,并不是同时进行的,而是交替进行的,而由于CPU的运算速度非常的快,会造成我们的一种错觉,就是在同一时间内进行了多种事情
而并发,则是真正意义上的同时进行多种事情。这种只可以在多核CPU的基础下完成。
还有就是多线程的安全问题?为什么会造成多线程的安全问题呢?我们可以想象一下,如果多个线程同时执行一个任务,name意味着他们共享同一种资源,由于线程CPU的资源不一定可以被谁抢占到,这是,第一条线程先抢占到CPU资源,他刚刚进行了第一次操作,而此时第二条线程抢占到了CPU的资源,name,共享资源还来不及发生变化,就同时有两条数据使用了同一条资源,具体请参考多线程买票问题。这个问题我们应该如何解决那?
有造成问题的原因我们可以看出,这个问题主要的矛盾在于,CPU的使用权抢占和资源的共享发生了冲突,解决时,我们只需要让一条线程战歌了CPU的资源时,阻止第二条线程同时抢占CPU的执行权,在代码中,我们只需要在方法中使用同步代码块即可。在这里,同步代码块不多进行赘述,可以自行了解。
四,线程池
又以上介绍我们可以看出,在一个应用程序中,我们需要多次使用线程,也就意味着,我们需要多次创建并销毁线程。而创建并销毁线程的过程势必会消耗内存。而在Java中,内存资源是及其宝贵的,所以,我们就提出了线程池的概念。
线程池:Java中开辟出了一种管理线程的概念,这个概念叫做线程池,从概念以及应用场景中,我们可以看出,线程池的好处,就是可以方便的管理线程,也可以减少内存的消耗。
那么,我们应该如何创建一个线程池那?Java中已经提供了创建线程池的一个类:Executor
而我们创建时,一般使用它的子类:ThreadPoolExecutor.
public ThreadPoolExecutor(int corePoolSize,
? int maximumPoolSize,
? long keepAliveTime,
? TimeUnit unit,
? BlockingQueue<Runnable> workQueue,
? ThreadFactory threadFactory,
? RejectedExecutionHandler handler)
这是其中最重要的一个构造方法,这个方法决定了创建出来的线程池的各种属性,下面依靠一张图来更好的理解线程池和这几个参数:
又图中,我们可以看出,线程池中的corePoolSize就是线程池中的核心线程数量,这几个核心线程,只是在没有用的时候,也不会被回收,maximumPoolSize就是线程池中可以容纳的最大线程的数量,而keepAliveTime,就是线程池中除了核心线程之外的其他的最长可以保留的时间,因为在线程池中,除了核心线程即使在无任务的情况下也不能被清除,其余的都是有存活时间的,意思就是非核心线程可以保留的最长的空闲时间,而util,就是计算这个时间的一个单位,workQueue,就是等待队列,任务可以储存在任务队列中等待被执行,执行的是FIFIO原则(先进先出)。threadFactory,就是创建线程的线程工厂,最后一个handler,是一种拒绝策略,我们可以在任务满了知乎,拒绝执行某些任务。
线程池的执行流程又是怎样的呢?
有图我们可以看出,任务进来时,首先执行判断,判断核心线程是否处于空闲状态,如果不是,核心线程就先就执行任务,如果核心线程已满,则判断任务队列是否有地方存放该任务,若果有,就将任务保存在任务队列中,等待执行,如果满了,再判断最大可容纳的线程数,如果没有超出这个数量,就开创非核心线程执行任务,如果超出了,就调用handler实现拒绝策略。
handler的拒绝策略:
有四种:第一种AbortPolicy:不执行新任务,直接抛出异常,提示线程池已满
? 第二种DisCardPolicy:不执行新任务,也不抛出异常
? 第三种DisCardOldSetPolicy:将消息队列中的第一个任务替换为当前新进来的任务执行
? 第四种CallerRunsPolicy:直接调用execute来执行当前任务
五,四种常见的线程池:
CachedThreadPool:可缓存的线程池,没有核心线程,非核心线程的数量为Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,
适用于耗时少,任务量大的情况。
SecudleThreadPool:周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。
适用于执行周期性的任务。
SingleThreadPool:只有一条线程来执行任务,
适用于有顺序的任务的应用场景。
FixedThreadPool:定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程
Q:谈谈对线程池的认识?
A:在一个应用程序中我们需要多次使用线程,每一次线程的创建及销毁都会消耗内存,而内存是宝贵的资源,使用线程池可以方便的管理线程,也可以减少内存的消耗。
java中提供了两种创建线程池的方法:
java.util.concurrent.ThreadPoolExecutor类直接创建
new ThreadPoolExecutor(2, 4, 1, TimeUnit.SECONDS, new LinkedBlockingDeque());
1
java.util.concurrent.Executors类直接创建
ExecutorService executorService = Executors.newFixedThreadPool(1);
ExecutorService executorService1 = Executors.newCachedThreadPool();
ExecutorService executorService2 = Executors.newSingleThreadExecutor();
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(6);
需要配置参数:
1、corePoolSize 线程池线程基本数量;
2、maximumPoolSize 线程池最大创建的最大线程数;
3、keepAliveTime 线程最大活跃时间;
4、TimeUnit 线程活跃的时间单位;
5、BlockingQueue 承载任务的阻塞队列;
阻塞队列分为四种:
ArrayBlockingQueue 基于数组的有界阻塞队列,实行先进先出规则(FIFO);
LinkedBlockingQueue 基于链表的阻塞队列,实行先进先出规则(FIFO),newFixedThreadPool()方法创建的线程池使用此队列,吞吐量大于ArrayBlockingQueue ;
SynchronousQueue 不存储元素的阻塞队列,每次插入必须等到另一个线程调用移除操作,否则一直阻塞,newCachedThreadPool()方法创建的线程使用此队列,吞吐量大于LinkedBlockingQueue ;
PriorityQueue 具有优先级的无界阻塞队列;
6、RejectedExecutionHandler 拒绝策略,当线程池及队列都饱和时采取的拒绝策略;
AbortPolicy 默认的拒绝策略,直接抛出异常;
CallerRunsPolicy 只有调用者所在线程执行任务;
DiscardOldestPolicy 抛弃队列中最近的一个任务,执行当前任务;
DiscardPolicy 直接抛弃,不执行;
7、threadFactory 创建线程的工厂;
Q:怎么创建自定义注解?
A:
@Documented//声明式注解
@Inherited//声明式注解
@Target({ElementType.METHOD,ElementType.TYPE})//定义注解的作用域
@Retention(RetentionPolicy.RUNTIME)//定义注解保留的时间
public @interface AnnotationTest {
? public String value() default "";
}
1、newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
2、newFixedThreadPool创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
3、newScheduledThreadPool创建一个定长线程池,支持定时及周期性任务执行。
4、newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors各个方法的弊端:1)newFixedThreadPool和newSingleThreadExecutor: 主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。2)newCachedThreadPool和newScheduledThreadPool: 主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
1、ArrayBlockingQueue是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。2、LinkedBlockingQueue一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列3、SynchronousQueue一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。4、PriorityBlockingQueue一个具有优先级的无限阻塞队列。
corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:
workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:
ArrayBlockingQueue
LinkedBlockingQueue
SynchronousQueue
PriorityBlockingQueue
ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和SynchronousQueue。线程池的排队策略与BlockingQueue有关。
threadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程做些更有意义的事情,比如设置daemon和优先级等等
handler:表示当拒绝处理任务时的策略,有以下四种取值:
1、AbortPolicy:直接抛出异常。2、CallerRunsPolicy:只用调用者所在线程来运行任务。3、DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。4、DiscardPolicy:不处理,丢弃掉。5、也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。
线程池任务执行流程:
当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。
当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行
当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务
当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理
当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程
当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭
高并发架构相关概念 并发:在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行;在互联网时代,所讲的并发,高并发通常是指并发访问,也就是在某个时间点,有多少个访问同时到来。通常一个系统的日PV在千万以上,有可能是一个高并发的系统。有的公司完全不走技术路线,全靠机器堆,这不在讨论范围内。 QPS:每秒钟请求或者查询的数量,在互联网领域,指每秒响应请求数(指HTTP请求);并发连接数是系统同时处理的请求数量 吞吐量:单位时间内处理的请求数量(通常由QPS与并发数决定) 响应时间:从请求发出到收到响应花费的时间。例如系统处理一个HTTP请求需要100ms。 PV:综合浏览量(page view),即页面浏览量或者点击量,一个访客在24小时内访问的页面数量;同一个人浏览网站同一页面,只记作一次PV UV:独立访客(unique visitor),即一定时间范围内相同访客多次访问网站,只计算为一个独立访客 带宽:计算带宽大小需关注两个指标,峰值流量和页面的平均大小 日网站带宽=PV/统计时间(换算到s)平均页面大小(单位KB)8;峰值一般是平均值的倍数,根据实际情况来定 峰值每秒请求数(QPS)=(总PV数80%)/(6小时秒数20%);80%的访问量集中在20%的时间 压力测试:测试能承受的最大并发,测试最大承受的QPS值 常用性能测试工具:ab,wrk,http_load,web_bench,siege,apache jmeter;ab全称是apache benchmark,apache官方推出的工具,创建多个并发访问线程,模拟多个访问者同时对某一trl地址进行访问,它的测试目标是基于url的,因此既可以用来测试apache的负载能力,也可以测试nginx,lighthttp,tomcat,IIS等其它web服务器的压力;ab的使用:模拟并发请求100次,总共请求5000次,ab -c 100 -n 5000 待测试网站;测试机器与被测试机器分开,不要对线上服务做压力测试,观察测试工具ab所在机器,以及被测试的前端机的CPU,内存,网络等都不超过最高限度的75% QPS达到极限:随着QPS的增长,每个阶段需要根据实际情况来进行优化,优化的方案也与硬件条件、网络带宽息息相关;QPS达到50,可以称之为小型网站,一般的服务器就可以应付;QPS达到100,假设关系型数据库的每次请求在0.01s完成,假设单页面只有一个SQL查询,那么100QPS意味着1s完成100次请求,但是此时并不能保证数据库查询能完成100次,数据库缓存层,数据库的负载均衡;QPS达到800,假设使用百兆带宽,意味着网站出口的实际带宽是8M左右,假设每个页面只有10k,在这个并发条件下,百兆带宽已经吃完,CDN加速,负载均衡;QPS达到1000,假设使用memcache缓存数据库查询数据,每个页面对memcache的请求远大于直接对db的请求,memcache的悲观并发数在2w左右,但有可能在之前内网带宽已经吃光,表现出不稳定,静态HTML缓存;QPS达到2000,这个级别下,文件系统访问锁都成为了灾难,做业务分离,分布式存储
高并发解决方案案例 流量优化:防盗链处理 前端优化:减少HTTP请求,合并css或js,添加异步请求,启用浏览器缓存和文件压缩,CDN加速,建立独立图片服务器, 服务端优化:页面静态化,并发处理,队列处理 数据库优化:数据库缓存,分库分表,分区操作,读写分离,负载均衡 web服务器优化:负载均衡,nginx反向代理,7,4层LVS软件
盗链:在自己的页面上展示一些并不在自己服务器上的内容,获得他人服务器上的资源地址,绕过别人的资源展示页面,直接在自己的页面上向最终用户提供此内容,常见的是小站盗用大站的图片,音乐,视频,软件等资源,通过盗链的方法可以减轻自己服务器的负担,因为真实的空间和流量均是来自别人的服务器
防盗链:防止别人通过一些技术手段绕过本站的资源展示页面,盗用本站的资源,让绕开本站资源展示页面的资源链接失效,可以大大减轻服务器及带宽的压力
工作原理:通过请求头中的referer或者签名,网站可以检测目标网页访问的来源网页,如果是资源文件,则可以跟踪到显示它的网页地址,一旦检测到来源不是本站即进行阻止或者返回制定的页面,通过计算签名的方式,判断请求是否合法,如果合法则显示,否则返回错误信息
实现方法:referer:nginx模块ngx_http_referer_module用于阻挡来源非法的域名请求,nginx指令valid_referers none | blocked | server_names | string...,none表示referer来源头部为空的情况,blocked表示referer来源头部不为空,但是里面的值被代理或者防火墙删除了,这些值都不以http://或者https://开头,server_names表示referer来源头部包含当前的server_names,全局变量$invalid_referer。不能彻底防范,只能提高门槛。也可以针对目录进行防盗链。
//在nginx的conf中配置location ~.*.(gif|jpg|png|flv|swf|rar|zip)$
{
? valid_referers none blocked zi.com *.zi.com;
? if($invalid_referer)
? {
? #return 403;
? rewrite ^/ http://www.zi.com/403.jpg; }
}
传统防盗链遇到的问题:伪造referer:可以使用加密签名解决
加密签名:使用第三方模块HttpAccessKeyModule实现Nginx防盗链。accesskey on|off 模块开关,accesskey_hashmethod md5|sha-1 签名加密方式,accesskey_arg GET参数名称,accesskey_signature 加密规则,在nginx的conf中设置
location ~.*.(gif|jpg|png|flv|swf|rar|zip)$
{
? accesskey on;
? accesskey_hashmethod md5;
? accesskey_arg sign;
? accesskey_signature "jason$remote_addr";
}
<?php$sign = md5(‘jason‘.$SERVER[‘REMOTE_ADDR‘]);echo ‘‘;
性能黄金法则:只有10%-20%的最终用户响应时间花在接收请求的HTML文档上,剩下的80%-90%时间花在HTML文档所引用的所有组件(img,script,css,flash等)进行的HTTP请求上。
如何改善:改善响应时间的最简单途径就是减少组件的数量,并由此减少HTTP请求的数量
HTTP连接产生的开销:域名解析--TCP连接--发送请求--等待--下载资源--解析时间
疑问:DNS缓存,查找DNS缓存也需要时间,多个缓存就要查找多次有可能缓存会被清除;Keep-Alive,HTTP1.1协议规定请求只能串行发送,前面的一个请求完成才能开始下个请求
减少HTTP请求的方式:图片地图:允许在一个图片上关联多个URL,目标URL的选择取决于用户单击了图片上的哪个位置,以位置信息定位超链接,把HTTP请求减少为一个,可以保证设计的完整性和功能的齐全性,使用map和area标签;
<map name="map">
? <area shape="rect" coords="0,0,30,30" href=... title="">
? ... </map>
CSS Sprites:CSS精灵,通过使用合并图片,通过指定css的background-image和background-position来显示元素。图片地图与css精灵的响应时间基本上相同,但比使用各自独立图片的方式要快50%以上
合并脚本和样式表:使用外部的js和css文件引用的方式,因为这要比直接写在页面中性能要更好一点;独立的一个js比用多个js文件组成的页面载入要快38%;把多个脚本合并为一个脚本,把多个样式表合并为一个样式表
图片使用base64编码减少页面请求数:采用base64的编码方式将图片直接嵌入到网页中,而不是从外部载入
HTTP缓存机制:如果请求成功会有三种情况:200 from cache:直接从本地缓存中获取相应,最快速,最省流量,因为根本没有向服务器进行请求;304 not modified:协商缓存,浏览器在本地没有命中的情况下请求头中发送一定的校验数据到服务端,如果服务端数据没有改变浏览器从本地缓存响应,返回304,快速,发送的数据很少,只返回一些基本的响应头信息,数据量很小,不发送实际响应体;200 OK:以上两种缓存全部失败,服务器返回完整响应,没有用到缓存,相对最慢。
浏览器认为本地缓存可以使用,不会去请求服务端。相关header:pragma:HTTP1.0时代的遗留产物,该字段被设置为no-cache时,会告知浏览器禁用本地缓存,即每次都向服务器发送请求;expires:HTTP1.0时代用来启用本地缓存的字段,浏览器与服务器的时间无法保持一致,如果时间差距大,就会影响缓存结果;cache-control:HTTP1.1针对expires时间不一致的解决方案,告知浏览器缓存过期的时间间隔而不是时刻,即使具体时间不一致,也不影响缓存的管理;可以设置的值:no-store:禁止浏览器缓存响应;no-cache:不允许直接使用本地缓存,先发起请求和服务器协商;max-age=delta-seconds:告知浏览器该响应本地缓存有效的最长期限,以秒为单位;优先级:pragma >cache-control > expires。当浏览器没有命中本地缓存,如本地缓存过期或者响应中声明不允许直接使用本地缓存,那么浏览器肯定会发起服务端请求;
服务端会验证数据是否修改,如果没有通知浏览器使用本地缓存。相关header:last-modified:通知浏览器资源的最后修改时间;if-modified-since:得到资源的最后修改时间后,会将这个信息通过它提交到服务器做检查,如果没有修改,返回304状态码;ETag:HTTP1.1推出,文件的指纹标识符,如果文件内容修改,指纹会改变;if-none-match:本地缓存失效,会携带此值去请求服务端,服务端判断该资源是否改变,如果没有改变,直接使用本地缓存,返回304
缓存策略的选择:适合缓存的内容:不变的图像,如logo,图标等,js,css静态文件,可下载的内容,媒体文件;建议使用协商缓存:html文件,经常替换的图片,经常修改的js,css文件,js和css文件的加载可以加入文件的签名来拒绝缓存,如a.css?签名或a.签名.js;不建议缓存的内容:用户隐私等敏感数据,经常改变的api数据接口
nginx配置缓存策略: 本地缓存配置:add_header指令:添加状态码为2xx和3xx的响应头信息,add_header name value [always];,可以设置Pragma/Expires/Cache-Control,可以继承;expires指令:通知浏览器过期时长,expires time;,为负值时表示Cache-Control: no-cache;,当为正或者0时,就表示Cache-Control: max-age=指定的时间;;当为max时,Cache-Control设置到10年; 协商缓存相关配置:Etag指令:指定签名;etag on|off;,默认是on
前端代码和资源的压缩:让资源文件更小,加快文件在网络中的传输,让网页更快的展现,降低带宽和流量开销;压缩方式:js,css,图片,html代码的压缩,Gzip压缩。js代码压缩:一般是去掉多余的空格和回车,替换长变量名,简化一些代码写法等,代码压缩工具很多UglifyJS(压缩,语法检查,美化代码,代码缩减,转化)、YUI Compressor(来自yahoo,只有压缩功能)、Closure Compiler(来自google,功能和UglifyJS类似,压缩的方式不一样),有在线工具tool.css-js.com,应用程序,编辑器插件。css代码压缩:原理和js压缩原理类似,同样是去除空白符,注释并且优化一些css语义规则等,压缩工具CSS Compressor(可以选择模式)。html代码压缩:不建议使用代码压缩,有时会破坏代码结构,可以使用Gzip压缩,当然也可以使用htmlcompressor工具,不过转换后一定要检查代码结构。img压缩:一般图片在web系统的比重都比较大,压缩工具:tinypng,JpegMini,ImageOptim。Gzip压缩:配置nginx服务,gzip on|off,gzip_buffers 32 4K|16 8K #缓冲(在内存中缓存几块?每块多大),gzip_comp_level [1-9] #推荐6 压缩级别(级别越高,压的越小,越浪费CPU计算资源),gzip_disable #正则匹配UA 什么样的uri不进行gzip,gzip_min_length 200 #开始压缩的最小长度,gzip_http_version 1.0|1.1 #开始压缩的http协议版本,gzip_proxied #设置请求者代理服务器,该如何缓存内容,gzip_types text/plain applocation/xml #对哪些类型的文件用压缩,gzip_vary on|off #是否传输gzip压缩标志。其他工具:自动化构建工具Grunt。
CDN:Content Delivery Network,内容分发网络,尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快更稳定;在网络各处放置节点服务器所构成的在现有的互联网基础之上的一层智能虚拟网络;CDN系统能够实时的根据网络流量和各节点的连接,负载情况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。本地cache加速,提高了企业站点(尤其含有大量img和静态页面站点)的访问速度;跨运营商的网络加速,保证不同网络的用户都得到良好的访问质量;远程访问用户根据DNS负载均衡技术智能自动选择cache服务器;自动生成服务器的远程Mirror cache服务器,远程用户访问时从cache服务器上读数据,减少远程访问的带宽,分担网络流量,减轻原站点web服务器负载等功能;广泛分布的CDN节点加上节点之间的只能智能冗余机制,可以有效的预防黑客入侵
CDN的工作原理:传统访问:用户在浏览器输入域名发起请求--解析域名获取服务器IP地址--根据IP地址找到对应的服务器--服务器响应并返回数据;使用CDN访问:用户发起请求--智能DNS的解析(根据IP判断地理位置,接入网类型,选择路由最短和负载最轻的服务器)--取得缓存服务器IP--把内容返回给用户(如果缓存中有)--向源站发起请求--将结果返回给用户--将结果存入缓存服务器
CDN适用场景:站点或者应用中大量静态资源的加速分发,如css,js,img和html;大文件下载;直播网站等
CDN的实现:BAT等都有提供CDN服务,可用LVS做4层负载均衡;可用nginx,Varnish,Squid,Apache TrafficServer做7层负载均衡和cache;使用squid反向代理,或者nginx等的反向代理
独立的必要性:分担web服务器的I/O负载-将耗费资源的图片服务分离出来,提高服务器的性能和稳定性;能够专门的图片服务器进行优化-为图片服务设置有针对性的缓存方案,减少带宽成本,提高访问速度;提高网站的可扩展性-通过增加图片服务器,提高图片吞吐能力
采用独立域名:原因:同一域名下浏览器的并发连接数有限制,突破浏览器连接数的限制;由于cookie的原因,对缓存不利,大部分web cache都只缓存不带cookie的请求,导致每次的图片请求都不能命中cache
独立后的问题:如何进行图片上传和图片同步:NFS共享方式;利用FTP同步
将现有PHP等动态语言的逻辑代码生成为静态HTML文件,用户访问动态脚本重定向到静态HTML文件的过程。对实时性要求不高的页面比较适合。原因:动态脚本通常会做逻辑计算和数据查询,访问量越大,服务器压力越大;访问量大时可能会造成CPU负载过高,数据库服务器压力过大;静态化可以降低逻辑处理压力,降低数据库服务器查询压力
静态化的实现方式: 使用模板引擎:可以使用smarty的缓存机制生成静态HTML缓存文件;$smarty->cache-dir = $ROOT."/cache";//缓存目录,$smarty->caching=true;//是否开启缓存,$smarty->cache_lifetime="3600";//缓存时间,$smarty->display(string template[, string cache_id[, string compile_id]]);,$smarty->clear_all_cache();//清除所有缓存,$smarty->clear_cache(‘a.html‘);//清除指定的缓存,$smarty->clear_cache(‘a.html‘, $art_id);//清除同一个模板下的指定缓存号的缓存 利用ob系列的函数:ob_start():打开输出控制缓冲,ob_ge_contents():返回输出缓冲区内容,ob_clean():清空输出缓冲区,ob_end_flush():冲刷出(送出)输出缓冲区内容并关闭缓冲,可以判断文件的inode修改时间,判断是否过期使用filectime函数
进程:计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作数据结构的基础,是一个“执行中的程序”;进程的三态模型:多道程序系统中,进程在处理器上交替运行,状态不断的发生变化;运行:当一个进程在处理机上运行时,称该进程处于运行状态,处于此状态的进程的数目小于等于处理器的数目,对于单处理机系统,处于运行状态的进程只有一个,在没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程;就绪:当一个进程获得了除处理机以外的一切所需资源,一旦得到处理机即可运行,则称此进程处于就绪状态,就绪进程可以按多个优先级来划分队列,如当一个进程由于时间片用完而进入就绪状态时,排入低优先级队列,当进程由I/O操作完成而进入就绪状态时,排入高优先级队列;阻塞:也称为等待或睡眠状态,一个进程正在等待某一事件发生(如请求I/O而等待I/O完成等)而暂时停止运行,这时即使把处理机分配给进程也无法运行;进程的五态模型:对于一个实际的系统,进程的状态及其转换更为复杂,新建态:对应于进程刚刚被创建时没有被提交的状态,并等待系统完成创建进程的所有必要信息;活跃就绪/静止就绪:进程在主存并且可被调度的状态/指进程被对换到辅存时的就绪状态,是不能被直接调度的状态,只有当主存中没有活跃就绪态进程,或者是挂起就绪态进程具有更高的优先级,系统将把挂起就绪态进程调回主存并转换为活跃就绪;运行,活跃阻塞/静止阻塞:指进程已在主存,一旦等待的时间产生便进入活跃就绪状态/进程对换到辅存时的阻塞状态,一旦等待的事件产生便进入静止就绪状态;终止态:进程已结束运行,回收除进程控制块之外的其他资源,并让其他进程从进程控制块中收集有关信息;由于用户的并发请求,为每一个请求都创建一个进程显然是行不通的,从系统资源开销方面或是响应用户请求的效率方面来看,因此线程的概念被引进。
线程:有时被称为轻量级进程,是程序执行流的最小单元。是进程中的一个实体,是被系统独立调度和分派的基本单位,自己不拥有系统资源,只拥有一点在运行中必不可少的资源但它可与同属一个进程的其它进程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程,同一进程中的多个线程之间可以并发执行。线程是程序中一个单一的顺序控制流程,进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指运行中的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作成为多线程。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。线程的状态:就绪:线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行:线程占有处理机正在运行;阻塞:线程在等待一个事件(如某个信号量),逻辑上不可执行。
协程:是一种用户态的轻量级线程,调度完全由用户控制;协程拥有自己的寄存器上下文和栈;协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
进程和线程的区别:线程是进程内的一个执行单元,进程内至少有一个线程,共享进程的地址空间,而进程有自己独立的地址空间;进程是资源分配和拥有的单元,同一个进程内的线程共享进程的资源;线程是处理器调度的基本单位,但进程不是;二者均可并发执行;每个独立的线程有一个程序运行的入口,顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制
线程和协程的区别:一个线程可以多个协程,一个进程也可以单独拥有多个协程;进程线程都是同步机制,而协程则是异步;协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态。
多进程:同一时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态;多开一个进程,多分配一份资源,进程间通讯不方便;
多线程:线程就是把一个进程分为很多片,每一片都可以是一个独立的流程,与多进程的区别是只会使用一个进程的资源,线程间可以直接通信;
同步阻塞:多进程:最早的服务器端程序都是通过多进程,多线程来解决并发I/O的问题;一个请求创建一个进程,然后子进程进入循环同步阻塞地与客户端连接进行交互,收发处理数据;多线程:线程中可以直接向某一个客户端连接发送数据;步骤:创建一个socket,进入while循环,阻塞在进程accept操作上,等待客户端连接进入,主进程在多进程模型下通过fork创建子进程,多线程模型下可以创建子线程,子进程/线程创建成功后进入while循环,阻塞在recv调用上,等待客户端向服务器发送数据,收到数据后服务器程序进行处理然后使用send向客户端发送响应,当客户端连接关闭时,子进程/线程退出并销毁所有资源。主进程/线程会回收掉此子进程/线程;缺点:这种模型严重依赖进程的数量解决并发问题,启动大量进程会带来额外的进程调度消耗
异步非阻塞:现在各种高并发异步IO的服务器程序都是基于epoll(无限数量连接,无需轮询)实现的。IO复用异步非阻塞程序使用经典的Reactor模型,Reactor顾名思义就是反应堆的意思,它本身不处理任何数据收发,只是可以监视一个socket句柄的事件变化。Reactor模型:Add:添加一个socket到Reactor,Set:修改socket对应的事件,如可读可写,Del:从Reactor中移除,Callback:事件发生后回调指定的函数。Nginx:多线程Reactor,swoole:多线程Reactor+多进程Worker
PHP的swoole扩展:PHP的异步,并行,高性能网络通信引擎,使用纯c语言编写,提供了PHP语言的异步多线程服务器,异步TCP/UDP网络客户端,异步mysql,异步redis,数据库连接池,asynctask,消息队列,毫秒定时器,异步文件读写,异步DNS查询;除了异步IO的支持之外,swoole为PHP多进程的模式设计了多个并发数据结构和IPC通信机制,可以大大简化多进程并发编程的工作;swoole2.0支持了类似Go语言的协程,可以使用完全同步的代码实现异步程序
消息队列:用户注册后,需要发注册邮件和注册短信;串行方式:将注册信息写入数据库成功后,发送注册邮件,再发送注册短信;并行方式:将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信;消息队列方式:将注册信息写入数据库成功后,将成功信息写入队列,此时直接返回成功给用户,写入队列的时间非常短,可以忽略不计,然后异步发送邮件和短信。应用解耦:场景说明:用户下单后,订单系统需要通知库存系统。假如库存系统无法访问,则订单减库存将失败,从而导致订单失败;订单系统与库存系统耦合;引用队列:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功,订阅下单的消息,采用拉/推的方式,获取下单信息,库存系统根据下单信息,进行库存操作。流量削峰:应用场景:秒杀活动,流量瞬时激增,服务器压力大。用户发起请求,服务器接收后,先写入消息队列,假如消息队列长度超过最大值,则直接报错或提示用户,后续程序读取消息队列再做处理,控制请求量,缓解高流量。日志处理:应用场景:解决大量日志的传输。日志采集程序将程序写入消息队列,然后通过日志处理程序的订阅消费日志。消息通讯:应用场景:聊天室。多个客户端订阅同一主题,进行消息发布和接收。常见消息队列产品:Kafka,ActiveMQ,ZeroMQ,RabbitMQ,Redis等
接口的并发请求:curl_multi_init
mysql等一些常见的关系型数据库的数据都存储在磁盘当中,在高并发场景下,业务应用对mysql产生的增删改查的操作造成的巨大的IO开销和查询压力,这无疑对数据库和服务器都是一种巨大的压力,为了解决此类问题,缓存数据的概念应运而生。极大的解决数据库服务器的压力,提高应用数据的响应速度。常见的缓存形式:内存缓存,文件缓存。
缓存数据是为了让客户端很少甚至不访问数据库服务器进行数据的查询,高并发下,能最大程度的降低对数据库服务器的访问压力。默认情况下:用户请求->数据查询->连接数据库服务器并查询数据->将数据缓存起来(html,内存,json,序列化数据)->显示给客户端;用户再次请求或者新用户访问->数据查询->直接从缓存中获取数据->显示给客户端
mysql的查询缓存:query_cache_type:查询缓存类型,有0,1,2三个取值,0则不使用查询缓存,1表示始终使用查询缓存,2表示按需使用查询缓存。query_cache_type为1时,亦可关闭查询缓存,SELECT SQL_NO_CACHE * FROM my_table WHERE condition。query_cache_type为2时,可按需使用查询缓存,SELECT SQL_CACHE * FROM my_table WHERE condition。query_cache_size:默认情况下为0,表示为查询缓存预留的内存为0,则无法使用查询缓存。SET GLOBAL query_cache_size = 134217728。查询缓存可以看做是SQL文本和查询结果的映射。第二次查询的SQL和第一次查询的SQL完全相同,则会使用缓存。SHOW STATUS LIKE ‘Qcache_hits‘; 查看命中次数。表的结构或数据发生改变时,查询缓存中的数据不再有效。清理缓存:FLUSH QUERY CACHE; //清理查询缓存内存碎片,RESET QUERY CACHE; //从查询缓存中移出所有查询,FLUSH TABLES; //关闭所有打开的表,同时该操作将会清空查询缓存中的内容
使用memcache缓存查询数据:对于大型站点,如果没有中间缓存层,当流量打入数据库层时,即便有之前的几层为我们挡住一部分流量,但是在大并发的情况下,还是会有大量请求涌入数据库层,这样对于数据库服务器的压力冲击很大,响应速度也会下降,因此添加中间缓存层很有必要。memcache是一套分布式的高速缓存系统,由livejournal的bradfitzpatrick开发,但目前被许多网站使用以提升网站的访问速度,尤其对于一些大型的,需要频繁访问数据库的网站访问速度提升效果十分显著。
memcache工作原理:是一个高性能的分布式的内存对象缓存系统,通过在内存里维护一个统一的巨大的hash表,能够用来存储各种格式的数据,包括图像,视频,文件以及数据库检索的结果等,简单的说就是将数据调用到内存,然后从内存中读取,从而大大提高读取速度。
memcache工作流程:先检查客户端的请求数据是否在memcache中,如有,直接把请求数据返回,不再对数据库进行任何操作;如果请求的数据不在memcache中,就去查数据库,把从数据库中获取的数据返回给客户端,同时把数据缓存一份到memcache中。
memcache方法:获取:get(key) 设置:set(key, val, expire) 删除:delete(key)
通用缓存机制:用查询的方法名+参数作为查询时的key value对中的key值
使用redis缓存查询数据:与memcache的区别:性能相差不大,redis在2.0版本后增加了自己的VM特性,突破物理内存的限制,memcache可以修改最大可用内存,采用LRU算法;redis依赖客户端来实现分布式读写,memcache本身没有数据冗余机制;redis支持快照,AOF,依赖快照进行持久化,aof增强了可靠性的同时,对性能有所影响,memcache不支持持久化,通常做缓存,提升性能;memcache在并发场景下,用cas保证一致性,redis事务支持比较弱,只能保证事务中的每个操作连续执行;redis支持多种类的数据类型;redis用于数据量较小的高性能操作和运算上,memcache用于在动态系统中减少数据库负载,提升性能,适合做缓存,提高性能
缓存其他数据:session:session_set_save_handler
优化方向:数据表数据类型优化,索引优化,sql语句优化,存储引擎的优化,数据表结构设计的优化,数据库服务器架构的优化
数据表数据类型优化:字段使用什么样的数据类型更合适,性能更快,tinyint、smallint、bigint,考虑空间和范围的问题;char、varchar,存储字符串长度是否固定;enum,特定固定的分类可以使用enum存储,效率更快;IP地址的存储,ip2long(),使用整型存储IP地址
索引的优化:建立合适的索引,索引在什么场景下效率最高,索引的创建原则:不是越多越好,在合适的字段上创建合适的索引,复合索引的前缀原则,like查询%的问题,全表扫描优化,or条件索引使用情况,字符串类型索引失效的问题
sql语句的优化:优化查询过程中的数据访问,优化长难句、特定类型的查询语句。使用limit,返回列不用*,变复杂为简单,切分查询,分解关联查询,优化count(),优化关联查询,优化子查询,优化group by和distinct,优化limit和union
存储引擎的优化:尽量使用innoDB存储引擎
数据表结构设计的优化:分区操作,通过特定的策略对数据表进行物理拆分,对用户透明,partition by;分库分表,水平拆分,垂直拆分
数据库架构的优化:主从复制,读写分离,双主热备,binlog日志,中继日志,主从库binlog的交换,事件传输;负载均衡,通过LVS的三种基本模式实现负载均衡,mycat数据库中间件实现负载均衡
七层负载均衡的实现:基于URL等应用层信息的负载均衡,nginx的proxy是它一个很强大的功能,实现了7层负载均衡,功能强大,性能卓越,运行稳定,配置简单灵活,能够自动剔除工作不正常的后端服务器,上传文件使用异步模式,支持多种分配策略,可以分配权重,分配方式灵活。
nginx负载均衡:内置策略:IP Hash,加权轮询;扩展策略:fair策略,通用hash,一致性hash 加权轮询:首先将请求都分给高权重的机器,直到该机器的权值降到了比其他机器低,才开始将请求分给下一个高权重的机器,当所有后端机器都down掉时,nginx会立即将所有机器的标志位清成初始状态,以避免造成所有的机器都处在timeout的状态;IP Hash:流程和轮询很类似,只是其中的算法和具体的策略有些变化,算法是一种变相的轮询算法;fair:根据后端服务器的响应时间判断负载情况,从中选出负载最轻的机器进行分流;通用hash,一致性hash:通用hash比较简单,可以以nginx内置的变量为key进行hash,一致性hash采用了nginx内置的一致性hash环,支持memcache
nginx配置:
http {
? upstream cluster{? #ip_hash;
? server srv1 weight=1;
? server srv2;
? server srv3;
? }
? server {
? listen 80;
? location / {
? proxy_pass http://cluster;
? }
? }
}
四层负载均衡的实现:通过报文中的目标地址和端口,再加上负载均衡设备设置的服务器选择方式,决定最终选择的内部服务器。LVS实现服务器集群负载均衡有三种方式,NAT,DR和TUN。
标签:play map对象 阶段 主题 cin 设计 header 数据量 分发
原文地址:https://www.cnblogs.com/lvhouhou/p/11541234.html