Processes and threads

Understand multithreaded must first understand the concept of process and thread, in operating system, the process is the smallest unit of resource distribution, typically an application will be opened within a computer system is a process, threads can be understood as the subtasks, multiple independent operation in the process of operating system is able to do the smallest unit of dispatching operation, but the thread does not have the resources, Only data in a process can be shared, so when multiple threads modify data in a process at the same time, thread-safety issues can occur.

Since multiple threads are allowed in a process, how to deal with the problems of concurrency and communication between threads in multithreading is the focus of learning multithreading programming. Understand multithreaded must first understand the concept of process and thread, in operating system, the process is the smallest unit of resource distribution, typically an application will be opened within a computer system is a process, threads can be understood as the subtasks, multiple independent operation in the process of operating system is able to do the smallest unit of dispatching operation, but the thread does not have the resources, Only data in a process can be shared, so when multiple threads modify data in a process at the same time, thread-safety issues can occur. Since multiple threads are allowed in a process, how to deal with the problems of concurrency and communication between threads in multithreading is the focus of learning multithreading programming.

Use of multithreading

In Java, there are generally two ways to create a Thread: by inheriting the Thread class or implementing the Runable interface, overriding the run method and calling the start() method to start a Thread and execute it. If you want to get the return value of the current thread execution, after JDK1.5, you can implement the Callable interface and then get the return value using FutureTask or thread pool. Since thread execution is random, the order in which a thread is started does not mean the order in which it is executed.

Inheriting Thread creates a Thread

/** * Created by Shanghai on 2019/4/13. */ Lijinpeng * Created by Shanghai on 2019/4/13
@Slf4j
public class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }
    @Override
    public void run(a) {
        log.info("Hi,I am a thread extends Thread,My name is:{}".this.getName()); }}Copy the code

Implement the Runable interface to create a thread


/** * implements the Runnable interface * class to allow multiple interfaces to be implemented in production. Threads are generally created in this way. Threads also require the implementation of Thread * User: lijinpeng * Created by Shanghai on 2019/4/13. */
@Slf4j
@Getter
public class ThreadRunable implements Runnable {

    private String name;

    public ThreadRunable(String name) {
        this.name = name;
    }

    public void run(a) {
        log.info("Hi,I am a thread implements Runnable,My name is:{}".this.getName()); }}Copy the code

Implement Callable interface to get thread execution results

/** * Implement the Callable interface to create a Thread with a return value * Thread use FutureTask and Thread, or use the Thread pool * User: lijinpeng * Created by Shanghai on 2019/4/13. */
@Slf4j
public class CallableThread implements Callable<Integer> {

    private AtomicInteger seed;
    @Getter
    private String name;

    public CallableThread(String name, AtomicInteger seed) {
        this.name = name;
        this.seed = seed;
    }

    public Integer call(a) throws Exception {
        // Generate an integer using a concurrency safe atomic class
        Integer value = seed.getAndIncrement();
        log.info("I am thread implements Callable,my name is:{} my value is:{}".this.name, value);
        returnvalue; }}Copy the code

Verify the startup of three threads


/** * User: lijinpeng * Created by Shanghai on 2019/4/13. */
@Slf4j
public class ThreadTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
     threadTest();
     runableTest();
     callableTest();
    }

    public static void threadTest(a) {
        MyThread threadA = new MyThread("threadA");
        threadA.start();
    }

    public static void runableTest(a) {
        ThreadRunable runable = new ThreadRunable("threadB");
        // Start a new Thread with the help of Thread
        Thread threadB = new Thread(runable);
        threadB.start();
    }

    public static void callableTest(a) throws ExecutionException, InterruptedException {
        AtomicInteger atomic = new AtomicInteger();
        CallableThread threadC1 = new CallableThread("threadC1", atomic);
        CallableThread threadC2 = new CallableThread("threadC2", atomic);
        CallableThread threadC3 = new CallableThread("threadC3", atomic);
        FutureTask<Integer> task1 = new FutureTask<Integer>(threadC1);
        FutureTask<Integer> task2 = new FutureTask<Integer>(threadC2);
        FutureTask<Integer> task3 = new FutureTask<Integer>(threadC3);
        Thread thread1 = new Thread(task1);
        Thread thread2 = new Thread(task2);
        Thread thread3 = new Thread(task3);
        thread1.start();
        thread2.start();
        thread3.start();
        while (task1.isDone()&&task2.isDone()&&task3.isDone())
        {
        }
        log.info(threadC1.getName()+"Execution Result :"+String.valueOf(task1.get()));
        log.info(threadC2.getName()+"Execution Result :"+String.valueOf(task2.get()));
        log.info(threadC2.getName()+"Execution Result :"+String.valueOf(task3.get())); }}Copy the code

