Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

AhmadLang / Java, How To Program, 2004

.pdf
Скачиваний:
626
Добавлен:
31.05.2015
Размер:
51.82 Mб
Скачать

Producer

writes

8

8

true

Consumer

reads

8

8

false

Producer

writes

9

9

true

Consumer

reads

9

9

false

Producer

writes

10

10

true

Producer

done producing.

 

 

Terminating Producer.

 

 

Consumer

reads

10

10

false

Consumer read values totaling 55.

Terminating Consumer.

Class SynchronizedBuffer (Fig. 23.11) contains five fields. Line 10 creates a new object of type ReentrantLock and assigns its reference to Lock variable accessLock. The ReentrantLock is created without the fairness policy because only a single Producer or Consumer will be waiting to acquire the Lock in this example. Lines 1314 create two Conditions using Lock method newCondition. Condition canWrite contains a queue for threads waiting while the buffer is full (i.e., there is data in the buffer that the Consumer has not read yet). If the buffer is full, the Producer calls method await on this Condition. When the Consumer reads data from a full buffer, it calls method signal on this Condition. Condition canRead contains a queue for threads waiting while the buffer is empty (i.e., there is no data in the buffer for the Consumer to read). If the buffer is empty, the Consumer calls method await on this Condition. When the Producer writes to the empty buffer, it calls method signal on this Condition. The int buffer (line 16) holds the shared data, and the boolean variable occupied (line 17) keeps track of whether the buffer currently holds data (that the Consumer should read) or not.

Line 22 in method set calls the lock method of the SynchronizedBuffer's accessLock. If the lock is available (i.e., no other thread has acquired this lock), method lock will return immediately (this thread now owns the lock) and the thread will continue. If the lock is unavailable (i.e., the lock is held by another thread), this method will wait until the lock is released by the other thread. After the lock is acquired, the try block in lines 2545 executes. Line 28 tests occupied to determine whether the buffer is full. If it is, lines 3031 output that the thread will wait. Line 32 calls Condition method await on the canWrite condition variable which will temporarily release the SynchronizedBuffer's lock and wait for a signal from the Consumer that the buffer is available for writing. When the buffer is available for writing, the method proceeds, writing to the buffer (line 35), setting occupied to true (line 39) and outputting that the producer wrote a value. Line 44 calls Condition method signal on condition variable canRead to notify the waiting Consumer that the buffer has new data to be read. Line 52 calls method unlock within a finally block to release the lock and allow the Consumer to proceed.

[Page 1074]

Common Programming Error 23.3

Place calls to Lock method unlock in a finally block. If an exception is thrown, unlock must still be called or deadlock could occur.

Line 60 of method get (lines 5794) calls method lock to acquire the lock for this object. This method will wait until the lock is available. Once the lock is acquired, line 66 tests whether occupied is false, indicating that the buffer has no data. If the buffer is empty, line 70 calls method await on condition

variable canRead. Recall that method signal is called on variable canRead in the set method (line 44).

When the condition variable is signaled, the get method continues. Line 75 sets occupied to false, line 77 stores the value of buffer in readValue and line 78 outputs the readValue. Then line 81 signals the condition variable canWrite. This will awaken the Producer if it is indeed waiting for the buffer to be emptied. Line 90 calls method unlock in a finally block to release the lock, and line 93 returns the value of the buffer to the calling method.

Software Engineering Observation 23.2

Always invoke method await in a loop that tests an appropriate condition. It is possible that a thread will reenter the runnable state before the condition it was waiting on is satisfied. Testing the condition again ensures that the thread will not erroneously execute if it was signaled early.

Common Programming Error 23.4

Forgetting to signal a thread that is waiting for a condition is a logic error. The thread will remain in the waiting state, which will prevent the thread from doing any further work. Such waiting can lead to indefinite postponement or deadlock.

Class SharedBufferTest2 (Fig. 23.12) is similar to class SharedBufferTest (Fig. 23.10).

SharedBufferTest2 contains method main (lines 830), which launches the application. Line 11 creates an ExecutorService with two threads to run the Producer and Consumer. Line 14 creates a SynchronizedBuffer object and assigns its reference to Buffer variable sharedLocation. This object stores the data that will be shared between the Producer and Consumer tHReads. Lines 1617 display the column heads for the output. Lines 2122 execute a Producer and a Consumer. Finally, line 29 calls method shutdown to end the application when the Producer and Consumer complete their tasks. When method main ends (line 30), the main thread of execution terminates.

