关于多线程:多线程2synchronized和volatile

41次阅读

共计 13071 个字符,预计需要花费 33 分钟才能阅读完成。

多线程极大的晋升了效率,然而也带来了隐患,比方两个线程同时操作一个对象,就可能造成数据异样。

为什么会呈现线程平安问题

以下是一个线程不平安的程序,运行后果有时是 10000,有时比 10000 小,而且每次都可能不同,这就是线程不平安,因为 count++ 的操作不是原子性的,分为三步,读改写,即先读取数据,在执行批改操作(+1),再将数据回写到内存中,但这样就会呈现问题,比方线程一和线程二获取到了数据 10,而后线程二进行批改和回写操作,将数据变为 11,此时线程一开始执行,然而此时线程一读取到的数据还是 10 而部署线程二批改后的 11,这样就会造成线程一和二各做一次减少操作,然而 count 从 10 变为了 11,即产生了数据异样。

public class ThreadUnsafeDemo {

    private static int count;

    private static class Thread1 extends Thread {public void run() {for (int i = 0; i < 5000; i++) {count++;}
        }
    }

    public static void main(String[] args) throws InterruptedException {Thread1 t1 = new Thread1();
        Thread1 t2 = new Thread1();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }

}

那么为什么会呈现线程不平安问题,次要有三点

  • 原子性:一个或者多个操作在 CPU 执行的过程中被中断(CPU 上下文切换)
  • 可见性:一个线程对共享变量的批改,另外一个线程不能立即看到
  • 有序性:程序执行的程序没有依照代码的先后顺序执行
    前两点后面曾经举例了,当初在解释一下第三点。为什么程序执行的程序会和代码的执行程序不统一呢?java 平台包含两种编译器:动态编译器(javac)和动静编译器(jit:just in time)。动态编译器是将.java 文件编译成.class 文件(二进制文件),之后便能够解释执行。动静编译器是将.class 文件编译成机器码,之后再由 jvm 运行。问题个别会呈现在动静编译器上,因为动静编译器为了程序的整体性能会对指令进行重排序,尽管重排序能够晋升程序的性能,然而重排序之后会导致源代码中指定的内存拜访程序与理论的执行程序不一样,就会呈现线程不平安的问题。

如何保障线程平安

针对原子性问题,JDK 中有 atomic 类,这些类自身通过 CAS 保障操作的原子性。

针对可见性问题,能够通过 synchronized 关键字加锁来解决。与此同时,java 还提供了一种轻量级的锁,即 volatile 关键字,要优于 synchronized 的性能,同样能够保障批改对其余线程的可见性。volatile 个别用于对变量的写操作不依赖于以后值的场景中,比方状态标记量等。

针对有序性问题,能够通过 synchronized 关键字定义同步代码块或者同步办法保障有序性,另外也能够通过 Lock 接口保障有序性。

synchronized

Synchronized 是 Java 中解决并发问题的一种最罕用的办法,也是最简略的一种办法。Synchronized 的作用次要有三个:

  1. 原子性:确保线程互斥的拜访同步代码;
  2. 可见性:保障共享变量的批改可能及时可见,其实是通过 Java 内存模型中的“对一个变量 unlock 操作之前,必须要同步到主内存中;如果对一个变量进行 lock 操作,则将会清空工作内存中此变量的值,在执行引擎应用此变量前,须要从新从主内存中 load 操作或 assign 操作初始化变量值”来保障的;
  3. 有序性:无效解决重排序问题,即“一个 unlock 操作后行产生 (happen-before) 于前面对同一个锁的 lock 操作”;

synchronized 内置锁 是一种 对象锁(锁的是对象而非援用变量),作用粒度是对象,能够用来实现对 临界资源的同步互斥拜访,是可重入的。其最大的作用是防止死锁,如:

子类同步办法调用了父类同步办法,如没有可重入的个性,则会产生死锁;

对于 synchronized 办法或者 synchronized 代码块,当出现异常时,JVM 会主动开释以后线程占用的锁,因而不会因为异样导致呈现死锁景象

应用办法

润饰办法

SynchronizedDemo 润饰的是代码块,SynchronizedDemo1 润饰的是办法,然而两者是等价的,都是锁定了对象,锁定了对象后对对象的操作只能期待上一个锁开释后进行

public class SynchronizedDemo {public void method() {synchronized (this) {System.out.println("Method 1 start");
        }
    }

    public static void main(String[] args) {SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
        synchronizedDemo.method();}
}
public class SynchronizedDemo1 {public synchronized void method() {System.out.println("Method 1 start");
    }