The following is the execution result of the program:

Conclusion:

  1. All of these three ways can start a Thread, and the specific task of starting the Thread is still handed over to the Thread class. Therefore, forRunableandCallableClass is single inheritance and interface is multi-implementation. Due to the complexity of business in the production environment, a class may have other functions, so interface implementation is generally used.
  2. It can be seen from the above thread declaration order and execution order results that thread execution is disordered. CPU execution task adopts polling mechanism to improve CPU utilization, and it will be called by CPU again after the thread obtains execution resources for ready queue, and this process has nothing to do with the program.

The life cycle of a thread

The running of a Thread is usually accompanied by the process of starting, blocking, stopping and so on. Thread starting can be implemented through the start() method of Thread class. As multiple threads may share process data, blocking usually occurs in the process of waiting for other threads to release a certain block of process resources. Threads can also be forcibly terminated by calling stop() or terminated due to an exception during thread execution. Understanding the life cycle of threads is the most important theoretical basis for learning multithreading.

The following figure shows the thread life cycle and the state transition process

The new state

When a Thread is created with Thread thead=new Thread(), the Thread is in the new state.

The ready state

When thread.start() is called, the thread enters the ready state, where it does not run but is in a ready queue for the CPU to call and ready to run.

Running state

When a thread is scheduled by the thread scheduler in the JVM, it enters the running state and executes the overridden run method.

The blocking state

The thread is still active, but for some reason the CPU has lost the right to schedule it, which can be divided into the following

  1. A synchronized block

At this time, thread A needs to acquire resource 1 of the process, but resource 1 is held by thread B. It must wait for thread B to release resource 1 before entering the ready thread pool of resource 1. After obtaining resource 1, the thread will wait to be scheduled by the CPU scheduler to run again. Synchronous blocking occurs when a thread is waiting for the right to use a resource. Using a locking mechanism in a program can cause synchronous blocking.

  1. Waiting for blocking

When executing the wait() and join() methods of Thread, this causes the current Thread to synchronously block, wait() suspends the current Thread, and releases any locks it holds. The current thread can be woken up with notify() or notifyall() of an Object on which the thread is waiting. The join() method blocks the current thread until it finishes executing. You can use join(time) to specify how long to wait, and then wake up the thread.

  1. Other block

The sleep() method is called to give up CPU resources, which does not release the lock held by the thread, or a blocking IO method is called and I/O requests are made, which blocks. The blocked thread will re-enter the ready state at an appropriate time (once the block is cleared) and wait for the thread scheduler to dispatch it again.

State of death

When a thread finishes executing the run method, it terminates or dies, which is a normal thread death process. Or terminate the thread by calling stop() with the display, which is not safe. You can also terminate a thread by throwing an exception.

Instance variables and thread safety

Multithreading accesses process resources

In multi-threaded task application, if multiple threads execute using different resources of the process, that is, no process resources are shared in the running, the running of each thread is not affected, and there will be no data security problems. If multiple threads share a block resource of a process, the block resource data will be modified at the same time, causing the final result to be inconsistent with the expected result, resulting in thread safety problems. As shown in figure:

Main memory vs. working memory

The Java memory model is divided into primary memory and working memory. Main memory is shared by all threads, while working memory is one for each thread and is not shared. Each thread also has its own working memory, which holds a main memory copy of variables used by the thread. All operations (reads, assignments) by a thread to a variable must be performed in working memory, rather than directly reading or writing variables in main memory. Different threads cannot directly access variables in each other’s working memory, and the transfer of variable values between threads needs to be completed through main memory. The interaction among threads, main memory and working memory is shown as follows:

