>Java >java지도 시간 >스레드 상태, 스레드 안전 문제 및 Java의 동기화 키워드 사용

스레드 상태, 스레드 안전 문제 및 Java의 동기화 키워드 사용

WBOY
WBOY앞으로
2023-05-07 14:46:081163검색

    Java의 스레드 상태

    운영 체제 수준에서 스레드에는 준비 상태와 차단됨의 두 가지 상태가 있습니다.

    하지만 Java에서는 스레드가 차단될 때 스레드가 차단되는 이유를 빨리 알기 위해 , 차단 상태가 더욱 구체화되었습니다.

    스레드 상태, 스레드 안전 문제 및 Java의 동기화 키워드 사용

    • NEW: 스레드 개체가 생성되었지만 시스템 수준의 스레드가 생성되지 않았거나 스레드 개체가 start()를 호출하지 않았습니다.

    • TERMINATED: 시스템의 스레드가 삭제되었지만 코드의 스레드 개체는 여전히 존재합니다. 즉, run()이 실행을 마친 후에도 Thread 개체는 여전히 존재합니다

    • RUNNABLE: 스레드가 준비 대기열은 언제든지 CPU에 의해 예약되고 실행될 수 있습니다

    • TIMED_WAITING: 스레드 실행 중에 스레드 개체는 sleep()을 호출하고 차단에 들어가며 절전 시간이 끝나면 준비 대기열로 돌아갑니다

    • BLOCKED: 스레드가 객체를 잠근(동기화한 후) 다른 스레드도 이 객체를 잠그려고 하면 첫 번째 스레드가 잠금 객체를 잠금 해제할 때만 후자 스레드가 객체를 잠글 수 있습니다.

    • WAITING: wait()를 사용하여 스레드가 wait()를 호출하면 개체가 먼저 잠금 해제됩니다. 통지()를 수행한 후 대기 중인 스레드가 깨어납니다. 또한 wait()에 값을 설정합니다.

    스레드 안전 문제 사례 분석

    동일한 변수에 여러 스레드 쓰기

    1. 개념: 호출되는 코드 문자열은 언제입니까? 스레드 안전 문제 우선, 스레드 보안 문제의 원인은 여러 스레드가 동시에 실행될 때 선제 실행이 발생한다는 것입니다. 여기서 선제 실행은 언제 스레드 안전 문제라고 합니까? 스레드는 동시적으로 여러 스레드가 코드를 선제적으로 실행하더라도 최종 결과에 영향을 미치지 않습니다. 이를 스레드 안전성이라고 합니다. 그러나 선제 실행으로 인해 예상과 다른 결과가 나타나는 것을 스레드 보안 문제라고 합니다.

    2. 일반적인 경우: 두 개의 스레드를 사용하여 동일한 숫자에 대해 100,000번 자동 증가 작업을 수행합니다.

    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

    분명히 예상되는 결과는 100,000이지만 계산 결과는 6,000,000 이상입니다.

    이유 분석:

    각 스레드의 힙 수에 대해서만 자동 증가 작업을 수행합니다. 우선, 자동 증가 기계 명령을 수행하는 데는 세 단계가 있다는 것을 이해해야 합니다. 메인 메모리에서 CPU 레지스터로의 카운트 값 -> 레지스터의 카운트 값을 1만큼 증가 -> 레지스터의 카운트 값을 메인 메모리로 새로 고침 다음 세 단계를 호출해 보겠습니다. 로드 -> 추가 ->save

    한 CPU에서 두 세트의 명령이 동시에 실행된다고 가정합니다(CPU 두 개를 그리는 것이 더 좋음)(동시 로딩은 없을 것입니다).

    스레드 상태, 스레드 안전 문제 및 Java의 동기화 키워드 사용

    위 그림의 상황과 같습니다 :

    스레드 상태, 스레드 안전 문제 및 Java의 동기화 키워드 사용

    관찰 결과 두 스레드 모두 count++를 한 번 실행했지만 두 ++의 결과는 만족스럽지 않았으며 이는 단 한 번의 자동 증가와 동일합니다.

    그리고 우리는 예측할 수 있습니다. 위 코드의 결과 범위는 5w-10w!, 왜일까요?

    위 두 그림은 두 개의 스레드를 하나로 사용하게 된 상황을 보여줍니다. 이 상태(또한 최악의 상태)이지만 계산 결과는 5w입니다. 그런 다음 두 스레드가 로드-추가-저장을 완전히 실행한 경우 다른 스레드가 해당 작업을 실행한 다음 순차적으로 실행됩니다.

    3. 위의 경우를 해결하는 방법은 무엇입니까?

    마지막에 언급한 경우도 직렬 실행이 가능하면 결과의 정확성이 보장됩니다. 그렇다면 Java에는 그러한 기능이 있습니다.

    스레드 상태, 스레드 안전 문제 및 Java의 동기화 키워드 사용

    즉, CPU1은 로드를 실행하기 전에 잠금 개체를 잠근 다음 저장한 후에만 잠금을 해제할 수 있습니다. 이때 로드-추가-저장의 원자성이 보장되므로 이 세 단계는 실행되지 않고 한 번에 실행이 완료됩니다.

    그러면 무엇입니까? 자체 증가분을 100,000번 계산하는 것과 하나의 메인 스레드만 사용하는 것의 차이점은 무엇입니까?

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

    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

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

    指令重排序问题

    生活案例:买菜

    스레드 상태, 스레드 안전 문제 및 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.线程安全问题

    • 罪恶之源是抢占式执行

    • 여러 스레드가 동일한 변수를 수정하며, 여러 스레드가 하나의 변수만 읽는 경우 스레드 안전 문제가 없습니다.

    • 수정 작업이 비원자적입니다.

    • 메모리 가시성으로 인한 스레드 안전 문제

    • 명령어 재정렬로 인한 스레드 안전성 문제

    3. 동기화의 본질과 사용법

    위 내용은 스레드 상태, 스레드 안전 문제 및 Java의 동기화 키워드 사용의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

    성명:
    이 기사는 yisu.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제