    public static void main(String[] args) {SynchronizedDemo1 synchronizedDemo = new SynchronizedDemo1();
        synchronizedDemo.method();}
}
润饰代码块
public class SynchronizedDemo2 {public static void main(String args[]) {SyncThread s1 = new SyncThread();
        SyncThread s2 = new SyncThread();
        Thread t1 = new Thread(s1);
        Thread t2 = new Thread(s2);

//        SyncThread s = new SyncThread();
//        Thread t1 = new Thread(s);
//        Thread t2 = new Thread(s);

        t1.start();
        t2.start();}

    static class SyncThread implements Runnable {
        private static int count;

        public SyncThread() {count = 0;}

        public void run() {synchronized (this) {for (int i = 0; i < 5; i++) {
                    try {System.out.println(Thread.currentThread().getName() + ":" + (count++));
                        Thread.sleep(100);
                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                }
            }
        }

        public int getCount() {return count;}
    }
}

运行后果

因为实例化了两个 SyncThread 对象,各锁各的,导致计数异样,要实现管制须要对 Class 加锁

Thread-0:0
Thread-1:1
Thread-0:2
Thread-1:2
Thread-1:3
Thread-0:3
Thread-0:4
Thread-1:4
Thread-0:5
Thread-1:6
public class SynchronizedDemo2 {public static void main(String args[]) {//        SyncThread s1 = new SyncThread();
//        SyncThread s2 = new SyncThread();
//        Thread t1 = new Thread(s1);
//        Thread t2 = new Thread(s2);

        SyncThread s = new SyncThread();
        Thread t1 = new Thread(s);
        Thread t2 = new Thread(s);

        t1.start();
        t2.start();}

    static class SyncThread implements Runnable {
        private static int count;

        public SyncThread() {count = 0;}

        public void run() {synchronized (this) {for (int i = 0; i < 5; i++) {
                    try {System.out.println(Thread.currentThread().getName() + ":" + (count++));
                        Thread.sleep(100);
                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                }
            }
        }

        public int getCount() {return count;}
    }
}

运行后果

因为只实例化了一个 SyncThread 对象,当两个并发线程 (thread1 和 thread2) 拜访同一个对象 (syncThread) 中的 synchronized 代码块时,在同一时刻只能有一个线程失去执行,另一个线程受阻塞,必须期待以后线程执行完这个代码块当前能力执行该代码块。Thread1 和 thread2 是互斥的,因为在执行 synchronized 代码块时会锁定以后的对象,只有执行完该代码块能力开释该对象锁,下一个线程能力执行并锁定该对象,所以计数正确

Thread-1:0
Thread-1:1
Thread-1:2
Thread-1:3
Thread-1:4
Thread-0:5
Thread-0:6
Thread-0:7
Thread-0:8
Thread-0:9
润饰局部代码块
public class SynchronizedDemo4 {public static void main(String args[]) {Counter counter = new Counter();
        Thread thread1 = new Thread(counter, "A");
        Thread thread2 = new Thread(counter, "B");
        thread1.start();
        thread2.start();}

    static class Counter implements Runnable {
        private int count;

        public Counter() {count = 0;}

        public void countAdd() {synchronized (this) {for (int i = 0; i < 5; i++) {
                    try {System.out.println(Thread.currentThread().getName() + ":" + (count++));
                        Thread.sleep(100);
                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                }
            }
        }

        // 非 synchronized 代码块,未对 count 进行读写操作,所以能够不必 synchronized
        public void printCount() {for (int i = 0; i < 5; i++) {
                try {System.out.println(Thread.currentThread().getName() + "count:" + count);
                    Thread.sleep(100);
                } catch (InterruptedException e) {e.printStackTrace();
                }
            }
        }

        public void run() {String threadName = Thread.currentThread().getName();
            if (threadName.equals("A")) {countAdd();
            } else if (threadName.equals("B")) {printCount();
            }
        }
    }
}

运行后果

B 线程的调用是非 synchronized, 并不影响 A 线程对 synchronized 局部的调用。从下面的后果中能够看出一个线程拜访一个对象的 synchronized 代码块时,别的线程能够拜访该对象的非 synchronized 代码块而不受阻塞

A:0
B count:1
A:1
B count:1
A:2
B count:3
B count:3
A:3
B count:4
A:4
润饰指定对象
public class SynchronizedDemo3 {public static void main(String args[]) {Account account = new Account("zhang san", 10000.0f);
        AccountOperator accountOperator = new AccountOperator(account);
        // 开启 10 个线程,进行存取款
        final int THREAD_NUM = 10;
        Thread threads[] = new Thread[THREAD_NUM];
        for (int i = 0; i < THREAD_NUM; i++) {threads[i] = new Thread(accountOperator, "Thread" + i);
            threads[i].start();}
    }