Operation instructions of threads on main storage: Lock, unlock, read, load, use, assign, Store, write

  • The read-load phase copies variables from main memory to the current working memory

  • The use and assign phases execute code to change shared variable values

  • The store and write phases refresh the values of the corresponding variables in main memory with working memory data.

  • Store and Write Execution time

    1. The AVA memory model rules do not allow thisreadandload,storeandwriteOne of the operations occurs separately, and the two operations must be performed sequentially, not sequentially, i.ereadwithloadBetween,storewithwriteOther instructions can be inserted between them.
    2. Allows a thread to discard its nearestassignOperation, that is, after a variable has changed in working memory, the change must be synchronized back to main memory. Once a variable changes in the current thread is actually onceassignAnd the nearest one is not allowed to be discardedassignSo there must have been a timestore and write“Again according to article 1Read and Load and Store and writeCan’t be a single occurrence, so oncestore and writeThere must have been a timeread and loadTherefore, it is inferred that the variable is executed for every change in the current threadRead, Load, use, assign, Store, and write
    3. volatileModifier variable, is inuseandassignPhase ensures that the retrieved variable is always in sync with the main memory variable

    Non-thread-safety issues

In A multithreaded environment use and assign is several times, but this action is not atomic, that is to say, in the thread A performed the read and the load from the main memory after loading the variable C, at this time if the thread B change the value of the variable C in main memory, because the thread A has been loaded variable C, unable to sense data have changed, From the point of view of thread A, the working memory and main memory variables A are out of sync. When thread A uses use and assign, A non-thread-safe problem occurs. This problem can be solved by using the volatile keyword. Volatile ensures that the thread gets the latest data from main memory each time it uses use and assign, and prevents instruction rearrangements. Volatile, however, only ensures visibility of variables, and does not make the loading steps atomic. So volatile is not thread-safe.

The following code looks like this:

Multiple business threads access the user balance balance, resulting in the total amount of deduction exceeds the user balance, resulting in the capital loss scenario caused by thread insecurity. In addition, each business thread is charged twice, indicating that the thread needs to load balance into the working memory when it is started, and that the thread then operates based on the loaded balance. How other threads change the value of balance is invisible to the current business thread.

/** * Created by Shanghai on 2019/4/13. */
@Slf4j
public class WithHoldPayThread extends Thread {
    // Amount of payment
    private Integer amt;
    // Business type
    private String busiType;

    public WithHoldPayThread(Integer amt, String busiType) {
        this.amt = amt;
        this.busiType = busiType;
    }

    @Override
    public void run(a) {
        int payTime = 0;
        while (WithHodeTest.balance > 0) {
            synchronized (WithHodeTest.balance) {
                boolean result = false;
                if (WithHodeTest.balance >= amt) {
                    WithHodeTest.balance -= amt;
                    result = true;
                    payTime++;
                }
                log.info("Business :{} Deduction Amount :{} Deduction Status :{}", busiType, amt,result);
            }
        }
        log.info("Business :{} Total payment :{} times", busiType, payTime); }}Copy the code

Test functions

/** * User: lijinpeng * Created by Shanghai on 2019/4/13. */
public class WithHodeTest {
    // User balance unit points
    public static volatile Integer balance=100;

    public static void main(String[] args) {
        WithHoldPayThread phoneFare = new WithHoldPayThread(50."Deposit phone bill");
        WithHoldPayThread waterFare = new WithHoldPayThread(50."Water Charge");
        WithHoldPayThread electricFare = new WithHoldPayThread(50."Payment of Electricity"); phoneFare.start(); waterFare.start(); electricFare.start(); }}Copy the code

Execution Result:

The results show that the debits are successful for each thread, which leads to thread-safety problems. The simplest solution to this problem is to add synchronized to the run method and volatile to balance.

   // User balance unit points
    public static  volatile Integer balance=100;
Copy the code
 @Override
    public void run(a) {
        int payTime = 0;
        while (WithHodeTest.balance > 0) {
            synchronized (WithHodeTest.balance) {
                boolean result = false;
                if (WithHodeTest.balance >= amt) {
                    WithHodeTest.balance -= amt;
                    result = true;
                    payTime++;
                }
                log.info("Business :{} Deduction Amount :{} Deduction Status :{}", busiType, amt,result);
            }
        }
        log.info("Business :{} Total payment :{} times", busiType, payTime);
    }