Study the outputs in Fig. 23.12. Observe that every integer produced is consumed exactly onceno values are lost, and no values are consumed more than once. The synchronization and condition variables ensure that the Producer and Consumer cannot perform their tasks unless it is their turn. The Producer must go first, the Consumer must wait if the Producer has not produced since the Consumer last consumed, and the Producer must wait if the Consumer has not yet consumed the value that the Producer most recently produced. Execute this program several times to confirm that every integer produced is consumed exactly once. In the sample output, note the lines indicating when the Producer and Consumer must wait to perform their respective tasks.

[Page 1076]

23.8. Producer/Consumer Relationship: Circular Buffer

The program in Section 23.7 uses thread synchronization to guarantee that two threads manipulate data in a shared buffer correctly. However, the application may not perform optimally. If the two threads operate at different speeds, one of the threads will spend more (or most) of its time waiting. For example, in the program in Section 23.7 we shared a single integer variable between the two threads. If the producer thread produces values faster than the consumer can consume them, then the producer thread waits for the consumer, because there are no other locations in memory in which to place the next value. Similarly, if the consumer consumes values faster than the producer produces them, the consumer waits until the producer places the next value in the shared location in memory. Even when we have threads that operate at the same relative speeds, those threads may occasionally become "out of sync" over a period of time, causing one of them to wait for the other. We cannot make assumptions about the relative speeds of concurrent threadsinteractions that occur with the operating system, the network, the user and other components can cause the threads to operate at different speeds. When this happens, threads wait. When threads wait excessively, programs become less efficient, user-interactive programs become less responsive and applications suffer longer delays.

[Page 1077]

To minimize the amount of waiting time for threads that share resources and operate at the same average speeds, we can implement a circular buffer that provides extra buffer space into which the producer can place values and from which the consumer can retrieve those values. Let us assume that the buffer is implemented as an array. The producer and consumer work from the beginning of the array. When either thread reaches the end of the array, it simply returns to the first element of the array to perform its next task. If the producer temporarily produces values faster than the consumer can consume them, the producer can write additional values into the extra buffer space (if any are available). This capability enables the producer to perform its task even though the consumer is not ready to receive the current value being produced. Similarly, if the consumer consumes faster than the producer produces new values, the consumer can read additional values (if there are any) from the buffer. This enables the consumer to keep busy even though the producer is not ready to produce additional values.

Note that the circular buffer would be inappropriate if the producer and the consumer operate consistently at different speeds. If the consumer always executes faster than the producer, then a buffer containing one location is enough. Additional locations would waste memory. If the producer always executes faster, only a buffer with an infinite number of locations would be able to absorb the extra production.

The key to using a circular buffer with a producer and consumer that operate at about the same speed is to provide it with enough locations to handle the anticipated "extra" production. If, over a period of time, we determine that the producer often produces as many as three more values than the consumer can consume, we can provide a buffer of at least three cells to handle the extra production. We do not want the buffer to be too small, because that would cause threads to wait longer. On the other hand, we do not want the buffer to be too large, because that would waste memory.

Performance Tip 23.4

Even when using a circular buffer, it is possible that a producer thread could fill the buffer, which would force the producer thread to wait until a consumer consumes a value to free an element in the buffer.

Similarly, if the buffer is empty at any given time, the consumer thread must wait until the producer produces another value. The key to using a circular buffer is to optimize the buffer size to minimize the amount of thread wait time.

The program in Fig. 23.13Fig. 23.14 demonstrates a producer and a consumer accessing a circular buffer (in this case, a shared array of three cells) with synchronization. In this version of the producer/consumer relationship, the consumer consumes a value only when the array is not empty and the producer produces a value only when the array is not full. The statements that created and started the thread objects in the main method of class SharedBufferTest2 (Fig. 23.12) now appear in class

CircularBufferTest (Fig. 23.14).

Figure 23.13. CircularBuffer synchronizes access to a circular buffer

containing three slots.

(This item is displayed on pages 1078 - 1080 in the print version)

1 // Fig. 23.13: CircularBuffer.java

2 // SynchronizedBuffer synchronizes access to a single shared integer.

