多线程并发问题(volatile、synchronized使用)

文章目录

  • 一、介绍
    • 1、并发问题场景
    • 2、锁的类型
  • 二、volatile
    • 1、变量不用volatile修饰
    • 2、变量使用volatile修饰
  • 三、synchronized
    • 1、修饰实例方法
      • 1)问题代码
      • 2)解决
    • 2、修饰静态方法
      • 1)问题代码
      • 2)解决
    • 3、修饰代码块


一、介绍

1、并发问题场景

  1. 序列号的递增
    1. 线程1查询DB中最大的序列号,在最大序列号的基础上+1
    2. 线程2也查询DB中最大的序列号,此时线程1和线程2查询到的最大序列号是一样的,因为线程1还没有更新DB
    3. 线程1更新DB,线程2更新DB,两个线程更新DB的序列号也一样的

2、锁的类型

  1. 可重入锁:在执行对象中所有同步方法不用再次获得锁
  2. 可中断锁:在等待获取锁过程中可中断
  3. 公平锁: 按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利
  4. 读写锁:对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写

二、volatile

  • volatile是java的关键字,用来修饰变量
  • 特性:
    1. 保证可见性
    2. 不保证原子性
    3. 有序性
  • 每个线程创建的时候,jvm会为每个线程创建一个工作内存,该工作内存是私有的,各个线程的工作内存相互独立,只能被当前线程所访问
  • 而JMM(Java Memory Model Java内存模型)中规定:所有的变量都储存在主内存中,所有线程都能访问,但线程对变量的任何操作都必须在工作内存中进行,首先要将主内存中的变量拷贝到自己的工作内存中,然后才能对变量进行操作,操作完成后再将变量写回主内存中
  • 应用场景:单例模式:懒汉式-双检锁

1、变量不用volatile修饰

  • 线程t1和main主线程分别把h对象拷贝一份到自己的工作内存,对于这两个线程来说,h对象里的num=0
  • 启动线程t1,睡眠2s,在此期间,主线程已经运行到了while语句,h.getNum()==0为true,所以一直在循环
  • 2s过后线程t1把num改为了1,并把最新的num=1发到了主内存
  • 此时发现main主线程还在while循环中,因为对于main主线程来说,它不知道num的值已经变为1,它还是自己工作内存中的num=0,所以它还在while循环中
public class HandleMsg {

    private int num=0;

    public void sum(){
        System.err.println("子线程start,num:"+this.num);
        try{
            TimeUnit.SECONDS.sleep(2);
            this.num=1;
        }catch(Exception e){
            e.printStackTrace();
        }
        System.err.println("子线程end,num:"+this.num);
    }

    public int getNum(){
        return num;
    }

}

HandleMsg h=new HandleMsg();

Thread t1=new Thread(h::sum);
t1.start();

while(h.getNum()==0){
	//main主线程一直循环,直到h的num的值不等于0
}

System.err.println("h的num的值不等于0了");


// 输出
//		子线程start,num:0
//		子线程end,num:1

2、变量使用volatile修饰

  • 线程t1和main主线程分别把h对象拷贝一份到自己的工作内存,对于这两个线程来说,h对象里的num=0
  • 启动线程t1,睡眠2s,在此期间,主线程已经运行到了while语句,h.getNum()==0为true,所以一直在循环
  • 2s过后线程t1把num改为了1,并把最新的num=1发到了主内存
  • 此时发现main主线程不在while循环了,而是输出了"h的num的值不等于0了",因为volatile的可见性,main主线程已经感知到h.getNum()=1了
public class HandleMsg {

    private volatile int num=0;

    public void sum(){
        System.err.println("子线程start,num:"+this.num);
        try{
            TimeUnit.SECONDS.sleep(2);
            this.num=1;
        }catch(Exception e){
            e.printStackTrace();
        }
        System.err.println("子线程end,num:"+this.num);
    }

    public int getNum(){
        return num;
    }

}

HandleMsg h=new HandleMsg();

Thread t1=new Thread(h::sum);
t1.start();

while(h.getNum()==0){
	//main主线程一直循环,直到h的num的值不等于0
}

System.err.println("h的num的值不等于0了");

// 输出
//		子线程start,num:0
//		子线程end,num:1
//		h的num的值不等于0了

三、synchronized

  • synchronized是Java中的一个关键字,可以保证被它修饰的方法、代码块在任何时刻只有一个线程执行
  • 随着jdk版本的更新,synchronized在不断的优化
  • 特性
    • 原子性:synchronized作用域部分整体不可分割,任何时刻,只有一个线程在执行
    • 可见性:它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的,即获取volatile变量的值都是最新的

1、修饰实例方法

1)问题代码

  • 最终输出的num应该是20000才对,但是每次输出都小于20000
  • num++不具备原子性,分为三步
    1. 读取i的值
    2. i+1
    3. 将值更新至内存
  • HandleMsg的实例对象只有1个,2个线程在操作它
  • t1读取num的值,t1将num的值+1,t1还没有将+1后的值更新至内存,t2读取num的值,t1将+1后的值更新至内存,此时t1和t2读取到的num的一样,所以更新至内存的值也一样
  • 出现多次这样的问题,就导致少加了好多次,最终导致了num小于20000
  • 成员变量num使用volatile修饰也会出现该问题,因为volatile不保证原子性
public class HandleMsg {

    //成员变量
    private int num=0;
    
    //将成员变量num累加10000次,每次+1
    public void sum(){
        //循环次数大时才能出现并发问题
        for(int i=0;i<10000;i++){
            num++;
        }
    }