Copy the code

Execution Result:

Basic API for threads

The Java Thread class Thread provides basic methods for Thread operations, such as isAlive() to determine whether a Thread isAlive, wait() join() to block a Thread, sleep() to put a Thread to sleep, stop() to stop a Thread, suspend and resume a Thread, etc. Some methods should be used with caution because they cause threads to be unsafe or monopolize resources that have been deprecated.

getId()

Gets the unique ID of a thread. This method can track the invocation of a business based on the thread number in real production

isAlive()

Check whether the thread is active. The active state is that the thread is started, running, or ready to run, and is not finished. You can call isAlive() to check whether the current thread task is finished.

sleep()

Causes the thread executing a task to suspend execution for the specified millisecond. The thread executing is the thread returned by this.currentThread(). Threads in this state do not release lock resources.

Suspend () and resume ()

Suspend () suspends a thread and resume resumes it.

/** * User: lijinpeng * Created by Shanghai on 2019/4/15. */
@Slf4j
public class SuspendAndResumeThread extends Thread {
    @Getter
    private  int number = 0;

    @Override
    public void run(a) {
        while (true) { number++; }}public static void main(String[] args) throws InterruptedException {
        SuspendAndResumeThread thread=new SuspendAndResumeThread();
        thread.start();
        Thread.sleep(200);
        thread.suspend();
        // The thread has paused
        log.info("A time:{} number={}",System.currentTimeMillis(),thread.getNumber());
        Thread.sleep(200);
        //B time should be the same as A time
        log.info("B time:{} number={}",System.currentTimeMillis(),thread.getNumber());
        // Wake up to continue execution
        thread.resume();
        Thread.sleep(200);
        log.info("C time:{} number={}",System.currentTimeMillis(),thread.getNumber()); }}Copy the code

The results were in line with expectations

Suspend () and resume() are simple ways to suspend and resume threads, but the JDK deprecated them because of the potential for resource hogging and data inconsistencies.

Resource exclusivity: The following code shows multiple payment businesses. In the synchronous code, payment business withdraw suspends the thread execution in the current thread, and subsequent payment businesses cannot enter the payment method, resulting in resource exclusivity

/** * User: lijinpeng * Created by Shanghai on 2019/4/17. */
@Slf4j
public class PaymentServiceImpl {
    private int balance;

    public PaymentServiceImpl(int balance) {
        this.balance = balance;
    }

    public synchronized void payService(int amt) {
        String buziType = Thread.currentThread().getName();
        log.info("Start a payment business..... Business type :{}", buziType);
        if (balance >= amt) {
            balance -= amt;
            // For cash withdrawal, deduct the balance first and then make payment
            if ("withdraw".equals(buziType)) {
                log.info("Transfer service will suspend current thread!");
                If the transfer fails, the thread will remain suspended and will not release the synchronization lock
                Thread.currentThread().suspend();
                log.info("Transfer successful!");
            }

            log.info("Business :{} deduction successful, current user balance :{}", buziType, balance);
        } else {
            log.info("Business :{} deduction failed, current user balance insufficient", buziType); }}public static void main(String[] args) throws InterruptedException {
        final PaymentServiceImpl paymentService = new PaymentServiceImpl(2000);
        Thread fastpay = new Thread(new Runnable() {
            public void run(a) {
                paymentService.payService(150); }},"fastpay");
        Thread withdraw = new Thread(new Runnable() {
            public void run(a) {
                paymentService.payService(50); }},"withdraw");
        Thread payfor = new Thread(new Runnable() {
            public void run(a) {
                paymentService.payService(50); }},"payfor");
        fastpay.start();
        Thread.sleep(1000);
        withdraw.start();
        Thread.sleep(1000); payfor.start(); }}Copy the code

Execution Result:

From the execution result, the Fastpay payment service is executed first and will not be affected by the suspend thread of withdraw service. When withdraw is executed, the thread is suspended. If resume is not applied in the current thread, the withdraw thread will continue to use the payService() method resources. The Payfor service cannot be executed and resources are monopolized.

Inconsistent data: The following code verifies that ThreadExcuterUtils was used to fetch the current thread name and execute the task. A thread pause occurred during the assignment of two parameters, causing the thread name and execute the task to be inconsistent.

