java线程池的初探
问题来源
发现学习很多技术都提到了线程池的技术,自己的线程池方面没有仔细研究过,现在看了点东西来这里总结下,最近发现写博客是一个很好的锻炼自己并且将学到的东西更加理解的一个方式。
问题探究
java的多线程技术应用很广,但凡是请求大的应用都会用到,但是线程是一个稀缺资源不能无限的创建,即使可以创建很多个线程,线程之间的切换也是十分浪费时间的,所以java推出了线程池概念,
线程池就是将一些线程资源放进一个池子中,当有请求的时候在池子中找到一个空闲的线程进行执行,这样我们可以高效的利用线程资源,避免了线程资源的浪费。
线程池
使用方式
- public static void main(String[] args) throws InterruptedException, ExecutionException {
- ExecutorService threadPool = Executors.newFixedThreadPool(4);
- for (int i = 0;i<100;i++) {
- //向线程池提交一个任务,交由线程池去执行
- //threadPool.execute(new PreRun());
- //这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,
- //去看submit()方法的 实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果
- Future<Object> f = (Future<Object>)threadPool.submit(new PreRun2());
- System.out.println(f.get());
- }
- //当等待超过设定时间时,会监测ExecutorService是否已经关闭,若关闭则返回true,否则返回false。
- //一般情况下会和shutdown方法组合使用
- threadPool.awaitTermination(1200, TimeUnit.MILLISECONDS);
- //将停止接受新的任务, 同时等待已经提交的任务完成, 包括尚未完成的任务
- threadPool.shutdown();
- //会启动一个强制的关闭过程, 尝试取消所有运行中的任务和排在队列中尚未开始的任务,并把排队中尚未开始的任务返回,
- //对于关闭后提交到ExecutorService中的任务, 会被(拒接执行处理器)rejected execution handler处理,
- //它会抛弃任务,或者使得execute方法抛出一个未检查的RejectedExecutionException
- threadPool.shutdownNow();
- }
runable类:
- static class PreRun2 implements Callable<Object>{
- private static int count;
- private final int id = count++;
- @Override
- public Object call() throws Exception {
- ThreadDemoUtils.sleep(1000);
- ThreadDemoUtils.printMessage("运行中");
- return id;
- }
- }
可以看到线程池的创建使用了
- Executors.newFixedThreadPool(4);
我们来看下Executors,这个类主要是通过各中配置创建需要的线程池。
先来看看java线程池的类图关系
可以看到最顶层的接口是Executor 只提供了一个void execute(Runnable command),这个方法是执行的意思,传进去一个参数然后执行,之后是ExecutorService接口,包含一些改变和观测线程池状态的方法,例如shutdownNow(),isShutdown(),还有一些执行线程的方法,例如submit()提交一个线程并且返回一个Future对象,Future可以获取线程运行的结果。AbstractExecutorService负责实现ExecutorService接口的一些方法。ThreadPoolExecutor继承了AbstractExecutorService重写了execute方法。
接下来我们看看ThreadPoolExecutor的一些参数
- public ThreadPoolExecutor(int corePoolSize,
- int maximumPoolSize,
- long keepAliveTime,
- TimeUnit unit,
- BlockingQueue<Runnable> workQueue,
- ThreadFactory threadFactory,
- RejectedExecutionHandler handler) {
- if (corePoolSize < 0 ||
- maximumPoolSize <= 0 ||
- maximumPoolSize < corePoolSize ||
- keepAliveTime < 0)
- throw new IllegalArgumentException();
- if (workQueue == null || threadFactory == null || handler == null)
- throw new NullPointerException();
- this.corePoolSize = corePoolSize;
- this.maximumPoolSize = maximumPoolSize;
- this.workQueue = workQueue;
- this.keepAliveTime = unit.toNanos(keepAliveTime);
- this.threadFactory = threadFactory;
- this.handler = handler;
- }
我们来分析下这些参数
- corePoolSize
核心线程池大小,这个参数标识线程池所可以容纳的最大的核心线程池数量,这个不是可以创建的最多的线程的数量,只是核心数量,我们之后会再次分析。
maximumPoolSize
线程池可以创建线程的最大数量。
- keepAliveTime
如果核心线程数已经满了,没有空闲的线程的时候,新的任务可以等待的最长时间。
- workQueue
当核心线程数量满了,没有空闲线程时,新任务放在这个指定的任务队列中。
- threadFactory
创建线程的工厂类。
- handler
当无法创建更多的线程时拒绝任务的处理策略
我们看到上面使用线程池的例子中,通过
- submit方法提交一个任务,然后得到一个Future对象,这个方法到底是怎么执行的呢,Future到底是什么?我们一起来看下
- public Future<?> submit(Runnable task) {
- if (task == null) throw new NullPointerException();
- RunnableFuture<Void> ftask = newTaskFor(task, null);
- execute(ftask);
- return ftask;
- }
这个方法会先创建一个RunableFuture对象,就是将我们传进来的Runable封装成一个统一的任务对象
- protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
- return new FutureTask<T>(runnable, value);
- }
- public FutureTask(Runnable runnable, V result) {
- this.callable = Executors.callable(runnable, result);
- this.state = NEW; // ensure visibility of callable
- }
看到会创建一个FutureTask对象,这个对象主要包含Callable和result ,如果传进来的是Runable,Executors.callable()方法会将runable转换为Callable对象(通过适配器模式)
也就是说不管传进来的是Runable还是Callable,他们都会将这个对象转换为FutreTask对象。
我们继续向下看
- execute(ftask);
- public void execute(Runnable command) {
- if (command == null)
- throw new NullPointerException();
- /*
- * Proceed in 3 steps:
- *
- * 1. If fewer than corePoolSize threads are running, try to
- * start a new thread with the given command as its first
- * task. The call to addWorker atomically checks runState and
- * workerCount, and so prevents false alarms that would add
- * threads when it shouldn't, by returning false.
- *
- * 2. If a task can be successfully queued, then we still need
- * to double-check whether we should have added a thread
- * (because existing ones died since last checking) or that
- * the pool shut down since entry into this method. So we
- * recheck state and if necessary roll back the enqueuing if
- * stopped, or start a new thread if there are none.
- *
- * 3. If we cannot queue task, then we try to add a new
- * thread. If it fails, we know we are shut down or saturated
- * and so reject the task.
- */
- int c = ctl.get();
- if (workerCountOf(c) < corePoolSize) {
- if (addWorker(command, true))
- return;
- c = ctl.get();
- }
- if (isRunning(c) && workQueue.offer(command)) {
- int recheck = ctl.get();
- if (! isRunning(recheck) && remove(command))
- reject(command);
- else if (workerCountOf(recheck) == 0)
- addWorker(null, false);
- }
- else if (!addWorker(command, false))
- reject(command);
- }
我们先来翻译一下这个注释
- /*
- * Proceed in 3 steps:
- *
- * 1. If fewer than corePoolSize threads are running, try to
- * start a new thread with the given command as its first
- * task. The call to addWorker atomically checks runState and
- * workerCount, and so prevents false alarms that would add
- * threads when it shouldn't, by returning false.
- *
- * 2. If a task can be successfully queued, then we still need
- * to double-check whether we should have added a thread
- * (because existing ones died since last checking) or that
- * the pool shut down since entry into this method. So we
- * recheck state and if necessary roll back the enqueuing if
- * stopped, or start a new thread if there are none.
- *
- * 3. If we cannot queue task, then we try to add a new
- * thread. If it fails, we know we are shut down or saturated
- * and so reject the task.
有3个步骤:
1.如果当前创建的线程数量小于核心线程数量,就为传进来的任务创建一个新的线程,
这个过程会调用addWorker方法原子的检查运行状态和工作线程来保证线程确实应该创建
如果不应该创建就返回false,继续下一个步骤
2. 如果任务成功入队队列,任然需要检查我们应该添加一个线程或者这个线程池是不是已经挂了
3.如果我们不能入队,我们就尝试创建一个新的线程,如果创建失败,我们就会开启一个新的线程,
如果失败就会启动拒绝策略处理- */
我们一个个方法来看,首先我们了解下线程池需要用到的所有状态
- /**
- * The main pool control state, ctl, is an atomic integer packing
- * two conceptual fields
- * workerCount, indicating the effective number of threads
- * runState, indicating whether running, shutting down etc
- *
- * In order to pack them into one int, we limit workerCount to
- * (2^29)-1 (about 500 million) threads rather than (2^31)-1 (2
- * billion) otherwise representable. If this is ever an issue in
- * the future, the variable can be changed to be an AtomicLong,
- * and the shift/mask constants below adjusted. But until the need
- * arises, this code is a bit faster and simpler using an int.
- *
- * The workerCount is the number of workers that have been
- * permitted to start and not permitted to stop. The value may be
- * transiently different from the actual number of live threads,
- * for example when a ThreadFactory fails to create a thread when
- * asked, and when exiting threads are still performing
- * bookkeeping before terminating. The user-visible pool size is
- * reported as the current size of the workers set.
- *
- * The runState provides the main lifecycle control, taking on values:
- *
- * RUNNING: Accept new tasks and process queued tasks
- * SHUTDOWN: Don't accept new tasks, but process queued tasks
- * STOP: Don't accept new tasks, don't process queued tasks,
- * and interrupt in-progress tasks
- * TIDYING: All tasks have terminated, workerCount is zero,
- * the thread transitioning to state TIDYING
- * will run the terminated() hook method
- * TERMINATED: terminated() has completed
- *
- * The numerical order among these values matters, to allow
- * ordered comparisons. The runState monotonically increases over
- * time, but need not hit each state. The transitions are:
- *
- * RUNNING -> SHUTDOWN
- * On invocation of shutdown(), perhaps implicitly in finalize()
- * (RUNNING or SHUTDOWN) -> STOP
- * On invocation of shutdownNow()
- * SHUTDOWN -> TIDYING
- * When both queue and pool are empty
- * STOP -> TIDYING
- * When pool is empty
- * TIDYING -> TERMINATED
- * When the terminated() hook method has completed
- *
- * Threads waiting in awaitTermination() will return when the
- * state reaches TERMINATED.
- *
- * Detecting the transition from SHUTDOWN to TIDYING is less
- * straightforward than you'd like because the queue may become
- * empty after non-empty and vice versa during SHUTDOWN state, but
- * we can only terminate if, after seeing that it is empty, we see
- * that workerCount is 0 (which sometimes entails a recheck -- see
- * below).
- */
- private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
- private static final int COUNT_BITS = Integer.SIZE - 3;
- private static final int CAPACITY = (1 << COUNT_BITS) - 1;
- // runState is stored in the high-order bits
- private static final int RUNNING = -1 << COUNT_BITS;// 111 00000000000000000000000000000
- private static final int SHUTDOWN = 0 << COUNT_BITS;// 000 00000000000000000000000000000
- private static final int STOP = 1 << COUNT_BITS;// 001 00000000000000000000000000000
- private static final int TIDYING = 2 << COUNT_BITS;// 010 00000000000000000000000000000
- private static final int TERMINATED = 3 << COUNT_BITS;// 100 00000000000000000000000000000
- // Packing and unpacking ctl
- private static int runStateOf(int c) { return c & ~CAPACITY; }//最高3位
- private static int workerCountOf(int c) { return c & CAPACITY; }//后29位
- private static int ctlOf(int rs, int wc) { return rs | wc; }
以下是线程池状态的讲解
- RUNNING: Accept new tasks and process queued tasks 运行时状态,可以接收新的任务
- * SHUTDOWN: Don't accept new tasks, but process queued tasks 关闭状态,不在接收新的任务,会运行队列中的任务
- * STOP: Don't accept new tasks, don't process queued tasks, 立即关闭状态,不在接收新的任务,也不会执行队列中的任务,并且会中断运行中的任务
- * and interrupt in-progress tasks
- * TIDYING: All tasks have terminated, workerCount is zero, 整理状态,所有的任务都结束了,工作任务为0,当STOP完成时。
- * the thread transitioning to state TIDYING
- * will run the terminated() hook method
- * TERMINATED: terminated() has completed 线程池完全结束
下面我们来看下addWorker()
- private boolean addWorker(Runnable firstTask, boolean core) {
- retry:
- for (;;) {
- int c = ctl.get();
- int rs = runStateOf(c);
- // Check if queue empty only if necessary.
- if (rs >= SHUTDOWN &&
- ! (rs == SHUTDOWN &&
- firstTask == null &&
- ! workQueue.isEmpty()))
- return false;
- for (;;) {
- int wc = workerCountOf(c);
- if (wc >= CAPACITY ||
- wc >= (core ? corePoolSize : maximumPoolSize))
- return false;
- if (compareAndIncrementWorkerCount(c))
- break retry;
- c = ctl.get(); // Re-read ctl
- if (runStateOf(c) != rs)
- continue retry;
- // else CAS failed due to workerCount change; retry inner loop
- }
- }
- boolean workerStarted = false;
- boolean workerAdded = false;
- Worker w = null;
- try {
- w = new Worker(firstTask);
- final Thread t = w.thread;
- if (t != null) {
- final ReentrantLock mainLock = this.mainLock;
- mainLock.lock();
- try {
- // Recheck while holding lock.
- // Back out on ThreadFactory failure or if
- // shut down before lock acquired.
- int rs = runStateOf(ctl.get());
- if (rs < SHUTDOWN ||
- (rs == SHUTDOWN && firstTask == null)) {
- if (t.isAlive()) // precheck that t is startable
- throw new IllegalThreadStateException();
- workers.add(w);
- int s = workers.size();
- if (s > largestPoolSize)
- largestPoolSize = s;
- workerAdded = true;
- }
- } finally {
- mainLock.unlock();
- }
- if (workerAdded) {
- t.start();
- workerStarted = true;
- }
- }
- } finally {
- if (! workerStarted)
- addWorkerFailed(w);
- }
- return workerStarted;
- }
这个方法整体分为两大部分,上面一部分是要将判断是不是可以添加一个线程,并且通过
compareAndIncrementWorkerCount(c)
来原子性的增加工作线程数量。下面一部分就是创建一个Worker工作线程,然后加锁将这个新建的worker放进workers中,并且启动这个工作线程,接下来我们看下Worker类
- private final class Worker
- extends AbstractQueuedSynchronizer
- implements Runnable
- {
- /**
- * This class will never be serialized, but we provide a
- * serialVersionUID to suppress a javac warning.
- */
- private static final long serialVersionUID = 6138294804551838833L;
- /** Thread this worker is running in. Null if factory fails. */
- final Thread thread;
- /** Initial task to run. Possibly null. */
- Runnable firstTask;
- /** Per-thread task counter */
- volatile long completedTasks;
- /**
- * Creates with given first task and thread from ThreadFactory.
- * @param firstTask the first task (null if none)
- */
- Worker(Runnable firstTask) {
- setState(-1); // inhibit interrupts until runWorker
- this.firstTask = firstTask;
- this.thread = getThreadFactory().newThread(this);
- }
- /** Delegates main run loop to outer runWorker */
- public void run() {
- runWorker(this);
- }
这个Worker实现了Runable接口,并持有一个Thread对象,当调用构造函数时这个Worker会创建一个线程并把Worker作为参数传进去
当该线程调用start方法时,就会调用系统api开辟一个新的线程,然后执行run方法
- final void runWorker(Worker w) {
- Thread wt = Thread.currentThread();
- Runnable task = w.firstTask;
- w.firstTask = null;
- w.unlock(); // allow interrupts
- boolean completedAbruptly = true;
- try {
- while (task != null || (task = getTask()) != null) {
- w.lock();
- // If pool is stopping, ensure thread is interrupted;
- // if not, ensure thread is not interrupted. This
- // requires a recheck in second case to deal with
- // shutdownNow race while clearing interrupt
- if ((runStateAtLeast(ctl.get(), STOP) ||
- (Thread.interrupted() &&
- runStateAtLeast(ctl.get(), STOP))) &&
- !wt.isInterrupted())
- wt.interrupt();
- try {
- beforeExecute(wt, task);
- Throwable thrown = null;
- try {
- task.run();
- } catch (RuntimeException x) {
- thrown = x; throw x;
- } catch (Error x) {
- thrown = x; throw x;
- } catch (Throwable x) {
- thrown = x; throw new Error(x);
- } finally {
- afterExecute(task, thrown);
- }
- } finally {
- task = null;
- w.completedTasks++;
- w.unlock();
- }
- }
- completedAbruptly = false;
- } finally {
- processWorkerExit(w, completedAbruptly);
- }
- }
看到runworker方法,他会不断的得到task,如果taks是null,就结束这个工作线程
- processWorkerExit(w, completedAbruptly)
- private void processWorkerExit(Worker w, boolean completedAbruptly) {
- if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
- decrementWorkerCount();
- final ReentrantLock mainLock = this.mainLock;
- mainLock.lock();
- try {
- completedTaskCount += w.completedTasks;
- workers.remove(w);
- } finally {
- mainLock.unlock();
- }
- tryTerminate();
- int c = ctl.get();
- if (runStateLessThan(c, STOP)) {
- if (!completedAbruptly) {
- int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
- if (min == 0 && ! workQueue.isEmpty())
- min = 1;
- if (workerCountOf(c) >= min)
- return; // replacement not needed
- }
- addWorker(null, false);
- }
- }
我们看到第一行,如果工作线程突然退出,就减少工作线程数量,然后将线程从workers中移除出去,然后开始尝试着结束线程池。
现在回到addWorker方法,这个方法是比较重要的方法,他负责创建并启动新的线程。能够保证线程数量不会超过限制。
继续看下execute方法
- public void execute(Runnable command) {
- if (command == null)
- throw new NullPointerException();
- int c = ctl.get();
- if (workerCountOf(c) < corePoolSize) {
- if (addWorker(command, true))
- return;
- c = ctl.get();
- }
- if (isRunning(c) && workQueue.offer(command)) {
- int recheck = ctl.get();
- if (! isRunning(recheck) && remove(command))
- reject(command);
- else if (workerCountOf(recheck) == 0)
- addWorker(null, false);
- }
- else if (!addWorker(command, false))
- reject(command);
- }
我们看到,如果线程数量小于核心线程数量,就继续创建线程,并将当前任务放置到创建的线程中运行,如果返回false说明线程池挂了或者线程数量超过了限制。
接下来判断线程池是否运行中,如果线程池运行中就执行入队列操作,将任务放在队列中,等待空闲的线程来执行该任务。我们接下来就看看队列的事
我们看到BlockingQueue有很多的实现,不同的实现对应不同的业务场景,我们来看下用的最多的LinkedBlockingQueue队列。
- public boolean offer(E e) {
- if (e == null) throw new NullPointerException();
- final AtomicInteger count = this.count;
- if (count.get() == capacity)
- return false;
- int c = -1;
- Node<E> node = new Node<E>(e);
- final ReentrantLock putLock = this.putLock;
- putLock.lock();
- try {
- if (count.get() < capacity) {
- enqueue(node);
- c = count.getAndIncrement();
- if (c + 1 < capacity)
- notFull.signal();
- }
- } finally {
- putLock.unlock();
- }
- if (c == 0)
- signalNotEmpty();
- return c >= 0;
- }
看下这个入队方法,如果队列的数量等于最大限制数量就返回false,入队失败,不然就加上一个入队的锁,然后再次判断是不是超过了最大限制数量,
如果没有超过就执行enqueue方法执行真正的入队方法。
- private void enqueue(Node<E> node) {
- // assert putLock.isHeldByCurrentThread();
- // assert last.next == null;
- last = last.next = node;
- }
其实就是将节点追加到尾结点。
执行完入队操作后再次检查是否等于最大限制量,如果不等于就让非满条件进行通知。最后在finally中释放锁,方法最后检查是否入队成功并且只队列中只有一个,然后让非空条件进行通知。出队列也是一样的分析过程。
继续来看,入队之后会再次检查线程池的状态,是否可以继续运行,还会检查workerCountOf(c)==0,这个的目的是有可能在入队的时候其他的线程退出了导致没有线程可以使用,这个时候就要加入一个备用线程让进入队列的线程的运行得到保障。
最后判断addWorker(task,false)是否成功,如果成功就退出方法,如果失败就执行拒绝策略。
java线程池的初探的更多相关文章
- Java线程池详解
一.线程池初探 所谓线程池,就是将多个线程放在一个池子里面(所谓池化技术),然后需要线程的时候不是创建一个线程,而是从线程池里面获取一个可用的线程,然后执行我们的任务.线程池的关键在于它为我们管理了多 ...
- Java线程池详解(一)
一.线程池初探 所谓线程池,就是将多个线程放在一个池子里面(所谓池化技术),然后需要线程的时候不是创建一个线程,而是从线程池里面获取一个可用的线程,然后执行我们的任务.线程池的关键在于它为我们管理了多 ...
- Java 线程池框架核心代码分析--转
原文地址:http://www.codeceo.com/article/java-thread-pool-kernal.html 前言 多线程编程中,为每个任务分配一个线程是不现实的,线程创建的开销和 ...
- Java线程池使用说明
Java线程池使用说明 转自:http://blog.csdn.net/sd0902/article/details/8395677 一简介 线程的使用在java中占有极其重要的地位,在jdk1.4极 ...
- (转载)JAVA线程池管理
平时的开发中线程是个少不了的东西,比如tomcat里的servlet就是线程,没有线程我们如何提供多用户访问呢?不过很多刚开始接触线程的开发攻城师却在这个上面吃了不少苦头.怎么做一套简便的线程开发模式 ...
- Java线程池的那些事
熟悉java多线程的朋友一定十分了解java的线程池,jdk中的核心实现类为java.util.concurrent.ThreadPoolExecutor.大家可能了解到它的原理,甚至看过它的源码:但 ...
- 四种Java线程池用法解析
本文为大家分析四种Java线程池用法,供大家参考,具体内容如下 http://www.jb51.net/article/81843.htm 1.new Thread的弊端 执行一个异步任务你还只是如下 ...
- Java线程池的几种实现 及 常见问题讲解
工作中,经常会涉及到线程.比如有些任务,经常会交与线程去异步执行.抑或服务端程序为每个请求单独建立一个线程处理任务.线程之外的,比如我们用的数据库连接.这些创建销毁或者打开关闭的操作,非常影响系统性能 ...
- Java线程池应用
Executors工具类用于创建Java线程池和定时器. newFixedThreadPool:创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程.在任意点,在大多数 nThread ...
随机推荐
- React Router学习
React Router教程 本教程引用马伦老师的的教程 React项目的可用的路由库是React-Router,当然这也是官方支持的.它也分为: react-router 核心组件 react-ro ...
- 《LINUX内核设计与实现》第三周读书笔记——第一二章
<Linux内核设计与实现>读书笔记--第一二章 20135301张忻 估算学习时间:共2小时 读书:1.5 代码:0 作业:0 博客:0.5 实际学习时间:共2.5小时 读书:2.0 代 ...
- 关于cocos2dx 关键字的问题
今天码代码,在创建新场景的时候,.h文件里 class Game : public cocos2d::Layer没有问题,在Game类里面,声明了它的成员之后,开始在.cpp文件里面实现这个类,到重 ...
- BaseServlet 继承 httpServlet
BaseServlet 核心 package cn.core; import java.io.IOException; import java.lang.reflect.Method; impor ...
- 数学口袋精灵app(小学生四则运算app)开发需求
数学口袋精灵APP,摒除了传统乏味无趣学习数学四则运算的模式,采用游戏的形式,让小朋友在游戏中学习,培养了小朋友对数学的兴趣,让小朋友在游戏中运算能力得到充分提升.快乐学习,成长没烦恼! 项目名字:“ ...
- Supervised Hashing with Kernels, KSH
Notation 该论文中应用到较多符号,为避免混淆,在此进行解释: n:原始数据集的大小 l:实验中用于监督学习的数据集大小(矩阵S行/列的大小) m:辅助数据集,用于得到基于核的哈希函数 r:比特 ...
- JDBC学习笔记——PreparedStatement的使用
PreparedStatement public interface PreparedStatement extends Statement;可以看到PreparedStatement是Stateme ...
- Android-TabLayout设置内容宽度以及下划线宽度
默认图: 效果图: 项目中使用到需要像今日头条那种实现顶部横向滑动标题功能,本人项目中使用TabLayout+ViewPager实现,但是,实现后默认的TabLayout间距特别大,并且下划线,文字大 ...
- Alpha 冲刺二
团队成员 051601135 岳冠宇 051604103 陈思孝 031602629 刘意晗 031602248 郑智文 031602234 王淇 会议照片 项目燃尽图 项目进展 暂无进展, 项目描述 ...
- 实体框架自定义代码优先约定(EF6以后)
仅限EF6仅向前 - 此页面中讨论的功能,API等在实体框架6中引入.如果您使用的是早期版本,则部分或全部信息不适用. 使用Code First时,您的模型是使用一组约定从您的类计算的.默认的Code ...