3import java.util.concurrent.locks.Lock;

4import java.util.concurrent.locks.ReentrantLock;

5import java.util.concurrent.locks.Condition;

6

7public class CircularBuffer implements Buffer

8{

9// Lock to control synchronization with this buffer

10private Lock accessLock = new ReentrantLock();

11

12// conditions to control reading and writing

13private Condition canWrite = accessLock.newCondition();

14private Condition canRead = accessLock.newCondition();

16private int[] buffer = { -1, -1, -1 };

18private int occupiedBuffers = 0; // count number of buffers used

19private int writeIndex = 0; // index to write next value

20private int readIndex = 0; // index to read next value

22// place value into buffer

23public void set( int value )

24{

25accessLock.lock(); // lock this object

27// output thread information and buffer information, then wait

28try

29{

30// while no empty locations, place thread in waiting state

31while ( occupiedBuffers == buffer.length )

32{

33

System.out.printf( "All buffers full. Producer waits.\n" );

34

canWrite.await();

//

await

until

a buffer element is free

35

} // end

while

 

 

 

 

36

 

 

 

 

 

 

37

buffer[

writeIndex ]

=

value;

// set

new buffer value

38

 

 

 

 

 

 

39// update circular write index

40writeIndex = ( writeIndex + 1 ) % buffer.length;

42occupiedBuffers++; // one more buffer element is full

43displayState( "Producer writes " + buffer[ writeIndex ] );

44canRead.signal(); // signal threads waiting to read from buffer

45} // end try

46catch ( InterruptedException exception )

47{

48exception.printStackTrace();

49} // end catch

50finally

51{

52accessLock.unlock(); // unlock this object

53} // end finally

54} // end method set

55

56// return value from buffer

57public int get()

58{

59int readValue = 0; // initialize value read from buffer

60accessLock.lock(); // lock this object

61

62// wait until buffer has data, then read value

63try

64{

65// while no data to read, place thread in waiting state

66while ( occupiedBuffers == 0 )

67{

68

System.out.printf( "All buffers empty. Consumer waits.\n" );

69

canRead.await();

// await

until

a buffer element

is filled

70

} // end

while

 

 

 

 

71

 

 

 

 

 

 

72

readValue

= buffer[

readIndex

]; //

read value from

buffer

73

 

 

 

 

 

 

74// update circular read index

75readIndex = ( readIndex + 1 ) % buffer.length;

77

occupiedBuffers--; // one more buffer element is empty

78displayState( "Consumer reads " + readValue );

79canWrite.signal(); // signal threads waiting to write to buffer

80} // end try

81// if waiting thread interrupted, print stack trace

82catch ( InterruptedException exception )

83{

84exception.printStackTrace();

85} // end catch

86finally

87{

88accessLock.unlock(); // unlock this object

89} // end finally

90

91return readValue;

92} // end method get

94// display current operation and buffer state

95public void displayState( String operation )

96{

97// output operation and number of occupied buffers

98System.out.printf( "%s%s%d)\n%s", operation,

99" (buffers occupied: ", occupiedBuffers, "buffers: " );

101for ( int value : buffer )

102

System.out.printf(

"

%2d ",

value ); // output values in buffer

103

 

 

 

 

104

System.out.print( "\n

 

"

);

105for ( int i = 0; i < buffer.length; i++ )

106System.out.print( "---- " );

107

 

 

108

System.out.print( "\n

" );

109for ( int i = 0; i < buffer.length; i++ )

110{

111if ( i == writeIndex && i == readIndex )

112System.out.print( " WR" ); // both write and read index

113else if ( i == writeIndex )

114

System.out.print( " W

" ); // just write index

115else if ( i == readIndex )

116System.out.print( " R " ); // just read index

117else

118

System.out.print( "

" ); // neither index

119

} // end for

 

120

 

 

121System.out.println( "\n" );

122} // end method displayState

123} // end class CircularBuffer

Figure 23.14. CircularBufferTest sets up a producer/consumer application and instantiates producer and consumer threads.

(This item is displayed on pages 1081 - 1084 in the print version)

1 // Fig 23.14: CircularBufferTest.java

2 // Application shows two threads manipulating a circular buffer.

3import java.util.concurrent.ExecutorService;

4import java.util.concurrent.Executors;

5

6public class CircularBufferTest

7{

8public static void main( String[] args )

9{

10// create new thread pool with two threads

11ExecutorService application = Executors.newFixedThreadPool( 2 );

13// create CircularBuffer to store ints

14Buffer sharedLocation = new CircularBuffer();

16try // try to start producer and consumer

17{

18application.execute( new Producer( sharedLocation ) );

19application.execute( new Consumer( sharedLocation ) );

20} // end try

21catch ( Exception exception )

22{

23exception.printStackTrace();

24} // end catch

25

26application.shutdown();

27} // end main

28} // end class CircularBufferTest

Producer writes 1 (buffers occupied: 1)

buffers:

1

-1

-1

 

----

----

----

 

R

W

 

Consumer

reads 1 (buffers occupied: 0)

buffers:

1

-1

-1

 

----

----

----

 

 

WR

 

All buffers empty. Consumer waits. Producer writes 2 (buffers occupied: 1)

buffers:

1

2

-1

 

----

----

----

 

 

R

W

Consumer reads 2 (buffers occupied: 0)

buffers:

1

2

-1

 

----

----

----

 

 

 

WR

Producer writes 3 (buffers occupied: 1)

buffers:

1

2

3

 

----

----

----

 

W

 

R

Consumer reads 3 (buffers occupied: 0)

buffers:

1

2

3

 

----

----

----

 

WR

 

 

Producer writes 4 (buffers occupied: 1)

buffers:

4

2

3

 

----

----

----

 

R

W

 

Producer writes 5 (buffers occupied: 2)

buffers:

4

5

3

 

----

----

----

 

R

 

W

Consumer reads 4 (buffers occupied: 1)

buffers:

4

5

3

 

----

----

----

 

 

R

W

Producer writes 6 (buffers occupied: 2)

buffers:

4

5

6

 

----

----

----

 

W

R

 

Producer writes 7 (buffers occupied: 3)

buffers:

7

5

6

 

----

----

----

 

 

WR

 

Consumer reads 5 (buffers occupied: 2)

buffers:

7

5

6

 

----

----

----

 

 

W

R

Producer writes 8 (buffers occupied: 3)

buffers:

7

8

6

 

----

----

----

 

 

 

WR

Consumer

reads 6

(buffers occupied: 2)

buffers:

7

 

8

6

 

----

----

----

 

R

 

 

W

Consumer

reads 7

(buffers occupied: 1)

buffers:

7

 

8

6

 

----

----

----

 

 

 

R

W

Producer writes 9 (buffers occupied: 2)

buffers:

7

8

9

 

----

----

----

 

W

R

 

Consumer reads 8 (buffers occupied: 1)

buffers:

7

8

9

 

----

----

----

 

W

 

R

Consumer reads 9 (buffers occupied: 0)

buffers:

7

8

9

 

----

----

----

 

WR

 

 

Producer

writes 10

(buffers occupied: 1)

buffers:

10

8

9

 

----

----

----

 

R

W

 

Producer

done

producing.

Terminating Producer.

Consumer

reads 10

(buffers occupied: 0)

buffers:

10

8

9

 

----

----

----

 

 

WR

 

Consumer

read

values totaling: 55.

Terminating Consumer.

[Page 1080]

The significant changes to the example in Section 23.7 occur in CircularBuffer (Fig. 23.13), which replaces SynchronizedBuffer (Fig. 23.11). Line 10 creates a new ReentrantLock object and assigns its reference to Lock variable accessLock. The ReentrantLock is created without a fairness policy because we have only two threads in this example and only one will ever be waiting. Lines 1314 create two

Conditions using Lock method newCondition. Condition canWrite contains a queue for threads waiting while the buffer is full. If the buffer is full, the Producer calls method await on this Conditionwhen the Consumer frees space in a full buffer, it calls method signal on this Condition. Condition canRead contains a queue for threads waiting while the buffer is empty. If the buffer is empty, the Consumer calls method await on this Conditionwhen the Producer writes to the buffer, it calls method signal on this Condition. Array buffer (line 16) is a three-element integer array that represents the circular buffer. Variable occupiedBuffers (line 18) counts the number of elements in buffer that are filled with data available to be read. When occupiedBuffers is 0, there is no data in the circular buffer and the Consumer must waitwhen occupiedBuffers is 3 (the size of the circular buffer), the circular buffer is full and the Producer must wait. Variable writeIndex (line 19) indicates the next location in which a value can be placed by a Producer. Variable readIndex (line 20) indicates the position from which the next value can be read by a Consumer.

CircularBuffer method set (lines 2354) performs the same tasks that it did in Fig. 23.11, with a few modifications. The while loop at lines 3135 determines whether the Producer must wait (i.e., all buffers are full). If so, line 33 indicates that the Producer is waiting to perform its task. Then line 34 invokes Condition method await to place the Producer tHRead in the waiting state on the canWrite condition variable. When execution eventually continues at line 37 after the while loop, the value written by the Producer is placed in the circular buffer at location writeIndex. Then line 40 updates writeIndex for the next call to CircularBuffer method set. This line is the key to the circularity of the buffer. When writeIndex is incremented past the end of the buffer, this line sets it to 0. Line 42 increments occupiedBuffers, because there is now at least one value in the buffer that the Consumer can read. Next, line 43 invokes method displayState to update the output with the value produced, the number of occupied buffers, the contents of the buffers and the current writeIndex and readIndex. Line 44 invokes Condition method signal to indicate that a Consumer thread waiting on the canRead condition variable (if there is a waiting thread) should transition to the runnable state. Line 52 releases accessLock by calling method unlock inside a finally block.

[Page 1081]

Method get (lines 5792) of class CircularBuffer also performs the same tasks as it did in Fig. 23.11, with a few minor modifications. The while loop at lines 6670 determines whether the Consumer must wait (i.e., all buffers are empty). If the Consumer thread must wait, line 68 updates the output to indicate that the Consumer is waiting to perform its task. Then line 69 invokes Condition method await to place the current thread in the waiting state on the canRead condition variable. When execution eventually

continues at line 72 after a signal call from the Producer, readValue is assigned the value at location readIndex in the circular buffer. Then line 75 updates readIndex for the next call to CircularBuffer method get. This line and line 40 create the circular effect of the buffer. Line 77 decrements the occupiedBuffers, because there is at least one open position in the buffer in which the Producer thread can place a value. Line 78 invokes method displayState to update the output with the consumed value, the number of occupied buffers, the contents of the buffers and the current writeIndex and readIndex. Line 79 invokes Condition method signal to transition the thread waiting to write into the CircularBuffer object into the runnable state. Line 88 releases accessLock inside a finally block to guarantee that the lock is released. Then line 91 returns the consumed value to the calling method.

Method displayState (lines 95122) outputs the state of the application. Lines 101102 output the current buffers. Line 102 uses method printf with a %2d format specifier to print the contents of each buffer with a leading space if it is a single digit. Lines 109119 output the current writeIndex and readIndex with the letters W and R respectively.

Class CircularBufferTest (Fig. 23.14) contains the main method that launches the application. Line 11 creates the ExecutorService with two threads, and line 14 creates a CircularBuffer object and assigns its reference to Buffer variable sharedLocation. Lines 1819 execute the Producer and Consumer. Line 26 calls method shutdown to end the application when the Producer and Consumer complete their tasks.

[Page 1084]

Each time the Producer writes a value or the Consumer reads a value, the program outputs the action performed (a read or a write) along with the contents of the buffer and the location of the write index and read index. In this output, the Producer first writes the value 1. The buffer then contains the value 1 in the first slot and the value -1 (the default value) in the other two slots. The write index is updated to the second slot, while the read index stays at the first slot. Next, the Consumer reads 1. The buffer contains the same values, but the read index has been updated to the second slot. The Consumer then tries to read again, but the buffer is empty and the Consumer is forced to wait. Note that only once in this execution of the program was it necessary for either thread to wait.

[Page 1084 (continued)]

23.9. Producer/Consumer Relationship: ArrayBlockingQueue

J2SE 5.0 includes a fully-implemented circular buffer class named ArrayBlockingQueue in package java.util.concurrent, which implements the BlockingQueue interface. The BlockingQueue interface implements the Queue interface, discussed in Chapter 19 and declares methods put and take, the blocking equivalents of Queue methods offer and poll, respectively. This means that method put will place an element at the end of the BlockingQueue, waiting if the queue is full. Method take will remove an element from the head of the BlockingQueue, waiting if the queue is empty. Class ArrayBlockingQueue implements the BlockingQueue interface using an array. This makes the data structure fixed size, meaning that it will not expand to accommodate extra elements. Class ArrayBlockingQueue encapsulates all the functionality of our circular buffer class (Fig. 23.13).

The program in Fig. 23.15Fig. 23.16 demonstrates a Producer and a Consumer accessing a circular buffer (in this case, an ArrayBlockingQueue) with synchronization. Class BlockingBuffer implements interface Buffer (Fig. 23.15) and contains an ArrayBlockingQueue instance variable that stores Integer objects (line 7). By choosing to implement Buffer, our application can reuse the Producer (Fig. 23.7) and Consumer (Fig. 23.8) classes.

[Page 1085]

Figure 23.15. BlockingBuffer creates a blocking circular buffer using the

ArrayBlockingQueue class.

1

//

Fig. 23.15: BlockingBuffer.java

2

//

Class synchronizes access to a blocking buffer.

3

import java.util.concurrent.ArrayBlockingQueue;

4

 

 

5public class BlockingBuffer implements Buffer

6{

7private ArrayBlockingQueue<Integer> buffer;

9public BlockingBuffer()

10{

11buffer = new ArrayBlockingQueue<Integer>( 3 );

12} // end BlockingBuffer constructor

13

14// place value into buffer

15public void set( int value )

16{

17try

18{

19buffer.put( value ); // place value in circular buffer

20System.out.printf( "%s%2d\t%s%d\n", "Producer writes ", value,

21

"Buffers occupied: ", buffer.size() );

22} // end try

23catch ( Exception exception )

24{

25exception.printStackTrace();

26} // end catch

27} // end method set

28

29// return value from buffer

30public int get()

31{

32int readValue = 0; // initialize value read from buffer

34try

35{

36readValue = buffer.take(); // remove value from circular buffer

37System.out.printf( "%s %2d\t%s%d\n", "Consumer reads ",

38

readValue, "Buffers occupied: ", buffer.size() );

39} // end try

40catch ( Exception exception )

41{

42exception.printStackTrace();

43} // end catch

44

45return readValue;

46} // end method get

47} // end class BlockingBuffer

Figure 23.16. BlockingBufferTest sets up a producer/consumer application using a blocking circular buffer.

(This item is displayed on pages 1086 - 1087 in the print version)

1 // Fig 23.16: BlockingBufferTest.java

2 // Application shows two threads manipulating a blocking buffer.

3import java.util.concurrent.ExecutorService;

4import java.util.concurrent.Executors;

5

6public class BlockingBufferTest

7{

8public static void main( String[] args )

9{

10// create new thread pool with two threads

11ExecutorService application = Executors.newFixedThreadPool( 2 );

13// create BlockingBuffer to store ints

14Buffer sharedLocation = new BlockingBuffer();

16try // try to start producer and consumer

17{

18application.execute( new Producer( sharedLocation ) );

19application.execute( new Consumer( sharedLocation ) );

20} // end try

21catch ( Exception exception )

22{

23exception.printStackTrace();

24} // end catch

25

26application.shutdown();

27} // end main

28} // end class BlockingBufferTest

Producer

writes

1

Buffers

occupied:

1

Consumer

reads

1

Buffers

occupied:

0

Producer

writes

2

Buffers

occupied:

1

Consumer

reads

2

Buffers

occupied:

0

Producer

writes

3

Buffers

occupied:

1

Consumer

reads

3

Buffers

occupied:

0

Producer

writes

4

Buffers

occupied:

1

Consumer

reads

4

Buffers

occupied:

0

Producer

writes

5

Buffers

occupied:

1

Consumer

reads

5

Buffers

occupied:

0

Producer

writes

6

Buffers

occupied:

1

Consumer

reads

6

Buffers

occupied:

0

Producer

writes

7

Buffers

occupied:

1

Producer

writes

8

Buffers

occupied:

2

Consumer

reads

7

Buffers

occupied:

1

Producer

writes

9

Buffers

occupied:

2

Consumer

reads

8

Buffers

occupied:

1

Producer

writes

10

Buffers

occupied:

2

Producer

done producing.

 

 

 

Terminating Producer.

 

 

 

Consumer

reads

9

Buffers

occupied:

1