/** * User: lijinpeng * Created by Shanghai on 2019/4/17. */
@Getter
@Slf4j
public class ThreadExcuterUtils {
    // The current thread name
    private String localThreadName;
    // Id of the current thread
    @Setter
    private String localTask;

    public void setThreadInfo(String threadName) {
        this.localThreadName = threadName;
        if ("gc_thread".equals(threadName)) { Thread.currentThread().suspend(); }}public static void main(String[] args) throws InterruptedException {
        final ThreadExcuterUtils utils = new ThreadExcuterUtils();
        new Thread(new Runnable() {
            public void run(a) {
                utils.setThreadInfo("log_thread");
                utils.setLocalTask("Log");
            }
        }).start();
        Thread.sleep(1000);
        new Thread(new Runnable() {
            public void run(a) {
                utils.setThreadInfo("gc_thread");
                utils.setLocalTask("GC garbage Collection");
            }
        }).start();
        Thread.sleep(1000);
        log.info(Current thread :{} Current task :{}",utils.getLocalThreadName(),utils.getLocalTask()); }}Copy the code

Execution Result:

The current thread is GC_thread, the corresponding task should be “GC garbage collection”, but the result is “log”, there are data inconsistency situation.

Suspend and Resume should be used in tandem to avoid resource monopolization and data inconsistencies. However, you should use suspend and resume in tandem to avoid resource monopolization and data inconsistency.

setPriority()

In an operating system, threads can be prioritized. Higher priority threads receive more CPU resources, which means that the CPU has the priority to execute the tasks in the higher priority thread object. Setting a thread priority helps the thread planner determine which thread should be selected for priority execution next time. Thread priority can be set by setPriority(). In Java, thread priorities are rated from 1 to 10, with a higher priority. If the number is less than 1 or greater than 10, throw new IlleageArgumentException().

The JDK pre-sets thread priority using three constants:

   public final static int MIN_PRIORITY = 1;

   /** * The default priority that is assigned to a thread. */
    public final static int NORM_PRIORITY = 5;

    /** * The maximum priority that a thread can have. */
    public final static int MAX_PRIORITY = 10;
Copy the code

1. Thread priorities are inheritable

If ThreadB extends ThreadA, ThreadA’s priority is 5, then ThreadB’s priority is 5

Thread priority is random

Setting the priority of a thread can only ensure that the thread with the highest priority gets the CPU resources first, but it does not necessarily mean that the thread with the lowest priority gets the execution rights first, which is determined by the CPU.

/** * User: lijinpeng * Created by Shanghai on 2019/4/17. */
@Slf4j
public class LowPriorityThread extends Thread {
    @Override
    public void run(a) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
        log.info("Fostered fostered fostered fostered fostered"); }}/** * User: lijinpeng * Created by Shanghai on 2019/4/17. */
@Slf4j
public class HighPriorityThread extends Thread {
    @Override
    public void run(a) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
        log.info(Painted painted painted painted "u"); }}public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            HighPriorityThread thread = new HighPriorityThread();
            LowPriorityThread thread1 = new LowPriorityThread();
            thread.setPriority(10);
            thread1.setPriority(1); thread.start(); thread1.start(); }}Copy the code

Execution Result:

From the execution result, most of the threads with higher priority will always finish before the threads with higher priority.

yield()

Yield () indicates that the current thread can yield CPU resources. The yield of CPU resources depends on CPU scheduling. When the CPU resource is tight, the thread execution resource may be reclaimed; if the CPU resource is sufficient, it may not be reclaimed. Using yield() may prolong the thread task time.

The following code validates the execution time comparison between not using YeILD and using YeILD threads:

/** * User: lijinpeng * Created by Shanghai on 2019/4/17. */
@Slf4j
public class YieldThread extends Thread {


    @Override
    public void run(a) {
// Thread.yield();
        long beginTime = System.currentTimeMillis();
        int result = 0;
        for (int i = 0; i < 500000000; i++) {
            result += i;
        }
        long endTime = System.currentTimeMillis();
        log.info("Current thread execution time :{} ms",endTime-beginTime);
    }

    public static void main(String[] args) {
        YieldThread thread=newYieldThread(); thread.start(); }}Copy the code

