Java-多线程学习分享-2

线程安全

当多个线程访问某个方法时,不管通过怎样的调用方法、或者说这些线程如何交替地执行,我们在主程序中不需要去做任何的处理,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类是线程安全的

public class TestThreadSecurity {
    int count = 1;

    public int testSecurityMethod1(Thread thread) {
        count++;
        System.out.println("线程名:" + thread.getName() + "执行方法");
        return count;
    }

    public static void main(String[] args) {
        TestThreadSecurity testThreadSecurity = new TestThreadSecurity();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                    int result = testThreadSecurity.testSecurityMethod1(Thread.currentThread());
                    System.out.println("线程名:" + Thread.currentThread().getName() + " result:" + result);
            }
        }, "t1");
        
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                int result = testThreadSecurity.testSecurityMethod1(Thread.currentThread());
                System.out.println("线程名:" + Thread.currentThread().getName() + " result:" + result);
            }
        }, "t2");
        t1.start();
        t2.start();
    }
}

运行结果:

线程名:t1执行方法
线程名:t2执行方法
线程名:t2 result:3
线程名:t1 result:3

很明显,t1线程的result被t2的result覆盖了,不是线程安全。
使用线程同步解决线程安全问题!!!

线程同步

当多个线程同时读写同一份共享资源(内存,文件,数据库)的时候,可能会引起冲突。这时候,我们就需要引入线程同步机制,对各个线程进行一个管理。

前提:

  • 只有多个线程的共享变量才需要进行同步。如果不是共享的或者是共享常量则没必要同步。
  • 线程同步并不是说让多个线程同时操作共享变量,而是当多个线程访问共享资源时,让它们一个一个的对共享资源进行操作。

多线程的三大特性

满足了多线程的三个特性,也就实现了线程同步。

一、原子性

一个操作或者多个操作,要么全部执行,要么就都不执行。

例:

x=10 // 原子操作,直接将数值10写入到工作内存
y=x // 非原子操作,先读取x的值,再将x的值写入工作内存

二、可见性

当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

一个进程中有一个主内存,进程中的各个线程有各自的工作内存。共享变量存在主内存中,当线程需要使用共享变量时,会先拷贝主内存中的共享变量到自己的工作内存,然后操作自己工作内存中的副本,操作完后将工作内存中的副本刷新到主内存中。

缓存一致性问题:
Java-多线程学习分享-2
3步骤中,线程A对共享变量x的操作对于线程B是不可知的,那么在4步骤中,线程B对共享变量x的操作是有问题的。

三、有序性

程序执行的顺序按照代码的先后顺序执行。

编译器和处理器会对指令进行重排序(为了效率),但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

指令重排序:

1、数据依赖性(as-if-serial): 不管怎么重排序,(单线程)程序的执行结果不能被改变。

例:

int a = 1;//1
int b = 1;//2
int c = a * b;//3

/**
* 因为3依赖于1和2,所以3一定在1和2之后执行。1和2之间没有依赖关系,所以1和2的执行顺序不一定。
* 所以执行顺序可能为:
* 1->2->3     
* 或者2->1->3
**/

2、Happens-Before原则:

  • 程序顺序规则:一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意操作。
  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁,而且前者线程解锁之后,对数据的操作对于后者加锁的线程是可见的。
  • volatile规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  • 传递性规则:如果A happens-before B,且B happens-before C,那么A happens-before C。
  • start()规则:它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。
  • join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。

重排序后的指令可以不按照这个规定的顺序执行,但是执行的结果必须是正确的,即按照规定顺序执行后的结果。所以实际上规则指定的是前一个操作的结果对后续操作是否是可见的

3、重排序对多线程的影响:

int a = 1;
boolean flag = true;
writerThread{
    a = 2;//1
    flag = false;//2
}
readThread{
    if(!flag){//3
        int i = a * 1;//4
    }
}

/**
* 下面两种排序的结果是不一样的
* 1、2-----------------1
*   -----3-----4-------
* 2、1-----------------2
*   ----3-------------
**/

java怎么实现线程同步及原理

一、synchronized

用synchronized关键字来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
synchronized是同步锁, synchronized修饰的代码相当于同一时刻单线程执行,故不存在原子性和有序性的问题。
synchronized 根据监视器锁规则的规定,保证了可见性:

  • 线程解锁前,必须把共享变量的最新值刷新到主内存中;
  • 线程加锁前,将清空工作内存*享变量的值,从主内存中重新取值。

原理:
java代码:

