Java多线程之“同步”
好习惯要坚持,这是我第二篇博文,任务略重,但是要坚持努力!!!
1.竞争条件
首先,我们回顾一下《Java核心技术卷》里讲到的多线程的“竞争条件”。由于各线程访问数据的次序,可能会产生讹误的现象,这样一个情况通常称为“竞争条件”。
那么,讹误具体是怎么产生的呢?本质上,是由于操作的非原子性。比如,假定两个线程同时执行指令 account[to] += amount;该指令可能会被处理如下:
1)将account[to]加载到寄存器。
2)增加amount[to]。
3)将结果写回account[to]。
现在,假定第一个线程执行步骤1和2,然后,它被剥夺了运行权。假定第二个线程被唤醒并修改了accounts数组中的同一项。然后,第1个线程被唤醒并完成第3步。这样,这一动作擦去了第二个线程所做的更新。于是,总金额不再正确。
---------------------------------------------我是分割线---------------------------------------------------------------------------------------------
好,我们再从java的内存模型来深层次讲讲“讹误”,这里有个概念叫做“缓存一致性”。
大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。
也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。举个简单的例子,比如下面的这段代码:
- i = i + 1;
当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。本文我们以多核CPU为例。比如同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。但是事实会是这样吗?
可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。也就是说,如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。
为了解决缓存不一致性问题,通常来说有以下2种解决方法:
1)通过在总线加LOCK#锁的方式
2)通过缓存一致性协议
这2种方式都是硬件层面上提供的方式。
在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。比如上面例子中 如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LCOK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。
但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。
所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
关于java内存模型,我有空会单独写一篇文章进行总结,这里仅仅浅谈一下。下面,我们再来谈谈锁对象,条件对象和synchronized关键字。
2.锁对象
有两种机制防止代码块受并发访问的干扰。Java语言提供了一个synchronized关键字达到这一目的,并且JavaSE 5.0引入了ReentrantLock类。
我们先看看ReentrantLock:
java.util.concurrent.locks.ReentrantLock 5.0 已实现的接口:Serializable, Lock
我们再来看看Lock接口: java.util.concurrent.locks.Lock 5.0,该接口下有2个方法:
(1) void lock() 获取这个锁:如果锁同时被另一个线程拥有则发生阻塞。
(2)void unlock() 释放这个锁。
让我们使用一个锁来保护Bank类的transfer方法。下面我们来看看3个类:
- package unsynch;
- import java.util.concurrent.locks.Lock;
- import java.util.concurrent.locks.ReentrantLock;
- /**
- * A bank with a number of bank accounts.
- * @version 1.30 2004-08-01
- * @author Cay Horstmann
- */
- public class Bank
- {
- private final double[] accounts;
- private Lock bankLock = new ReentrantLock();
- /**
- * Constructs the bank.
- * @param n the number of accounts
- * @param initialBalance the initial balance for each account
- */
- public Bank(int n, double initialBalance)
- {
- accounts = new double[n];
- for (int i = 0; i < accounts.length; i++)
- accounts[i] = initialBalance;
- }
- /**
- * Transfers money from one account to another.
- * @param from the account to transfer from
- * @param to the account to transfer to
- * @param amount the amount to transfer
- */
- public void transfer(int from, int to, double amount)
- {
- bankLock.lock();
- try{
- if (accounts[from] < amount) return;
- System.out.print(Thread.currentThread());
- accounts[from] -= amount;
- System.out.printf(" %10.2f from %d to %d", amount, from, to);
- accounts[to] += amount;
- System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
- }
- finally{
- bankLock.unlock();
- }
- }
- /**
- * Gets the sum of all account balances.
- * @return the total balance
- */
- public double getTotalBalance()
- {
- double sum = 0;
- for (double a : accounts)
- sum += a;
- return sum;
- }
- /**
- * Gets the number of accounts in the bank.
- * @return the number of accounts
- */
- public int size()
- {
- return accounts.length;
- }
- }
- package unsynch;
- /**
- * A runnable that transfers money from an account to other accounts in a bank.
- * @version 1.30 2004-08-01
- * @author Cay Horstmann
- */
- public class TransferRunnable implements Runnable
- {
- private Bank bank;
- private int fromAccount;
- private double maxAmount;
- private int DELAY = 10;
- /**
- * Constructs a transfer runnable.
- * @param b the bank between whose account money is transferred
- * @param from the account to transfer money from
- * @param max the maximum amount of money in each transfer
- */
- public TransferRunnable(Bank b, int from, double max)
- {
- bank = b;
- fromAccount = from;
- maxAmount = max;
- }
- public void run()
- {
- try
- {
- while (true)
- {
- int toAccount = (int) (bank.size() * Math.random());
- double amount = maxAmount * Math.random();
- bank.transfer(fromAccount, toAccount, amount);
- Thread.sleep((int) (DELAY * Math.random()));
- }
- }
- catch (InterruptedException e)
- {
- }
- }
- }
- package unsynch;
- /**
- * This program shows data corruption when multiple threads access a data structure.
- * @version 1.30 2004-08-01
- * @author Cay Horstmann
- */
- public class UnsynchBankTest
- {
- public static final int NACCOUNTS = 100;
- public static final double INITIAL_BALANCE = 1000;
- public static void main(String[] args)
- {
- Bank b = new Bank(NACCOUNTS, INITIAL_BALANCE);
- int i;
- for (i = 0; i < NACCOUNTS; i++)
- {
- TransferRunnable r = new TransferRunnable(b, i, INITIAL_BALANCE);
- Thread t = new Thread(r);
- t.start();
- }
- }
- }
这段程序模拟一个有若干账户的银行。随机地生成在这些账户之间转移钱款的交易。每一个账户有一个线程。每一笔交易中,会从线程所服务的账户中随机转移一定数目的钱款到另一个随机账户。尝试一下,添加加锁代码到transfer方法并且再次运行程序,你永远可以运行它,而银行的余额不会出现讹误。
假定一个线程调用transfer,在执行结束前被剥夺了运行权。假定第二个线程也调用transfer,由于第二个线程不能获得锁,将在调用lock方法时被阻塞。他必须等待第一个线程完成transfer方法的执行之后才能再度被激活。当第一个线程释放锁时,那么第二个线程才能开始运行。
注意每一个Bank对象有自己的ReentrantLock对象。如果两个线程试图访问同一个Bank对象,那么锁以串行方式提供服务。但是,如果两个线程访问不同的Bank对象,每一个线程得到不同的锁对象,两个线程都不会发生阻塞。本该如此,因为线程在操纵不同的Bank实例的时候,线程之间不会互相影响。
锁是可重入的,因为线程可以重复地获得已经持有的锁。锁保持一个持有计数(hhldcount)来跟踪对lock方法的嵌套调用。线程在每一次调用lock都要调用unlock来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同锁的方法。例如,transfer方法调用getTotalBalance方法,这也会封锁bankLock对象,此时bankLock对象的持有计数为2。当getTotalBalance方法退出的时候,持有计数变回1。当transfer方法退出的时候,持有计数变为0。线程释放锁。
警告:把解锁操作括在finally子句之内是至关重要的。如果临界区的代码抛出异常,锁必须被释放。否则,其它线程将永远被阻塞。
3.条件对象
条件对象经常被称为条件变量。一个锁对象可以有一个或多个相关的条件对象。你可以用newCondition方法获得一个条件对象。
下面我们再来看看Java API 中的Conditon接口的定义:java.util.concurrent.locks.Condition 5.0,它有几个方法:
void await() 将该线程放到条件的等待集中。
void signalAll() 解除该条件的等待集中的所有线程的阻塞状态(通过竞争)
void signal() 从该条件的等待集中随机地选择一个线程,解除其阻塞状态。
下面我们通过一个代码的示例来说明这个条件对象如何使用:
- package synch;
- import java.util.concurrent.locks.*;
- /**
- * A bank with a number of bank accounts that uses locks for serializing access.
- * @version 1.30 2004-08-01
- * @author Cay Horstmann
- */
- public class Bank
- {
- private final double[] accounts;
- private Lock bankLock;
- private Condition sufficientFunds;
- /**
- * Constructs the bank.
- * @param n the number of accounts
- * @param initialBalance the initial balance for each account
- */
- public Bank(int n, double initialBalance)
- {
- accounts = new double[n];
- for (int i = 0; i < accounts.length; i++)
- accounts[i] = initialBalance;
- bankLock = new ReentrantLock();
- sufficientFunds = bankLock.newCondition();
- }
- /**
- * Transfers money from one account to another.
- * @param from the account to transfer from
- * @param to the account to transfer to
- * @param amount the amount to transfer
- */
- public void transfer(int from, int to, double amount) throws InterruptedException
- {
- bankLock.lock();
- try
- {
- while (accounts[from] < amount)
- sufficientFunds.await();
- System.out.print(Thread.currentThread());
- accounts[from] -= amount;
- System.out.printf(" %10.2f from %d to %d", amount, from, to);
- accounts[to] += amount;
- System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
- sufficientFunds.signalAll();
- }
- finally
- {
- bankLock.unlock();
- }
- }
- /**
- * Gets the sum of all account balances.
- * @return the total balance
- */
- public double getTotalBalance()
- {
- bankLock.lock();
- try
- {
- double sum = 0;
- for (double a : accounts)
- sum += a;
- return sum;
- }
- finally
- {
- bankLock.unlock();
- }
- }
- /**
- * Gets the number of accounts in the bank.
- * @return the number of accounts
- */
- public int size()
- {
- return accounts.length;
- }
- }
- package synch;
- /**
- * This program shows how multiple threads can safely access a data structure.
- * @version 1.30 2004-08-01
- * @author Cay Horstmann
- */
- public class SynchBankTest
- {
- public static final int NACCOUNTS = 100;
- public static final double INITIAL_BALANCE = 1000;
- public static void main(String[] args)
- {
- Bank b = new Bank(NACCOUNTS, INITIAL_BALANCE);
- int i;
- for (i = 0; i < NACCOUNTS; i++)
- {
- TransferRunnable r = new TransferRunnable(b, i, INITIAL_BALANCE);
- Thread t = new Thread(r);
- t.start();
- }
- }
- }
- package synch;
- /**
- * A runnable that transfers money from an account to other accounts in a bank.
- * @version 1.30 2004-08-01
- * @author Cay Horstmann
- */
- public class TransferRunnable implements Runnable
- {
- private Bank bank;
- private int fromAccount;
- private double maxAmount;
- private int DELAY = 10;
- /**
- * Constructs a transfer runnable.
- * @param b the bank between whose account money is transferred
- * @param from the account to transfer money from
- * @param max the maximum amount of money in each transfer
- */
- public TransferRunnable(Bank b, int from, double max)
- {
- bank = b;
- fromAccount = from;
- maxAmount = max;
- }
- public void run()
- {
- try
- {
- while (true)
- {
- int toAccount = (int) (bank.size() * Math.random());
- double amount = maxAmount * Math.random();
- bank.transfer(fromAccount, toAccount, amount);
- Thread.sleep((int) (DELAY * Math.random()));
- }
- }
- catch (InterruptedException e)
- {
- }
- }
- }
这段代码显然比上段代码多了一些东西,为什么要多这些东西呢?我们这么做是为了细化银行的模拟程序。我们避免选择没有足够资金的账户作为转出账户。
如果transfer方法发现余额不足,它调用sufficientFunds.await();当前线程现在被阻塞了,并放弃了锁。一旦一个线程调用await方法,它进入该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞状态,直到另一个线程调用同一条件上的signalAll方法时为止。
当另一个线程转账是,它应该调用sufficientFunds.signalAll();
这一调用重新激活因为这一条件而等待的所有线程。当这些线程从等待集当中移出时,它们再次成为可行的,调度器将再次激活它们。同时,它们将试图重新进入该对象。一旦锁成为可用的,它们中的某个将从await调用返回,获得该锁并从被阻塞的地方继续执行。
此时,线程应该再次测试该条件。由于无法确保该条件被满足——signalAll方法仅仅是通知正在等待的线程:此时有可能已经满足条件,值得再次去检测该条件。
最后,有一点需要注意:当一个线程调用await()时,它没有办法重新激活自身。它寄希望于其他线程。如果没有其他线程来重新激活等待的线程,它就永远不再运行了。这将导致令人不快的死锁(deadlock)现象。总结一下:每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。
4.synchronized关键字
大多数情况下,我们并不需要Lock和Condition接口为程序设计人员提供的高度的锁定控制。从1.0版本开始,java中的每一个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。
换句话说,
public synchronized void method() 等价于 public void method()
{ {
method body this.intrinsicLock.lock();
} try
{
method body
}
finally{this.intrinsicLock.unlock();}
}
例如,可以简单地声明Bank类的transfer方法为synchronized,而不是使用一个显式的锁。
同样的,下面我们通过一段代码来理解synchronized关键字。
- package synch2;
- /**
- * A bank with a number of bank accounts that uses synchronization primitives.
- * @version 1.30 2004-08-01
- * @author Cay Horstmann
- */
- public class Bank
- {
- private final double[] accounts;
- /**
- * Constructs the bank.
- * @param n the number of accounts
- * @param initialBalance the initial balance for each account
- */
- public Bank(int n, double initialBalance)
- {
- accounts = new double[n];
- for (int i = 0; i < accounts.length; i++)
- accounts[i] = initialBalance;
- }
- /**
- * Transfers money from one account to another.
- * @param from the account to transfer from
- * @param to the account to transfer to
- * @param amount the amount to transfer
- */
- public synchronized void transfer(int from, int to, double amount) throws InterruptedException
- {
- while (accounts[from] < amount)
- wait();
- System.out.print(Thread.currentThread());
- accounts[from] -= amount;
- System.out.printf(" %10.2f from %d to %d", amount, from, to);
- accounts[to] += amount;
- System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
- notifyAll();
- }
- /**
- * Gets the sum of all account balances.
- * @return the total balance
- */
- public synchronized double getTotalBalance()
- {
- double sum = 0;
- for (double a : accounts)
- sum += a;
- return sum;
- }
- /**
- * Gets the number of accounts in the bank.
- * @return the number of accounts
- */
- public int size()
- {
- return accounts.length;
- }
- }
尤其需要注意的是,内部对象锁只有一个相关条件,可能是不够的!在代码中应该使用哪一种?Lock和Condition对象还是同步方法?下面是一些建议。
1)最好既不使用Lock/Condition也不使用synchronized关键字。在许多情况下你可以使用java.util.concurrent包中的一种机制,它会为你处理所有的加锁。
2)如果synchronized关键字适合你的程序,那么请尽量使用它,这样可以减少编码的代码量,减少出错的几率。
3)如果特别需要Lock/Condition结构提供的独有特性时,才使用Lock/Condition。
如上所述,内部对象只有一个相关条件。wait方法添加一个线程到等待集中,notifyAll/notify方法解除等待线程的阻塞状态。换句话说,调用wait或notifyAll等价于
intrinsicCondition.await();
intrinsicCondition.signalAll();
注释:wait,notifyAll以及notify方法是Object类的final方法。Condition方法必须被命名为await,signalAll和signal以便它们不会与那些方法发生冲突。
下面,我们再来看看java.lang.Object内的几个相关方法:
- void notifyAll()
解除那些在该对象上调用wait方法的线程的阻塞状态。该方法只能在同步方法或同步块内部调用。如果当前线程不是对象锁的持有者,该方法抛出一IllegalMonitorStateException异常。
- void notify()
随机选择一个在该对象上调用wait方法的线程,解除其阻塞状态。该方法只能在一个同步方法或同步块中调用,如果当前线程不是对象锁的持有者,该方法抛出一个IllgalMonitorStateException异常。
- void wait()
导致线程进入等待状态只到它被通知。该方法只能在一个同步方法中调用。如果当前线程不是对象锁的持有者,该方法抛出一个IllegalMonitorStateException异常。
- void wait(long millis)
- void wait(long millis,int nanos)
需要尤其注意以上红色字体部分。这里有2层意思:(1)这意味着,使用wait(),notifyAll(),notify()时,必须使用synchronized关键字!(2)然而,使用synchronized时,未必会用wait()等方法。synchronized方法只是让线程排队,就是同步代码块,但是排队后一个线程获得内部锁后,未必就满足继续执行下去的条件!所以,考虑到余额不足时要阻塞,就必须使用wait(),如果要考虑多个条件,则要考虑使用Lock/Conditon了。
5.同步阻塞
每一个java对象有一个锁,线程可以通过调用同步方法获得锁,还有一种机制可以获得锁,通过进入一个同步阻塞,即同步块!我们有时会遇到如下“特殊的”锁,例如:
public class Bank
{
private double [] accounts;
private Object lock = new Object();
...
public void transfer (int from,int to,int amount)
{
synchronized(lock)
{
accounts[from] -=amount;
accounts[to] += amount;
}
System.out.println(...);
}
}
在此,lock对象被创建仅仅是用来使用每个java对象持有的锁。程序猿使用一个对象的锁来实现额外的原子操作,实际上成为客户端锁定。
客户端锁定是非常脆弱的,通常不推荐使用。
----------------------------------------------我是分割线-------------------------------------------------
到这里,同步第一部分讲完了。写这一部分整整用了我三个晚上!确实,写博客是个慢工夫,但是印象深刻,脉络清晰。看着《java核心技术卷》厚厚一本,而我进度如蜗牛!还有很多事要做,特别忙,真是心急如焚。这是个非常蛋疼的问题,然而不积跬步无以至千里,这是作为一个优秀程序猿的必经之路,望君加油!
Java多线程之“同步”的更多相关文章
- Java多线程的同步控制记录
Java多线程的同步控制记录 一.重入锁 重入锁完全可以代替 synchronized 关键字.在JDK 1.5 早期版本,重入锁的性能优于 synchronized.JDK 1.6 开始,对于 sy ...
- Java多线程之同步集合和并发集合
Java多线程之同步集合和并发集合 不管是同步集合还是并发集合他们都支持线程安全,他们之间主要的区别体现在性能和可扩展性,还有他们如何实现的线程安全. 同步集合类 Hashtable Vector 同 ...
- Java多线程编程(同步、死锁、生产消费者问题)
Java多线程编程(同步.死锁.生产消费): 关于线程同步以及死锁问题: 线程同步概念:是指若干个线程对象并行进行资源的访问时实现的资源处理保护操作: 线程死锁概念:是指两个线程都在等待对方先完成,造 ...
- Java多线程synchronized同步
非线程安全问题 “非线程安全”问题存在于“实例变量”中,如果是方法内部的私有变量,则不存在“非线程问题”.也即是说,方法中的变量永远是线程安全的. 如果多个线程共同访问1个对象中的实例变量,则可能线程 ...
- 转载:浅谈Java多线程的同步问题【很好我就留下来,多分共享】
转载:http://www.cnblogs.com/phinecos/archive/2010/03/13/1684877.html#undefined 多线程的同步依靠的是对象锁机制,synchro ...
- 浅谈Java多线程的同步问题 【转】
多线程的同步依靠的是对象锁机制,synchronized关键字的背后就是利用了封锁来实现对共享资源的互斥访问. 下面以一个简单的实例来进行对比分析.实例要完成的工作非常简单,就是创建10个线程,每个线 ...
- Java多线程的同步机制(synchronized)
一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在 java里边就是拿到某个同步对象的锁(一个对象只有一把锁): 如果这个时候同步对象的锁被其他线程拿走了,他(这个 ...
- Java多线程 - 线程同步
多线程操作同一个对象时,容易引发线程安全问题.为了解决线程安全问题,Java多线程引入了同步监视器. 同步代码块 同步代码块语法格式如下: synchronized(obj){ //此处的代码即为同步 ...
- JAVA多线程线程同步问题
线程同步 在多线程的编程环境下,可能看着没有问题的代码在运行几千上万或者更多次后,出现了一些看着很奇怪的问题,出现这样的问题的原因就是可能会有两个或者更多个线程进入了同一块业务处理代码中导致了判断失效 ...
- JAVA - 多线程的同步
多线程的同步 1. 锁对象. 应用场景:当某个数据可能被其他线程修改时,给涉及到数据的方法上锁,保证同一时刻只有拥有这个锁的线程能访问该数据,其他要调用这个方法的线程被阻塞.注意:必须是不同线程访问同 ...
随机推荐
- Machine Learning笔记整理 ------ (二)训练集与测试集的划分
在实际应用中,一般会选择将数据集划分为训练集(training set).验证集(validation set)和测试集(testing set).其中,训练集用于训练模型,验证集用于调参.算法选择等 ...
- Thunder团队第六周 - Scrum会议1
Scrum会议1 小组名称:Thunder 项目名称:i阅app Scrum Master:王航 工作照片: 参会成员: 王航(Master):http://www.cnblogs.com/wangh ...
- Thunder团队第二周 - Scrum会议1
Scrum会议1 小组名称:Thunder 项目名称:爱阅app Scrum Master:王航 工作照片: 参会成员: 王航(Master):http://www.cnblogs.com/wangh ...
- Android 网络编程 API笔记 - java.net 包 权限 地址 套接字 相关类 简介
Android 网络编程相关的包 : 9 包, 20 接口, 103 类, 6 枚举, 14异常; -- Java包 : java.net 包 (6接口, 34类, 2枚举, 12异常); -- An ...
- 计算器软件实现系列(七)WPF+SQL+策略模式
一 整体概述 本次设计主要是在WPF的页面中实现的,属于表现层的更换,数据库部分用的还是数据库的封装,其中引用了策略模式 二 设计思路 1 在出题页面,进行试题的编辑,在编辑后会自动保存到数据库中 ...
- android入门 — ListView
ListView主要是用来解决大量数据展示的问题,它的用途很广泛,几乎所有的app都会用到,比如说知乎.今日头条.微博.通讯录等. ListView允许用户通过上下滑动的方式将屏幕外的数据滚动到屏幕中 ...
- python学习笔记02:运行python程序
1.启动cmd命令行,输入python后回车,运行python解释器: 输入python代码后回车: print('Hello World')
- array_intersect_assoc 与array_intersect区别
1.array_intersect_assoc — 带索引检查计算数组的交集 说明 array array_intersect_assoc ( array $array1 , array $array ...
- [STAThread] 作用
[STAThread]是一种线程模型,用在程序的入口方法上(在C#和VB.NET里是Main()方法),来指定当前线程的ApartmentState 是STA. [STAThread]是声明开始线程用 ...
- MySQL、HBase、ES的特点和区别
MySQL:关系型数据库,主要面向OLTP,支持事务,支持二级索引,支持sql,支持主从.Group Replication架构模型(本文全部以Innodb为例,不涉及别的存储引擎). HBase:基 ...