• 廖雪峰Java11多线程编程-2线程同步-1同步代码块


    1.线程安全问题

    多个线程同时运行,线程调度由操作系统决定,程序本身无法决定。
    如果多个线程同时读写共享变量,就可能出现问题。
    假设有AddThread和DecThread,它们分别对同一个共享变量做加和减运算LOOP次,最终结果应该是0。但某些时候比如LOOP为10000时,结果是错误的。

    class AddThread extends Thread{
        public void run(){
            for(int i=0;i<Main.LOOP;i++){
                Main.count += 1;
            }
        }
    }
    
    class DecThread extends Thread{
        public void run(){
            for(int i=0;i<Main.LOOP;i++){
                Main.count -= 1;
            }
        }
    }
    
    public class Main {
        final static int LOOP = 10000;
        public static int count = 0;
        public static void main(String[] args) throws InterruptedException{
            Thread t1 = new AddThread();
            Thread t2 = new DecThread();
            t1.start();
            t2.start();
            //等待这两个线程执行结束
            t1.join();
            t2.join();
            System.out.println(count);
        }
    }
    
    ## 2.原子操作 * 因此对共享变量进行写入时,必须保证是原子操作 * 原子操作是指不能被中断的一个或一系列操作

    当执行 n = n +1时,编译器会把它编译为3条字节码指令,分别是ILOAD, IADD, ISTORE。所以对于这个简单的赋值语句,它并不是一个原子操作,这就可能导致两个线程在执行这条语句的时候,会出现问题。
    假设1:n=100,Thread1执行语句n为101,Thread2再执行n为102。
    假设2:Thread1刚执行完ILOAD指令,就被操作系统暂停了,然后Thread2调度执行,结果n变成了101,此后Thread1再度被操作系统调度执行,结果也是101。即n+1的指令被2个线程调用了2次,最终只加了1.

    所以我们要保证当Thread1执行时,Thread2不能执行,直到Thread1执行完毕,Thread2才能开始执行。这样运行的结果就是正确的。
    要实现这个效果,就要对ILOAD之前和ISTORE之后进行加锁和解锁。

    3.同步代码块

    Java使用synchronized对一个对象进行加锁:

    • 为了保证一系列操作作为原子操作,必须保证一系列操作过程中不被其他线程执行
            synchronized (lock){
                n=n+1;
            }
    

    当一个线程想要执行synchronized语句块时,必须首先获得指定对象的锁,这个对象就是synchronized括号里的对象,然后线程再执行synchronized语句块,执行结束以后释放锁。
    在执行synchronized语句块时,如果Thread1执行到任何语句时,被操作系统中断。其他线程如Thread2因为无法获取lock对象的锁,从而导致Thread2无法进入synchronized语句块,Thread2就必须等待,直到Thread1再次被调用,并执行完synchronized语句块释放了锁,Thread2才能获得lock对象锁,进入synchronized语句块。
    synchronized保证了代码块和任意时刻最多只有一个线程能执行。

    • 因为一个对象的锁只能被一个线程获得,其他线程必须等待。

    synchronized的问题:

    • 性能下降。因为synchronized代码块无法并发执行,所以性能会下降。此外加锁和解锁都会消耗一定的时间,所以synchronized会降低程序的执行效率。

    如何使用synchronized:

    • 1.找出修改共享变量的线程代码块
    • 2.选择一个实例作为锁
    • 3.使用synchronized(lock Object){...}

    注意:

    • 对于同一个变量的修改,必须要获取同一个锁,如果2个线程获取的是不同的锁,它们是没有办法进行同步的。
    • 不用担心异常。无论有无异常,在synchronized结束时都会释放锁。
    class AddThread extends Thread{
        public void run(){
            for(int i=0;i<Main.LOOP;i++){
                synchronized (Main.LOCK) {
                    Main.count += 1;
                }
            }
        }
    }
    
    class DecThread extends Thread{
        public void run(){
            for(int i=0;i<Main.LOOP;i++){
                synchronized (Main.LOCK) {//对于同一个变量的修改,要使用同一个锁
                    Main.count -= 1;
                }//无论有无异常,都会在此释放锁
            }
        }
    }
    
    public class Main {
        final static int LOOP = 10000;
        public static int count = 0;
        public static final Object LOCK = new Object();
        public static void main(String[] args) throws InterruptedException{
            Thread t1 = new AddThread();
            Thread t2 = new DecThread();
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(count);
        }
    }
    

    4.JVM的原子操作

    JVM定义了几种原子操作:

    • 基本类型(long和double除外)赋值
    • 引用类型赋值

    注意:

    • 原子操作时不需要同步的。
    • 可以把非原子操作变为原子操作
    • 局部变量不需要同步
        //原子操作不需要同步
        public void set(int m){
            synchronized (obj){
                this.value = m;
            }
        }
        //->
        public void set(int m){
            this.value = m;
        }
    //对2个int类型进行赋值,它不是一个原子操作。但可以先构造一个int数组,然后利用引用类型赋值,把它变成1个原子操作。
    class Pair{
        int first;
        int last;
        public void set(int first,int last){
            synchronized (this){
                this.first = first;
                this.last = last;
            }
        }
    }
    //->
    class Pair{
        int[] pair;
        public void set(int first,int last){
            int[] ps = new int[]{first,last};
            this.pair = ps;
        }
    }
        //a,b,s1,s2,r都是局部变量,各个线程的局部变量是完全独立的,互不影响,所以这个方法不需要同步。
        public int avg(int a, int b){
            int s1 = a*a + b*b;
            int s2 = a + b;
            int r = s1/s2;
            return r;
        }
    

    5.总结:

    • 多线程同时修改变量,会造成逻辑错误
      * 需要通过synchronized同步
      * 同步的本质就是给指定对象加锁
      * 注意加锁对象必须是同一个实例
    • 对JVM定义的单个原子操作不需要同步
  • 相关阅读:
    解决ArrayList线程不安全
    TraceView工具的使用
    Service
    Android之移动热修复
    06 swap命令,进程管理,rmp命令与yum命令,源码安装python
    04 linux用户群组和权限
    03 linux命令的操作
    Unity 5.x 导入教学Demo
    Creo二次开发—内存处理
    求一个数的二进制数中所含1的个数的代码实现
  • 原文地址:https://www.cnblogs.com/csj2018/p/10997948.html
Copyright © 2020-2023  润新知