    static class Account {
        String name;
        float amount;

        public Account(String name, float amount) {
            this.name = name;
            this.amount = amount;
        }

        // 存钱
        public void deposit(float amt) {
            amount += amt;
            try {Thread.sleep(100);
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }

        // 取钱
        public void withdraw(float amt) {
            amount -= amt;
            try {Thread.sleep(100);
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }

        public float getBalance() {return amount;}
    }

    /**
     * 账户操作类
     */
    static class AccountOperator implements Runnable {
        private Account account;

        public AccountOperator(Account account) {this.account = account;}

        public void run() {
            // 锁定指定对象 account
            synchronized (account) {account.deposit(500);
                System.out.println("贷款实现" + Thread.currentThread().getName() + ":" + account.getBalance());
                account.withdraw(500);
                System.out.println("取款实现" + Thread.currentThread().getName() + ":" + account.getBalance());
            }
        }
    }
}

运行后果

在 AccountOperator 类中的 run 办法里,咱们用 synchronized 给 account 对象加了锁。这时,当一个线程拜访 account 对象时,其余试图拜访 account 对象的线程将会阻塞,直到该线程拜访 account 对象完结。也就是说谁拿到那个锁谁就能够运行它所管制的那段代码。

贷款实现 Thread0:10500.0
取款实现 Thread0:10000.0
贷款实现 Thread9:10500.0
取款实现 Thread9:10000.0
贷款实现 Thread8:10500.0
取款实现 Thread8:10000.0
贷款实现 Thread5:10500.0
取款实现 Thread5:10000.0
贷款实现 Thread3:10500.0
取款实现 Thread3:10000.0
贷款实现 Thread7:10500.0
取款实现 Thread7:10000.0
贷款实现 Thread6:10500.0
取款实现 Thread6:10000.0
贷款实现 Thread4:10500.0
取款实现 Thread4:10000.0
贷款实现 Thread2:10500.0
取款实现 Thread2:10000.0
贷款实现 Thread1:10500.0
取款实现 Thread1:10000.0
润饰静态方法
public class SynchronizedDemo5 {public static void main(String args[]) {SyncThread syncThread1 = new SyncThread();
        SyncThread syncThread2 = new SyncThread();
        Thread thread1 = new Thread(syncThread1, "SyncThread1");
        Thread thread2 = new Thread(syncThread2, "SyncThread2");
        thread1.start();
        thread2.start();}

    static class SyncThread implements Runnable {
        private static int count;

        public SyncThread() {count = 0;}

        public synchronized static void method() {for (int i = 0; i < 5; i++) {
                try {System.out.println(Thread.currentThread().getName() + ":" + (count++));
                    Thread.sleep(100);
                } catch (InterruptedException e) {e.printStackTrace();
                }
            }
        }

        public synchronized void run() {method();
        }
    }
}

运行后果

syncThread1 和 syncThread2 是 SyncThread 的两个对象,但在 thread1 和 thread2 并发执行时却放弃了线程同步。这是因为 run 中调用了静态方法 method,而静态方法是属于类的,所以 syncThread1 和 syncThread2 相当于用了同一把锁。

SyncThread1:0
SyncThread1:1
SyncThread1:2
SyncThread1:3
SyncThread1:4
SyncThread2:5
SyncThread2:6
SyncThread2:7
SyncThread2:8
SyncThread2:9
润饰一个类
public class SynchronizedDemo6 {public static void main(String args[]) {SyncThread syncThread1 = new SyncThread();
        SyncThread syncThread2 = new SyncThread();
        Thread thread1 = new Thread(syncThread1, "SyncThread1");
        Thread thread2 = new Thread(syncThread2, "SyncThread2");
        thread1.start();
        thread2.start();}

    static class SyncThread implements Runnable {
        private static int count;

        public SyncThread() {count = 0;}

        public static void method() {synchronized (SyncThread.class) {for (int i = 0; i < 5; i++) {
                    try {System.out.println(Thread.currentThread().getName() + ":" + (count++));
                        Thread.sleep(100);
                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                }
            }
        }

        public synchronized void run() {method();
        }
    }
}

运行后果

给 class 加锁和上例的给静态方法加锁是一样的,所有对象专用一把锁,保障程序输入

SyncThread2:0
SyncThread2:1
SyncThread2:2
SyncThread2:3
SyncThread2:4
SyncThread1:5
SyncThread1:6
SyncThread1:7
SyncThread1:8
SyncThread1:9

应用总结

