zoukankan      html  css  js  c++  java
  • 廖雪峰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定义的单个原子操作不需要同步
  • 相关阅读:
    HDU 3951 (博弈) Coin Game
    HDU 3863 (博弈) No Gambling
    HDU 3544 (不平等博弈) Alice's Game
    POJ 3225 (线段树 区间更新) Help with Intervals
    POJ 2528 (线段树 离散化) Mayor's posters
    POJ 3468 (线段树 区间增减) A Simple Problem with Integers
    HDU 1698 (线段树 区间更新) Just a Hook
    POJ (线段树) Who Gets the Most Candies?
    POJ 2828 (线段树 单点更新) Buy Tickets
    HDU 2795 (线段树 单点更新) Billboard
  • 原文地址:https://www.cnblogs.com/csj2018/p/10997948.html
Copyright © 2011-2022 走看看