Java多线程对象及变量的并发访问

时间:2021-10-13 18:06:13

目录:

  1. synchronized总结
  2. 写一个死锁
  3. 线程安全的三大特性
  4. java内存模型
  5. synchronized与volatile对比
  6. Atomic原子类
  7. CAS机制(compare and swap)
  8. 乐观锁悲观锁

1、 synchronized

1.1、方法内的变量为线程安全的

“非线程安全”问题存在于实例变量中,如果一个变量是方法内的变量,那么这个变量是线程安全的,也不会出现“非线程安全”问题。

代码:

package Thread.thread2;

public class Num {
    //private int num;
     public void addI(String str){
        int num =0;
        if(str.equals("a")){
            num=100;
            System.out.println("a set over");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {
            num = 200;
            System.out.println("b set over");
        }
        System.out.println("num = "+num);

    }
}
package Thread.thread2;

public class ThreadA extends Thread{
    private Num num;
    public ThreadA(Num num){
        super();
        this.num = num;
    }

    @Override
    public void run() {
        super.run();
        num.addI("a");
    }
}
package Thread.thread2;

public class ThreadB extends Thread{
    private Num num;
    public ThreadB(Num num){
        super();
        this.num = num;
    }

    @Override
    public void run() {
        super.run();
        num.addI("b");
    }
}
package Thread.thread2;

public class TestRun {
    public static void main(String[] args) {
        Num num = new Num();
        ThreadA threadA = new ThreadA(num);
        ThreadB threadB = new ThreadB(num);
        threadA.start();
        threadB.start();
    }
}

运行结果:

a set over
b set over
num = 200
num = 100

解读:上面的代码,两个线程明明访问的同一方法,a等了b两秒,讲道理a最后应该打印出b修改后的值,但为什么还是100呢?

原因:

  1. 方法内部的变量为方法私有的变量,其生存走起随着方法的结束而终结。
  2. 每个线程执行的时候都会把局部变量存放在各自栈帧的工作内存中(栈帧进入虚拟机栈),虚拟机栈线程间不共享,故不存在线程安全的问题。

针对第二条原因扩展说明:

Java虚拟机在执行Java程序的时候会把它管理的内存划分为5个不同的区域,其中 【方法区】和【堆】是线程共享的,而【虚拟机栈】、【程序计数器】、【本地方法栈】是线程不共享的,如下图所示:

Java多线程对象及变量的并发访问

1.2、实例变量非线程安全可以通过synchronized来解决

代码 将上面代码修改如下:

package Thread.thread2;

public class Num {
     private int num;
     public void addI(String str){
        //int num =0;
        if(str.equals("a")){
            num=100;
            System.out.println("a set over");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {
            num = 200;
            System.out.println("b set over");
        }
        System.out.println("num = "+num);

    }
}

结果:

a set over
b set over
num = 200
num = 200

这次a也打印出了200,因为这次两个线程修改的是同一个变量。

对于这样的问题我们可以用synchronized方法来解决,只需要在共同访问的方法上加上该关键字就可以了,如下:

synchronized public void addI(String str){
    ...
}

1.3、synchronized方法与synchronized代码块对比

synchronized方法使用有个弊端,就是如果同步方法很费时间的代码的话,效率会很慢。我们可以使用同步代码块来将需要同步的代码加锁,而费时间的代码让其异步。

1.4、synchronized方法加的是什么锁?

答:this锁

1.5、synchronized加载static方法上的是什么锁?

答:字节码锁,类名.class。一般情况下不用static。JVM编译的时候是存在方法区的,是垃圾回收机制不会回收的地方。

1.6、其他总结