public class SynchronizedTest {
    // 使用synchronized修饰方法:
    public synchronized void doSth1(){
        System.out.println("Hello World");
    }

    // 使用synchronized修饰代码块:
    public void doSth2(){
        synchronized (SynchronizedTest.class){
            System.out.println("Hello World");
        }
    }
}

字节码指令:

......
public synchronized void doSth1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
......
public void doSth2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #5                  // class com/thunisoft/thread/controller/SynchronizedTest
         2: dup
         3: astore_1
         4: monitorenter
         5: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #3                  // String Hello World
        10: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        13: aload_1
        14: monitorexit
        15: goto          23
        18: astore_2
        19: aload_1
        20: monitorexit
        21: aload_2
        22: athrow
        23: return
......

synchronized修饰方法:
JVM采用ACC_SYNCHRONIZED标记符来实现同步 。
当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。
如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。

synchronized修饰代码块:
JVM采用monitorentermonitorexit两个指令来实现同步。
可以把执行monitorenter指令理解为加锁,执行monitorexit理解为释放锁。 每个对象维护着一个记录被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程获得锁(执行monitorenter)后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行monitorexit指令)的时候,计数器再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁。

二、volatile

volatile规则实现了其可见性和有序性。

  1. 当对volatile变量执行写操作后,JVM会把工作内存中的最新变量值强制刷新到主内存。
  2. 写操作会导致其他线程中的缓存无效。
  3. 线程使用缓存中变量时,先判断本地工作内存中此变量是否失效,若失效便从主内存中获取最新值。

原理:
采用内存屏障来实现可见性和有序性。
内存屏障:
阻止屏障两侧的指令重排序,即屏障后面的代码不能跟屏障前面的代码交换执行顺序。

  • Load Barrier:对于Load Barrier之后的指令 ,工作内存中的数据失效,需要从主内存中加载最新数据。
  • Store Barrier:对于Store Barrier之前的指令,需要将工作内存中的数据写入主内存,让其对其他线程可见。

volatile不能保证原子性:
当线程执行的操作不是原子操作时(i++),就会出现缓存一致性问题。

三、lock

锁类型:

  • 可重入锁:某个线程已经获得某个锁,该线程可以再次获取该锁而不会出现死锁。(ReentrantLock和synchronized)
  • 可中断锁:在等待获取锁过程中可中断。(ReentrantLock)
  • 公平锁: 按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利。(ReentrantLock默认非公平锁,可以通过new ReentrantLock(ture)设置公平锁)
  • 读写锁:对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写。(ReentrantReadWriteLock)

ReentrantLock获取锁方式:

  • lock()获取锁;unlock() 释放锁。
  • tryLock()获取锁,获取成功则返回true,获取失败则返回false。立即返回不等待。
  • tryLock(long timeout,TimeUnit unit)获取锁,获取成功则返回true,获取失败则返回false。timeout为等待时长,unit为时间单位。
  • lockInterruptibly()获取锁时,在等待阶段,可以被interrupt()方法中断等待。

要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁。

lock和synchronized的区别:

  1. synchronized是java内置关键字,在jvm层面加锁,Lock是个java类,在代码层面加锁。
  2. synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁。
  3. synchronized会自动释放锁,Lock需在finally中手工释放锁,否则容易造成线程死锁。
  4. 用synchronized关键字的线程如果获取不到锁就会一直等待。使用Lock的线程就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了。
  5. Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
  6. synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可/非公平。

四、原子变量

需要使用线程同步的根本原因在于对普通变量的操作不是原子的。

原子操作:
原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作,这几种行为要么同时完成,要么都不完成。

原理:

private volatile int value;
  1. 使用volatile实现可见性和有序性
  2. 使用cas实现原子性

CAS:
原数据V,预期值A,新数据B
Java-多线程学习分享-2
执行过程:

  1. 从主内存将V读取到工作内存中,即A
  2. 将A修改为B
  3. 判断A和V是否相同,如果相同则将B写入到主内存;否则从步骤一重新执行。

ABA:

  1. 线程1从主内存读取原值A;
  2. 线程2从主内存读取原值A;
  3. 线程2修改原值为B,并写到主内存中;
  4. 线程2修改原值为A ,并写到主内存中;
  5. 线程1修改原值为C,执行cas操作,认为原值A并未被修改,将C写到主内存中。

解决方案:增加版本号。

上一篇:同步方法及同步块


下一篇:vue2使用脚手架配置prettier报错:‘prettier/prettier‘: context.getPhysicalFilename is not a function