  • 当 synchronized 作用在实例办法时,监视器锁(monitor)便是对象实例(this),即以后实例,然而如果创立了多个实例,各锁各的就无法控制输入;
  • 当 synchronized 作用在某一个对象实例时,监视器锁(monitor)便是括号括起来的对象实例;
  • 当 synchronized 作用在静态方法时,监视器锁(monitor)便是对象的 Class 实例,因为 Class 数据存在于永恒代,因而静态方法锁相当于该类的一个全局锁;
  • 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就能够运行它所管制的那段代码。
  • 实现同步开销微小,甚至可能造成死锁,所以尽量避免无谓的同步控制。

实现原理

执行 SynchronizedDemo 后,查看字节码

Compiled from "SynchronizedDemo.java"
public class com.example.offer.thread.demo3.SynchronizedDemo {public com.example.offer.thread.demo3.SynchronizedDemo();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void method();
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter
       4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #3                  // String Method 1 start
       9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      12: aload_1
      13: monitorexit
      14: goto          22
      17: astore_2
      18: aload_1
      19: monitorexit
      20: aload_2
      21: athrow
      22: return
    Exception table:
       from    to  target type
           4    14    17   any
          17    20    17   any

  public static void main(java.lang.String[]);
    Code:
       0: new           #5                  // class com/example/offer/thread/demo3/SynchronizedDemo
       3: dup
       4: invokespecial #6                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: invokevirtual #7                  // Method method:()V
      12: return
}

重点是其中的 monitorenter 和 monitorexit

  1. monitorenter:每个对象都是一个监视器锁(monitor)。当 monitor 被占用时就会处于锁定状态,线程执行 monitorenter 指令时尝试获取 monitor 的所有权,过程如下:

    1. 如果 monitor 的进入数为 0,则该线程进入 monitor,而后将进入数设置为 1,该线程即为 monitor 的所有者;
    2. 如果线程曾经占有该 monitor,只是从新进入,则进入 monitor 的进入数加 1;
    3. 如果其余线程曾经占用了 monitor,则该线程进入阻塞状态,直到 monitor 的进入数为 0,再从新尝试获取 monitor 的所有权;
  2. monitorexit:执行 monitorexit 的线程必须是 objectref 所对应的 monitor 的所有者。指令执行时,monitor 的进入数减 1,如果减 1 后进入数为 0,那线程退出 monitor,不再是这个 monitor 的所有者。其余被这个 monitor 阻塞的线程能够尝试去获取这个 monitor 的所有权。