  • 多个线程访问一个对象中的方法时候,顺序是轮流执行的,并非同步执行的。
  • 多个对象访问一个同步方法时,运行结果是异步的(就是多个对象产生了多个锁。),哪个对象的线程先执行带synchronized关键字的方法,哪个对象的线程先执行,然后异步执行。
  • A线程访问一个synchronized方法或synchronized同步块,B线程访问非synchronized方法或非synchronized同步块时候,B线程可以随意调用其他的非synchronized方法
  • 两个线程访问同一个对象的两个同步的方法时候,结果是同步执行的,不存在脏读的现象
  • synchronized拥有锁重入的功能,也就是在使用synchronized时候,当一个线程得到一个对象锁时候,再次请求此对象锁是可以再次得到该对象的锁的。
  • 可重入锁也支持在父子类继承的环境中
  • 出现异常,锁自动释放。
  • 如果子类继承父类的synchronized方法(如:synchronized public void x(){})时候,子类在重写的时候,如果不标注synchronized的话,子类就会吃掉“synchronized”,就是子类中的x方法是非synchronized,导致运行的时候并不同步。所以在重写的x(),子类就要就要写上synchronized,运行就是同步的。
  • 当一个线程访问一个synchronized同步块时,另一个线程仍然可以访问对象中的非synchronized(this)同步代码块。
  • synchronized(非this对象):如果一个类中有很多个synchronized方法,这时虽然能出现同步,但会受到阻塞,所以影响运行效率,但如果使用synchronized(非this对象),它与synchronized方法是异步的,不与其他同步方法争夺this锁
  • 同步代码块放在非同步synchronized方法中进行声明,线程调用是无序的,但是在使用synchronized(非this对象),可以解决脏读的现象
  • 多个线程执行synchronized(非对象x)呈同步效果

2、 死锁

死锁产生的原因:两个锁互相等待而导致。写个死锁代码如下:

package Thread.deadLock;

public class Mythread implements Runnable {
    private Object obj = new Object();
    public boolean flag = true;
    @Override
    public void run() {
       if(flag){
           while (true){
               synchronized (obj){
                   try {
                       Thread.sleep(50);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   synchronized (this){
                       System.out.println("1");
                   }
               }
           }
       }else {
          while (true){
              synchronized (this){
                  try {
                      Thread.sleep(50);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  synchronized (obj){
                      System.out.println("2");
                  }
              }
          }
       }
    }

}
package Thread.deadLock;

public class TestDeadLock {
    public static void main(String[] args) {
        Mythread mythread = new Mythread();
        new Thread(mythread).start();
        try {
            Thread.sleep(30);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        mythread.flag = false;
        new Thread(mythread).start();
    }
}

3、 线程安全的三大特性

原子性、可见性、有序性。

3.1、什么是原子性?

一个操作或者连续的多个操作,要么全部执行要么全部不执行。事物的原子性一样的道理。而多线程执行的过程运行的时候就会出现不能保证原子性的问题。因此我们需要使用同步和锁来确保这个特性。

3.2、什么是可见性?

一个线程对共享变量的修改能够及时的被其他线程看到。共享变量是存放在主内存中的,每个线程访问共享变量会在自己的工作内存中存一个副本而不是在主内存中直接操作共享变量。

共享变量可见性实现的原理:

  • 把工作内存1更改过得共享变量刷新到主内存中
  • 将主内存中的最新共享变量更新到工作内存2中

Java语言层面实现可见性的方式(不包过jdk1.5后引用的包的高级特性): synchronized volatile

3.4、什么是有序性?

Java代码有序的执行;但也会有指令重排序的问题。

重排序:代码执行的顺序与书写的顺序不同,指令重排序是编译器或处理器为了提高性能而做的优化

  • 编译器优化的重排序(编译器优化)
  • 指令级并行重排序(多核处理器优化)
  • 内存系统重排序(处理器优化)

as-if-serial:无论怎么重排序都应该与书写顺序执行的结果一致,当然这是单线程下能够保证;如下代码:

int num1 = 1; //第一行
int num2 = 2; //第二行
int num = num1 + num2; //第三行 

单线程:无论第一、第二行怎么重排序也不会到了第三行的下边。

但是多线程下就会有问题了。

4、 Java内存模型

Java内存模型(java memory model,JMM)描述了Java程序中的各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量的这样的细节。

两条规则:

  • 线程对共享变量的所有操作都是在自己的工作内存中操作,不能直接在主内存上读写。
  • 不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值得传递需要通过主内存来完成。

5、 synchronized 与volatile对比

注意:紧靠volatile不能保证线程的安全性(原子性)只能保证可见性

  • volatile轻量级只能修饰变量,synchronized重量级还可以修饰方法
  • volatile只能保证数据的可见性,不能用来同步,因为多个线程并发访问volatile修饰的变量不会阻塞。如何实现可见性的?通过加入内存屏障和禁止指令重排序优化实现的。
  • synchronized不仅保证可见性,而且还能保证原子性,线程会阻塞。原子性不用说,加锁了保证多操作的同步了。而实现可见性是在线程解锁前,必须把共享变量的最新值刷新到主内存中;线程加锁时,将清空工作内存*享变量的值,从而使用共享变量时需要从主内存中重新读取新的值。

6、 AtomicInteger原子类

https://mp.weixin.qq.com/s/f9PYMnpAgS1gAQYPDuCq-w

7、 CAS机制(compare and swap)

https://mp.weixin.qq.com/s/nRnQKhiSUrDKu3mz3vItWg

8、 乐观锁悲观锁v

https://mp.weixin.qq.com/s/LLGla7tI-W7zWaT3vCviVw