Home  >  Article  >  Java  >  Thread status, thread safety issues and the use of synchronized keyword in Java

Thread status, thread safety issues and the use of synchronized keyword in Java

WBOY
WBOYforward
2023-05-07 14:46:081105browse

    Thread status in java

    At the operating system level, a thread has two states: ready and blocked.

    But In order to quickly know the reason why a thread is blocked when a thread is blocked, Java has further refined the blocking status.

    Thread status, thread safety issues and the use of synchronized keyword in Java

    • ##NEW : The thread object has been created, but the system-level thread has not been created, or the thread object has not called start()

    • TERMINATED: The thread in the system has been destroyed, but The thread object in the code is still there, that is, after run() has finished running, the Thread object is still there

    • RUNNABLE: The thread is in the ready queue and may be scheduled for execution by the CPU at any time

    • TIMED_WAITING: During thread execution, the thread object calls sleep() and enters blocking. When the sleep time is up, it will return to the ready queue

    • BLOCKED : After one thread locks (synchronized) an object, and another thread also wants to lock the object, it will fall into the BLOCKED state. Only when the first thread unlocks the lock object can the latter thread possibly lock the object. The object is locked.

    • WAITING: Use wait() with synchronized. Once a thread calls wait(), the object will be unlocked first and wait until another thread performs notify() ), then the thread in wait will be awakened. Of course, you can also set a maximum waiting time in wait() to prevent dead waits.

    Thread safety issue case analysis

    Multiple threads writing to the same variable

    1. Concept: When does a string of code have a thread safety problem? First of all, the source of the evil of thread safety problems is, When multiple threads execute concurrently, there will be a phenomenon of preemptive execution. The preemptive execution here executes machine instructions! When does a string of code have thread safety issues? When multiple threads execute concurrently, no matter how many threads there are, No matter how they preemptively execute their code, it will not affect the final result, which is called thread safety. However, due to preemptive execution, different results than expected appear, which is called a thread safety problem and a bug!

    2. Typical case: Use two threads to perform an auto-increment operation on the same number 100,000 times:

    3. public class Demo1 {
          private static int count=0;
          public static void main(String[] args) {
              Thread t1=new Thread(()->{
                  for(int i=0;i<50000;i++){
                      count++;
                  }
              });
              t1.start();
              Thread t2=new Thread(()->{
              t2.start();
              try {
                  t1.join();
                  t2.join();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println(count);
          }
      }
      //打印结果:68994
    Obviously the expected result is 100,000, but the calculation It comes out to be more than 6w, which is a thread safety problem.

    Analysis of the reason:

    Only perform an auto-increment operation for the heap count of each thread: First, you must Understand, there are three steps to perform an auto-increment machine instruction: get the count value from the main memory to the cpu register -> increment the count value in the register by 1 -> refresh the count value in the register to the main memory , let’s call these three steps: load->add->save

    We assume that two sets of instructions are executed concurrently on one CPU (drawing two CPUs to represent it) (it will not A situation like loading at the same time occurs):

    Thread status, thread safety issues and the use of synchronized keyword in Java

    If the above picture appears:

    Thread status, thread safety issues and the use of synchronized keyword in Java

    Observation found: two Each thread executed count once, but the results twice were not satisfactory, which is equivalent to only one auto-increment. The above is a thread safety problem.

    And we can predict the results of the above code Range: between 5w-10w!, why?

    The above two pictures show the situation where thread safety problems occur. The result is that two additions are used as one. If two threads It has been in this state (also the worst state), but the calculation result is 5w. Then if the two threads have been one thread completely executed load-add-save, the other thread will execute such Operation, then it will be executed serially, but it is not 10w.

    3. How to solve the above case?

    The case also mentioned at the end, as long as serial execution can be achieved , can ensure the correctness of the results, then java does have such a function for us to use, that is, the use of the synchronized keyword.

    Thread status, thread safety issues and the use of synchronized keyword in Java

    That is to say: before cpu1 executes load First lock the lock object, and then unlock it after saving. Only then can cpu2 lock the object and perform a series of operations. At this time, the atomicity of load-add-save is guaranteed, making this Either don't execute the three steps, and execute them in one go.

    Then you may ask, what is the difference between this and using only one main thread to calculate the self-increment 100,000 times? How to create multiple What’s the point of threads?

    意义很大,因为我们创建的线程很多时候不仅仅只是一个操作,光针对自增我们可以通过加锁防止出现线程安全问题,但是各线程的其他操作要是不涉及线程安全问题那就可以并发了呀,那此时不就大大提升了执行效率咯.

    4.具体如何加锁呢?

    此处先只说一种加锁方式,先把上述案例的问题给解决了再说.

    使用关键字synchronized,此处使用的是给普通方法加synchronized修饰的方法(除此之外,synchronized还可以修饰代码块和静态方法)

    class Counter{
        private int count;
        synchronized public void increase(){
            this.count++;
        }
        public int getCount(){
            return this.count;
        }
    }
    public class Demo2 {
        private static int num=50000;
        public static void main(String[] args) {
            Counter counter=new Counter();//此时对象中的count值默认就是0
            Thread t1=new Thread(()->{
                for (int i = 0; i < num; i++) {
                    counter.increase();
                }
            });
            t1.start();
    
            Thread t2=new Thread(()->{
                for (int i = 0; i < num; i++) {
                    counter.increase();
                }
            });
            t2.start();
    
            try {
                t1.join();
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println(counter.getCount());
        }
    }//打印10W

    内存可见性问题

    首先说明:这是有编译器优化导致的,其次要知道cpu读取变量时:先从主内存将变量的值存至缓存或者寄存器中,cpu计算时再在寄存器中读取这个值.

    当某线程频繁的从内存中读取一个不变的变量时,编译器将会把从内存获取变量的值直接优化成从寄存器直接获取.之所以这样优化,是因为,cpu从主内存中读取一个变量比在缓存或者寄存器中读取一个变量的值慢成千上万倍,如果每每在内存中读到的都是同一个值,既然缓存里头已经有这个值了,干嘛还大费周折再去主内存中进行获取呢,直接从缓存中直接读取就可以了,可提升效率.

    但是:一旦一个线程被优化成上述的情况,那如果有另一个线程把内存中的值修改了,我被优化的线程还傻乎乎的手里拿着修改之前的值呢,或者内存中的变量值被修改了,被优化的线程此时已经感应不到了.

    具体而言:

    public class Demo3 {
        private static boolean flag=false;
        public static void main(String[] args) {
            Thread t1=new Thread(()->{
                while(!flag){
                    System.out.println("我是优化完之后直接读取寄存器中的变量值才打印的哦!");
                }
            });
            t1.start();
    
            flag=true;
            System.out.println("我已经在主线程中修改了标志位");
        }
    }

    运行上述代码之后,程序并不会终止,而是一直在那打印t1线程中的打印语句.

    如何解决上述问题:

    引入关键字volatile:防止内存可见性问题,修饰一个变量,那某线程想获取该变量的值的时候,只能去主内存中获取,其次它还可以防止指令重排序,指令重排问题会在线程安全的单例模式(懒汉)进行介绍.具体:

    public class Demo3 {
        private static volatile boolean flag=false;
        public static void main(String[] args) {
            Thread t1=new Thread(()->{
                while(!flag){
                    System.out.println("我是优化完之后直接读取寄存器中的变量值才打印的哦!");
                }
            });
            t1.start();
    
            try {
                Thread.sleep(1);//主线程给t1留有充足的时间先跑起来
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag=true;
            System.out.println("我已经在主线程中修改了标志位");
        }
    }
    //打印若干t1中的打印语句之后,主线程main中修改标志位之后,可以终止t1

    注意:上述优化现象只会出现在频繁读的情况,如果不是频繁读,就不会出现那样的优化.

    指令重排序问题

    生活案例:买菜

    Thread status, thread safety issues and the use of synchronized keyword in Java

    如果是傻乎乎的按照菜单从上到下的去买菜,从路线图可以看出,不必要的路是真的没少走.

    如果执行代码时,编译器认为某些个代码调整一下顺序并不会影响结果,那代码的执行顺序就会被调整,就比如可以把上面买菜的顺序调整成:黄瓜->萝卜->青菜->茄子

    单线程这样的指令重排一般不会出现问题,但是多线程并发时,还这样优化,就容易出现问题

    针对这样的问题,如果是针对一个变量,我们可以使用volatile修饰,如果是针对代码块,我们可以使用synchronized.

    synchronized的用法

    • synchronized起作用的本质

    • 修饰普通方法

    • 修饰静态方法

    • 修饰代码块

    synchronized起作用的本质

    因为我们知道java中所有类都继承了Object,所以所有类都包含了Object的部分,我们可以称这继承的部分是"对象头",使用synchronized进行对象头中的标志位的修改,就可以做到一个对象的锁一个时刻只能被一个线程所持有,其他线程此时不可抢占.这样的设置,就好像把一个对象给锁住了一样.

    修饰普通方法

    如前述两个线程给同一个count进行自增的案例.不再赘述.此时的所对象就是Counter对象

    修饰静态方法⚡️

    与普通方法类似.只不过这个方法可以类名直接调用.

    修饰代码块

    首先修饰代码块需要执行锁对象是谁,所以这里可以分为三类,一个是修饰普通方法的方法体这个代码块的写法,其次是修饰静态方法方法体的写法,最后可以单独写一个Object的对象,来对这个Object对象进行上锁.

    class Counter{
        private int count;
        public void increase(){
            synchronized(this){
                count++;
            }
        }
        public int getCount(){
            return this.count;
        }
    }
    class Counter{
        private static int count;
        public static void increase(){
            synchronized(Counter.class){//注意这里锁的是类对象哦
                count++;
            }
        }
        public int getCount(){
            return this.count;
        }
    }
    class Counter{
        private static int count;
        private static Object locker=new Object();
        public static void increase(){
            synchronized(locker){
                count++;
            }
        }
        public int getCount(){
            return this.count;
        }
    }

    注意:java中这种随手拿一个对象就能上锁的用法,是java中一种很有特色的用法,在别的语言中,都是有专门的锁对象的.

    Conclusion

    java中的线程状态,以及如何区分线程安全问题 罪恶之源是抢占式执行多线程对同一个变量进行修改,多线程只读一个变量是没有线程安全问题的修改操作是非原子性的内存可见性引起的线程安全问题指令重排序引起的线程安全问题 synchronized的本质和用法

    1.java中的线程状态,以及如何区分
    2.线程安全问题

    • 罪恶之源是抢占式执行

    • If multiple threads modify the same variable, there is no thread safety problem if multiple threads only read one variable

    • The modification operation is non-atomic

    • Thread safety issues caused by memory visibility

    • Thread safety issues caused by instruction reordering

    3.The essence and usage of synchronized

    The above is the detailed content of Thread status, thread safety issues and the use of synchronized keyword in Java. For more information, please follow other related articles on the PHP Chinese website!

    Statement:
    This article is reproduced at:yisu.com. If there is any infringement, please contact admin@php.cn delete