廖雪峰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定义的单个原子操作不需要同步
原文地址:https://www.cnblogs.com/csj2018/p/10997948.html