博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
线程同步机制(一)--Synchronized,Lock
阅读量:7005 次
发布时间:2019-06-27

本文共 20036 字,大约阅读时间需要 66 分钟。

  hot3.png

 多个执行线程共享一个资源的情形是最常见的并发编程情景之一。在并发应用中常常遇到这样的情景:多个线程读或者写相同的数据,或者访问相同的文件或者数据库连接。为了防止这些共享资源可能出现错误或者数据不一致,人们引入了临界区(critical section)概念。临界区是一个用以访问共享资源的代码块,这个代码块中同一时间只允许一个线程执行。

    为了实现这个临界区,Java提供了同步机制。当一个线程试图访问一个临界区时,它将使用一种同步机制来查看是不是已经有其他线程进入临界区。如果没有,它就进入临界区,如果有,他就被同步机制挂起,直到进入的线程离开临界区。如果在等待进入临界区的线程不止一个,JVM会选择其中的一个,其余将继续等待。

    一、使用synchronized实现同步方法。如果一个对象已用synchronized关键字来生命,那么只有一个执行线程被允许访问它。如果其他线程试图访问这个对象的其他方法,它将被挂起,知道第一个线程执行完正在运行的方法。

    也就是说,每一个用synchronized关键字声明的方法都是临界区。静态方法则有不同的行为。用synchronized关键字声明的静态方法,同时只能够被一个线程访问,但是其他线程可以访问这个对象的其他非静态方法。必须谨慎这一点。