    public int getNum(){
        return num;
    }

}

//创建1个对象
HandleMsg handleMsg=new HandleMsg();

//创建2个线程
Thread t1=new Thread(handleMsg::sum);
Thread t2=new Thread(handleMsg::sum);

//启动线程
t1.start();
t2.start();

//等待线程t1、t2执行完之后再执行下面的代码
t1.join();
t2.join();

int num=handleMsg.getNum();

2)解决

  • 实例方法用synchronized修饰
  • 这里的锁是对象锁,这里的对象只有1个就是handleMsg
  • 当t1线程获取锁执行synchronized方法时,其他线程就无法获取该对象的锁,也就不能访问该对象的其它synchronized修饰的方法,因为1个对象只有一把锁。但是其它线程可以访问该对象的其它非synchronized修饰的方法
public class HandleMsg {

    //成员变量
    private int num=0;
    
    //将成员变量num累加10000次,每次+1
    //使用synchronized修饰方法
    public synchronized void sum(){
        //循环次数大时才能出现并发问题
        for(int i=0;i<10000;i++){
            num++;
        }
    }

    public int getNum(){
        return num;
    }

}

//创建1个对象
HandleMsg handleMsg=new HandleMsg();

//创建2个线程
Thread t1=new Thread(handleMsg::sum);
Thread t2=new Thread(handleMsg::sum);

//启动线程
t1.start();
t2.start();

//等待线程t1、t2执行完之后再执行下面的代码
t1.join();
t2.join();

int num=handleMsg.getNum();

2、修饰静态方法

1)问题代码

  • 最终输出的num应该是20000才对,但是每次输出都小于20000
  • 如果t1个线程访问实例对象h1的synchronized方法,对象锁是h1,t2线程访问实例对象h2的synchronized方法,对象锁是h2,t1和t2会获得各自的对象锁,这样是可以的
  • 因为2个对象锁并不同相同,如果两个线程操作数据是共享数据,那么并发安全问题还是会出现
public class HandleMsg {

    //静态变量,属于类,创建多个对象时,只加载1次
    private static int num=0;

    //将成员变量num累加10000次,每次+1
    //synchronized修饰
    public synchronized void sum(){
        for(int i=0;i<10000;i++){
            num+=1;
        }
    }

    public int getNum(){
        return num;
    }

}

//创建2个对象
HandleMsg h1=new HandleMsg();
HandleMsg h2=new HandleMsg();

//创建2个线程
Thread t1=new Thread(h1::sum);
Thread t2=new Thread(h2::sum);

//启动线程
t1.start();
t2.start();

//等待线程t1、t2执行完之后再执行下面的代码
t1.join();
t2.join();

System.err.println(h1.getNum());
System.err.println(h2.getNum());

2)解决

  • synchronized修饰的是静态方法,这里的锁是HandleMsg类,无论多少实例对象,类锁只有1个,调用方法和实例对象无关:类.sum()
public class HandleMsg {

    //静态变量,属于类,创建多个对象时,只加载1次
    private static int num=0;

    //将成员变量num累加10000次,每次+1
    //静态方法
    //synchronized修饰
    public synchronized static void sum(){
        for(int i=0;i<10000;i++){
            num+=1;
        }
    }

    public int getNum(){
        return num;
    }

}

//创建2个对象
HandleMsg h1=new HandleMsg();
HandleMsg h2=new HandleMsg();

//创建2个线程
Thread t1=new Thread(HandleMsg::sum);
Thread t2=new Thread(HandleMsg::sum);

//启动线程
t1.start();
t2.start();

//等待线程t1、t2执行完之后再执行下面的代码
t1.join();
t2.join();

System.err.println(h1.getNum());
System.err.println(h2.getNum());

3、修饰代码块

  • 有时候我们写的方法体可能比较大,而需要同步的代码只有一小部分,如果直接对整个方法进行同步操作,会降低效率,此时我们可以对需要同步的代码进行同步
public class HandleMsg {

    private int num=0;

    //将成员变量num累加10000次,每次+1
    public void sum(){
        Thread thread=Thread.currentThread();
        System.err.println(thread.getId()+"  sum start");

        /**
         * 同步块
         * 
         * this
         *    代表调用该方法实例对象,是对象锁
         *    如:HandleMsg h=new HandleMsg();  h.sum();  this = h
         *    HandleMsg类如果有多个实例对象,每个实例对象都有自己独立的对象锁,互不干扰
         *    如果多个线程操作1个实例对象(单例),适合使用
         *
         * HandleMsg.class
         *    代表HandleMsg类,是类锁
         *    无论HandleMsg类有几个实例对象都只有1把锁
         *    静态方法包裹的代码块时,适合使用
         */
        synchronized(this){
            System.err.println(thread.getId()+"  add");
            for(int i=0;i<10000;i++){
                num+=1;
            }
        }

        System.err.println(thread.getId()+"  sum end");
    }

    public int getNum(){
        return num;
    }

}

//创建1个对象
HandleMsg h=new HandleMsg();

//创建2个线程
Thread t1=new Thread(h::sum);
Thread t2=new Thread(h::sum);

//启动线程
t1.start();
t2.start();

//等待线程t1、t2执行完之后再执行下面的代码
t1.join();
t2.join();

System.err.println(h.getNum());


// 输出
//		12  sum start
//		13  sum start       不属于同步块中的代码,所以其它线程可以执行
//		12  add
//		12  sum end
//		13  add
//		13  sum end
//		20000

你可能感兴趣的:(线程Thread,java,开发语言)