The following is the comparison of the two execution results. It can be found that the execution method yiled() is used lengthened. When THE CPU resources are tight, the difference between the two execution time is more obvious.

Do not use the yeild

Use the yeild

Stop of thread

Stopping a thread means stopping the task that the thread is doing and giving up the current operation. However, stopping a thread operation is not as simple as using a break in a Java for loop, because the thread may use shared data in main memory to execute the task. Once giving up the thread task, Improper handling of the shared data it operates on can cause non-thread-safe problems. To stop a Thread in progress, you can use the thread.stop () method, which, like suspend and resume, is outdated and can produce unpredictable results. A safer way to do this now is to use thread.interrupt (). Using this method in a Thread signals the Thread to stop. This does not stop the Thread, but we can tell if the run() marker is there to terminate the task. To summarize, there are three ways to stop a thread in Java:

  1. The thread terminates upon completion of normal execution.
  2. usestopForced termination of threads, if shared resources are used, can produce unexpected results and is deprecated.
  3. useinterrupt()Terminal threads.

Determining thread status

 // Clears the thread state
 public static boolean interrupted(a)
 // Thread status is not clear
 public boolean isInterrupted(a)
Copy the code

The first method interrupted() is static and checks whether the thread executing this code is terminated. Calling this method clears the current thread status, such as true for the first call and false for the second call. (Rarely used)

The second method belongs to the thread object and is used to determine whether the current thread is in the terminated state. This method does not clear the thread state, so if the thread state is unchanged, the thread state obtained by multiple calls to this method is consistent for some time. (Very common)

The exception method stops the thread

If thread.isinterrupted () isInterrupted, the status of the thread can be checked to determine whether the thread continues. If the thread isInterrupted, the status of the thread can be interrupted by throwing an exception or by returning the exception.

The following code looks like this:

The thread class prints the current time at an interval of 1s, and determines the thread status before printing each time. If the thread continues to execute without termination, it will throw an exception if terminated!

/** * User: lijinpeng * Created by Shanghai on 2019/4/17. */
@Slf4j
public class LogRecordThread extends Thread {
    @Override
    public void run(a) {
        try {
            while (true) {

                if (!this.isInterrupted()) {
                    Thread.sleep(1000);
                    log.info("Logging in progress! Current time :{}", System.currentTimeMillis());
                } else {
                    throw newInterruptedException(); }}}catch (InterruptedException ex) {
            log.error("Current thread terminated, task terminated!");
           // ex.printStackTrace();}}public static void main(String[] args) throws InterruptedException {
        LogRecordThread thread = new LogRecordThread();
        thread.start();
        Thread.sleep(5000); thread.interrupt(); }}Copy the code

Running results:

As shown above, the thread terminates as expected. Alternatively, you can terminate a threaded task by returning it directly.

Stop the thread in sleep

If a thread is in the sleep state, calling Thread.interrupt () throws InterruptedException and clears the stopped status value to false. If thread.interrupt() is called while the thread is sleeping, the thread status value remains the same as before.

/** * User: lijinpeng * Created by Shanghai on 2019/4/17. */
@Slf4j
public class SleepInterruptThread extends Thread {
    @Override
    public void run(a) {
        try {
            log.info("Start executing thread task....");
            Thread.sleep(3000);
            log.info("Thread task completed!");
        } catch (InterruptedException ex) {
          log.error("Thread terminated, whether the thread is interrupted :{}".this.isInterrupted()); ex.printStackTrace(); }}public static void main(String[] args) throws InterruptedException {
        SleepInterruptThread thread=new SleepInterruptThread();
        thread.start();
        Thread.sleep(1000); thread.interrupt(); }}Copy the code

Running results:

Stop Violence Stops the thread

Stopping a thread can be done by calling thread.stop, but it is no longer recommended by the JDK because stopping a thread can cause processing to fail, incomplete data, and inconsistencies similar to suspend and resume. If you want to stop the thread, just use the exception method above.

summary

This article mainly introduces three ways to create a thread, several states of the thread and state transition, thread safety issues, thread basic API and how to stop a thread, learning to master these in fact also master the core of multithreading, although the thread concurrency safety problem is the focus of our attention, But you can’t really understand how to create and solve thread-safety problems without knowing the above points, so it’s worth noting.