To Top
# Java ### Contents 1. [Prelude](#prelude) 1. [OOP](#oop) 1. [Streams](#streams) 1. [References and Immutability](#references-and-immutability) 1. [Inheritance](#inheritance) 1. [Concurrency](#concurrency) 1. [Java Threads](#java-threads) 1. [Abstract Classes and More Interfaces](#abstract-classes-and-more-interfaces) 1. [Object Equality and Miscellaneous Information](#object-equality-and-miscellaneous-information) 1. [Advanced Generics](#advanced-generics) 1. [Exceptions](#exceptions) 1. [Functional Interfaces and Iterators](#functional-interfaces-and-iterators) 1. [Abstract Data Types](#abstract-data-types) 1. [Concurrency and Data Structures](#concurrency-and-data-structures) 1. [Binary Search Trees](#binary-search-trees) ### Prelude ```java // Classes must be made using this // Each file only allowed 1 class // Class must have same name as file name public class p{ private final Int x = 3; // private variable, immutable since final and is an Int assertEquals("hello", "hello"); // syntax for assertions assertEquals(3.2, 1.5, 0.01); // the last parameter is margin of error, // used for comparing doubles // there is also assertTrue and assertFalse private final List
xs = List.of(1, 2, 3); // from java.util.list System.out.println("Hello world"); private final char[][] board; public p(){ board = new char[5][5]; // make a 5x5 array String ab = "hehe"; switch (ab) { case "hehe" -> "Haha!"; case "dodo", "pigeon" -> "bird"; default -> "hoho..."; }; // switch case expression works // you can merge branches as above // you can only use particular values in switch // you can use it with // enumerated types, string, integers and chars switch (ab) { case "something": { // this works too /// stuff break; } } } } ``` Tests using JUnit in IntelliJ can be made by mirroring the directory structure of the source directory in the test directory, importing JUnit and then writing a test by defining an `@Test` member in a class. If you're overriding a default function, you should use the `@Override` notation for clarity (it's not required though). Primitive types: `byte, short, int, long, float, double, boolean, char` (`char` is a single 16 bit unicode character) - You can't have top level functions outside of classes, but you can have static methods on objects (static ones are independent of any instanced objects of that type) - As usual, try to keep things `final` (const) - `main` function generation shortcut in IntelliJ is to type `psvm +
` - StringBuilder exists and you can use it - For loops and stuff also expectedly exist Basic collections: - `List
` (immutable), `ArrayList
` (mutable) - type inference works with these if you have some initialiser data - (java.util.List and java.util.ArrayList respectively) - `Set
` (immutable), `HashSet
` (mutable) - (java.util.Set, java.util.HashSet respectively) - `Map
` (immutable), `HashMap
` (mutable) - (java.util.Map, java.util.HashMap respectively) You can make generic types by using type parameters, just specify them: ```java ////... public static
pair<> pairOf(T first, U second) { return new pair
(first, second); // you could use this for example, for a static method // the pair
is for actually making the new object // pair<> in the function signature infers T and U from // the type parameters before it } ///... ``` You can't instantiate generics using primitive types though (must be a reference type) - i.e you can't do `pair
`; you'd need to do `pair
` instead. But you can still add/get `int` from collections of `Integer`s, the compiler will automatically box/unbox to or from the appropriate reference type. ### OOP Visibility modifiers for fields/methods: - `public` (anyone can use), default/package private (can be used from within the same package), `private` (only within same class), `protected` (visible to children) Encapsulation is also emphasises in Java (getter/setter methods, although setter methods are discouraged). ```java public interface thing { int doSomething(); // you don't provide an implementation here // you also don't provide fields - constants can be provided though } // polymorphism is achieved via interfaces public class Thingy implements thing { Thingy() { // ... } int doSomething() { return 1; // functions specified in the interface // must be implemented by classes implementing // the interface // ... } } public class Thingy2 implements thing { Thingy2() { // ... } int doSomething() { return 2; } } ``` You can, for example, loop over a container of `thing`s, and the items can be `Thingy` or anything else that implements `thing`: ```java var things = List.of(new Thingy(), new Thingy2()); for(thing t : things){ System.out.println(t.doSomething()); } // prints: // 1 // 2 ``` ### Streams For containers like ArrayList/Set you do `container.stream()` to get a stream, and then you can use `.map` to map items, you provide either a method reference: ```java class classA { classA () { // ... } private static doubleNum(Integer num) { return num*2; } List
doubleElems(List
nums) { return nums .stream() .map(classA::doubleNum) .collect(Collectors.toList()) // Collectors.toSet() for set etc } List
doubleElems2(List
nums) { return nums .stream() .map(x -> x * 2) .collect(Collectors.toList()) } } ``` Slides 11-13 parseString, instance methods, constructors (i.e `Integers::new`) Slides 16-19 on `filter` `reduce` also exists, slides 20 - 27 ```java Optional
x; // used for optional stuff // isPresent(), get(), orElse(T alternative) methods exist for it // you can use optional with reduce container.stream().reduce((x, y) -> x + 1 + y ).orElse(2) // slides 03, slide 30 ``` You can also make a Map directly from a stream: ```java container.collect(Collectors.toMap(/*key map function*/, /*value map function*/)) // i.e System.out.println(List.of(1, 2, 3).collect(Collectors.toMap(key -> key, value -> value + 1))) // output: { 1=2, 2=3, 3=4 } // -> duplicate keys would lead to an exception ``` - Lists/ordered collections result in ordered streams - Sets/unordered collections results in unordered streams Slides 36-47 of slides 03 is just a general overview of lists, sets and maps (specifically, should tend to use ArrayList, HashMap and HashSet). - Always declare using interface types, i.e `List l1 = new ArrayList();` ### References and Immutability - Primitives are copied on assignment, whereas objects have their references copied - Classes should provide `.clone()` methods to copy objects, so you don't accidentally pass object references, and instead copy objects - Objects with no references are garbage collected - `==` compares references - Need to use `.equals()` instead to check equality (and obviously need to provide implementations of `.equals()` for this) - If immutable objects are *exactly* the same, then they may refer to the same objects (like strings) - All in all, use `final` when possible alongside immutable objects to prevent unexpected behaviour. ```java public class Point { private final int x; private final int y; public Point(int a, int b) { x = a; y = b; } public String toString() { // provide a toString implementation to convert an object to a string return "(" + x + ", " + y + ")"; } } ///////////////////////////////// var p = new Point(1, 1); System.out.println("Point: " + p); // "Point: (1, 1)" ``` To read inputs: ```java BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); String line = br.readLine(); // readLine() may throw an IOException, use try/catch to deal with it // if `line == null` then end of input has been reached ``` ### Inheritance - A superclass/parent class is a generalisation of any subclass - A subclass is a specialisation of a parent class. - `protected` is a visibility modifier to allow only subclasses to see/use something ```java public class AdjustableLamp extends Lamp { public AdjustableLamp(int param1){ super(param1); // initialise the parent class } @Override // override default functionality - don't need to specify @Override, // but good practice since compiler can check you really are // overriding something public void toggleOn(){ // ... } } ``` ```java Lamp l = new AdjustableLamp(); // in this case Lamp is the apparent type of l // and AdjustableLamp is the actual type // the apparent type must be either same as // or superclass of the actual type ``` - Java uses late binding, where the bind between a method call and the actual method to be executed is made at runtime (according to the actual type) Use inheritance sparingly, favour composition, since complex inheritance hierarchies can be hard to maintain. - Everything is a subclass of object (except primitives) - You can _upcast_ and _downcast_: - `upcasting` is basically using a less specific type to refer to a more specific one, it is done automatically - `downcasting` does the opposite, casting to a more explicit type, and could obviously fail, causing a runtime exception - A `final` class is one that cannot be extended - A `final` method is one that cannot be overriden ### Concurrency - Concurrency (logical parallelism) - composition of independently executing units - Parallelism - Efficient execution of multiple tasks on multiple processing units - concurrency without parallelism is possible (JS/single core CPUs) - parallelism without concurrency is possible (SIMD instructions) - A process is an independent unit of execution, has an identifier, program counter and its own memory space (there are ways to communicate between them - pipes, files, shared memory etc) - OS controls these - 07 Concurrency Slide 19 (for processes in general, rather than just programs): - `ready -> (dispatch) -> running`: a process dispatched from ready to running has been selected to run - `running -> (interrupt) -> ready`: a running process can be interrupted by the OS and put into the ready state - `running -> (exit)`: just normal process exit after it finishes processing - `running -> (wait) -> blocked`: when a process is waiting for some resource, during this time it will be in the blocked state (they still take up memory) - `blocked -> (notify) -> ready`: when the resource is availabl/the wait isn't needed anymore, then the OS will notify the process, and it will be back in the ready state - `created -> (admit) -> ready`: when the process is created, it will only have resources allocated after the OS admits the process, and then the process is in the ready state - `blocked -> (suspend) -> blocked`: a process could be suspended by the operating system, where it'd be put into slower storage (like a hard drive) - all its memory data would be moved - `blocked -> (activate) -> blocked`: a suspended blocked process could be brought back into normal memory - `blocked and suspended -> (notify) -> ready and suspended`: i.e a blocked and suspended state may have some resources available, so the OS notifies the process, moving it to ready and suspended state - `ready -> (activate) -> ready`: a blocked and ready process could be activated and brough back into faster memory to be a ready process - `ready -> (suspend) -> ready`: a ready process could be suspended by the OS, so it's now a blocked and ready process - `created -> (admit) -> ready and suspended`: depending on OS resources, a program could be put into the ready suspended state by the OS - `running -> (exit) -> zombie`: this is for processes that have exited, but not been waited upon by the parent process - this allows the parent process to inspect the child's state (i.e some exit code is returned) - It's called a zombie process because it's still consuming system resources, but not executing - `zombie -> (wait) -> reaped`: this is the final part of the processes life, when it has been waited upon by the parent process, and it actually returns an exit code or something as mentioned above #### Threads Threads have their own local memory, and shared global memory. They have an ID, a program counter, and priority like for processes. Thread states: - ready/running threads are marked as runnable - threads can be blocked using locks/waiting for resources/or in a synchronised section of code - timed waiting is due to a sleep from a thread - threads can also be in waiting states - due to waiting on something, or waiting for something to join, and you'll get out of the wait via notify (to wake up the thread), or notifyAll (any thread waiting on a specific lock can all wake up) - there are also spurious wake ups - where due to no apparent reason a thread may wake up, and then the thread should check if its waking conditions have been satisfied, and then it should go to sleep, waiting on that same condition again Processes don't share memory, threads do. ### Java Threads - you can extend the `Thread` class, and override the `run()` method - then you can call `.start()` on it to run the thread - implement the `Runnable` interface, and implement the `run()` method - this isn't a thread, you do `new Thread(a).start()`, where `a` implements `Runnable` - you can compose with threads: - ```java Thread t = new Thread(() -> { // do something }); t.start(); ``` Slide 28 of 07 Concurrency has a diagram of Java runtime memory. - Heap is shared between threads Slide 31-34 Race Conditions and Data Races. #### Locks Mutual exclusion property says no more than one thread is in a critical section at any time, where a critical section is the part of a program that accesses some shared resource. Java has locks, and has reentrant locks to help not get into deadlock. Do locking in a try/catch block, in case an exception is thrown. ```java class A { public synchronized void run() { // implicit locking } public void run2() { synchronized(this) { // synchronized blocks } } }; ``` *Coffman conditions* required for there to be a deadlock: - Threads have exclusive access to some shared resource (**mutual exclusion**) - A thread can hold a resources while waiting for another (**hold and wait**) - Reources can't be forcibly taken off of threads that hold them (**no preemption**) - Two or more thread form a circular chain where each thread in the chain is waiting for resources that next thread has locked (**circular wait**) You need all of these to hold for there to be a deadlock. ### Abstract Classes and More Interfaces - Uses the `abstract` keyword, prevents a class from being directly instantiated - just extended You'd only need interfaces *and* abstract classes if you'd need multiple slightly differing abstract classes implementing the same interface (i.e interface of 'tool' in a game, and then could have abstract classes of weapons, mining equipment and cooking utensils for example). You could also use an interface + abstract class when the abstract class may be too specific, and you might also use the interface for mocking during testing, as in, you might implement a fake class just to test the interface works. Frame it as another way to reduce code duplication, but be weary of overengineering. #### Interfaces - Interfaces can also specify named constants (are implicty public static final) - Methods that are implementing interfaces must be public - Interfaces can extend one another - Classes can implement multiple interfaces - Slides 08, page 32 on method name conflicts when using multiple interfaces - If implementing multiple interfaces, and they each have, for example a method `foo`: - If `foo`s in the interfaces have different signatures, then counts as overloading - If `foo`s have same signature and return type, then they would only require 1 implementation (the interface for the method is the same) - Else if `foo`s have same signatures and different return types then there's a compile error - Methods can also provide default implementations for functions if you need - ```java public interface Square { int getWidth(); int getHeight(); default int area() { // can be overriden in an implementing class return getWidth() * getHeight(); } // since these can be added to an interface // without really modifying existing code, // they are also called defender methods, since // they protect the implementing classes from breaking // when the interface adds something new } ``` If two interfaces provide a conflicting implementation of a default method, the class implementing them must provide an implementation of that method to override the interface defaults. With the addition of default methods, the only thing that abstract classes have that interfaces do not is the ability to have non static final fields. ### Object Equality and Miscellaneous Information - You can check types at runtime: - ```java if (x instanceof y) { // is true if x's type is y or a subclass of y } ``` - The `public boolean equals(...)` method must be an equivalence relation: - Reflexive, symmetric, transitive and consistent (i.e if we do `o1.equals(o2)`, and `o1` and `o2` don't change, then `o1.equals(o2)` must have the same result) - Also `o.equals(null)` must be false - Should override equals whenever it is meaningful to say that two things are equal - Slides 09 page 7, how to write an equals method: - Check if types of current and incoming objects make sense - Downcast the incoming object to the correct type - Then compare the fields of the incoming and current object for equality - When using an object in a HashSet/HashMap, if you override `boolean equals()` you must override `int hashCode()` too - To satisfy the requirement `o1.equals(o2) => o1.hashCode() == o2.hashCode()` - A high quality hash coding scheme would mostly yield a different hash code for objects that are not equal, aim for a high quality hash coding scheme when overriding `int hashCode()` Some other general tips: - Mark all methods that are supposed to override methods in a parent class/interface as `@Override` - Mark a method as `final` so that it cannot be overriden - Mark a class as `final` to prevent it from being subclassed #### Narrowing and Widening - A **covariant return type** of a method is one that can be replaced by a "narrower" type when the method is overriden in a subclass - **Argument-type contravariance** is when a subclass overriding method accepts a wider argument type than in the super class - ```java public class A {} public class B extends A {} public class C { public void bar(B b) { // ... } public A foo() { // ... } } public class D extends C { @Override public void bar(A a) { // ... // this will not compile since argument-type contravariance // is not allowed in Java // just removing the @Override will fix it though, // at which point foo() is simply overloaded } @Override public B foo() { // ... } } // ------------------------------------- A myA = new A(); B myB = new B(); D myD = new D(); A dReturn = myD.foo(); // covariant return type, D.foo() returns something of type B, // (so the subclass method narrowed the return type) // but since B extends A, it still works // below not allowed in Java myD.bar(myA); // argument-type contravariance, the subclass method uses a wider type, // this is allowed, and using a more specific type is obviously also allowed, // since B subclasses A // Java doesn't allow this though, can simulate via overloading, and this // would lead to complex rules for selecting which method to call myD.bar(myB); ``` - Overriding rules: - By default method visibility is default/package private - Private methods cannot be overriden - You can override default/`protected` methods with a higher visibility access modifier, i.e `protected`/`public` ### Advanced Generics - If **C** is a generic class, and class **B** extends **A**, then **C\
** is not a subtype of **C\
** - That is, **C\
** and **C\
** are not *covariant* - The type system rules don't hold for arrays though, you just get a runtime error if you try to mix **A** and **B** with arrays - Java has wildcards **?**, it can match any type - `Set>` differs from `Set
` since you can add anything which subclasses `Object` to it, but the former is for one specific type - You can assume anything you get from `Set>` is `Object` and can do some generic `Object` operation on it (like print it) - You can also have stuff like **A extends B>**, then anything in the container **A** will be at the very least of type **B**, and so you can call any methods of **B** on anything you get from the container **A** (you still can't put anything in it though) - Bounded generics: - ```java public class Something { public static
T doSomething(Set
things) { // use the elements which are of class AnotherClass, or a subclass of it } } ``` - *Type erasure* basically removes all the generics information when the Java code is compiled, and replaces it with downcasts where necessary - It's done for the sake of compatibility with old code, but it also means that there is a single compiled version of each class, rather than one for each type that instantiated it - You can do things like `p instance of Pair, ?` but not `p instanceof Pair
` since the generic information was erased before runtime (and `instanceof` may be a runtime check) There is also **A\ extends T>** and **A\ super T>** in Java, [look here](https://stackoverflow.com/questions/4343202/difference-between-super-t-and-extends-t-in-java) for an explanation of them, but basically you can read **T** from the former, and can write nothing, and can read only **Object** from the latter, and can write **T**, but accepts anything with a superclass of **T**. These are referred to as bounded generics. ### Exceptions - `try`/`catch`/`throws` can be used for exceptions - ```java public class Something { public void doSomething(int x) { if(x > 5) { throw new SomeException(); } else { // ... } } public void mainFunction(int x) throws SomeException { // if doSomething throws the exception to its caller (propagating the exception) doSomething(x); } } ``` - You cannot ignore exceptions, and you can have multiple catch blocks to catch multiple kinds of exception - I.e, you can put a bunch of code that throws multiple kinds of exception in the try block, and catch all the exceptions they throw in the subsequent catch blocks (they wouldn't all run though obviously, since try block ends when one exception is thrown) - You can have nested try catch blocks - Exceptions have a couple of methods, since they are objects: - `e.getMessage();` - `e.printStackTrace();` You can write your own custom exception class by doing `class X extends Exception`, more on slide 16 of Slides 11 - There is an exception hierarchy on slide 17, checked/unchecked exception on slide 18-19 - You can also catch errors, but you shouldn't do it (stuff like `OutOfMemoryError` or `StackOverflowError`) - Slide 22-23 has some stuff on how to order catch blocks in light of the exception inheritance hierarchy - Covariant return types, discussed above, can also be used for exceptions - An overriding method may also omit the exception type: - ```java public class C { public void foo() throws SomeException { ... } } public class D extends C { @Override public void foo() { ... } // doesn't throw anything } ``` - It is ok for an overriden method to not throw an exception even if the method it is overriding it did - The `finally` block after `try`/`catch` is always run no matter what - Remember that every checked exception must be: - Caught by the calling method and dealt with (i.e `try`/`catch` block) - Or declared (with the `throws` keyword in the method signature) and propagated - Unchecked exceptions are automatically propagated - To catch all exceptions, for the final catch block, do `catch (Exception e) { ... }` ### Functional Interfaces and Iterators - A functional interface A is an interface with just one abstract method - You can use a lambda whose type matches the single method of A, and set it equal to a variable of type A - ```java public interface Test { public void doSomething(String smthn); } Test t = (String smthn) -> { System.out.println("Hello " + smthn); }; t.doSomething("world"); // will print "Hello world" ``` - It is like making an anonymous class implementing the interface and using that lambda as the implementation of the single method, but Java takes care of it behind the scenes You can pass multiple arguments to a function in Java like this `public void something(T... ts)` and then iterate over all of the passed arguments as an array like `for (T t : ts)` #### Comparator, Function and more - `Comparator` is a functional interface, and so you can provide a lambda wherever a comparator is needed, so long as the provided lambda type matches that of the functional interface - `Function` is similarly a functional interface, which is why you can pass a lambda to `map` since that accepts `Function - `Predicate` is used by `filter`, `BiFunction` and `BinaryOperator` are both used by `reduce`, and they are all functional interfaces too - Primitives cannot be used for specifying types for generic functions - Use the `@FunctionalInterface` annotation that an interface is meant to be a functional interface, to make it clear that an interface is always meant to be functional #### Iterators ```java for(T t : collection) { // you can loop over a collection like this } for(Iterator
iter = collection.iterator(); iter.hasNext(); /* */) { iter.next(); // or equivelantly you can loop over the collection like this } // using the iterator method, you can more easily control // how you loop over elements // i.e, say if I wanted to skip over half the elements: for(Iterator
iter = collection.iterator(); iter.hasNext(); /* */) { iter.next(); if(iter.hasNext()) { T t = iter.next(); // do something with t - we've skipped over every other element like this } } ``` ### Abstract Data Types - 8 primitive data types: `boolean, char, byte, short, int, long, float, double`, you can always have arrays of anything too - Each has a set of values, a data representation, and a set of operations - and these cannot be overloaded/changed in any way - A variable of class type is a reference data type - The possible values are its instantiated objects and the operations are its methods and it is a pointer to a block of memory in the heap - A data type is: - A set of values - A data representation - And a set of operations applicable to all values - An abstract data type (ADT) is: - A set of values - And a set of operations applicable to all values - But the data representation shouldn't matter - An ADT should have a **contract** that specifies: - Valid values - All operations (operation name, parameter type(s), result type and observable behaviour), but not the implementation of said operations - The data representation should be private ### Linear Data Structures - Stacks/Queues/Lists, slides 33-44 of slides 13 ADTs have a good example of how to not use `null`, and instead make better use of `Optional
` in making an implementation of a List/Stack - Lists can be implemented as `ArrayList`s and `LinkedList`s - Stacks are Last in First Out data structures (`LIFO`) - Can use it to check if a string has equal amounts of opening and closing brackets (can also just use a counter though obviously) - They can be implemented as arrays or linked lists - Queues are first in first out (`FIFO`), can be array based or use a linked list - Priority queues also exist, sorted by priority (i.e some nodes have higher priority than others) - Can use a circular buffer to implement it ### Concurrency and Data Structures - Atomic variables - `boolean compareAndSet(int expectedVal, int newVal)` is only true if update succeeded (expected value matched the actual value) - This could lead to something not being able to update, so don't use if there's high chance of resource contention - An easy way to have no thread issues is to have locks everywhere, this however means we lose out on performance - Another way is to have locks on each node, and to lock current and previous node while iterating over the list (so can do add/find/remove without issues) (hand-over-hand locking) - Java has the `volatile` keyword for variables, so that the variable is stored in main memory rather than the thread local memory, as long as you only need to do single atomic changes (read/write), so volatile stuff is shared between threads (this is a way to do *optimistic locking*) - For a list using volatile to store the `nextNode` pointer, you can search for an item easily, but if you want to modify it, you'll need to lock it, and then validate the item (ensure that the item wasn't changed between the time you checked what it was, and when you locked it): - First you'll need to make sure the previous node to that is accessible from the head node - Then you'll need to ensure the previous node actually still points to this one Lazy removal could be used with an atomic flag (mark a node in a list as marked for deletion atomically (atomic boolean), then remove it later). - You can combine lazy removal with the optimistic locking above - The validity of the node, and the next node need to be accessed atomically for adding/removing nodes, which can be done via `AtomicMarkableReference`, which likely uses the `cmpxchg16b` instruction on x86-64 underneath, which allows for atomic compare and swap with 128 bits (i.e pointer + boolean for example) - Failed operations are still repeated until they succeed Slide 141 of `14 - Concurrent Data.pdf` has a quick summary of pros/cons of each of the approaches (coarse locking, fine locking, optimisting locking with volatile attributes, lazy removal and lock free). ### Binary Search Trees - Trees are references to their root nodes, and are empty if the root is null - Nodes are arranged in levels, height/shortest path to a node can be computed recursively, and a balanced tree is one which's height and shortest path are the same or differ by at most 1 - For balanced trees, adding, removing and deleting elements are $\mathcal{O}(\log n)$ - There are many ways of traversing trees - Inorder, preorder, postorder, depth first and breadth first #### AVL Tree - AVL invariant is that the difference between the height and shortest path is at most 1 - Rebalances the tree after insertion/removal, and it involes 'rotating' the tree (left/right rotations) to balance it - There are 4 ways to rebalance the tree: right, left, left-right and right-left, by getting the difference in height between the left and right subtrees - Slides 51-56 of `15 - BSTs.pdf` explains when and how to do the various rotations #### Red-black Tree - Has the BST invariant, and every node is red or black - Red nodes have black parents (*colour invariant*) - Every path from the root to a leaf contains the same number of black nodes (*black height invariant*) It uses the same rotations for rebalancing as an AVL tree. - There are many cases you need to deal with for a Red-black Tree, and all operations are still $\mathcal{O}(\log n)$ - The cases for rotating/recolouring are on slides 12-14 of `16 - Red Black Trees.pdf`, and happen after adding/removing nodes - AVL can be coloured to become a Red-black Tree You can make these trees thread safe using coarse locking (obviously), and a little more finnicky but hand over hand locking too (described in the last bit of the slides). Other concurrency techniques can be used too (the ones described in the section above).