package org.concurrency.synchronization;/** * @author Administrator * 银行账户模型 */public class Account { private double balance;//余额 public double getBalance() {  return balance; } public void setBalance(double balance) {  this.balance = balance; } /**  * 转账,使余额增加  * */ public synchronized void addAccount(double amount){  double tmp = balance;  System.out.printf("before_add_Account : Current Balance: %f\n",tmp);  try {   Thread.sleep(10);  } catch (InterruptedException e) {   // TODO Auto-generated catch block   e.printStackTrace();  }  tmp += amount;  this.balance = tmp; } /**  * 转出,使余额减少  * */ public synchronized void subtractAmount(double amount){  double tmp = balance;  System.out.printf("before_subtract_Account : Current Balance: %f\n",tmp);  try {   Thread.sleep(10);  } catch (InterruptedException e) {   // TODO Auto-generated catch block   e.printStackTrace();  }  tmp -= amount;  this.balance = tmp;  System.out.printf("subtract_Account : Current Balance: %f\n",this.getBalance()); }}package org.concurrency.synchronization;/** * @author Administrator * ATM模拟类 */public class Bank implements Runnable { private Account account;  public Bank(Account account) {  super();  this.account = account; } @Override public void run() {  // TODO Auto-generated method stub  for(int i = 0;i<10;i++){   account.subtractAmount(1000);  } }}package org.concurrency.synchronization;/** * @author Administrator * 公司模拟类 */public class Company implements Runnable { private Account account;  public Company(Account account) {  super();  this.account = account; } @Override public void run() {  // TODO Auto-generated method stub  for(int i = 0;i<10;i++){   account.addAccount(1000);  } }}package org.concurrency.synchronization;/** * @author Administrator * 线程启动类 */public class Main { public static void main(String[] args) {  // TODO Auto-generated method stub  Account account = new Account();  account.setBalance(1000);    Company company = new Company(account);  Thread companyThread = new Thread(company);    Bank bank = new Bank(account);  Thread bankThread = new Thread(bank);      companyThread.start();  bankThread.start();    try {   companyThread.join();   bankThread.join();      System.out.printf("Account : Final Balance: %f\n",account.getBalance());  } catch (InterruptedException e) {   // TODO Auto-generated catch block   e.printStackTrace();  } }}

运行结果:

before_add_Account : Current Balance: 1000.000000before_add_Account : Current Balance: 2000.000000before_add_Account : Current Balance: 3000.000000before_subtract_Account : Current Balance: 4000.000000subtract_Account : Current Balance: 3000.000000before_subtract_Account : Current Balance: 3000.000000subtract_Account : Current Balance: 2000.000000before_subtract_Account : Current Balance: 2000.000000subtract_Account : Current Balance: 1000.000000before_subtract_Account : Current Balance: 1000.000000subtract_Account : Current Balance: 0.000000before_subtract_Account : Current Balance: 0.000000subtract_Account : Current Balance: -1000.000000before_subtract_Account : Current Balance: -1000.000000subtract_Account : Current Balance: -2000.000000before_subtract_Account : Current Balance: -2000.000000subtract_Account : Current Balance: -3000.000000before_subtract_Account : Current Balance: -3000.000000subtract_Account : Current Balance: -4000.000000before_subtract_Account : Current Balance: -4000.000000subtract_Account : Current Balance: -5000.000000before_subtract_Account : Current Balance: -5000.000000subtract_Account : Current Balance: -6000.000000before_add_Account : Current Balance: -6000.000000before_add_Account : Current Balance: -5000.000000before_add_Account : Current Balance: -4000.000000before_add_Account : Current Balance: -3000.000000before_add_Account : Current Balance: -2000.000000before_add_Account : Current Balance: -1000.000000before_add_Account : Current Balance: 0.000000Account : Final Balance: 1000.000000

    在示例中使用了临时变量,故意制造了一个错误场景:tmp先获取账户余额,然后进行数额累加,最后把最终结果更新为账户余额。此外,通过Thread.sleep()方法增加了延时,使得正在执行的线程休眠,而此时其他线程也可能执行这个方法,因此可能会变更余额,引发错误。而synchronized关键字避免了这类错误的发生。

    在没有synchronized关键字的情况下,一个线程读取了账户余额然后进入休眠,这个时候其他线程读取这个账户余额,最终这两个方法都将修改同一个余额,这将产生数据不一致,去除synchronized关键字,运行结果,最初和最后的金额不一致,并且多次运行,将获取多个不同的结果

before_subtract_Account : Current Balance: 1000.000000before_add_Account : Current Balance: 1000.000000before_add_Account : Current Balance: 0.000000subtract_Account : Current Balance: 0.000000before_subtract_Account : Current Balance: 0.000000subtract_Account : Current Balance: -1000.000000before_subtract_Account : Current Balance: -1000.000000before_add_Account : Current Balance: -1000.000000before_add_Account : Current Balance: -2000.000000subtract_Account : Current Balance: -2000.000000before_subtract_Account : Current Balance: -2000.000000before_add_Account : Current Balance: -1000.000000subtract_Account : Current Balance: -1000.000000before_subtract_Account : Current Balance: -1000.000000before_add_Account : Current Balance: 0.000000subtract_Account : Current Balance: -2000.000000before_subtract_Account : Current Balance: -2000.000000subtract_Account : Current Balance: 1000.000000before_subtract_Account : Current Balance: 1000.000000before_add_Account : Current Balance: 1000.000000subtract_Account : Current Balance: 0.000000before_subtract_Account : Current Balance: 0.000000before_add_Account : Current Balance: 2000.000000subtract_Account : Current Balance: -1000.000000before_subtract_Account : Current Balance: -1000.000000before_add_Account : Current Balance: 3000.000000subtract_Account : Current Balance: -2000.000000before_subtract_Account : Current Balance: -2000.000000before_add_Account : Current Balance: 4000.000000subtract_Account : Current Balance: -3000.000000Account : Final Balance: 5000.000000

 synchronized关键字会降低应用程序的性能,因此只能在并发情况中需要修改共享数据的方法上使用它。如果多个线程访问同一个synchronized方法,则只有一个线程可以访问,其他线程将等待。

可以递归调用被synchronized声明的方法。当线程访问一个对象的同步方法时,它还可调用这个对象的其他同步方法。

我们通过synchronized关键字来保护代码块的访问。应该这样利用synchronized关键字:方法的其余部分保持在synchronized代码块之外,以获取更好的性能。临界区(同一时间只能被一个线程访问的代码块)的访问应该尽可能的短。

二、使用非依赖属性实现同步。当使用synchronized关键字来保护代码块时,必须把对象引用作为传入参数。通常情况下,使用this关键字来引用执行方法所属对象。在类中有多个非依赖来属性,他们被多个线程共享,则必须同步每一个变量的访问,同一时刻只允许一个线程访问一个属性变量,其他线程访问令一个属性变量。下面实例将演示一个电影院售piao的场景。

package org.concurrency.synchronization;/** * @author Administrator * 电影院类 */public class Cinema { private long vacanciesCinema1; private long vacanciesCinema2;  private final Object controlCinema1; private final Object controlCinema2; public Cinema() {  controlCinema1 = new Object();  controlCinema2 = new Object();   vacanciesCinema1 = 20;   vacanciesCinema2 = 20; }  /**  * 卖出piao  * */ public boolean selTickets1(int number){  synchronized (controlCinema1) {   if(number 

 工作原理

用synchronized关键字保护代码块时,我们使用对象作为它的传入参数。JVM保证同一时间只有一个线程能够访问这个对象的代码保护块。这个例子使用了一个对象来控制对vacanciesCinema1属性的访问,所以同一时间只有一个线程能够修改这个属性;使用了另一个对象来控制vacanciesCinema2属性的访问,所以同一时间只有一个线程能够修改这个属性。但是,这个离职允许同事运行两个线程:一个修改vacanciesCinema1属性,另一个修改vacanciesCinema2属性。

运行结果总是如下:

vacanciesCinema1:卖出3张vacanciesCinema2:卖出2张vacanciesCinema2:卖出4张vacanciesCinema1:卖出2张vacanciesCinema1:卖出2张vacanciesCinema2:卖出2张vacanciesCinema1:卖出1张vacanciesCinema2:退回2张vacanciesCinema1:退回3张vacanciesCinema1:卖出5张vacanciesCinema2:卖出2张vacanciesCinema1:卖出3张vacanciesCinema2:卖出2张vacanciesCinema2:卖出2张vacanciesCinema1:卖出2张vacanciesCinema2:卖出2张Room 1 Vacancies: 5Room 2 Vacancies: 6

 三、在同步代码中使用条件

    在并发编程中一个典型的问题就是生产者-消费者(Producer - Consumer)问题。我们有一个数据缓冲区,一个或多个数据生产者将把数据存入这个缓冲区,一个或多个数据消费者将数据从缓冲区取走。

    这个缓冲区是一个共享数据结构,必须使用同步机制控制对它的访问,例如使用synchronized关键字,但是会受到更多的限制。如果缓冲区是满的,生产者就不能再放入数据,如果数据是空的,消费者就不能读取数据。

    对这些场景,Java在Object类中提供了wait()、notify()和notifyAll()方法。线程可以在同步代码块中使用wait()方法。不能再同步代码块中调用wait()方法。当一个线程调用wait()方法时,JVM将这个线程置入休眠,并且释放控制这个同步代码块的对象,同时允许其他线程执行这个对象控制的其他同步代码块。为了唤醒这个线程,必须在这个对象控制的某个同步代码块儿中调用notify()或者notifyAll()方法。

package org.concurrency.synchronization.producer;import java.util.Date;import java.util.LinkedList;import java.util.List;public class EventStorage { private int maxSize; private List
 storage; public EventStorage() {  maxSize = 0;  storage = new LinkedList
(); }  public synchronized void set(){  while(storage.size() == maxSize){   try {    wait();   } catch (InterruptedException e) {    // TODO Auto-generated catch block    e.printStackTrace();   }  }  storage.add(new Date());  System.out.printf("Set : %d",storage.size());  notifyAll(); }  public synchronized void get(){  while(storage.size() == 0){   try {    wait();   } catch (InterruptedException e) {    // TODO Auto-generated catch block    e.printStackTrace();   }  }  System.out.printf("Get: %d: %s",storage.size(),((LinkedList
)storage).poll());  notifyAll(); }}package org.concurrency.synchronization.producer;public class Producer implements Runnable { private EventStorage storage;  public Producer(EventStorage storage) {  super();  this.storage = storage; } @Override public void run() {  // TODO Auto-generated method stub  for(int i = 0;i<10;i++){   storage.set();  } }}package org.concurrency.synchronization.producer;public class Consumer implements Runnable { private EventStorage storage;  public Consumer(EventStorage storage) {  super();  this.storage = storage; } @Override public void run() {  // TODO Auto-generated method stub  for(int i = 0;i<10;i++){   storage.get();  } }}package org.concurrency.synchronization.producer;public class Main_Producer { public static void main(String[] args) {  // TODO Auto-generated method stub  EventStorage storage = new EventStorage();  Thread thread1 = new Thread(new Producer(storage));  Thread thread2 = new Thread(new Consumer(storage));    thread1.start();  thread2.start();   }}

四、使用锁实现同步

       Java提供了基于Lock接口及其实现类的同步机制,它比synchronized关键字更强大也更灵活。

  • 支持更灵活的同步代码块结构。使用synchronized关键字时,只能在同一个synchronized块结构中获取和释放控制。Lock接口允许实现更复杂的临界区结构(即控制的获取和释放不在同一个块结构中)

  • 相比synchronized关键字,Lock借口提供了更多的功能。其中一个新功能是tryLock()方法的实现。这个方法试图获取锁,如果锁已经被其他线程获取,它将返回false并继续往下执行代码。

  • Lock接口允许分离读和写的操作,允许多个读线程和只有一个写线程

示例学习如何使用锁来同步代码,并且使用Lock接口和它的实现类--ReentrantLock类来创建一个临界区。

package org.concurrency.synchronization.lock;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;/** * @author kucs * 创建一个打印队列类 */public class PrintQueue { //声明一个锁对象 private final Lock queueLock = new ReentrantLock(); //实现打印方法printJob() public void printJob(Object document){  //在打印方法内部,通过调用lock()方法获取对锁对象的控制  queueLock.lock();  //打印模拟文档  try {   Long duration = (long) (Math.random() * 10000);   System.out.println(Thread.currentThread().getName()+":"     + "PrintQueue:Printing a Job during "+(duration/1000)+" seconds");   Thread.sleep(duration);  } catch (Exception e) {   // TODO Auto-generated catch block   e.printStackTrace();  }finally{   //通过unlock()方法释放对锁对象的控制   queueLock.unlock();  } }}package org.concurrency.synchronization.lock;/** * @author Administrator * 创建打印工作类Job */public class Job implements Runnable { private PrintQueue printQueue;  public Job(PrintQueue printQueue) {  this.printQueue = printQueue; } @Override public void run() {  // TODO Auto-generated method stub  System.out.printf("%s: Gong to print a document\n", Thread.currentThread().getName());  printQueue.printJob(new Object());  System.out.printf("%s: The document has been printed\n",Thread.currentThread().getName()); }}package org.concurrency.synchronization.lock;public class Main { public static void main(String[] args) {  // TODO Auto-generated method stub  PrintQueue printQueue = new PrintQueue();  Thread[] threads = new Thread[10];  for(int i = 0;i < threads.length;i++){   threads[i] = new Thread(new Job(printQueue));  }  for (Thread thread : threads) {   thread.start();  } }}

运行结果:

Thread-0: Gong to print a documentThread-9: Gong to print a documentThread-6: Gong to print a documentThread-8: Gong to print a documentThread-7: Gong to print a documentThread-5: Gong to print a documentThread-4: Gong to print a documentThread-3: Gong to print a documentThread-2: Gong to print a documentThread-1: Gong to print a documentThread-0:PrintQueue:Printing a Job during 7 secondsThread-9:PrintQueue:Printing a Job during 2 secondsThread-0: The document has been printedThread-9: The document has been printedThread-6:PrintQueue:Printing a Job during 1 secondsThread-6: The document has been printedThread-8:PrintQueue:Printing a Job during 3 secondsThread-8: The document has been printedThread-7:PrintQueue:Printing a Job during 7 secondsThread-7: The document has been printedThread-5:PrintQueue:Printing a Job during 9 secondsThread-4:PrintQueue:Printing a Job during 4 secondsThread-5: The document has been printedThread-4: The document has been printedThread-3:PrintQueue:Printing a Job during 3 secondsThread-3: The document has been printedThread-2:PrintQueue:Printing a Job during 4 secondsThread-2: The document has been printedThread-1:PrintQueue:Printing a Job during 9 secondsThread-1: The document has been printed

    这个范例主要部分是打印队列PrintQueue中的PrintJob()方法。我们使用锁实现一个临界区,并且保证同一时间只有一个执行线程访问这个临界区时,必须创建ReentrantLock对象。在这个临界区的开始,必须通过lock()方法获取对锁的控制。当线程A访问这个方法时,如果没有其他线程获取对这个锁的控制,lock()方法将让线程A获得锁并且允许它立刻执行临界区代码。否则,如果其他线程B正在执行这个锁保护的临界区代码。lock()方法将让线程A休眠直到线程B执行完临界区代码

    在线程离开临界区时,我们必须使用unlock()方法来释放它持有的锁,以让其他线程来访问临界区。如果在离开临界区的时候没有调用unlock()方法,其他线程将永远等待,从而导致死锁(DeadLock)情景。如果再临界区使用了try-catch块,不要忘记将unlock()方法放入finally部分。

    Lock接口(和它的实现类ReentrantLock)还提供了另一种方法来获取锁,即tryLock()方法。跟lock()方法最大的不同是:线程使用tryLock()不能够获取锁,tryLock()会立即返回,它不会将线程置入睡眠。tryLock()方法返回一个布尔值,true表示线程获取了锁,false表示线程没有获取锁。

    注意:tryLock()方法返回false,程序不会执行临界区代码,如果执行了,这样应用可能会出现错误的结果。

 五、使用读写锁实现同步数据访问

    锁机制最大的改进之一就是ReadWriteLock接口和它的唯一实现类ReentrantReadWriteLock。这个类有两个锁,一个是读操作锁,一个是写操作锁。使用读操作锁时可以允许多个线程同时访问。但是使用写操作锁时只允许一个线程进行。在一个线程执行写操作时,其他线程不能够执行读操作。

    我们将通过范例来学习如何使用ReadWriteLock接口编程。这个范例将使用ReadWriteLock接口控制对价格对象的访问,价格对象存储了两个产品的价格。

package org.concurrency.synchronization.lock.readwritelock;import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock;/** * @author Administrator 价格信息类 */public class PriceInfo { private double price1; private double price2; // 声明读写锁ReadWriteLock对象lock private ReadWriteLock lock; // 构造器初始化这3个属性 public PriceInfo() {  price1 = 1.0;  price2 = 2.0;  lock = new ReentrantReadWriteLock(); } public double getPrice1() {  // 使用读锁来获取对这个属性的访问  lock.readLock().lock();  double value = price1;  lock.readLock().unlock();  return value; } public double getPrice2() {  // 使用读锁来获取对这个属性的访问  lock.readLock().lock();  double value = price2;  lock.readLock().unlock();  return value; } public void setPrices(double price1,double price2){  lock.writeLock().lock();  this.price1 = price1;  this.price2 = price2;  lock.writeLock().unlock(); }}package org.concurrency.synchronization.lock.readwritelock;/** * @author Administrator * 读取价格信息类 */public class Reader implements Runnable { private PriceInfo priceInfo;  public Reader(PriceInfo priceInfo) {  super();  this.priceInfo = priceInfo; } @Override public void run() {  // TODO 循环读取两个价格10此  for(int i = 0;i<10;i++){   System.out.printf("%s: Reader: Price 1: %f\n",Thread.currentThread().getName(),priceInfo.getPrice1());   System.out.printf("%s: Reader: Price 2: %f\n",Thread.currentThread().getName(),priceInfo.getPrice2());  }   }}package org.concurrency.synchronization.lock.readwritelock;/** * @author Administrator * 创建写入类Writer,修改价格信息 */public class Writer implements Runnable { private PriceInfo priceInfo;  public Writer(PriceInfo priceInfo) {  super();  this.priceInfo = priceInfo; } @Override public void run() {  // TODO 修改价格信息3次  for(int i = 0;i<3;i++){   System.out.printf("Writer: Attempt to modfy the price.\n");   priceInfo.setPrices(Math.random() * 10, Math.random() * 8);   System.out.printf("Writer: Prices have been modified.\n");   try {    Thread.sleep(2);   } catch (InterruptedException e) {    // TODO Auto-generated catch block    e.printStackTrace();   }  } }}package org.concurrency.synchronization.lock.readwritelock;public class Main { public static void main(String[] args) {  // TODO Auto-generated method stub  PriceInfo priceInfo = new PriceInfo();  //创建5个读取类Reader对象  Reader[] readers = new Reader[5];  Thread[] threadReader = new Thread[5];    for(int i = 0;i<5;i++){   readers[i] = new Reader(priceInfo);   threadReader[i] = new Thread(readers[i]);  }    //创建1个写入类对象  Writer writer = new Writer(priceInfo);  Thread threadWriter = new Thread(writer);    //启动这6个线程  for (Thread thread : threadReader) {   thread.start();  }  threadWriter.start(); }}

运行结果截图:

Writer: Attempt to modfy the price.Thread-2: Reader: Price 1: 1.000000Thread-1: Reader: Price 1: 1.000000Thread-0: Reader: Price 1: 1.000000Thread-4: Reader: Price 1: 1.000000Thread-3: Reader: Price 1: 1.000000Thread-4: Reader: Price 2: 2.636754Thread-0: Reader: Price 2: 2.636754Thread-1: Reader: Price 2: 2.636754Thread-2: Reader: Price 2: 2.636754Writer: Prices have been modified.

    读操作锁通过ReadWriteLock接口的readLock()方法获取,这个锁实现了Lock接口,所以我们可以使用lock(),unlock()和tryLock()方法。写操作锁是通过ReadWriteLock接口的writeLock()方法获取的,这个锁同样实现了Lock接口

六、在锁中使用多条件(Multiple Condition)

 一个锁可能关联一个或者多个条件,这些条件通过Condition接口声明。目的是允许线程获取锁并且查看等待的某一个条件是否满足,如果不满足就挂起线程,直到某个线程唤醒它们。Condition接口提供了挂起线程和唤起线程的机制。
 与锁绑定的所有条件对象都是通过Lock接口声明的newCondition()方法创建的。在使用条件的时候,必须获取这个条件所绑定的锁,所以带条件的代码必须在Lock的lock()方法和unlock()方法之间.
 当线程调用条件的await方法时,他将自动释放这个条件绑定的锁,其他某个线程才可以获取这个锁并且执行这个锁保护的另一个临界区代码。
 当一个线程调用了对象的signal()或者signalAll()方法后,一个或多个在该条件上挂起的线程将被唤醒,但这并不保证让�们挂起的条件得到满足,所以必须在while循环中调用await,在条件成立之前不能离开这个循环,如果条件不成立,将再次调用await().
 如果一个线程调用了条件的await()方法,却未调用它的signal()方法,这个线程将永久休眠。
 下面将通过生产者-消费者问题演示锁条件的使用。

package org.concurrency.synchronization.lock.condition;import java.util.LinkedList;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.ReentrantLock;public class Buffer { /**存放共享数据*/ private LinkedList
 buffer; /**存放buffer长度*/ private int maxSize; /**用来修改buffer的代码控制*/ private ReentrantLock lock; private Condition lines; private Condition space; /**用来表明缓冲区是否还有数据*/ private boolean pendingLines; public Buffer(int maxSize) {  this.maxSize = maxSize;  buffer = new LinkedList
();  lock = new ReentrantLock();  lines = lock.newCondition();  space = lock.newCondition();  pendingLines = true; }  /**  * 把字符串写到字符缓冲区内  * */ public void insert(String line){  lock.lock();  try {   while(buffer.size() == maxSize){    space.await();   }   buffer.offer(line);   System.out.printf("%s: Inserted Line: %d\n",Thread.currentThread().getName(),buffer.size());   lines.signalAll();  } catch (InterruptedException e) {   // TODO Auto-generated catch block   e.printStackTrace();  } finally{   lock.unlock();  } }  /**  * 返回缓冲区中第一个字符串  * */ public String get(){  String line = null;  lock.lock();  try {   while((buffer.size() == 0) && (hasPendingLines())){    lines.await();   }   if(hasPendingLines()){    line = buffer.poll();    System.out.printf("%s: Line Readed: %d\n",Thread.currentThread().getName(),buffer.size());    space.signalAll();   }  } catch (InterruptedException e) {   // TODO Auto-generated catch block   e.printStackTrace();  } finally{   lock.unlock();  }  return line; } public boolean hasPendingLines() {  // TODO Auto-generated method stub  return pendingLines || buffer.size() > 0; } public void setPendingLines(boolean pendingLines) {  this.pendingLines = pendingLines; } }package org.concurrency.synchronization.lock.condition;/** * @author Administrator * 模拟文本文件类 * 属性content用来存储文件内容 * index表示要从这个文件中读取的行号 */public class FileMock { private String content[]; private int index;  public FileMock(int size,int lenght){  content = new String[size];  for (int i = 0;i

本文出自 “” 博客,请务必保留此出处

转载于:https://my.oschina.net/mrku/blog/693733

你可能感兴趣的文章
Reinhold就Jigsaw投票一事向JCP提交公开信
查看>>
Microsoft发布了Azure Bot Service和LUIS的GA版
查看>>
第三天 函数
查看>>
string 高频使用
查看>>
Javascript中的抛物线 ~ 加入购物车小动画
查看>>
学习|学习的奥秘
查看>>
Windows 命令行下解决python utf-8中文输出的终极解决方案
查看>>
Go 性能优化技巧 10/10
查看>>
一个通过物理地址查询网卡所属厂商的Python库——mac.py
查看>>
【编码】切割单词流并逆向、大小写反转输出-牛客联合笔试编程题(一)-2016.04.08...
查看>>
Vim实战指南(五):文本替换
查看>>
Flask 教程 第二十三章:应用程序编程接口(API) ...
查看>>
索尼推出新专利,可提供磁场定位追踪
查看>>
rundeck yum 安装完成后跳转http://localhost:4440/menu/home问题解决 ...
查看>>
E3新秀Immerex发布VRG-9020,会是一款颠覆VR行业的头显吗? ...
查看>>
Euler 今日问世!国内首个工业级的图深度学习开源框架,阿里妈妈造 ...
查看>>
非root用户开启tomcat报错Permission denied
查看>>
Spring Boot系列(十)Spring Boot整合Elasticsearch全文搜索引擎 ...
查看>>
解决 EXT4 使用无法挂载
查看>>
linux find detail
查看>>