    monitorexit 指令呈现了两次,第 1 次为同步失常退出开释锁;第 2 次为产生异步退出开释锁;

通过下面两段形容,咱们应该能很分明的看出 Synchronized 的实现原理,Synchronized 的语义底层是通过一个 monitor 的对象来实现,其实 wait/notify 等办法也依赖于 monitor 对象,这就是为什么只有在同步的块或者办法中能力调用 wait/notify 等办法,否则会抛出 java.lang.IllegalMonitorStateException 的异样的起因。

volatile

volatile 关键字的作用:保障了变量的可见性(visibility)。被 volatile 关键字润饰的变量,如果值产生了变更,其余线程立马可见,避免出现脏读的景象,volatile 实质是在通知 jvm 以后变量在寄存器(工作内存)中的值是不确定的,须要从主存中读取,volatile 仅能应用在变量级别

synchronized 关键字是避免多个线程同时执行一段代码,那么就会很影响程序执行效率,而 volatile 关键字在某些状况下性能要优于 synchronized,然而要留神 volatile 关键字是无奈代替 synchronized 关键字的,因为 volatile 关键字无奈保障操作的原子性。通常来说,应用 volatile 必须具备以下 2 个条件:

1)对变量的写操作不依赖于以后值

2)该变量没有蕴含在具备其余变量的不变式中

实际上,这些条件表明,能够被写入 volatile 变量的这些有效值独立于任何程序的状态,包含变量的以后状态。

事实上,我的了解就是下面的 2 个条件须要保障操作是原子性操作,能力保障应用 volatile 关键字的程序在并发时可能正确执行。

应用场景

状态标记
volatile boolean inited = false;
// 线程 1:
context = loadContext();  
inited = true;            
 
// 线程 2:
while(!inited){sleep()
}
doSomethingwithconfig(context);

双重校验

第一次校验:因为单例模式只须要创立一次实例,如果前面再次调用 getInstance 办法时,则间接返回之前创立的实例,因而大部分工夫不须要执行同步办法外面的代码,大大提高了性能。如果不加第一次校验的话,那跟下面的懒汉模式没什么区别,每次都要去竞争锁。

第二次校验:如果没有第二次校验,假如线程 t1 执行了第一次校验后,判断为 null,这时 t2 也获取了 CPU 执行权,也执行了第一次校验,判断也为 null。接下来 t2 取得锁,创立实例。这时 t1 又取得 CPU 执行权,因为之前曾经进行了第一次校验,后果为 null(不会再次判断),取得锁后,间接创立实例。后果就会导致创立多个实例。所以须要在同步代码外面进行第二次校验,如果实例为空,则进行创立。

须要留神的是,private volatile static Singleton instance = null; 须要加 volatile 关键字,否则会呈现谬误。问题的起因在于 JVM 指令重排优化的存在。在某个线程创立单例对象时,在构造方法被调用之前,就为该对象调配了内存空间并将对象的字段设置为默认值。此时就能够将调配的内存地址赋值给 instance 字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用 getInstance,取到的就是状态不正确的对象,程序就会出错,所以须要 volatile 禁止指令重排序优化。

instance = new Singleton(); 中 new 命令并不是一个原子指令,它分为三步

调配对象内存

调用结构器办法,执行初始化

将对象援用赋值给变量

虚拟机理论运行时,以上指令可能产生重排序。以上代码 2,3 可能产生重排序,然而并不会重排序 1 的程序。也就是说 1 这个指令都须要先执行,因为 2,3 指令须要依靠 1 指令执行后果。

Java 语言规规定了线程执行程序时须要恪守 intra-thread semantics。intra-thread semantics 保障重排序不会扭转单线程内的程序执行后果。这个重排序在没有扭转单线程程序的执行后果的前提下,能够进步程序的执行性能。

尽管重排序并不影响单线程内的执行后果,然而在多线程的环境就带来一些问题。

class Singleton{
    private volatile static Singleton instance = null;
     
    private Singleton() {}
     
    public static Singleton getInstance() {if(instance==null) {synchronized (Singleton.class) {if(instance==null)
                    instance = new Singleton();}
        }
        return instance;
    }
}

应用办法

润饰变量
public class VolatileDemo {

    private static int count;

    public static void main(String args[]) {SyncThread s = new SyncThread();
        Thread t1 = new Thread(s);
        Thread t2 = new Thread(s);

        t1.start();
        t2.start();}

    static class SyncThread implements Runnable {public SyncThread() {count = 0;}
        public void run() {synchronized (this) {for (int i = 0; i < 5; i++) {
                    try {System.out.println(Thread.currentThread().getName() + ":" + (count++));
                        Thread.sleep(100);
                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                }
            }
        }

        public int getCount() {return count;}
    }
}

运行后果

Thread-0:0
Thread-1:1
Thread-0:2
Thread-1:3
Thread-0:4
Thread-1:5
Thread-0:6
Thread-1:7
Thread-0:8
Thread-1:9

应用总结

一旦一个共享变量(类的成员变量、类的动态成员变量)被 volatile 润饰之后,那么就具备了两层语义:
1)保障了不同线程对这个变量进行操作时的可见性,即一个线程批改了某个变量的值,这新值对其余线程来说是
立刻可见的。
2)禁止进行指令重排序。
volatile 实质是在通知 jvm 以后变量在寄存器(工作内存)中的值是不确定的,须要从主存中读取;
synchronized 则是锁定以后变量,只有以后线程能够拜访该变量,其余线程被阻塞住。

实现原理

有 volatile 变量润饰的共享变量进行操作时会减少一行汇编命令,命令有前缀 lock,而 lock 前缀的指令在多核处理器中会触发以下两件事件

  • 将以后处理器缓存的数据回写到主内存
  • 回写主内存操作会引起其余 CPU 中缓存了该内存地址的数据有效

synchronized 和 volatile 的区别

1.volatile 仅能应用在变量级别;
synchronized 则能够应用在变量、办法、和类级别的

2.volatile 仅能实现变量的批改可见性,并不能保障原子性;

synchronized 则能够保障变量的批改可见性和原子性

3.volatile 不会造成线程的阻塞;
synchronized 可能会造成线程的阻塞。

4.volatile 标记的变量不会被编译器优化;
synchronized 标记的变量能够被编译器优化

正文完
 0